Способы обхода отладчиков режима пользователя — Архив WASM.RU
Введение.
В этой статье я рассмотрю способы одурачивания отладчиков режима пользователя. Я попытаюсь разложить по полочкам все известные мне методы и предложить читателю несколько своих. Что я понимаю под одурачиванием? Во-первых, это обнаружение отладчика в своем процессе, обход функций, перебирающих модули, защита от трассировки и еще несколько примечательных вещей. Во-вторых, это несколько радикальных и не очень методов против дампа. В данной статье я рассмотрю только отладчики режима пользователя, очень мало будет сказано про отладчики режима ядра. О них и так очень много написано (советую прочитать статью Zero Ice'a в этом же номере журнала). Для того, чтобы читатель был в курсе откуда я взял всю информацию об устройстве отладчиков режима пользователя, я дам несколько ссылок в конце статьи. Там и описание msdn отладочных функций и туториал Izceliona "Win32 Debug API". Хочу еще заметить, что все примеры я тестировал и разрабатывал только для NT систем, хотя может оказаться, что примеры работают и в линейке 9x. Все предложения и замечания можно отсылать мне на e_f_i_m at rambler dot ru.
Обнаружение отладчика в процессе.
Во-первых, надо знать как отладчик попадает в процесс. Сделать это он может двумя методами: создать ваш процесс CreateProcess с флагом DEBUG_PROCESS или вызвать DebugActiveProcess, чтобы присоединиться к уже выполняемому процессу. В любом случае, он оставляет "следы" в вашем процессе, которыми пользуется функция IsDebuggerPresent. "Ну и в чем же проблема?" - спросит читатель - "Вызываем IsDebuggerPresent и знаем есть ли отладчик или нет!". Все сомнения в "липовости" метода исчезают, когда диссасемблируешь эту функцию. Вот буквально ее код, без исправлений и украшательств, каким его можно увидеть набрав в SoftIce'e под ХР (build 2600, без sp) u IsDebuggerPresent:
Код (Text):
mov eax, [fs:00000018] mov eax, [eax+30] movzx eax, byte ptr [eax+2] retВсе! И это механизм защиты от нежелательной отладки Microsoft??? Выглядит довольно забавно. Соответственно отладчику надо всего лишь подкорректировать нужное значение в PEB структуре и эта функция корректно работать не будет. Этим и занимаются различные плагины, например, к OllyDBG
Стоит пояснить, как используется регистр fs в 3м (в 0м этот регистр используется по-иному) кольце Windows'ом. Дело в том, что регистр fs указывает на Thread Environment Block, сокращенно TEB. Об этом очень много писали, например Шрайбер ("Недокументированные возможности Windows 2000" - неплохая книга…), плюс недавно volodya из HI-TECH в рассылке от wasm.ru рассеял пару неточностей. Стоит дать ссылку на полное описание этой структуры, тем более, что мы будем использовать ее дальше (все ссылки см. в конце статьи).
Что же делать? Как обнаружить отладчик в нашем процессе, если легальные методы недостаточно надежны? Надо определять присутствие отладчика по косвенным признакам. Что в нашем случае косвенный признак? Это состояние dwCreationFlags, которое передается, как параметр CreateProcess'у. Но дело в том, что нет никаких стандартных средств узнать состояние этого параметра. Более того, даже недокументированных методов я не нашел! Изучение структуры PEB ни к чему не привело. Структуры режима ядра я сразу отбросил, хотя потом покопавшись все равно ничего не нашел. Таким образом, методов узнать dwCreationFlags нет, этот метод не подойдет. Пришлось придумывать самопальные способы, что, конечно, не плюс. Все же я изобрел метод, хоть и корявый. Тем не менее, он работает, как часы во всех windows'ах. Совершенно случайно я наткнулся на ничем не примечательную функцию под скромным названием DebugBreak. Сначала я не уделил ей особого внимания, т.к. ее предназначение всего-то останавливать отладчик. Это что-то типа точки останова, которая ставиться отлаживаемым же процессом. Но когда я прочитал ее описание в SDK, я чуть не свалился со стула от радости, увидев таки лазейку, которая оставила нам микрософт! Я приведу здесь описание этой функции, как она есть в SDK:
The DebugBreak function causes a breakpoint exception to occur in the current process so that the calling thread can signal the debugger and force it to take some action. If the process is not being debugged, the search logic of a standard exception handler is used. In most cases, this causes the calling process to terminate because of an unhandled breakpoint exception.
VOID DebugBreak(VOID)
Parameters
This function has no parameters.
Return Values
This function does not return a value.
See Also
DebugActiveProcessОказывается, если нас не отлаживают, то управление передается seh обработчику! Алгоритм наших действий прост! Смотрите код на ассемблере:
Код (Text):
; ##################################################################### ; ##################### IS_THERE_DEBUGGEE? ########################## ; ##################################################################### ; ############################ About ################################### ; ##################################################################### ; Определяет под отладчиком ли мы режима пользователя. ; Компилятор - тестировал на MASM v8.0 ; Компилировать: вот фаил MakeIt.bat: ;--------------------------------------- Start of MakeIt.bat ---------------------------------- ; @echo off ; ; if exist 1.obj del 1.obj ; if exist 1.exe del 1.exe ; ; c:\masm32\bin\ml /c /coff /nologo 1.asm ; c:\masm32\bin\Link /SUBSYSTEM:WINDOWS /MERGE:.rdata=.text 1.obj ; ; dir 1.* ; ; pause ;---------------------------------------- End of MakeIt.bat ----------------------------------- ; #################################################################### ; ###################### Made by R4D][ ################################# ; #################################################################### ; ##################################################################### ; ############################ Directives ################################ ; ##################################################################### .386 .model flat, stdcall option casemap :none ; ##################################################################### ; ##################################################################### ; ############################# Includes and Libs ######################### ; ##################################################################### include c:\masm32\include\windows.inc include c:\masm32\include\kernel32.inc includelib c:\masm32\lib\user32.lib includelib c:\masm32\lib\kernel32.lib ; ##################################################################### ; ############################# CODE ################################# ; #################################################################### .code start: ; Устанавливаем новый seh обработчик assume fs: nothing push offset seh_handler push dword ptr fs:[0] mov fs:[0],esp ; Вызываем DebugBreak call DebugBreak WeUnderDebugger: ; Если DebugBreak не вызвал ошибок, то мы здесь => нас отлаживают!!! ; Выходим ;) jmp exit seh_handler: ; Если мы здесь, то все ОК и нас не отлаживают!!! mov esi, [esp+0ch] ; В esi указатель на CONTEXT assume esi: PTR CONTEXT ; Устанавливаем новый eip и выходим mov [esi].regEip, offset WeArentBeingDebugged xor eax, eax ret ; Здесь основная программа... WeArentBeingDebugged: nop nop nop exit: push 0 call ExitProcess end start ; #####################################################################Если вы не поняли прошлый листинг, вот код на С:
Код (Text):
#include "stdafx.h" #include <windows.h> int APIENTRY realWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { // Этот WinMain выполняется только если мы не под дебаггером! MessageBox(0,"Uhhhhhooooooo!!!",NULL,MB_OK); return 0; } int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { __try { DebugBreak(); } __except(EXCEPTION_EXECUTE_HANDLER) { realWinMain(hInstance,hPrevInstance,lpCmdLine,nCmdShow); } return 0; } Что же такое DebugBreak? А не что иное, как int 3 retТак что если хотите сохранить в программе несколько байт можете использовать int 3 вместо DebugBreak. Что произойдет в отладчике, когда мы выполним int 3? WaitForDebugEvent вернет dwDebugEventCode = EXCEPTION_DEBUG_EVENT и наша программа приостановиться... Таким образом, если вдруг производители отладчиков откажутся от использования EXCEPTION_DEBUG_EVENT, им придется отказаться от установки брэйкпоинтов, что несомненно затруднит процесс нежелательной отладки вашей программы.
Чтобы была полная картина происходящего надо дать хотя бы краткое описание функции WaitForDebugEvent.
Код (Text):
BOOL WaitForDebugEvent( LPDEBUG_EVENT lpDebugEvent, // address of structure for event information DWORD dwMilliseconds // number of milliseconds to wait for event );Эта функция вызывается в отладчике и заставляет застыть поток в ожидании отладочного события. Она работает как и другие Wait* функции, поэтому в последнем параметре принимает время ожидания, вместо которого обычно стоит INFINITE, что значит ждать бесконечно долго. В первом параметре функция принимает указатель на структуру DEBUG_EVENT, вот как она выглядит:
Код (Text):
typedef struct _DEBUG_EVENT { // de DWORD dwDebugEventCode; DWORD dwProcessId; DWORD dwThreadId; union { EXCEPTION_DEBUG_INFO Exception; CREATE_THREAD_DEBUG_INFO CreateThread; CREATE_PROCESS_DEBUG_INFO CreateProcessInfo; EXIT_THREAD_DEBUG_INFO ExitThread; EXIT_PROCESS_DEBUG_INFO ExitProcess; LOAD_DLL_DEBUG_INFO LoadDll; UNLOAD_DLL_DEBUG_INFO UnloadDll; OUTPUT_DEBUG_STRING_INFO DebugString; RIP_INFO RipInfo; } u; } DEBUG_EVENT;О том, что обозначает каждый из полей структуры, я говорить не буду, отправлю вас лучше к SDK.
Защита от трассировки. Сначала нужно разобраться, что такое трассировка и какие есть средства для ее осуществления. Трассировка - это пошаговое выполнение отлаживаемой программы. Отладчики режима пользователя (да и режима ядра) используют для решения данной задачи регистр eflags, а именно его бит tf, ответственный за трассировку. Как это выглядит? Если установлен вышеуказанный бит, то процессор ПЕРЕД каждой инструкцией генерирует исключение int 1, сбрасывает tf и приостанавливает поток. WaitForDebugEvent в отладчике же возвращает EXCEPTION_DEBUG_EVENT + EXCEPTION_SINGLE_STEP. Причем отладчик устанавливает бит eflags с помощью мощной функции SetThreadContext, которая позволяет в любом потоке изменить любой из регистров (ну, почти любой, gdtr и остальные с ним связанные, вам, конечно, не дадут изменить ). Тут решение просто напрашивается само собой: Проверять бит tf и если он установлен выходить. Но не тут-то было. Если внимательно посмотреть на этот абзац можно заметить некоторую особенность, из-за которой все провалиться. Так как исключение генерируется ДО выполнения инструкции и бит сбрасывается тогда же, то мы ничего не увидим в eflags. Я довольно долго думал над этой проблемой, пока мне в голову не пришла интересная идейка... Давайте мыслить логически: Если поток выполняется пошагово, то на каждый шаг тратиться гораздо больше времени, чем в обычных условиях, не так ли? Значит, чтобы понять, что нас трассируют надо просто замерить, сколько выполняется определенный, довольно маленький кусок кода и если это время больше секунды, то мы всяко под трассировкой. С другой стороны, это не факт, ибо кто-то в этот момент времени может нас просто приостановить (SuspendThread). Все же вот реализация этого метода:
Сначала, как полагается ассемблер:
Код (Text):
; ##################################################################### ; ##################### IS_THERE_TRACE? ############################## ; ##################################################################### ; ############################ About ################################### ; ##################################################################### ; Определяет трасируемся ли мы отладчиком режима пользователя. ; Компилятор - тестировал на MASM v8.0 ; Компилировать: вот фаил MakeIt.bat: ;--------------------------------------- Start of MakeIt.bat ---------------------------------- ; @echo off ; ; if exist 1.obj del 1.obj ; if exist 1.exe del 1.exe ; ; c:\masm32\bin\ml /c /coff /nologo 1.asm ; c:\masm32\bin\Link /SUBSYSTEM:WINDOWS /MERGE:.rdata=.text 1.obj ; ; dir 1.* ; ; pause ;---------------------------------------- End of MakeIt.bat ----------------------------------- ; #################################################################### ; ###################### Made by R4D][ ################################# ; #################################################################### ; ##################################################################### ; ############################ Directives ################################ ; ##################################################################### .386 .model flat, stdcall option casemap :none ; ##################################################################### ; ##################################################################### ; ############################# Includes and Libs ######################### ; ##################################################################### include c:\masm32\include\windows.inc include c:\masm32\include\user32.inc include c:\masm32\include\kernel32.inc includelib c:\masm32\lib\user32.lib includelib c:\masm32\lib\kernel32.lib ; ##################################################################### ; ##################################################################### ; ###################### DATA ######################################### ; ##################################################################### .data szMes db "Here u can write original code of da program ;)",0 ; ##################################################################### ; ##################################################################### ; ###################### CODE ######################################## ; #################################################################### .code start: call GetTickCount mov ebx, eax ; Здесь у нас 100 nop'ов (90h = опкод nop'a) db 100 dup(90h) call GetTickCount sub eax, ebx cmp eax, 1000 jge exit invoke MessageBox, 0, offset szMes, NULL, MB_OK exit: push 0 call ExitProcess end start ; #####################################################################Реализация кода на С не составит никаких проблем:
Код (Text):
#include "stdafx.h" #include <windows.h> int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { DWORD d = GetTickCount(); // Не хватило у меня фантазии на что-то большее <img src="styles/smiles_s/smile3.gif" class="mceSmilie" alt=":smile3:" title="Smile3 :smile3:"> GetLastError(); GetLastError(); GetLastError(); GetLastError(); GetLastError(); GetLastError(); GetLastError(); GetLastError(); GetLastError(); GetLastError(); GetLastError(); GetLastError(); GetLastError(); GetLastError(); GetLastError(); GetLastError(); DWORD d1 = GetTickCount()-d; if (d1>=1000) return 0; MessageBox(0,"Uhhhhoooo",NULL,MB_OK); return 0; }Тут должно быть все понятно. Чтобы увеличить надежность метода я советую вам вызывать это многократно на протяжении всей программы, чтоб не получилось так, что вы из-за прихоти windows (а вдруг появиться real-time поток, который все процессорное время сожрет?) вы накажите обыкновенного пользователя... В то же время не стоит "маячить" перед глазами взломщика. Идеально подошел бы неявный метод защиты, описанный у Криса Касперского, когда программа "знает", что нас отлаживают, но ничего радикального не предпринимает, просто неправильно выполняя свои функции... Так, например, можно отключить всю защиту и предоставить взломщику голый код программы, без защиты. Разработанный им crack не будет работать вовсе.
Пока я занимался вопросом антитрассировки, я придумал универсальный метод обнаружения отладчика в процессе, который действует и на отладчики режима ядра. Смотрите код:
Код (Text):
; ################################################################################################# ; ############################ KILL_ALL_DA_DEBUGGEES ########################################## ; ################################################################################################# ; ############################ About ############################################################### ; ################################################################################################# ; Универсальный метод обнаружения отладки. ; Компилятор - тестировалось на MASM v8.0 ; Компилировать: вот фаил MakeIt.bat: ;--------------------------------------- Start of MakeIt.bat ---------------------------------- ; @echo off ; ; if exist 1.obj del 1.obj ; if exist 1.exe del 1.exe ; ; c:\masm32\bin\ml /c /coff /nologo 1.asm ; c:\masm32\bin\Link /SUBSYSTEM:WINDOWS /MERGE:.rdata=.text 1.obj ; ; dir 1.* ; ; pause ;---------------------------------------- End of MakeIt.bat ----------------------------------- ; ################################################################################################# ; ############################### Made by R4D][ ################################################### ; ################################################################################################# ; ################################################################################################# ; ############################ Directives ######################################################### ; ################################################################################################# .386 .model flat, stdcall option casemap :none ; ################################################################################################# ; ################################################################################################# ; ############################ Includes and Libs ################################################## ; ################################################################################################# include c:\masm32\include\windows.inc include c:\masm32\include\kernel32.inc includelib c:\masm32\lib\user32.lib includelib c:\masm32\lib\kernel32.lib ; ################################################################################################# ; ################################################################################################# ; #################################### CODE ####################################################### ; ################################################################################################# .code start: ; Устанавливаем обработчик seh assume fs: nothing push offset seh_handler push dword ptr fs:[0] mov fs:[0],esp ; Устанавливаем флаг трассировки pushf pop eax or eax, 100h push eax popf jmp exit seh_handler: mov esi, [esp+0ch] assume esi: PTR CONTEXT mov [esi].regEip, offset WeWasntUnderDebuggee xor eax, eax ret WeWasntUnderDebuggee: nop nop nop nop exit: push 0 call ExitProcess end start ; #################################################################################################Как видите, просто устанавливаем флаг tf, далее перед следующей инструкцией возникнет #DB (int 1), которая в нормальном случае будет необработанным исключением. Если же мы под отладкой (all kind of debuggee ;)), то обработчик прерывания 1 установлен отладчиком и int 1 будет обработан. Обратите внимание, что это только если нас трассируют! В то же время, если нас просто прогоняли под отладчиком, не используя трассировку, то в момент после popf произойдет останов в отладчике (только режима пользователя, на SoftIce это не расспространяется, как дело обстоит с kd проверить не могу...), как будто нас до этого трассировали
Сокрытия модуля.
Допустим, вам надо укрыть свой модуль от "чужих" глаз. Для этого надо разобраться в том, как получают информацию о модулях такие приложения, как PETools. Обычно это делается с помощью снапшотов (snapshots) или с помощью функции NtQuerySystemInformation. В конце статьи вы можете найти ссылки на описание toolhelp функций и NtQueryInformation. При дисассемблирование, например, Module32FirstW бросается в глаза работа с регистром fs в режиме пользователя (происходит call 77e77604 (в ХР build 2600), где идет оперирование с указанным выше регистром). Я дам вам часть структуры PEB, которая нам понадобиться. А именно поле Ldr типа PPEB_LDR_DATA (Шрайбер ошибочно полагал, что там (смещение от PEB=00C) находиться ProcessModuleInfo типа PPROCESS_MODULE_INFO) в Process Environment Block'e (PEB), указатель на который (PEB) можно найти в fs:[30h] (он всегда равен 7ffdf000). Почему 30h? Потому что 0h указывает на TEB, а в TEB по смещению 0x30 находиться PEB. Поэтому и fs:[30h].
Код (Text):
struct _PEB_LDR_DATA { /*000*/ unsigned long Length; /*004*/ unsigned char Initialized; /*008*/ void* SsHandle; /*00C*/ struct _LIST_ENTRY InLoadOrderModuleList; /*014*/ struct _LIST_ENTRY InMemoryOrderModuleList; /*01C*/ struct _LIST_ENTRY InInitializationOrderModuleList; };Нас интересуют последние 3 поля, которые представляют собой входы в двусвязные списки (очень часто используются Windows). Вот как определил _LIST_ENTRY Шрайбер:
Код (Text):
typedef struct _LIST_ENTRY { /*000*/ struct _LIST_ENTRY *Flink; /*004*/ struct _LIST_ENTRY *Blink; /*008*/ } LIST_ENTRY;Причем в Flink указывает на следующий элемент, а Blink на предыдущий. После 004 может находиться все что угодно, а в нашем случае это _LDR_DATA_TABLE_ENTRY:
Код (Text):
struct _LDR_DATA_TABLE_ENTRY { /*000*/ struct _LIST_ENTRY InLoadOrderLinks; /*008*/ struct _LIST_ENTRY InMemoryOrderLinks; /*010*/ struct _LIST_ENTRY InInitializationOrderLinks; /*018*/ void* DllBase; /*01c*/ void* EntryPoint; /*020*/ unsigned long SizeOfImage; /*024*/ struct _UNICODE_STRING FullDllName; /*02c*/ struct _UNICODE_STRING BaseDllName; /*034*/ unsigned long Flags; /*038*/ unsigned short LoadCount; /*03a*/ unsigned short TlsIndex; /*03c*/ struct _LIST_ENTRY HashLinks; /*03c*/ void* SectionPointer; /*040*/ unsigned long CheckSum; /*044*/ unsigned long TimeDateStamp; /*044*/ void* LoadedImports; };Мы видим, что первые 3 элемента структуры - это _LIST_ENTRY, названия которых соответствуют названиям различных списков. Очевидно, что это не что иное, как указатели на те же самые структуры, но разных списков! А если в название списка совпадает с тем, с которым мы сейчас работаем, то это просто *Flink и *Blink ;). Все это просто очень сильно облегчает нам работу. Итак, определимся, что мы будем делать:
- переходим к Peb.Ldr - переходим к InLoadOrderModuleList (к примеру) - сохраняем начальный элемент списка - перечисляем InLoadOrderModuleList с помощью первых 8 байт, которые занимают *Flink и *Blink (определяем конец списка засчет того, что Flink последнего элемента указывает на начало списка, а мы его сохранили ) - сравниваем имена BaseDllName с нужным модулем - Если в прошлом шаге сравнение показало, что это нужная нам запись, то удаляем ее из всех списков, иначе продолжаем...
Как вы, наверное, заметили, для определения BaseDllName (ровно, как и FullDllName) используется структура UNICODE_STRING. Вот ее определение по Шрайберу:
Код (Text):
typedef struct _UNICODE_STRING { /*000*/ USHORT Length; /*002*/ USHORT MaximumLength; /*004*/ PWSTR Buffer; /*008*/ } UNICODE_STRING;Нас, естественно, интересует только Buffer, поэтому смещаться будем к 004.
Еще непонятки может вызвать способ удаления элемента из списка. Делается это так: в Flink'е прошлого элемента записываем Flink удаляемого элемента, соответственно в Blink следующего элемента записываем Blink удаляемого.
Код (Text):
DeleteListEntry proc USES eax ebx ListEntry: DWORD mov eax, ListEntry mov eax, [eax+4] mov ebx, [eax] ; Flink mov ebx, [ebx] mov dword ptr [eax], ebx mov ebx, [eax+4] ; Blink mov ebx, [ebx+4] mov dword ptr [eax+4], ebx ret DeleteListEntry endpЭто довольно небезопасный код, потому что не используется seh, не стоит использовать его as is в случае, когда вы точно не уверены в том, что вы обрабатываете именно двусвязный список, и код будет обращаться к существующей странице памяти!
Далее следует код на ассемблере. Существует одна особенность, о которой я расскажу после кода...
Код (Text):
; Удаляет модуль из списков... Параметр - КОРОТКОЕ (без пути) имя модуля DelModuleFromPEBNtA proc USES ecx ebx eax modname: PCHAR local pfirstmod : DWORD local modn[255] : DWORD ; Переводим ANSI строку в UNICODE invoke MultiByteToWideChar, CP_ACP, 0, modname, -1, addr modn, 255 assume fs: nothing mov eax, fs:[30h] ; Здесь находиться Peb mov eax, [eax+0Ch] ; Смещаемся к структуре Ldr mov eax, [eax+0Ch] ; Смотрим на список InLoadOrderModuleList ; Сохраняем адрес первого в списке модуля, чтобы не войти в бесконечный цикл (это же двусвязный список) mov pfirstmod, eax continue: mov ecx, [eax+30h] ; BaseDllName push eax invoke lstrcmpiW, ecx, addr modn test eax, eax pop eax je Found mov eax, [eax] ; Переходим к следующему элементу(Flink) cmp eax, pfirstmod jne continue ret Found: ; Собственно удаляем элемент из Ldr.InLoadOrderModuleList push eax call DeleteListEntry ; Смещаемся к Ldr.InMemoryOrderModuleList add eax, 8 ; И удаляем элемент оттуда ;) push eax call DeleteListEntry comment @ add eax, 8 push eax call DeleteListEntry @ ret DelModuleFromPEBNtA endpТеперь обещанная особенность: внимательный читатель, наверное, заметил, что удаление из списка InInitializationOrderModuleList закомментировано. Это потому что этот список, судя по всему, интенсивно используется системой... Я не знаю, для какой цели, но если, к примеру, удалить из списка главный модуль нашего приложения, то оно незамедлительно выгрузиться БЕЗ каких-либо ошибок... Если же удалить user32.dll, то у нас будут проблемы с MessageBox'ом, к примеру. Он вызываться-то будет и его код будет получать управление(то есть модуль не выгружается, как мог бы подумать проницательный читатель), но будет где-то в вызывать Access Violation... В тоже время удаление из этого списка ntdll.dll не вызовет никаких проблем... На самом деле это не проблема, ибо этот список не используется (по крайней мере я не разу не видел...) отладочными функциями для получения списка модулей.
Рассмотрим пример. В примере, я буду удалять модуль из user32.dll и смотреть, какая будет реакция у различных отладочных функций и программ. Далее следует код (без вышеописанных функций):
Код (Text):
; ################################################################## ; ########################## TEB Playing ############################# ; ################################################################## ; ############################ About ################################ ; ################################################################## ; Компилятор - тестировался на MASM v8.0 ; Компилировать: далее MakeIt.bat: ;--------------------------------------- Start of MakeIt.bat ---------------------------------- ; @echo off ; ; if exist 1.obj del 1.obj ; if exist 1.exe del 1.exe ; ; c:\masm32\bin\ml /c /coff /nologo 1.asm ; c:\masm32\bin\Link /SUBSYSTEM:WINDOWS /MERGE:.rdata=.text 1.obj ; ; dir 1.* ; ; pause ;---------------------------------------- End of MakeIt.bat ----------------------------------- ; ################################################################## ; ######################### Made by R4D][ ############################ ; ################################################################## ; ################################################################## ; ########################## Directives ############################### ; ################################################################## .386 .model flat, stdcall option casemap :none ; ################################################################## ; ################################################################## ; ######################## Includes and Libs ########################### ; ################################################################## include c:\masm32\include\windows.inc include c:\masm32\include\user32.inc include c:\masm32\include\kernel32.inc include c:\masm32\include\masm32.inc include c:\masm32\macros\macros.asm includelib c:\masm32\lib\user32.lib includelib c:\masm32\lib\kernel32.lib includelib c:\masm32\lib\masm32.lib ; ################################################################## ; ################################################################## ; ############################ DATA ################################ ; ################################################################## .data nmodname db "user32.dll",0 ; ################################################################## ; ################################################################## ; ############################ CODE ################################ ; ################################################################## .code start: main proc local SnpSht :DWORD local me :MODULEENTRY32 local IsItEnd :BOOL ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; Удаляем из списков user32.dll ;; push offset nmodname call DelModuleFromPEBNtA ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; Проверяем на снэпшоты ;;;;;;;;; invoke CreateToolhelp32Snapshot, TH32CS_SNAPMODULE, 0 mov SnpSht, eax mov me.dwSize, sizeof MODULEENTRY32 invoke Module32First, SnpSht, addr me mov IsItEnd, TRUE .while IsItEnd!=FALSE invoke MessageBox, 0, addr me.szModule, addr me.szModule, MB_OK invoke Module32Next, SnpSht, addr me mov IsItEnd, eax .endw invoke CloseHandle, SnpSht xor eax, eax invoke MessageBox, 0, SADD("Look now in PETools module list"), SADD("Check this"), MB_OK invoke ExitProcess,0 main endp end start ; ##################################################################Если вы откомпилируете и запустите эту программу, то увидите, что снапшоты не показывают модуль user32, что и немудрено. Чтобы проверить NtQuerySystemInformation, не надо ничего писать, ибо за нас все давно написали ребята, которые делали PETools. Вы можете удостовериться, что эта программа использует именно эту функцию, просто поставив breakpoint на NtQuerySystemInformation в SoftIce'e и пронаблюдать, как PETools ее вызывает. PETools так же не показывает скрываемый модуль. Можно сделать вывод, что NtQuerySystemInformation тоже берет информацию из PEB. Теперь плохие новости. Отладчики режима ядра располагают более достоверной информацией. Но это и не мудрено. Система должна ведь хранить список модулей еще и в структуре режима ядра (я к сожалению, ничего не знаю ни об этой структуре, ни о том, как SI ее получает, но я уверен, что google об этом знает все ), иначе это была бы даже не дыра, а просто убийство, ибо как иначе система следила бы за тем, какие модули выгружать из памяти при выгрузке процесса? Хранить столь важные сведения в структуре режима пользователя было бы опасно с точки зрения целостности всей системы. В то же время если бы эти сведения не дублировались в PEB, то было бы проблематично получение этой информации отладчикам режима пользователя... Поэтому данный метод не панацея от всех и вся.
Защита от дампа.
Дамп - это снимок памяти, который записывается на носитель. Дамп - это очень мощное оружие в умелых руках. Дамп помогает против зашифрованных программ, ибо "сфотографировать" память можно в любой момент, в то время, как перед тем как инструкция выполнится она должна быть расшифрована. У дампа очень много применений, но поговорим лучше, как не позволить отладчику (или специально-предназначенной для этого программе - дамперу) "сфотографировать" нас.
Очень интересный метод предложил volodya в статье "Об упаковщиках в последний раз". Этот метод заключается в изменении размера образа, таким образом, чтобы дампер обращался к несуществующим страницам памяти. В этом случае дампер "рухнет". Я же предлагаю менее радикальный метод, подстановки 0 на место того места, где должен быть размер образа. Этот метод плох тем, что этот 0 сильно бросается в глаза... Тем не менее, вы можете подставить вместо нуля, что угодно. Например, вы можете хранить все самые важные подпрограммы в конце вашего модуля, а вместо размера модуля подставлять размер_вашего_ модуля - размер_подпрограмм. Информация о размере модуля храниться в PEB структуре. Немного подумав, я решил, что забавно было бы так же поменять базу модуля...
Код (Text):
assume fs: nothing mov eax, fs:[30h] ; PEB mov eax, [eax+0Ch] ; PEB_LDR_DATA mov eax, [eax+0Ch] ; Ldr.InLoadOrderModuleList.Flink lea ebx, [eax+20h] ; LDR_DATA_TABLE_ENTRY.SizeOfImage lea ecx, [eax+18h] ; LDR_DATA_TABLE_ENTRY.DllBase mov dword ptr [ebx], 0 ; Подставляйте сюда, что хотите <img src="styles/smiles_s/smile3.gif" class="mceSmilie" alt=":smile3:" title="Smile3 :smile3:"> mov dword ptr [ecx], 0 ; Подставляйте сюда, что хотите <img src="styles/smiles_s/smile3.gif" class="mceSmilie" alt=":smile3:" title="Smile3 :smile3:">Некоторые же дамперы могут читать вышеперечисленные параметры из заголовка PE, так что не помешает так же и там подправить значение
Код (Text):
; Узнаем базу нашего модуля push 0 call GetModuleHandle mov ebx, eax ; Там, как известно находиться DOS заглушка assume eax: PTR IMAGE_DOS_HEADER ; В которой смещение на pe заголовок mov eax, [eax].e_lfanew add eax, ebx assume eax: PTR IMAGE_NT_HEADERS32 ; В котором (а именно в OptionalHeader'e) и храниться размер и база модуля lea ebx, [eax].OptionalHeader.SizeOfImage lea ecx, [eax].OptionalHeader.ImageBase push ecx push ebx ; На всякий слуяай изменяем атрибуты страниц в этом районе на RW invoke VirtualProtect, ebx, 2, PAGE_READWRITE, offset dOld pop ebx pop ecx ; Пишите сюда, что хотите ;) mov dword ptr [ebx], 0 mov dword ptr [ecx], 0В итоге, можно предложить такой код на С против дампа:
Код (Text):
#include "stdafx.h" #include <windows.h> void AntiDump(DWORD dwNewImageSize, DWORD dwNewBase) { // Без ассемблера не обойтись... // Доступ к регистру fs можно получить // только в ассемблерной вставке... // Тем не менее, можно было бы добавить мегабайты // описаний структур PEB и сократить ассемблерную // вставку до 1 строчки, но я предпочел обойтись // "жестко" забитыми смещениями... // Описание PEB и связанных с ней структур // смотрите в одной из ссылок в конце статьи. __asm { mov eax, fs:[30h] mov eax, [eax+0Ch] mov eax, [eax+0Ch] lea ebx, [eax+20h] lea ecx, [eax+18h] mov eax, dwNewImageSize mov dword ptr [ebx], eax mov eax, dwNewBase mov dword ptr [ecx], eax }; PIMAGE_DOS_HEADER pDH = (PIMAGE_DOS_HEADER)GetModuleHandle(NULL); PIMAGE_NT_HEADERS32 pINH = (PIMAGE_NT_HEADERS32)((*pDH).e_lfanew+(DWORD)GetModuleHandle(NULL)); DWORD pOp; VirtualProtect(&((*pINH).OptionalHeader.SizeOfImage),4, PAGE_EXECUTE_READWRITE,&pOp); (*pINH).OptionalHeader.SizeOfImage = dwNewImageSize; (*pINH).OptionalHeader.ImageBase = dwNewBase; } int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { AntiDump(0,0); MessageBox(0,"See da result in dumper ;)",NULL,MB_OK); return 0; }Немного подумав над проблемой дампа, я пришел к выводу, что можно неплохо было бы поместить внутри кода некоторые "дыры", у которых атрибуты страниц будут PAGE_NOACCESS. Мы не будем обращаться к этому коду, а вот программа, делающая дамп обязательно обратиться и если в ней нет проверки на атрибуты страниц - потерпит крах. НО этот метод защиты крайне неэффективный по причине того, что опытный взломщик составит карту памяти (OllyDbg, например, предоставляет такие услуги), и увидев наши дыры просто обойдет их, сделав дамп вашей программы по частям. К тому же довольно сложно рассчитать границы страниц, так чтобы мы "попали" VirtualProtect'oм именно так, что атрибуты изменились только у одной страницы-дыры. А если прибавить сюда неэффективность подобного метода (целую страницу (4Кб) на защиту только в одном месте!), то становиться ясно, что этот метод не подойдет нам.
Что еще можно предложить из методов обхода дампа? Можно, например, изменять базу нашего модуля на базу какой-нибудь другой dllки. А можно и вовсе назвать его ntoskrnl.exe и подставить базу и размер того же модуля. Представляю лицо взломщика, когда он увидит подобную картину . В общем-то, все это дело фантазии, как и все о чем я написал в этой статье.
Хочу сказать спасибо Zero Ice'у, который поддерживал меня на протяжении написания всей статьи. Так же он дисассемблировал DebugBreak, показав, что это не что иное как int 3.
Так же хочу поблагодарить рассылку от wasm.ru. Как видите, ваш материал помогает и, я бы врят ли обратил столь пристальное внимание на такой кладезь знаний, как PEB, если бы не вы...
Ссылки.
- Уроки Iszelion'a
- Полное описание PEB
- Статья volodya'и "Об упаковщиках в последний раз"
- описание toolhelp функций
- описание NtQueryInformation
- Debug API
Статья была первоначально опубликована в журнале argc&argv. © R4DX
Способы обхода отладчиков режима пользователя
Дата публикации 8 май 2004