Способы обхода отладчиков режима пользователя (крэкинг)
Введение.
В этой статье я рассмотрю способы одурачивания отладчиков режима пользователя. Я попытаюсь разложить по полочкам все известные мне методы и предложить читателю несколько своих. Что я понимаю под одурачиванием? Во-первых, это обнаружение отладчика в своем процессе, обход функций, перебирающих модули, защита от трассировки и еще несколько примечательных вещей. Во-вторых, это несколько радикальных и не очень методов против дампа. В данной статье я рассмотрю только отладчики режима пользователя, очень мало будет сказано про отладчики режима ядра. О них и так очень много написано (советую прочитать статью 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:
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 обработчику! Алгоритм наших действий прост! Смотрите код на ассемблере:
; #####################################################################
; ##################### 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
; #####################################################################
Если вы не поняли прошлый листинг, вот код на С:
#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.
BOOL WaitForDebugEvent(
LPDEBUG_EVENT lpDebugEvent, // address of structure for event information
DWORD dwMilliseconds // number of milliseconds to wait for event
);
Эта функция вызывается в отладчике и заставляет застыть поток в ожидании отладочного события. Она работает как и другие Wait* функции, поэтому в последнем параметре принимает время ожидания, вместо которого обычно стоит INFINITE, что значит ждать бесконечно долго. В первом параметре функция принимает указатель на структуру DEBUG_EVENT, вот как она выглядит:
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). Все же вот реализация этого метода:
Сначала, как полагается ассемблер:
; #####################################################################
; ##################### 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
; #####################################################################
Реализация кода на С не составит никаких проблем:
#include "stdafx.h"
#include <windows.h>
int APIENTRY WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow)
{
DWORD d = GetTickCount();
// Не хватило у меня фантазии на что-то большее :)
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 не будет работать вовсе.
Пока я занимался вопросом антитрассировки, я придумал универсальный метод обнаружения отладчика в процессе, который действует и на отладчики режима ядра. Смотрите код:
; #################################################################################################
; ############################ 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].
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 Шрайбер:
typedef struct _LIST_ENTRY
{
/*000*/ struct _LIST_ENTRY *Flink;
/*004*/ struct _LIST_ENTRY *Blink;
/*008*/ }
LIST_ENTRY;
Причем в Flink указывает на следующий элемент, а Blink на предыдущий. После 004 может находиться все что угодно, а в нашем случае это _LDR_DATA_TABLE_ENTRY:
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. Вот ее определение по Шрайберу:
typedef struct _UNICODE_STRING
{
/*000*/ USHORT Length;
/*002*/ USHORT MaximumLength;
/*004*/ PWSTR Buffer;
/*008*/ }
UNICODE_STRING;
Нас, естественно, интересует только Buffer, поэтому смещаться будем к 004.
Еще непонятки может вызвать способ удаления элемента из списка. Делается это так: в Flink’е прошлого элемента записываем Flink удаляемого элемента, соответственно в Blink следующего элемента записываем Blink удаляемого.
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 в случае, когда вы точно не уверены в том, что вы обрабатываете именно двусвязный список, и код будет обращаться к существующей странице памяти!
Далее следует код на ассемблере. Существует одна особенность, о которой я расскажу после кода…
; Удаляет модуль из списков... Параметр - КОРОТКОЕ (без пути) имя модуля
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 и смотреть, какая будет реакция у различных отладочных функций и программ. Далее следует код (без вышеописанных функций):
; ##################################################################
; ########################## 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 структуре. Немного подумав, я решил, что забавно было бы так же поменять базу модуля…
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 ; Подставляйте сюда, что хотите :)
mov dword ptr [ecx], 0 ; Подставляйте сюда, что хотите :)
Некоторые же дамперы могут читать вышеперечисленные параметры из заголовка PE, так что не помешает так же и там подправить значение 🙂
; Узнаем базу нашего модуля
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
В итоге, можно предложить такой код на С против дампа:
#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;.
[C] R4DX
Источник WASM.RU /07.05.2004/