• 🔥 Бесплатный курс от Академии Кодебай: «Анализ защищенности веб-приложений»

    🛡 Научитесь находить и использовать уязвимости веб-приложений.
    🧠 Изучите SQLi, XSS, CSRF, IDOR и другие типовые атаки на практике.
    🧪 Погрузитесь в реальные лаборатории и взломайте свой первый сайт!
    🚀 Подходит новичкам — никаких сложных предварительных знаний не требуется.

    Доступ открыт прямо сейчас Записаться бесплатно

Статья Как усложнить анализ приложения

Logo.jpg

Международным днём защиты информации по праву считается 30 ноября. Маховик этого праздника раскрутил обычный студент «Роберт Моррис», который в 1988 году создал своего интернет-червя. Какой им был нанесён вред – это отдельная история, зато он заставил весь мир понять, почему кибербезопасности нужно уделять особое внимание. Если коротко, то червь Морриса использовал несколько приёмов стелсирования своей тушки:
  1. Удаление своего исполняемого файла после запуска.
  2. Размер аварийного дампа устанавливался в ноль.
  3. Каждые три минуты порождался дочерний поток, а родительский завершался (при этом постоянно менялся pid, и обнулялось время работы).
  4. Все текстовые строки были закодированы по xor 81h.
Справедливости ради нужно отметить, что все перечисленные приёмы актуальные и по сей день,
просто за 35 лет безопасность систем вышла уже на новый уровень, а потому реализация будет отличаться.

В данном материале я попытаюсь выразить своё мнение на этот счёт. Основной посыл – это шекель в копилку реверс-инженеров, чтобы в своей практике они могли обнаружить уже знакомые участки кода. Краткий список основных моментов выглядит примерно так:
  1. Борьба с отладчиком – программное удаление точки-входа на ОЕР.
  2. Борьба с дизассемблером – вызов API-функций по хэшу, печать строк по зашифрованным указателям.
  3. Создание копии своего исполняемого файла из образа в памяти.
  4. Запуск ксеро-копии, с последующим удалением себя.

1. Противодействие отладке

Сейчас все уважающие себя отладчики имеют встроенные механизмы «анти-анти-дебаг», что полностью сводит на нет попытки с нашей стороны скрыться от них. Вы только посмотрите на список боеприпасов плагина x64dbg под названием «ScyllaHide» – да это же зверь, который показывает даже то, о чём некоторые из нас и не подозревали. Ну и как теперь выживать в таких условиях?

ScyllaHide.png

Таким образом, любой из перечисленных выше вариантов в зародыше обречён на провал, но одна лазейка всё-же осталась.
Откроем в x64dbg любой файл, и сразу перейдём на вкладку «Журнал», где будет лог процесса первичной его инициализации:

Код:
Инициализация отладчика...
. . . .
Инициализация успешно завершена!
Загрузка плагинов...
Обработка командной строки...
Процесс запущен: 00400000 F:\Install\DEBUG\ASM\CODE\test.exe

Точка останова по адресу 00403000 (останов в точке входа) установлена!

DLL загружена: 77810000 C:\Windows\SysWOW64\ntdll.dll
DLL загружена: 75D20000 C:\Windows\SysWOW64\kernel32.dll
DLL загружена: 75B30000 C:\Windows\SysWOW64\KernelBase.dll
DLL загружена: 75440000 C:\Windows\SysWOW64\user32.dll
DLL загружена: 770B0000 C:\Windows\SysWOW64\msvcrt.dll

Достигнута системная точка останова!
INT3 "останов в точке входа" на <test.EntryPoint> (00403000)!

Здесь видно, что движок отладки ставит точку на ОЕР ещё до загрузки системных библиотек. В практической реализации это выглядит как запись опкода инструкции int3=0xCC по адресу 0х00403000, в результате чего отладчик останавливается при достижении данного адреса. Суть в том, что мы можем обратно восстановить оригинальную инструкцию на точке-входа в программу, тогда процесс отладки останется для юзера уже за бортом, по типу клавиши F9 «Выполнить всё». Ясно, что секция-кода для этого должна быть открыта на запись, а во-вторых осуществлять подмену нужно исключительно посредством колбэков TLS. Например у меня код программы специально начинается с инструкции NOP, и я успешно борюсь с отладчиком так:

C-подобный:
proc  AntiDebug       ;<---------- Вызывается из секции TLS
      invoke  GetModuleHandle,0  ;
      mov     ebx,eax            ; наш базовый адрес в памяти: «ImageBase»
      mov     ecx,[eax+3ch]      ; RVA на РЕ-заголовок
      add     ebx,ecx            ; Base + RVA = вирт.адрес заголовка
      mov     [peHeader],ebx     ; запомнить..
      mov     ebx,[eax+28h]      ; RVA точки-входа из заголовка: «ОЕР»
      add     ebx,eax            ; делаем из него вирт.адрес
      mov     byte[ebx],90h      ; затереть брейк отладчика на ОЕР: (90h = опкод инструкции NOP)
      ret
endp

Буквально всех аверов абсолютно не беспокоит открытый на запись код, иначе светлая жизнь легальных анпакеров типа UPX, ASpack оказалась-бы под большим вопросом. В купе с пересчётом своей контрольной суммы, данный вариант анти-дебага даёт не плохие результаты на практике.


2. Противодействие дизассемблерам

Аналогичную ситуацию наблюдаем и с дизассемблерами – какой бы план мы не придумали, такие гиганты как IDA, Ghidra, Radare и прочие, всё-равно разорвут его как тузик грелку. Но складывать оружие без боя тоже глупо, особенно если учесть, что умные их анализаторы частенько проседают. Шифрование и обфускация всей тушки очень накладно, а вот рассмотреть простые (и в тоже время эффективные) методы не помешает.

В первую очередь конечно-же текстовые строки, ухватившись за которые аналитик сможет раскрутить буквально весь клубок защиты. Как в отладчиках, так и в дизассемблерах имеется крутая фишка под названием «String/Cross-References», что подразумевает «поиск перекрёстных ссылок на строки». Кстати авторы x64dbg и IDA-Pro почему-то не назначили ей хоткей, хотя в IDA можно быстро организовать это дело комбинацией Ctrl+1.

2.1. Печать строк по указателям

Идея в том, чтобы хранить строки пусть даже и в открытом виде, а вот обращаться к ним уже через проксоренный указатель. В результате инструменты анализа увидят строку, но кто и от куда к ней обращается, останется в тайне.

C-подобный:
Message  db  '            _/-----===^^^===-----\_',10,10
         db  '~.~.~.~.~ Windows Stealth technology ~.~.~.~.~',10
         db  '~.~.~.~.~.~. Find me, if you can! ~.~.~.~.~.~',10,10,0

msgAddr  dd  Message

Посмотрим на фрагмент выше.. По адресу скажем 0х00402104 имеем переменную «Message», которая указывает на начало строки с текстом. Однако из своего кода я обращаюсь к этой строке через вторую переменную «msgAddr», а не напрямую. Первая вообще нигде не фигурирует в коде, а нужна лишь для того, чтобы получить адрес строки исключительно на этапе разработки приложения. Если в дефолте значение второй переменной равно адресу строки 0х00402104, то для пущей надёжности имеет смысл проксорить это значение например ключом 0xDEAD9876, тогда получим указывающий в космос адрес 0xDEEDB972. Операция XOR удобна здесь тем, что является обратимой, а потому перед обращением к данной строке достаточно повторно применить ксор с таким-же ключом.


2.2. Вызов функций Win-API по хэшу

Второй момент – это скрыть от посторонних глаз перечень используемых в коде системных функций. Просто взглянув на импорт можно уже сделать вывод о том, чем именно занимается программа, делает-ли она запросы в сеть, и многое другое. В общем с какой стороны не посмотри, но защищённый софт без скрытия API будет напоминать ворота без забора. Мощным оружием в данном ключе является вызов функций не по их фактическим именам, а по хэш-значениям этих имён напрямую из секции-экспорта библиотеки. Эта техника оставляет нервно курить в сторонке даже динамический вызов посредством пары LoadLibrary() + GetProcAddress(), которым полюбому приходится передавать имена функций в открытом виде.

Чтобы реализовать «Hash-API» на практике, во-первых нужно определиться с самим алгоритмом хэширования. Широкое распространение получил здесь самый простой и быстрый «Djb2A». Для тех кто в танке напомним, что хэш (в отличии от шифрования) – это одностороняя функция, т.е. вычислив один раз хэш какой-либо строки, вы не сможете получить эту строку обратно. Вот исходник вспомогательной утилиты, которой просто передаём имена функций, а из выхлопной трубы получаем готовые к употреблению хэши для основной программы. Значения Seed и Salt можете без проблем поменять на свои:

Hash.png

C-подобный:
format   pe console
entry    start
include 'win32ax.inc'
;//-------------------
.data
vAlloc     db  'VirtualAlloc',0
kExec      db  'WinExec',0
kSleep     db  'Sleep',0
kCmdLine   db  'GetCommandLineA',0
kDelete    db  'DeleteFileA',0
kCreate    db  '_lcreat',0
kWrite     db  '_lwrite',0
kClose     db  '_lclose',0

Table      dd  vAlloc,  kExec,  kSleep, kCmdLine
           dd  kDelete, kCreate,kWrite, kClose
Count      =   8
Hash       dd  3731   ;//<--------------- начальное значение хэша (Seed)

;//----------
.code
start:  mov     edi,Table
        mov     ecx,Count

GetHashList:            
        push    edi ecx
        mov     esi,[edi]
        push    esi
        xor     eax,eax
        xor     ebx,ebx
;//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@:     lodsb                ;//  AL = очередной символ имени
        or      al,al        ;//  выйти, если конец строки
        je      @f           ;//
        mov     ebx,[Hash]   ;//  EBX = текущее хэш-значение
        shl     ebx,3        ;//  умножить его на 8  <------------(Salt)
        add     ebx,[Hash]   ;//  + текущее хэш-значение
        add     ebx,eax      ;//  + код очередного символа из AL
        mov     [Hash],ebx   ;//  запомнить!
        jmp     @b           ;//  следующий символ имени..
;//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@:     pop     esi
       cinvoke  printf,<10,' %08x   %s',0>,[Hash],esi

        pop     ecx edi
        add     edi,4
        mov     [Hash],3731  ;//  восстановить для следующего API
        loop    GetHashList  ;//  повторить для всех имён из списка

@exit: cinvoke  _getch
       cinvoke  exit,0
 
;//----------
section  '.idata' import data readable
library   msvcrt,'msvcrt.dll'
include  'api\msvcrt.inc'

Получив таким образом хэши, остаётся пропарсить таблицу-экспорта той библиотеки, из которой мы собираемся импортировать функции – в данном случае я ограничился всего одной Kernel32.dll. Указатель на экспорт прописывается в каталоге «DATA_DIRECTORY» РЕ-заголовка, на системах х32 он лежит по смещению 0x78 от начала заголовка, а на х64 оффсет его 0x88.

В таблице экспорта для нас в приоритете всего 3 поля (выделены красным): общее число именованных функций по смещению(24), линк на таблицу адресов(28), и линк на таблицу указателей на имена функций(32). Все поля в этих таблицах хранят относительные RVA-адреса, а потому нужно добавлять к ним полученную через GetModuleHandle() базу DLL в памяти. Размеры полей 4-байта dword, и одинаковы они как для х32, так и для х64.

Чтобы обойти весь экспорт, нужно на каждой итерации цикла делать шаг сразу в обоих таблицах. То-есть берём первый указатель на имя из средней таблицы, вычисляем хэш этого имени и если совпало, значит адресом функции будет первое значение из верхней таблицы. Иначе смещаемся в обоих таблицах на 4-байта вперёд, и повторяем поиск. Это именно то, чем за кулисами занимается готовая функция GetProcAddress(). Отметим, что либа Kernel32.dll всего выдаёт на экспорт аж 1324 функции, однако поиск в ней нужной единицы занимает буквально мгновение.

Export.png

Развивая тему дальше, ничто не мешает вообще избавиться от секции-импорта в программе, ведь хотим мы того или нет, система проецирует Ntdll и Kernel32 во все процессы, включая наш. Прочитав поле Ldr.InMemoryOrderModuleList в структуре РЕВ, можно без WinAPI узнать базу этих библиотек в памяти, но это вариант для хардкорщиков, а не для истинных (т.е. ленивых) программистов как мы.

pebLdr.png


3. Создание копии своего исполняемого файла из образа в памяти.

Деторождение – типичная практика для малвари. Кто-то тупо копирует свой исходный файл на диске, а некоторые зверки поступают более изящным способом – его и положим на анатомический стол. Плюсом копии уже загруженного в память образа является то, что каждый последующий наш потомок будет отличаться от родителя, т.е. фактически получим генно-модифицированный код, или полиморфизм. Например записали мы в процессе работы нашего приложения что-то новое в секцию кода/данных, а потом раз и сделали из содержимого памяти новый исполняемый файл. В своём примере я буду просто передавать имя своего/текущего файла, чтобы по окончании работы потомок смог найти и удалить его с диска. Реализация копеешная, зато эффект создаёт мощный.

PeFile.png

Структура РЕ-файла такова, что всё его содержимое делится на равные страницы по 512-байт. Конкретный размер файловой страницы указывается в поле «FileAlignment» (выравнивание файла), и хранится он в заголовке. Таким образом, размер неупакованного исполняемого файла будет всегда кратен 512-байтам. Но когда файл загружается из диска в память, страницы увеличиваются в размерах до 4096-байт, а разница забивается нулями. Этот размер так-же оговаривается в поле заголовка «SectionAlignment», и как-правило совпадает он с размером виртуальной страницы ОЗУ.

Системный загрузчик образов «ImageLDR» поочерёдно копирует в память буквально все файловые секции, в результате чего мы можем получить точную копию оригинала. Для этого сначала нужно получить базу нашего образа в памяти через CetModuleHandle(0), и сместиться от неё на 0xF8 байт ниже. База есть ничто иное как РЕ-заголовок, а смещение поможет попасть в таблицу-секций.

Каждую из секций описывает одинаковая структура размером 0x28 байт, и в ней найдём смещение соответствующей секции в памяти, и (что немаловажно) размер этой секции на диске. Теперь просто переходим по указанному адресу в памяти, и копируем «PhysicalSize» байт в новый файл. Ясно, что если мы хотим получить пропатченную копию своей/чужой программы, осуществлять эту операцию нужно уже после записи всех изменений.

Section.png

C-подобный:
;// Выделяем память через VirtualAlloc()
       stdcall  GetFuncAddress,[vAlloc] ;// EAX = адрес функции
        mov     esi,[peHeader]
        mov     ebx,[esi + 52]          ;// EBX = размер нашей тушки в памяти
       stdcall  eax,0,ebx,MEM_RESERVE + MEM_COMMIT,PAGE_READWRITE
        mov     [fileBuff],eax

;// Копируем весь РЕ-заголовок файла = 1024 байта
        xchg    edi,eax
        mov     esi,[imageBase]
        mov     ecx,256
        rep     movsd

;// В цикле копируем все остальные секции,
;// динамически вычисляя их (1)адрес в памяти, и (2)размер на диске
        mov     esi,[peHeader]
        movzx   ebx,word[esi + 6] ;//<--- всего секций
        add     esi,0xF8          ;//<--- указатель на таблицу-секций

@@:     push    esi ebx
        mov     ecx,[esi + 16]    ;// размер на диске
        shr     ecx,2             ;// ...(в двордах)
        mov     esi,[esi + 12]    ;// адрес в памяти
        add     esi,[imageBase]
        rep     movsd             ;// скопировать в файловый буфер!
        pop     ebx esi      
        add     esi,0x28          ;// сл.секция в таблице
        dec     ebx               ;// всё скопировали?
        jnz     @b                ;// нет - повторить..

;//*********************************************
;// Создать копию файла из памяти!
;//*********************************************
       stdcall  GetFuncAddress,[kCreate]
       stdcall  eax,newFile,0
        push    eax eax

       stdcall  GetFuncAddress,[kWrite]
        pop     ebx
       stdcall  eax,ebx,[fileBuff],0xE00

       stdcall  GetFuncAddress,[kClose]
        pop     ebx
       stdcall  eax,ebx

Чтобы каждый из потомков рождался с уникальным именем, нужно предусмотреть генератор рандома. Для этого подходит инструкция XLATB, которая ожидает в регистре EBX указатель на строку с алфавитом, а в регистре AL рандомное значение, чтобы выбрать из алфавита произвольный символ. Выглядит это примерно так:

C-подобный:
symTable:
   times 26  db  % +('a'-1)  ;// abcdefghijklmnopqrstuvwxyz
   times 10  db  % +('0'-1)  ;// 0123456789
;//--------------

        rdtsc                 ;// EAX = счётчик тактов CPU
        mov     ebx,symTable  ;// EBX = адрес алфавита
        mov     edi,newFile   ;// куда сохранять рандомную строку
        mov     ecx,4         ;// длина строки
@@:     cmp     al,35         ;// проверить рандом на выход за границы алфавита
        jbe     @01           ;// ок, если меньше/равно
        sub     al,35         ;// иначе: пытаемся загнать в рамки
        jmp     @b            ;//
@01:    xlatb                 ;// AL = рандомный символ из алфавита
        stosb                 ;// сохранить его в буфер EDI
        shr     eax,8         ;// сл.рандомный байт из EAX
        loop    @b            ;//

4. Заключение

По понятным причинам я не буду выкладывать здесь готовый исходник, а вот уже скомпилированный файл для анализа в отладчике или дизассемблере положу в скрепку. Ничего левого в нём нет, а счётчик жизни выставлен всего на 5 итераций (не на бесконечность). При запуске выводит на консоль телетайпом строку, после чего создаёт свою копию в той-же директории, и убивает предыдущего. При этом если параллельно запустить диспетчер задач Windows, то можно заметить, что процесс c именем «banzai_xx.exe» существует в единственном экземпляре, и по истечении 2-сек меняется его PID. Удачных исследований, пока!

Test.gif
 

Вложения

Привет, хорошая получилась статья, только хотел узнать, IDA вроде как не ставит бряк на OEP, можно просто выбрать адрес, и по нему поставить бряк, вопрос: можно ли как то все таки запатчить в любом случае измененный опкод по адресу, если отлаживают
 
IDA вроде как не ставит бряк на OEP
OEP указывается в заголовке файла, и IDA берёт его от туда, запускаясь сразу в режиме дизассемблера, а не отладчика. У неё отладчик активируется в ручную, а пример рассчитан на автоматику именно дебага. На счёт пропатчить не знаю, ..если только занопить/вырезать TLS, ну или в каталоге Data_Directory удалить запись под номером(9).
 
  • Нравится
Реакции: trulala01
Попытался воспроизвести у себя анти-отладчик, но он работает не совсем так, как ожидалось (см. приложения). Права на запись секции .text выставил, CRT и прочий шлак был отключён. Что не так?
 

Вложения

  • Screenshot_4.png
    Screenshot_4.png
    10 КБ · Просмотры: 93
  • Screenshot_6.png
    Screenshot_6.png
    40,4 КБ · Просмотры: 87
Мы в соцсетях:

Взломай свой первый сервер и прокачай скилл — Начни игру на HackerLab