Тема противодействия отладке давно заезжена, и в сети можно встретить огромное кол-во классических (миссионерских) вариантов, от обычной проверки флагов функцией IsDebuggerPresent(), до более изощрённых финтов, типа срыв-стека приложения, или расширения его секций. Но разрабы таких инструментов как: Ghidra, x64Dbg, OllyDbg, IDA, Immunity и других, тоже почитывают те-же материалы, что и мы с вами. Движимые инстинктом выживания и удержанием своих позиций на рынке, они стараются учитывать эти нюансы в своих продуктах, в результате чего добрый десяток известных алгоритмов анти-дебага палится ещё на взлёте, различными их плагинами.
Однако в работе отладчиков есть и механизмы, от которых разрабы не могут избавиться в принципе, т.к. они являются фундаментальными для данного рода программ – это точки-останова, или Breakpoint (кому как нравится). По сей день, ни один из отладчиков так и не научился противостоять сносу бряков, на что мы и сделаем ставку в этой статье. Общая идея такова, что если правильно выбрать "время и место", то факт восстановления ВР в прежнее состояние, позволит нашей программе выйти из-под контроля отладки, выполнив свой код на одном дыхании так, как-будто его запустили под реальным процессором. Авансом скажу, что это простой, элегантный и чертовски полезный баг, затрагивающий буквально все заточенные под Win32/64 отладчики.
Оглавление:
1. Принцип работы отладчиков
Практика – дело хорошее, но при внештатных ситуациях, пробелы в теории могут запросто выбить нас из седла. Поэтому для начала рассмотрим, какие высокие принципы заложены в работу отладчиков. По большому счёту, свой юзермодный дебаггер сможет написать даже программист средней руки, поскольку в системах класса Win всё уже предусмотрено для этого, в нёдрах библиотек Kernel32, Dbgeng и Dbghelp.dll. Эта группа функций известна под общим названием DebugAPI, оптом и в розницу предлагающие в наше распоряжение услуги следующего характера:
1. Загpузка или подключение к клиенту для его отладки;
Первое, что должен сделать отладчик, это функцией CreateProcess() создать отлаживаемый процесс. Причём в аргументе этой функции нужно обязательно указать флаг DEBUG_PROCESS, чтобы сообщить системе, что родительский процесс планирует войти в "цикл-отладки" для управления дочерним процессом. Все современные отладчики могут подключаться и к уже запущенным процессам из диспетчера-задач. В этом случае отладчик зовёт из либы Kernel32.dll функцию DebugActiveProcess().
Поскольку отладчик подключается к клиенту посредством CreateProcess(), то оба они будут исполняться в разных адресных пространствах. То-есть будем иметь два самостоятельных процесса, связанных невидимой нитью отладки. Благодаря этому, устойчивость Win к процессам дебага весьма высока, и не зависит от поведения клиента. Даже если отлаживаемый процесс начнёт беспорядочно записывать мусор в память, эта запись всё-равно не сможет привести к сбою отладчика, т.к. в данном случае процесс стреляет в ногу себе, а не родителю.
2. Отладчик принимает от клиента уведомления об отладочных событиях;
После того-как клиент принят на борт, отладчик запоминает байт с текущего его указателя EIP и меняет этот байт на точку-останова INT-3 (опкод 0xCC). Теперь отладчик входит в свой цикл, путём вызова функции WaitForDebugEvent() для приёма отладочных уведомлений от клиента. С этого момента, через системный "порт-отладки" клиент периодически будет посылать отладчику сообщения, о происходящих в его тушке событиях Event. Вот прототип функции ожидания:
Тут мы указываем только время ожидания (обычно -1), а стpуктуpу DEBUG_EVENT по требованию клиента заполнит система. Она сбрасывает в неё технические детали отладочного события:
3. Изменить принадлежащую клиенту память;
На данном этапе клиент заморожен и у отладчика появляется возможность вывести на экран любые сведения о нём, типа состояние регистров через GetThreadContext(), дизассемблерный листинг кода и прочую информацию. Клиент будет спать до тех пор, пока отладчик не снимет ранее установленный бряк, сдвинув его на следующую инструкцию. Здесь-то код нашего приложения (аля клиент) и должен будет взять бразды правления в свои руки, самостоятельно восстановив исходный байт, вместо бряка INT-3. В результате, процессор не обнаружив никаких точек-останова начнёт выполнять все последующие инструкции, пока не встретит терминальную функцию ExitProcess(). При иных (благоприятных для отладчика) обстоятельствах он продолжит свой алгоритм.
4. Даёт возможность клиенту сделать очередной шаг;
Завершив обработку некоторого события, отладчик вызывает ContinueDebugEvent(), тем-самым нарушив крепкий сон клиента, и цикл повторится вновь.
Нужно отметить, что перечисленные функции отладочного API могут быть вызваны только тем потоком, который при помощи CreateProcess() создавал процесс с флагом отладки DEBUG. Эта информация хранится в дескрипторе отладчика, куда её заносит системный протекторат. Таким образом, скелет типичного дебаггера может выглядеть примерно так:
2. Основная идея
В предыдущих 4-х пунктах была рассмотрена последовательность действий отладчика, когда он уже подцепил к себе клиента. Но в контексте данной темы нас будет больше интересовать то, что происходит за кулисами апартаментов отладчика (на подготовительном этапе загрузки), когда система только планирует его контекст.
Сначала процесс клиента загружается системой в выделенную ему память. Отладчик отслеживает этот момент и взяв из PE-заголовка клиента точку-входа в него (EntryPoint), устанавливает по этому адресу свою/программную точку-остановка INT-3. После этого, отладчик опять возвращает руль загрузчику образов Win, который посредством функции KiUserApcDispatcher() создает первичный поток клиента, и начинает подгружать в его память статически прилинкованные к нему библиотеки DLL. Эти шаги регистрируются в логе отладчика x64Dbg, а вот Оля здесь филонит, пытаясь скрыть от нас реальное положение дел:
Здесь виндо, что отладчики устанавливают первый свой бряк до загрузки всех библиотек в память, а значит из процедуры инициализации этих библиотек DllEntryPoint() у нас появляется возможность отправить данный Breakpoint к праотцам, вернув оригинальный байт программы на своё почётное место. В этом случае, после того-как отработает лоадер системных DLL, отладчик уже не остановится на точке-входа в отлаживаемую программу, и в обычном режиме со-скоростью реактивного гепарда, процессор пойдёт топтать весь программный наш код, от его начала и до самого конца (если по-ходу не встретит диалоговых окон или процедур взаимодействия с пользователем). Отладчику остаётся лишь с тоскою наблюдать за мимо пролетающими инструкциями.
Выходит чтобы осуществить наш план, достаточно прицепить к своему EXE какую-нибудь DLL (на рис.выше это myDll.dll), на входе в которую сбросить к чертям системную точку-останова
Посмотрим на прототип функции DllEntryPoint(), которая по-факту является точкой-входа в любую библиотеку. На выходе из этой функции, мы обязательно должны возвратить системному лоадеру в регистре
Значит имеем три аргумента функции и все их через стек передаёт в нашу библиотеку системный загрузчик образов. Обратите внимание на причину вызова DLL_PROCESS_ATTACH – этот флаг наша либа может получить только в трёх случаях: (1) в момент запуска EXE на исполнение и инициализации DLL, (2) первого обращения к экспортируемым ею функциям, (3) в результате динамического вызова библиотеки посредством LoadLibrary(). Если происходит повторный запуск какой-нибудь функции из DLL, данный флаг не выставляется, т.к. загрузчик берёт инфу уже из информационной структуры РЕВ. Аргумент "lpReserved" указывает на способ загрузки библиотеки – статический (вместе с приложением) или динамический (в произвольный момент, по требованию). Остальные флаги сейчас нам абсолютно не интересны.
3. Практика. Сброс программной точки-останова INT-3
Собрав во-едино всё выше/сказанное можно сделать вывод, что отладчик вмешивается в наш код и вставляет прерывание INT-3 везде, где ему нужно остановиться. При этом оригинальный байт он запоминает, а на его место записывает опкод данного прерывания =
Значит алго нашей библиотеки DLL должен быть примерно таким:
Обязательным условием является здесь то, что эту библиотеку нужно сохранить только под именем myDll.dll, поскольку в её секции-экспорта '.edata' указано именно это имя. Ничто не мешает вам задать любое/другое, лишь-бы они совпадали.
Теперь осталось написать саму программу, которая будет статически подгружать в своё пространство эту либу. Чтобы от кода была хоть какая-то практическая польза продемонстрируем, как можно вытащить базы всех загружаемых системных DLL не обращаясь к Win32-API (это было-бы слишком просто), а прямо из структуры окружения "Process-Environment-Block", или просто РЕВ.
Если в двух словах, то на 32-битных системах в сегментном регистре
Формат структуры такой, что в ней хранятся целых три вложенные структуры, но нам нужен только порядок очерёдности загрузки dll'ок именно в память. Поэтому берём указатель по смещению
Вот пример EXE-файла, который тянет за собой нашу либу (она делает всю/черновую работу), а что-бы не бездействовать – сам выводит на консоль базовые адреса постоянно присутствующих в каждом файле библиотек:
Если всё сделано правильно, то жмякнув в окне компилятора fasm клавишу F9, на выходе получаем исправно функционирующий файл. Однако вскормив его отладчику обнаруживаем, что отладчик не только не успел дизассемблировать ни одного тушканчика кода и вывести его листинг в левое окно, у него даже не оказалось времени сформировать стек.
На рис.ниже результат где видно, что код в принципе отработал до конца, однако это вовсе не заслуга отладчика, а скажем мягко – наоборот. Оля так вообще сразу капитулировала, честно отобразив буквально во-всех своих окнах молоко. Как отлаживать такой файл?
4. Заключение
Ясно, что этот код не претендует ни на что, т.к. вся его подноготная лежит на поверхности и с нею можно ознакомиться не только в отладчике, но и в любом дизассемблере. Но дизассму тоже можно противостоять (как-правило шифрованием тушки) и это совсем другая история. Целью данного повествования было продемонстрировать именно беспомощность всех современных инструментов отладки к подобного рода сносу их бряков. Если и покрутить в настройках свойствами Breakpoint'ов, это всё-равно не даст желаемых результатов, поскольку неопытный реверсер попадёт в непроходимые джунгли непонятного кода и поспешит скорее на выход. А от бывалых взломщиков и защищаться бесполезно – вскроют одним пальцем.
Как обычно, в скрепке цепляю готовые исполняемые файлы, чтобы у заинтересованных была возможность опробовать технику на практике. Всем удачи, и до скорого!!!
Однако в работе отладчиков есть и механизмы, от которых разрабы не могут избавиться в принципе, т.к. они являются фундаментальными для данного рода программ – это точки-останова, или Breakpoint (кому как нравится). По сей день, ни один из отладчиков так и не научился противостоять сносу бряков, на что мы и сделаем ставку в этой статье. Общая идея такова, что если правильно выбрать "время и место", то факт восстановления ВР в прежнее состояние, позволит нашей программе выйти из-под контроля отладки, выполнив свой код на одном дыхании так, как-будто его запустили под реальным процессором. Авансом скажу, что это простой, элегантный и чертовски полезный баг, затрагивающий буквально все заточенные под Win32/64 отладчики.
Оглавление:
1. Принцип работы отладчиков;
2. Основная идея;
3. Практика. Сброс точки-останова INT-3;
4. Заключение.
---------------------------------------------------1. Принцип работы отладчиков
Практика – дело хорошее, но при внештатных ситуациях, пробелы в теории могут запросто выбить нас из седла. Поэтому для начала рассмотрим, какие высокие принципы заложены в работу отладчиков. По большому счёту, свой юзермодный дебаггер сможет написать даже программист средней руки, поскольку в системах класса Win всё уже предусмотрено для этого, в нёдрах библиотек Kernel32, Dbgeng и Dbghelp.dll. Эта группа функций известна под общим названием DebugAPI, оптом и в розницу предлагающие в наше распоряжение услуги следующего характера:
1. Загpузка или подключение к клиенту для его отладки;
Первое, что должен сделать отладчик, это функцией CreateProcess() создать отлаживаемый процесс. Причём в аргументе этой функции нужно обязательно указать флаг DEBUG_PROCESS, чтобы сообщить системе, что родительский процесс планирует войти в "цикл-отладки" для управления дочерним процессом. Все современные отладчики могут подключаться и к уже запущенным процессам из диспетчера-задач. В этом случае отладчик зовёт из либы Kernel32.dll функцию DebugActiveProcess().
Поскольку отладчик подключается к клиенту посредством CreateProcess(), то оба они будут исполняться в разных адресных пространствах. То-есть будем иметь два самостоятельных процесса, связанных невидимой нитью отладки. Благодаря этому, устойчивость Win к процессам дебага весьма высока, и не зависит от поведения клиента. Даже если отлаживаемый процесс начнёт беспорядочно записывать мусор в память, эта запись всё-равно не сможет привести к сбою отладчика, т.к. в данном случае процесс стреляет в ногу себе, а не родителю.
2. Отладчик принимает от клиента уведомления об отладочных событиях;
После того-как клиент принят на борт, отладчик запоминает байт с текущего его указателя EIP и меняет этот байт на точку-останова INT-3 (опкод 0xCC). Теперь отладчик входит в свой цикл, путём вызова функции WaitForDebugEvent() для приёма отладочных уведомлений от клиента. С этого момента, через системный "порт-отладки" клиент периодически будет посылать отладчику сообщения, о происходящих в его тушке событиях Event. Вот прототип функции ожидания:
C-подобный:
BOOL WaitForDebugEvent
lpDebugEvent ;// адpес стpуктуpы DEBUG_EVENT
dwMilliseconds ;// сколько ждать события (-1 = вечно, INFINITE)
Тут мы указываем только время ожидания (обычно -1), а стpуктуpу DEBUG_EVENT по требованию клиента заполнит система. Она сбрасывает в неё технические детали отладочного события:
C-подобный:
struct DEBUG_EVENT
dwDebugEventCode dd 0 ;// код события
dwProcessId dd 0 ;// Pid,
dwThreadId dd 0 ;// ..и Tid клиента.
u DEBUGSTRUCT ;// расширенные сведения о событии.
ends
;//------------------
DebugEventCode:
CREATE_PROCESS_DEBUG_EVENT ;// клиент создаёт пpоцесс.
EXIT_PROCESS_DEBUG_EVENT ;// соответственно закрывает его.
CREATE_THEAD_DEBUG_EVENT ;// аналогично для потоков..
EXIT_THREAD_DEBUG_EVENT ;// ^^^
LOAD_DLL_DEBUG_EVENT ;// ..и внешних модулей.
UNLOAD_DLL_DEBUG_EVENT ;// ^^^
EXCEPTION_DEBUG_EVENT ;// ВАЖНО!!! клиент сгенерил исключение Step/BreakPoint/etc..
OUTPUT_DEBUG_STRING_EVENT ;// клиент хочет послать строку.
RIP_EVENT ;// системная ошибка с посмертной надписью!
3. Изменить принадлежащую клиенту память;
На данном этапе клиент заморожен и у отладчика появляется возможность вывести на экран любые сведения о нём, типа состояние регистров через GetThreadContext(), дизассемблерный листинг кода и прочую информацию. Клиент будет спать до тех пор, пока отладчик не снимет ранее установленный бряк, сдвинув его на следующую инструкцию. Здесь-то код нашего приложения (аля клиент) и должен будет взять бразды правления в свои руки, самостоятельно восстановив исходный байт, вместо бряка INT-3. В результате, процессор не обнаружив никаких точек-останова начнёт выполнять все последующие инструкции, пока не встретит терминальную функцию ExitProcess(). При иных (благоприятных для отладчика) обстоятельствах он продолжит свой алгоритм.
4. Даёт возможность клиенту сделать очередной шаг;
Завершив обработку некоторого события, отладчик вызывает ContinueDebugEvent(), тем-самым нарушив крепкий сон клиента, и цикл повторится вновь.
Нужно отметить, что перечисленные функции отладочного API могут быть вызваны только тем потоком, который при помощи CreateProcess() создавал процесс с флагом отладки DEBUG. Эта информация хранится в дескрипторе отладчика, куда её заносит системный протекторат. Таким образом, скелет типичного дебаггера может выглядеть примерно так:
C-подобный:
;// Выбираем процесс для отладки
start: nop
invoke CreateProcess,...,DEBUG_PROCESS,...
;// Цикл отладки! Ставим время на макс и ждём сообщения..
;// Если пришла sms'ка "конец программы", то выходим из цикла.
@trace: nop
invoke WaitForDebugEvent,..-1,..
cmp DebugEventCode,[EXIT_PROCESS_DEBUG_EVENT]
je @StopTrace
;//---- здесь обрабатываем событие отладки -------------
invoke GetThreadContext,..
cinvoke printf,...
;// Даём добро клиенту на продолжение, и уходим на начало цикла
invoke ContinueDebugEvent,...
jmp @trace
@StopTrace:
nop
2. Основная идея
В предыдущих 4-х пунктах была рассмотрена последовательность действий отладчика, когда он уже подцепил к себе клиента. Но в контексте данной темы нас будет больше интересовать то, что происходит за кулисами апартаментов отладчика (на подготовительном этапе загрузки), когда система только планирует его контекст.
Сначала процесс клиента загружается системой в выделенную ему память. Отладчик отслеживает этот момент и взяв из PE-заголовка клиента точку-входа в него (EntryPoint), устанавливает по этому адресу свою/программную точку-остановка INT-3. После этого, отладчик опять возвращает руль загрузчику образов Win, который посредством функции KiUserApcDispatcher() создает первичный поток клиента, и начинает подгружать в его память статически прилинкованные к нему библиотеки DLL. Эти шаги регистрируются в логе отладчика x64Dbg, а вот Оля здесь филонит, пытаясь скрыть от нас реальное положение дел:
Здесь виндо, что отладчики устанавливают первый свой бряк до загрузки всех библиотек в память, а значит из процедуры инициализации этих библиотек DllEntryPoint() у нас появляется возможность отправить данный Breakpoint к праотцам, вернув оригинальный байт программы на своё почётное место. В этом случае, после того-как отработает лоадер системных DLL, отладчик уже не остановится на точке-входа в отлаживаемую программу, и в обычном режиме со-скоростью реактивного гепарда, процессор пойдёт топтать весь программный наш код, от его начала и до самого конца (если по-ходу не встретит диалоговых окон или процедур взаимодействия с пользователем). Отладчику остаётся лишь с тоскою наблюдать за мимо пролетающими инструкциями.
Выходит чтобы осуществить наш план, достаточно прицепить к своему EXE какую-нибудь DLL (на рис.выше это myDll.dll), на входе в которую сбросить к чертям системную точку-останова
Breakpoint at 00402000 set!
, чтобы оригинальные байтики программы смотрелись рядом гармонично, без всяких бряков и прочего мусора. Дело в том, что на этапе загрузки образа в память, системный загрузчик даёт команду "рассчитаться на первый/второй" всем библиотекам, которые перечислены в секции-импорта приложения, и каждая из них должна отрапортовать.Посмотрим на прототип функции DllEntryPoint(), которая по-факту является точкой-входа в любую библиотеку. На выходе из этой функции, мы обязательно должны возвратить системному лоадеру в регистре
EAX
значение true=1
если инициализация прошла успешно, или-же false=0
если что-то пошло не так (зависит от нашего настроения). В последнем случае, система выстрелит в юзера модальным окном следующего содержания (см.сишный хидер ntstatus.h), после чего тупо прихлопнет приложение. Например можно внутри DllEntryPoint() искать отладчик и обнаружив таковой, возвратить загрузчику EAX=0
:
C-подобный:
DllEntryPoint()
hinstDLL ;// VA-база DLL в памяти
fdwReason ;// одна из 4-х причин вызова точки-входа в DLL
lpvReserved ;// статическое(1) или динамическое(0) подключение
;//---------------------
fdwReason:
DLL_PROCESS_ATTACH = 1 ;// DLL подключается к процессу при его запуске
DLL_PROCESS_DETACH = 0 ;// DLL удаляется из пространства процесса
DLL_THREAD_ATTACH = 2 ;// Процесс создаёт новый поток (на основной не распространяется)
DLL_THREAD_DETACH = 3 ;// Поток отработал своё и завершается
Значит имеем три аргумента функции и все их через стек передаёт в нашу библиотеку системный загрузчик образов. Обратите внимание на причину вызова DLL_PROCESS_ATTACH – этот флаг наша либа может получить только в трёх случаях: (1) в момент запуска EXE на исполнение и инициализации DLL, (2) первого обращения к экспортируемым ею функциям, (3) в результате динамического вызова библиотеки посредством LoadLibrary(). Если происходит повторный запуск какой-нибудь функции из DLL, данный флаг не выставляется, т.к. загрузчик берёт инфу уже из информационной структуры РЕВ. Аргумент "lpReserved" указывает на способ загрузки библиотеки – статический (вместе с приложением) или динамический (в произвольный момент, по требованию). Остальные флаги сейчас нам абсолютно не интересны.
3. Практика. Сброс программной точки-останова INT-3
Собрав во-едино всё выше/сказанное можно сделать вывод, что отладчик вмешивается в наш код и вставляет прерывание INT-3 везде, где ему нужно остановиться. При этом оригинальный байт он запоминает, а на его место записывает опкод данного прерывания =
0хСС
. При первой загрузки отлаживаемого образа в память, отладчик перезаписывает байт, который в аккурат попадает на "EntryPoint" в программу (см.рис.1). Поскольку приложение пишем мы сами, то можно предварительно вставить туда заранее известный нам опкод.. например 90h
соответствующий инструкции NOP
(No-Operation, ничего не делать). Мы подсунем эту "утку" отладчику, а позже сами восстановим его. Значит алго нашей библиотеки DLL должен быть примерно таким:
1. Внутри точки-входа в DLL проверить флаг на ATTACH и если гуд, то приступить к поиску адреса "EntryPoint" в приложение. Для этого, сначала находим базу образа памяти, по смещению
3Ch
от которой будет лежать указатель на РЕ-заголовок. Далее, сместившись на РЕ+28h
получаем точку-входа в программу.
2. У любого приложения секция-кода всегда должна быть защищена от записи (привет самомодификация), иначе аверы могут поднять шум. Поскольку нам нужно будет перезаписывать байт у точки-входа в код, значит зовём функцию VirtualProtect() и всю защиту от записи как турбиной сдувает. Теперь имеем доступ на R/W, хотя в мерах предосторожности разумно будет по окончании махинаций обратно снять атрибут Write.
3. Пользуясь моментом, быстро восстанавливаем свой
NOP
, отправляя в топку Breakpoint отладчика. На заключительном этапе, рапортуем системному загрузчику образов EAX=1
давая ему знать, что "в Багдаде всё спокойно" и приложение можно смело стартовать. Вот пример такой библиотеки:
C-подобный:
;//------------- FASM-code ----------------------
;// Библиотека DLL,
;// с фиксированной базой 10000000h (без релоков)
;//----------------------------------------------
format pe console dll at 0x10000000
include 'win32ax.inc'
entry DllEntryPoint ;//<--- определяем точку-входа
;//---------
.data
old dd 0
buff db 0
;//---------
.code
proc DllEntryPoint, hInstance, fdReason, LpReserved
cmp [fdReason],DLL_PROCESS_ATTACH ;// проверка флага на ATTACH
jne @f ;// пропустить если другой..
;//--- Иначе: получить полное имя EXE-файла
;//--- и найти его базу в памяти (он-же дескриптор Handle)
pusha
invoke GetModuleFileName,0,buff,128
invoke GetModuleHandle,buff
xchg esi,eax ;// ESI = база
;//--- Вычисляем точку-входа в EXE
mov ebx,[esi+0x3C] ;//
add ebx,esi ;// указатель на РЕ-заголовок
mov ebx,[ebx+0x28] ;//
add ebx,esi ;// EBX = точка-входа в EXE
;//--- Открываем секции-кода доступ на запись!
;//--- EBX = начало страницы памяти, 4096 её размер,
;//--- PAGE_WRITE = новый атрибут, в "old" сбросится старый атрибут.
push ebx
invoke VirtualProtect,ebx,4096,PAGE_EXECUTE_READWRITE,old
pop ebx
;//--- Внимание!!!
;//--- Перезаписываем "Breakpoint" отладчика,
;//--- восстанавливая свой NOP на место (опкод 90h)
mov byte[ebx],0x90
;//--- Дело сделано.
;//--- Восстанавливаем прежние атрибуты секции-кода
invoke VirtualProtect,ebx,4096,[old],old
popa
@@: mov eax,1 ;// TRUE = OK (системному лоадеру)
ret
endp
;//<<---<<---- КОНЕЦ ТОЧКИ-ВХОДА В БИБЛИОТЕКУ --->>--->>----//
;//------------------------
;// DLL должна экспортировать мин одну функцию,
;// иначе она не загрузится в память процесса.
;//------------------------
proc About
pusha
cinvoke printf,\
<10,' *bpCrackme* ver.0.1',\
10,' ------------------------',\
10,' Full patch....: %s',10,0>,buff
popa
ret
endp
;//---------
section '.edata' export data readable
export 'myDll.dll',About,'About'
;//---------
section '.idata' import data readable writeable
library msvcrt,'msvcrt.dll',kernel32,'kernel32.dll'
import msvcrt, printf,'printf'
include 'api\kernel32.inc'
Обязательным условием является здесь то, что эту библиотеку нужно сохранить только под именем myDll.dll, поскольку в её секции-экспорта '.edata' указано именно это имя. Ничто не мешает вам задать любое/другое, лишь-бы они совпадали.
Теперь осталось написать саму программу, которая будет статически подгружать в своё пространство эту либу. Чтобы от кода была хоть какая-то практическая польза продемонстрируем, как можно вытащить базы всех загружаемых системных DLL не обращаясь к Win32-API (это было-бы слишком просто), а прямо из структуры окружения "Process-Environment-Block", или просто РЕВ.
Если в двух словах, то на 32-битных системах в сегментном регистре
FS
лежит указатель на структуру ТЕВ (на х64 это GS), а сместившись внутри неё на 30h
найдём адрес структуры РЕВ. В этом клондайке зарыто много интересного, в том числе и адрес ещё одной, не менее интересной структуры под названием "PEB_LDR_DATA". Как видно по корню LDR, информация принадлежит как-раз лоадеру системных библиотек. Формат структуры такой, что в ней хранятся целых три вложенные структуры, но нам нужен только порядок очерёдности загрузки dll'ок именно в память. Поэтому берём указатель по смещению
14h
внутри PEB_LDR_DATA, и натыкаемся на таблицу "InMemoryOrderModuleList". Каждая запись в этой таблице имеет размер 10h
, поэтому перемещаясь с таким шагом, получаем полный паспорт системных модулей.Вот пример EXE-файла, который тянет за собой нашу либу (она делает всю/черновую работу), а что-бы не бездействовать – сам выводит на консоль базовые адреса постоянно присутствующих в каждом файле библиотек:
C-подобный:
format pe console
include 'win32ax.inc'
entry start
;//--------------
.data
buff db 0
;//--------------
.code
start: nop ;//<----- байт, куда отладчик поспешит поставить свой "Breakpoint",
nop ;// а наша DLL его по-полной обломает.
invoke About ;// зовём функцию из своей DLL,
;// иначе она не будет загружена в память.
;//--- Поиск и перечисление все баз из структуры РЕВ
;//-------------------------------------------------
mov esi,[ fs:30h] ;// ESI = указатель на PEB
mov esi,[esi+0Ch] ;//<-------- структура PEB_LDR_DATA
mov esi,[esi+14h] ;//<-------- InMemoryOrderModuleList
mov eax,[esi+10h] ;// EAX = своя база
mov esi,[esi] ;// связной список LIST_ENTRY
mov ebx,[esi+10h] ;// EBX = база Ntdll.dll
mov esi,[esi] ;//
mov ecx,[esi+10h] ;// ECX = база Kernel32.dll
mov esi,[esi] ;//
mov edx,[esi+10h] ;// EDX = база KernelBase.dll (Win7+)
mov esi,[esi] ;//
mov ebp,[esi+10h] ;// EBP = база msvcrt.dll
cinvoke printf,<10,' ProcBase......: %08X',\
10,' Ntdll.dll.....: %08X',\
10,' Kernel32.dll..: %08X',\
10,' KernelBase.dll: %08X',\
10,' Msvcrt.dll....: %08X',0>,eax,ebx,ecx,edx,ebp
cinvoke gets,buff
cinvoke exit,0
;//--------------------
section '.idata' import data readable writeable
library msvcrt,'msvcrt.dll',myDll,'myDll.dll'
import msvcrt, printf,'printf',gets,'gets',exit,'exit'
import myDll, About, 'About' ;//<---------- импорт fn.из своей DLL!!!
Если всё сделано правильно, то жмякнув в окне компилятора fasm клавишу F9, на выходе получаем исправно функционирующий файл. Однако вскормив его отладчику обнаруживаем, что отладчик не только не успел дизассемблировать ни одного тушканчика кода и вывести его листинг в левое окно, у него даже не оказалось времени сформировать стек.
На рис.ниже результат где видно, что код в принципе отработал до конца, однако это вовсе не заслуга отладчика, а скажем мягко – наоборот. Оля так вообще сразу капитулировала, честно отобразив буквально во-всех своих окнах молоко. Как отлаживать такой файл?
4. Заключение
Ясно, что этот код не претендует ни на что, т.к. вся его подноготная лежит на поверхности и с нею можно ознакомиться не только в отладчике, но и в любом дизассемблере. Но дизассму тоже можно противостоять (как-правило шифрованием тушки) и это совсем другая история. Целью данного повествования было продемонстрировать именно беспомощность всех современных инструментов отладки к подобного рода сносу их бряков. Если и покрутить в настройках свойствами Breakpoint'ов, это всё-равно не даст желаемых результатов, поскольку неопытный реверсер попадёт в непроходимые джунгли непонятного кода и поспешит скорее на выход. А от бывалых взломщиков и защищаться бесполезно – вскроют одним пальцем.
Как обычно, в скрепке цепляю готовые исполняемые файлы, чтобы у заинтересованных была возможность опробовать технику на практике. Всем удачи, и до скорого!!!
Вложения
Последнее редактирование: