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

Дата публикации 8 май 2004

Способы обхода отладчиков режима пользователя — Архив 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):
  1.  
  2. mov eax, [fs:00000018] 
  3. mov eax, [eax+30]
  4. movzx eax, byte ptr [eax+2]
  5. 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):
  1.  
  2. ; #####################################################################
  3. ; ##################### IS_THERE_DEBUGGEE? ##########################
  4. ; #####################################################################
  5. ; ############################ About ###################################
  6. ; #####################################################################
  7. ; Определяет под отладчиком ли мы режима пользователя.
  8. ; Компилятор -  тестировал на MASM v8.0                    
  9. ; Компилировать: вот фаил MakeIt.bat:
  10. ;--------------------------------------- Start of MakeIt.bat ----------------------------------
  11. ; @echo off
  12. ;
  13. ; if exist 1.obj del 1.obj
  14. ; if exist 1.exe del 1.exe
  15. ;
  16. ; c:\masm32\bin\ml /c /coff /nologo 1.asm
  17. ; c:\masm32\bin\Link /SUBSYSTEM:WINDOWS /MERGE:.rdata=.text 1.obj
  18. ;
  19. ; dir 1.*
  20. ;
  21. ; pause
  22. ;---------------------------------------- End of MakeIt.bat -----------------------------------
  23. ; ####################################################################
  24. ; ###################### Made by R4D][ #################################
  25. ; ####################################################################
  26.  
  27. ; #####################################################################
  28. ; ############################ Directives ################################
  29. ; #####################################################################
  30.     .386                                
  31.     .model flat, stdcall              
  32.     option casemap :none                
  33. ; #####################################################################
  34.  
  35. ; #####################################################################
  36. ; ############################# Includes and Libs #########################
  37. ; #####################################################################
  38.       include c:\masm32\include\windows.inc
  39.       include c:\masm32\include\kernel32.inc
  40.  
  41.       includelib c:\masm32\lib\user32.lib
  42.       includelib c:\masm32\lib\kernel32.lib        
  43. ; #####################################################################
  44. ; ############################# CODE #################################
  45. ; ####################################################################
  46. .code
  47. start:        
  48. ; Устанавливаем новый seh обработчик
  49.     assume fs: nothing                    
  50.     push offset seh_handler
  51.     push dword ptr fs:[0]  
  52.     mov fs:[0],esp
  53. ; Вызываем DebugBreak
  54.     call DebugBreak
  55. WeUnderDebugger:
  56. ; Если DebugBreak не вызвал ошибок, то мы здесь => нас отлаживают!!!
  57. ; Выходим ;)
  58.     jmp exit
  59. seh_handler:
  60. ; Если мы здесь, то все ОК и нас не отлаживают!!!
  61.     mov esi, [esp+0ch]      ; В esi указатель на CONTEXT
  62.     assume esi: PTR CONTEXT
  63. ; Устанавливаем новый eip и выходим
  64.     mov [esi].regEip, offset WeArentBeingDebugged
  65.     xor eax, eax
  66.     ret
  67. ; Здесь основная программа...
  68. WeArentBeingDebugged:  
  69.     nop
  70. nop
  71. nop
  72. exit:
  73.     push 0
  74.     call ExitProcess
  75. end start
  76. ; #####################################################################

Если вы не поняли прошлый листинг, вот код на С:

Код (Text):
  1.  
  2. #include "stdafx.h"
  3. #include <windows.h>
  4.  
  5. int APIENTRY realWinMain(HINSTANCE hInstance,
  6.                      HINSTANCE hPrevInstance,
  7.                      LPSTR     lpCmdLine,
  8.                      int       nCmdShow)
  9. {
  10. // Этот WinMain выполняется только если мы не под дебаггером!
  11.     MessageBox(0,"Uhhhhhooooooo!!!",NULL,MB_OK);
  12.     return 0;
  13. }
  14.  
  15. int APIENTRY WinMain(HINSTANCE hInstance,
  16.                      HINSTANCE hPrevInstance,
  17.                      LPSTR     lpCmdLine,
  18.                      int       nCmdShow)
  19. {
  20.     __try {
  21.         DebugBreak();
  22.     }
  23.     __except(EXCEPTION_EXECUTE_HANDLER) {
  24.         realWinMain(hInstance,hPrevInstance,lpCmdLine,nCmdShow);
  25.     }
  26.     return 0;
  27. }
  28.  
  29.   Что же такое DebugBreak? А не что иное, как
  30.  
  31. int 3
  32. ret

:smile3: Так что если хотите сохранить в программе несколько байт можете использовать int 3 вместо DebugBreak. Что произойдет в отладчике, когда мы выполним int 3? WaitForDebugEvent вернет dwDebugEventCode = EXCEPTION_DEBUG_EVENT и наша программа приостановиться... Таким образом, если вдруг производители отладчиков откажутся от использования EXCEPTION_DEBUG_EVENT, им придется отказаться от установки брэйкпоинтов, что несомненно затруднит процесс нежелательной отладки вашей программы.

Чтобы была полная картина происходящего надо дать хотя бы краткое описание функции WaitForDebugEvent.

Код (Text):
  1.  
  2. BOOL WaitForDebugEvent(
  3.  
  4.     LPDEBUG_EVENT lpDebugEvent, // address of structure for event information  
  5.     DWORD dwMilliseconds    // number of milliseconds to wait for event
  6.    );

Эта функция вызывается в отладчике и заставляет застыть поток в ожидании отладочного события. Она работает как и другие Wait* функции, поэтому в последнем параметре принимает время ожидания, вместо которого обычно стоит INFINITE, что значит ждать бесконечно долго. В первом параметре функция принимает указатель на структуру DEBUG_EVENT, вот как она выглядит:

Код (Text):
  1.  
  2. typedef struct _DEBUG_EVENT { // de  
  3.     DWORD dwDebugEventCode;
  4.     DWORD dwProcessId;
  5.     DWORD dwThreadId;
  6.     union {
  7.         EXCEPTION_DEBUG_INFO Exception;
  8.         CREATE_THREAD_DEBUG_INFO CreateThread;
  9.         CREATE_PROCESS_DEBUG_INFO CreateProcessInfo;
  10.         EXIT_THREAD_DEBUG_INFO ExitThread;
  11.         EXIT_PROCESS_DEBUG_INFO ExitProcess;
  12.         LOAD_DLL_DEBUG_INFO LoadDll;
  13.         UNLOAD_DLL_DEBUG_INFO UnloadDll;
  14.         OUTPUT_DEBUG_STRING_INFO DebugString;
  15.  
  16.         RIP_INFO RipInfo;
  17.     } u;
  18. } DEBUG_EVENT;

О том, что обозначает каждый из полей структуры, я говорить не буду, отправлю вас лучше к SDK.

Защита от трассировки. Сначала нужно разобраться, что такое трассировка и какие есть средства для ее осуществления. Трассировка - это пошаговое выполнение отлаживаемой программы. Отладчики режима пользователя (да и режима ядра) используют для решения данной задачи регистр eflags, а именно его бит tf, ответственный за трассировку. Как это выглядит? Если установлен вышеуказанный бит, то процессор ПЕРЕД каждой инструкцией генерирует исключение int 1, сбрасывает tf и приостанавливает поток. WaitForDebugEvent в отладчике же возвращает EXCEPTION_DEBUG_EVENT + EXCEPTION_SINGLE_STEP. Причем отладчик устанавливает бит eflags с помощью мощной функции SetThreadContext, которая позволяет в любом потоке изменить любой из регистров (ну, почти любой, gdtr и остальные с ним связанные, вам, конечно, не дадут изменить :smile3: ). Тут решение просто напрашивается само собой: Проверять бит tf и если он установлен выходить. Но не тут-то было. Если внимательно посмотреть на этот абзац можно заметить некоторую особенность, из-за которой все провалиться. Так как исключение генерируется ДО выполнения инструкции и бит сбрасывается тогда же, то мы ничего не увидим в eflags. Я довольно долго думал над этой проблемой, пока мне в голову не пришла интересная идейка... Давайте мыслить логически: Если поток выполняется пошагово, то на каждый шаг тратиться гораздо больше времени, чем в обычных условиях, не так ли? Значит, чтобы понять, что нас трассируют надо просто замерить, сколько выполняется определенный, довольно маленький кусок кода и если это время больше секунды, то мы всяко под трассировкой. С другой стороны, это не факт, ибо кто-то в этот момент времени может нас просто приостановить (SuspendThread). Все же вот реализация этого метода:

Сначала, как полагается ассемблер:

Код (Text):
  1.  
  2. ; #####################################################################
  3. ; ##################### IS_THERE_TRACE? ##############################
  4. ; #####################################################################
  5. ; ############################ About ###################################
  6. ; #####################################################################
  7. ; Определяет трасируемся ли мы отладчиком режима пользователя.
  8. ; Компилятор -  тестировал на MASM v8.0                    
  9. ; Компилировать: вот фаил MakeIt.bat:
  10. ;--------------------------------------- Start of MakeIt.bat ----------------------------------
  11. ; @echo off
  12. ;
  13. ; if exist 1.obj del 1.obj
  14. ; if exist 1.exe del 1.exe
  15. ;
  16. ; c:\masm32\bin\ml /c /coff /nologo 1.asm
  17. ; c:\masm32\bin\Link /SUBSYSTEM:WINDOWS /MERGE:.rdata=.text 1.obj
  18. ;
  19. ; dir 1.*
  20. ;
  21. ; pause
  22. ;---------------------------------------- End of MakeIt.bat -----------------------------------
  23.  
  24. ; ####################################################################
  25. ; ###################### Made by R4D][ #################################
  26. ; ####################################################################
  27.  
  28. ; #####################################################################
  29. ; ############################ Directives ################################
  30. ; #####################################################################
  31.     .386                                
  32.     .model flat, stdcall              
  33.     option casemap :none                
  34. ; #####################################################################
  35.  
  36. ; #####################################################################
  37. ; ############################# Includes and Libs #########################
  38. ; #####################################################################
  39.  
  40.       include c:\masm32\include\windows.inc  
  41.       include c:\masm32\include\user32.inc
  42.       include c:\masm32\include\kernel32.inc
  43.  
  44.       includelib c:\masm32\lib\user32.lib
  45.       includelib c:\masm32\lib\kernel32.lib        
  46. ; #####################################################################
  47.  
  48.  
  49. ; #####################################################################
  50. ; ###################### DATA #########################################
  51. ; #####################################################################
  52. .data  
  53. szMes       db "Here u can write original code of da program ;)",0  
  54. ; #####################################################################
  55.  
  56. ; #####################################################################
  57. ; ###################### CODE ########################################
  58. ; ####################################################################
  59. .code
  60. start:                
  61.     call GetTickCount
  62.     mov ebx, eax
  63. ; Здесь у нас 100 nop'ов (90h = опкод nop'a)
  64. db 100 dup(90h)        
  65.     call GetTickCount
  66.     sub eax, ebx
  67.     cmp eax, 1000
  68.     jge exit
  69.     invoke MessageBox, 0, offset szMes, NULL, MB_OK
  70. exit:   push 0
  71.     call ExitProcess
  72. end start
  73. ; #####################################################################

Реализация кода на С не составит никаких проблем:

Код (Text):
  1.  
  2. #include "stdafx.h"
  3. #include <windows.h>
  4.  
  5. int APIENTRY WinMain(HINSTANCE hInstance,
  6.                      HINSTANCE hPrevInstance,
  7.                      LPSTR     lpCmdLine,
  8.                      int       nCmdShow)
  9. {
  10.     DWORD d  = GetTickCount();
  11. // Не хватило у меня фантазии на что-то большее <img src="styles/smiles_s/smile3.gif" class="mceSmilie" alt=":smile3:" title="Smile3    :smile3:">
  12.     GetLastError();
  13.     GetLastError();
  14.     GetLastError();
  15.     GetLastError();
  16.     GetLastError();
  17.     GetLastError();
  18.     GetLastError();
  19.     GetLastError();
  20.     GetLastError();
  21.     GetLastError();
  22.     GetLastError();
  23.     GetLastError();
  24.     GetLastError();
  25.     GetLastError();
  26.     GetLastError();
  27.     GetLastError();
  28.     DWORD d1 = GetTickCount()-d;
  29.     if (d1>=1000) return 0;
  30.     MessageBox(0,"Uhhhhoooo",NULL,MB_OK);
  31.     return 0;
  32. }

Тут должно быть все понятно. Чтобы увеличить надежность метода я советую вам вызывать это многократно на протяжении всей программы, чтоб не получилось так, что вы из-за прихоти windows (а вдруг появиться real-time поток, который все процессорное время сожрет?) вы накажите обыкновенного пользователя... В то же время не стоит "маячить" перед глазами взломщика. Идеально подошел бы неявный метод защиты, описанный у Криса Касперского, когда программа "знает", что нас отлаживают, но ничего радикального не предпринимает, просто неправильно выполняя свои функции... Так, например, можно отключить всю защиту и предоставить взломщику голый код программы, без защиты. Разработанный им crack не будет работать вовсе.

Пока я занимался вопросом антитрассировки, я придумал универсальный метод обнаружения отладчика в процессе, который действует и на отладчики режима ядра. Смотрите код:

Код (Text):
  1.  
  2. ; #################################################################################################
  3. ; ############################ KILL_ALL_DA_DEBUGGEES ##########################################
  4. ; #################################################################################################
  5. ; ############################ About ###############################################################
  6. ; #################################################################################################
  7. ; Универсальный метод обнаружения отладки.
  8. ; Компилятор - тестировалось на MASM v8.0                    
  9. ; Компилировать: вот фаил MakeIt.bat:
  10. ;--------------------------------------- Start of MakeIt.bat ----------------------------------
  11. ; @echo off
  12. ;
  13. ; if exist 1.obj del 1.obj
  14. ; if exist 1.exe del 1.exe
  15. ;
  16. ; c:\masm32\bin\ml /c /coff /nologo 1.asm
  17. ; c:\masm32\bin\Link /SUBSYSTEM:WINDOWS /MERGE:.rdata=.text 1.obj
  18. ;
  19. ; dir 1.*
  20. ;
  21. ; pause
  22. ;---------------------------------------- End of MakeIt.bat -----------------------------------
  23. ; #################################################################################################
  24. ; ############################### Made by R4D][ ###################################################
  25. ; #################################################################################################
  26.  
  27. ; #################################################################################################
  28. ; ############################ Directives #########################################################
  29. ; #################################################################################################
  30.     .386                                
  31.     .model flat, stdcall              
  32.     option casemap :none                
  33. ; #################################################################################################
  34.  
  35. ; #################################################################################################
  36. ; ############################ Includes and Libs ##################################################
  37. ; #################################################################################################
  38.       include c:\masm32\include\windows.inc
  39.       include c:\masm32\include\kernel32.inc
  40.  
  41.       includelib c:\masm32\lib\user32.lib
  42.       includelib c:\masm32\lib\kernel32.lib        
  43. ; #################################################################################################
  44.  
  45. ; #################################################################################################
  46. ; #################################### CODE #######################################################
  47. ; #################################################################################################
  48. .code
  49. start:                
  50. ; Устанавливаем обработчик seh
  51.     assume fs: nothing                    
  52.     push offset seh_handler
  53.     push dword ptr fs:[0]  
  54.     mov fs:[0],esp
  55. ; Устанавливаем флаг трассировки                   
  56.     pushf
  57.     pop eax
  58.     or eax, 100h
  59.     push eax
  60.     popf
  61.     jmp exit
  62. seh_handler:
  63.     mov esi, [esp+0ch]
  64.     assume esi: PTR CONTEXT
  65.     mov [esi].regEip, offset WeWasntUnderDebuggee
  66.     xor eax, eax
  67.     ret
  68. WeWasntUnderDebuggee:
  69.     nop  
  70.     nop
  71.     nop
  72.     nop        
  73. exit:   push 0
  74.     call ExitProcess
  75. end start
  76. ; #################################################################################################

Как видите, просто устанавливаем флаг tf, далее перед следующей инструкцией возникнет #DB (int 1), которая в нормальном случае будет необработанным исключением. Если же мы под отладкой (all kind of debuggee ;)), то обработчик прерывания 1 установлен отладчиком и int 1 будет обработан. Обратите внимание, что это только если нас трассируют! В то же время, если нас просто прогоняли под отладчиком, не используя трассировку, то в момент после popf произойдет останов в отладчике (только режима пользователя, на SoftIce это не расспространяется, как дело обстоит с kd проверить не могу...), как будто нас до этого трассировали :smile3:

Сокрытия модуля.

Допустим, вам надо укрыть свой модуль от "чужих" глаз. Для этого надо разобраться в том, как получают информацию о модулях такие приложения, как 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):
  1.  
  2.    struct _PEB_LDR_DATA {
  3.    /*000*/ unsigned long        Length;
  4.    /*004*/ unsigned char        Initialized;
  5.    /*008*/ void*            SsHandle;
  6.    /*00C*/ struct _LIST_ENTRY   InLoadOrderModuleList;
  7.    /*014*/ struct _LIST_ENTRY   InMemoryOrderModuleList;
  8.    /*01C*/ struct _LIST_ENTRY   InInitializationOrderModuleList;
  9.    };

Нас интересуют последние 3 поля, которые представляют собой входы в двусвязные списки (очень часто используются Windows). Вот как определил _LIST_ENTRY Шрайбер:

Код (Text):
  1.  
  2. typedef struct _LIST_ENTRY
  3.     {
  4. /*000*/ struct _LIST_ENTRY *Flink;
  5. /*004*/ struct _LIST_ENTRY *Blink;
  6. /*008*/ }
  7.     LIST_ENTRY;

Причем в Flink указывает на следующий элемент, а Blink на предыдущий. После 004 может находиться все что угодно, а в нашем случае это _LDR_DATA_TABLE_ENTRY:

Код (Text):
  1.  
  2.   struct _LDR_DATA_TABLE_ENTRY {
  3.      /*000*/ struct _LIST_ENTRY         InLoadOrderLinks;
  4.      /*008*/ struct _LIST_ENTRY         InMemoryOrderLinks;
  5.      /*010*/ struct _LIST_ENTRY         InInitializationOrderLinks;
  6.      /*018*/ void*              DllBase;
  7.      /*01c*/ void*              EntryPoint;
  8.      /*020*/ unsigned long          SizeOfImage;
  9.      /*024*/ struct _UNICODE_STRING FullDllName;
  10.      /*02c*/ struct _UNICODE_STRING BaseDllName;
  11.      /*034*/ unsigned long          Flags;
  12.      /*038*/ unsigned short             LoadCount;
  13.      /*03a*/ unsigned short             TlsIndex;
  14.      /*03c*/ struct _LIST_ENTRY         HashLinks;
  15.      /*03c*/ void*              SectionPointer;
  16.      /*040*/ unsigned long          CheckSum;
  17.      /*044*/ unsigned long          TimeDateStamp;
  18.      /*044*/ void*              LoadedImports;
  19.    };

Мы видим, что первые 3 элемента структуры - это _LIST_ENTRY, названия которых соответствуют названиям различных списков. Очевидно, что это не что иное, как указатели на те же самые структуры, но разных списков! А если в название списка совпадает с тем, с которым мы сейчас работаем, то это просто *Flink и *Blink ;). Все это просто очень сильно облегчает нам работу. Итак, определимся, что мы будем делать:

- переходим к Peb.Ldr - переходим к InLoadOrderModuleList (к примеру) - сохраняем начальный элемент списка - перечисляем InLoadOrderModuleList с помощью первых 8 байт, которые занимают *Flink и *Blink (определяем конец списка засчет того, что Flink последнего элемента указывает на начало списка, а мы его сохранили ) - сравниваем имена BaseDllName с нужным модулем - Если в прошлом шаге сравнение показало, что это нужная нам запись, то удаляем ее из всех списков, иначе продолжаем...

Как вы, наверное, заметили, для определения BaseDllName (ровно, как и FullDllName) используется структура UNICODE_STRING. Вот ее определение по Шрайберу:

Код (Text):
  1.  
  2. typedef struct _UNICODE_STRING
  3.         {
  4. /*000*/ USHORT  Length;
  5. /*002*/ USHORT  MaximumLength;
  6. /*004*/ PWSTR       Buffer;
  7. /*008*/     }
  8.         UNICODE_STRING;

Нас, естественно, интересует только Buffer, поэтому смещаться будем к 004.

Еще непонятки может вызвать способ удаления элемента из списка. Делается это так: в Flink'е прошлого элемента записываем Flink удаляемого элемента, соответственно в Blink следующего элемента записываем Blink удаляемого.

Код (Text):
  1.  
  2. DeleteListEntry proc USES eax ebx ListEntry: DWORD
  3.     mov eax, ListEntry
  4.     mov eax, [eax+4]
  5.  
  6.     mov ebx, [eax]              ; Flink
  7.     mov ebx, [ebx]
  8.     mov dword ptr [eax], ebx
  9.    
  10.     mov ebx, [eax+4]                ; Blink
  11.     mov ebx, [ebx+4]
  12.     mov dword ptr [eax+4], ebx         
  13.     ret
  14. DeleteListEntry endp

Это довольно небезопасный код, потому что не используется seh, не стоит использовать его as is в случае, когда вы точно не уверены в том, что вы обрабатываете именно двусвязный список, и код будет обращаться к существующей странице памяти!

Далее следует код на ассемблере. Существует одна особенность, о которой я расскажу после кода...

Код (Text):
  1.  
  2. ; Удаляет модуль из списков... Параметр - КОРОТКОЕ (без пути) имя модуля
  3. DelModuleFromPEBNtA proc USES ecx ebx eax modname: PCHAR              
  4. local pfirstmod        : DWORD                                              
  5. local modn[255]      : DWORD                  
  6. ; Переводим ANSI строку в UNICODE
  7.     invoke MultiByteToWideChar, CP_ACP, 0, modname, -1, addr modn, 255
  8.     assume fs: nothing                        
  9.     mov eax, fs:[30h]               ; Здесь находиться Peb
  10.     mov eax, [eax+0Ch]          ; Смещаемся к структуре Ldr
  11.     mov eax, [eax+0Ch]          ; Смотрим на список InLoadOrderModuleList
  12. ; Сохраняем адрес первого в списке модуля, чтобы не войти в бесконечный цикл (это же двусвязный список)
  13.     mov pfirstmod, eax
  14. continue:      
  15.     mov ecx, [eax+30h]              ; BaseDllName                          
  16.     push eax                                                                            
  17.     invoke lstrcmpiW, ecx, addr modn                                     
  18.     test eax, eax
  19.     pop eax
  20.     je Found                                                                                
  21.     mov eax, [eax]              ; Переходим к следующему элементу(Flink)
  22.     cmp eax, pfirstmod
  23.     jne continue
  24.     ret
  25. Found:          
  26. ; Собственно удаляем элемент из Ldr.InLoadOrderModuleList
  27.     push eax
  28.     call DeleteListEntry
  29. ; Смещаемся к Ldr.InMemoryOrderModuleList
  30.     add eax, 8                                  
  31. ; И удаляем элемент оттуда ;) 
  32.     push eax
  33.     call DeleteListEntry  
  34. comment @
  35.     add eax, 8
  36.     push eax
  37.     call DeleteListEntry      
  38. @
  39.     ret
  40. DelModuleFromPEBNtA endp    

Теперь обещанная особенность: внимательный читатель, наверное, заметил, что удаление из списка InInitializationOrderModuleList закомментировано. Это потому что этот список, судя по всему, интенсивно используется системой... Я не знаю, для какой цели, но если, к примеру, удалить из списка главный модуль нашего приложения, то оно незамедлительно выгрузиться БЕЗ каких-либо ошибок... Если же удалить user32.dll, то у нас будут проблемы с MessageBox'ом, к примеру. Он вызываться-то будет и его код будет получать управление(то есть модуль не выгружается, как мог бы подумать проницательный читатель), но будет где-то в вызывать Access Violation... В тоже время удаление из этого списка ntdll.dll не вызовет никаких проблем... На самом деле это не проблема, ибо этот список не используется (по крайней мере я не разу не видел...) отладочными функциями для получения списка модулей.

Рассмотрим пример. В примере, я буду удалять модуль из user32.dll и смотреть, какая будет реакция у различных отладочных функций и программ. Далее следует код (без вышеописанных функций):

Код (Text):
  1.  
  2. ; ##################################################################
  3. ; ########################## TEB Playing #############################
  4. ; ##################################################################
  5. ; ############################ About ################################
  6. ; ##################################################################
  7. ; Компилятор - тестировался на MASM v8.0                    
  8. ; Компилировать: далее MakeIt.bat:
  9. ;--------------------------------------- Start of MakeIt.bat ----------------------------------
  10. ; @echo off
  11. ;
  12. ; if exist 1.obj del 1.obj
  13. ; if exist 1.exe del 1.exe
  14. ;
  15. ; c:\masm32\bin\ml /c /coff /nologo 1.asm
  16. ; c:\masm32\bin\Link /SUBSYSTEM:WINDOWS /MERGE:.rdata=.text 1.obj
  17. ;
  18. ; dir 1.*
  19. ;
  20. ; pause
  21. ;---------------------------------------- End of MakeIt.bat -----------------------------------
  22. ; ##################################################################
  23. ; ######################### Made by R4D][ ############################
  24. ; ##################################################################
  25.  
  26. ; ##################################################################
  27. ; ########################## Directives ###############################
  28. ; ##################################################################
  29.     .386                                
  30.     .model flat, stdcall              
  31.     option casemap :none                
  32. ; ##################################################################
  33.  
  34. ; ##################################################################
  35. ; ######################## Includes and Libs ###########################
  36. ; ##################################################################
  37.       include c:\masm32\include\windows.inc
  38.       include c:\masm32\include\user32.inc
  39.       include c:\masm32\include\kernel32.inc
  40.       include c:\masm32\include\masm32.inc
  41.       include c:\masm32\macros\macros.asm
  42.  
  43.       includelib c:\masm32\lib\user32.lib
  44.       includelib c:\masm32\lib\kernel32.lib
  45.       includelib c:\masm32\lib\masm32.lib
  46. ; ##################################################################
  47.  
  48. ; ##################################################################
  49. ; ############################ DATA ################################
  50. ; ##################################################################
  51. .data
  52.     nmodname db "user32.dll",0
  53. ; ##################################################################
  54.  
  55. ; ##################################################################
  56. ; ############################ CODE ################################
  57. ; ##################################################################
  58. .code
  59. start:                
  60. main proc            
  61. local SnpSht         :DWORD
  62. local me               :MODULEENTRY32
  63. local IsItEnd          :BOOL          
  64. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; Удаляем из списков user32.dll ;;
  65.       push offset nmodname
  66.       call DelModuleFromPEBNtA                                                            
  67. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; Проверяем на снэпшоты ;;;;;;;;;
  68.         invoke CreateToolhelp32Snapshot, TH32CS_SNAPMODULE, 0
  69.         mov SnpSht, eax
  70.         mov me.dwSize, sizeof MODULEENTRY32
  71.         invoke Module32First, SnpSht, addr me
  72.         mov IsItEnd, TRUE
  73.         .while IsItEnd!=FALSE      
  74.             invoke MessageBox, 0, addr me.szModule, addr me.szModule, MB_OK
  75.             invoke Module32Next, SnpSht, addr me
  76.             mov IsItEnd, eax
  77.         .endw
  78.         invoke CloseHandle, SnpSht
  79.         xor eax, eax
  80.  
  81.    
  82.       invoke MessageBox, 0, SADD("Look now in PETools module list"), SADD("Check this"), MB_OK
  83.       invoke ExitProcess,0  
  84. main endp  
  85. end start
  86. ; ##################################################################

Если вы откомпилируете и запустите эту программу, то увидите, что снапшоты не показывают модуль user32, что и немудрено. Чтобы проверить NtQuerySystemInformation, не надо ничего писать, ибо за нас все давно написали ребята, которые делали PETools. Вы можете удостовериться, что эта программа использует именно эту функцию, просто поставив breakpoint на NtQuerySystemInformation в SoftIce'e и пронаблюдать, как PETools ее вызывает. PETools так же не показывает скрываемый модуль. Можно сделать вывод, что NtQuerySystemInformation тоже берет информацию из PEB. Теперь плохие новости. Отладчики режима ядра располагают более достоверной информацией. Но это и не мудрено. Система должна ведь хранить список модулей еще и в структуре режима ядра (я к сожалению, ничего не знаю ни об этой структуре, ни о том, как SI ее получает, но я уверен, что google об этом знает все :smile3:), иначе это была бы даже не дыра, а просто убийство, ибо как иначе система следила бы за тем, какие модули выгружать из памяти при выгрузке процесса? Хранить столь важные сведения в структуре режима пользователя было бы опасно с точки зрения целостности всей системы. В то же время если бы эти сведения не дублировались в PEB, то было бы проблематично получение этой информации отладчикам режима пользователя... Поэтому данный метод не панацея от всех и вся.

Защита от дампа.

Дамп - это снимок памяти, который записывается на носитель. Дамп - это очень мощное оружие в умелых руках. Дамп помогает против зашифрованных программ, ибо "сфотографировать" память можно в любой момент, в то время, как перед тем как инструкция выполнится она должна быть расшифрована. У дампа очень много применений, но поговорим лучше, как не позволить отладчику (или специально-предназначенной для этого программе - дамперу) "сфотографировать" нас.

Очень интересный метод предложил volodya в статье "Об упаковщиках в последний раз". Этот метод заключается в изменении размера образа, таким образом, чтобы дампер обращался к несуществующим страницам памяти. В этом случае дампер "рухнет". Я же предлагаю менее радикальный метод, подстановки 0 на место того места, где должен быть размер образа. Этот метод плох тем, что этот 0 сильно бросается в глаза... Тем не менее, вы можете подставить вместо нуля, что угодно. Например, вы можете хранить все самые важные подпрограммы в конце вашего модуля, а вместо размера модуля подставлять размер_вашего_ модуля - размер_подпрограмм. Информация о размере модуля храниться в PEB структуре. Немного подумав, я решил, что забавно было бы так же поменять базу модуля...

Код (Text):
  1.  
  2.     assume fs: nothing                    
  3.     mov eax, fs:[30h]           ; PEB  
  4.     mov eax, [eax+0Ch]      ; PEB_LDR_DATA
  5.     mov eax, [eax+0Ch]      ; Ldr.InLoadOrderModuleList.Flink
  6.     lea ebx, [eax+20h]          ; LDR_DATA_TABLE_ENTRY.SizeOfImage
  7.     lea ecx, [eax+18h]          ; LDR_DATA_TABLE_ENTRY.DllBase
  8.     mov dword ptr [ebx], 0      ; Подставляйте сюда, что хотите <img src="styles/smiles_s/smile3.gif" class="mceSmilie" alt=":smile3:" title="Smile3    :smile3:">
  9.     mov dword ptr [ecx], 0      ; Подставляйте сюда, что хотите <img src="styles/smiles_s/smile3.gif" class="mceSmilie" alt=":smile3:" title="Smile3    :smile3:">

Некоторые же дамперы могут читать вышеперечисленные параметры из заголовка PE, так что не помешает так же и там подправить значение :smile3:

Код (Text):
  1.  
  2. ; Узнаем базу нашего модуля
  3.     push 0
  4.     call GetModuleHandle
  5.     mov ebx, eax
  6. ; Там, как известно находиться DOS заглушка
  7.     assume eax: PTR IMAGE_DOS_HEADER
  8. ; В которой смещение на pe заголовок
  9.     mov eax, [eax].e_lfanew
  10.     add eax, ebx
  11.     assume eax: PTR IMAGE_NT_HEADERS32
  12. ; В котором (а именно в OptionalHeader'e) и храниться размер и база модуля
  13.     lea ebx, [eax].OptionalHeader.SizeOfImage
  14.     lea ecx, [eax].OptionalHeader.ImageBase
  15.     push ecx
  16.     push ebx
  17. ; На всякий слуяай изменяем атрибуты страниц в этом районе на RW
  18.     invoke VirtualProtect, ebx, 2, PAGE_READWRITE, offset dOld  
  19.     pop ebx
  20.     pop ecx
  21. ; Пишите сюда, что хотите ;)
  22.     mov dword ptr [ebx], 0
  23.     mov dword ptr [ecx], 0

В итоге, можно предложить такой код на С против дампа:

Код (Text):
  1.  
  2. #include "stdafx.h"
  3. #include &lt;windows.h&gt;
  4.  
  5. void AntiDump(DWORD dwNewImageSize, DWORD dwNewBase)
  6. {
  7. // Без ассемблера не обойтись...
  8. // Доступ к регистру fs можно получить
  9. // только в ассемблерной вставке...
  10. // Тем не менее, можно было бы добавить мегабайты
  11. // описаний структур PEB и сократить ассемблерную
  12. // вставку до 1 строчки, но я предпочел обойтись
  13. // "жестко" забитыми смещениями...
  14. // Описание PEB и связанных с ней структур
  15. // смотрите в одной из ссылок в конце статьи.
  16.     __asm
  17.     {
  18.     mov eax, fs:[30h]  
  19.     mov eax, [eax+0Ch]
  20.     mov eax, [eax+0Ch]
  21.     lea ebx, [eax+20h]
  22.     lea ecx, [eax+18h]
  23.     mov eax, dwNewImageSize
  24.     mov dword ptr [ebx], eax   
  25.     mov eax, dwNewBase
  26. mov dword ptr [ecx], eax
  27.     };
  28.     PIMAGE_DOS_HEADER pDH    = (PIMAGE_DOS_HEADER)GetModuleHandle(NULL);
  29.     PIMAGE_NT_HEADERS32 pINH = (PIMAGE_NT_HEADERS32)((*pDH).e_lfanew+(DWORD)GetModuleHandle(NULL));
  30.     DWORD pOp;
  31.     VirtualProtect(&((*pINH).OptionalHeader.SizeOfImage),4,
  32. PAGE_EXECUTE_READWRITE,&pOp);
  33.     (*pINH).OptionalHeader.SizeOfImage = dwNewImageSize;
  34. (*pINH).OptionalHeader.ImageBase = dwNewBase;
  35. }
  36.  
  37. int APIENTRY WinMain(HINSTANCE hInstance,
  38.                      HINSTANCE hPrevInstance,
  39.                      LPSTR     lpCmdLine,
  40.                      int       nCmdShow)
  41. {
  42.     AntiDump(0,0);
  43.     MessageBox(0,"See da result in dumper ;)",NULL,MB_OK);
  44.     return 0;
  45. }

Немного подумав над проблемой дампа, я пришел к выводу, что можно неплохо было бы поместить внутри кода некоторые "дыры", у которых атрибуты страниц будут PAGE_NOACCESS. Мы не будем обращаться к этому коду, а вот программа, делающая дамп обязательно обратиться :smile3: и если в ней нет проверки на атрибуты страниц - потерпит крах. НО этот метод защиты крайне неэффективный по причине того, что опытный взломщик составит карту памяти (OllyDbg, например, предоставляет такие услуги), и увидев наши дыры просто обойдет их, сделав дамп вашей программы по частям. К тому же довольно сложно рассчитать границы страниц, так чтобы мы "попали" VirtualProtect'oм именно так, что атрибуты изменились только у одной страницы-дыры. А если прибавить сюда неэффективность подобного метода (целую страницу (4Кб) на защиту только в одном месте!), то становиться ясно, что этот метод не подойдет нам.

Что еще можно предложить из методов обхода дампа? Можно, например, изменять базу нашего модуля на базу какой-нибудь другой dllки. А можно и вовсе назвать его ntoskrnl.exe и подставить базу и размер того же модуля. Представляю лицо взломщика, когда он увидит подобную картину :smile3:. В общем-то, все это дело фантазии, как и все о чем я написал в этой статье.

Хочу сказать спасибо Zero Ice'у, который поддерживал меня на протяжении написания всей статьи. Так же он дисассемблировал DebugBreak, показав, что это не что иное как int 3.

Так же хочу поблагодарить рассылку от wasm.ru. Как видите, ваш материал помогает и, я бы врят ли обратил столь пристальное внимание на такой кладезь знаний, как PEB, если бы не вы...

Ссылки.

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

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


0 1.931
archive

archive
New Member

Регистрация:
27 фев 2017
Публикаций:
532