Способы обхода отладчиков режима пользователя — Архив WASM.RU

Все статьи

Способы обхода отладчиков режима пользователя — Архив 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:

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, если бы не вы...

Ссылки.

  1. Уроки Iszelion'a
  2. Полное описание PEB
  3. Статья volodya'и "Об упаковщиках в последний раз"
  4. описание toolhelp функций
  5. описание NtQueryInformation
  6. Debug API

Статья была первоначально опубликована в журнале argc&argv.

2002-2013 (c) wasm.ru