Статья писалась исключительно для форума LIMB.cc.
Автор: ghotycal57.
Введение
Доброго времени суток.
Анти-дебаг - или, выражаясь точнее, механизмы противодействия отладке и отладчику - применяются с целью затруднить или полностью предотвратить динамический анализ исполняемого кода. В контексте reverse engineering это означает создание условий, при которых поведение программы искажается, прерывается или становится недостоверным при попытке её трассировки.
Большинство широко известных техник анти-отладки опираются на высокоуровневые API, такие как
Следовательно, если наша цель - не формальная проверка, а реальное обнаружение вмешательства, необходимо опускаться на уровень ниже. В данной статье мы рассмотрим два ключевых механизма: доступ к внутренним структурам процесса через PEB, где напрямую хранится информация о состоянии отладки, а также использование механизма исключений Windows (
Механизм обработки исключений для меня вызывает больше интереса нежели два варианта выше. В нормальном режиме выполнение программы при возникновении исключения (например, int3, access violation или guard page) передаётся SEH обработчику. Однако при наличии отладчика управление может быть перехвачено раньше, ещё до передачи в обработчики процесса. Это создаёт наблюдаемый эффект: поведение программы при генерации исключения меняется в зависимости от того, присутствует ли отладчик. Вообщем, рассмотрим в дальнейшем.
Начинаем.
1. IsDebuggerPresent / CheckRemoteDebuggerPresent
Первый способ реализуется непосредственно через API Windows, логика довольно проста - условно на примере
Данная функция возвращает ненулевое значение, если текущий процесс выполняется под управлением отладчика. Внутренне её реализация сводится к чтению соответствующего флага из структуры PEB, а именно поля BeingDebugged. То есть это некая обертка, чтобы не делать вручную работу с PEB самим.
Простейший пример использования выглядит следующим образом:
Программа очень проста.
Мы последовательно вызываем функции
Далее вызывается IsDebuggerPresent, которая возвращает результат напрямую через регистр
После этого, в более корректной реализации, можно использовать CheckRemoteDebuggerPresent, передав ей дескриптор процесса и указатель на переменную, в которую будет записан результат проверки. В отличие от предыдущей функции, здесь используется косвенная передача результата через память.
Давайте же посмотрим что у нас получится в отладчике:
После открытия отладчика всё становится на свои места. Как уже упоминалось ранее, функция IsDebuggerPresent не содержит сложной логики и по сути является обёрткой над чтением структуры PEB.
Инструкция
Далее инструкция
Если в этом байте находится 1 - процесс запущен под отладчиком. Если 0 - отладчик отсутствует.
Последняя инструкция ret завершает выполнение функции, возвращая управление вызывающему коду.
Что же касается второй функции anti-debug-а, то работает она несколько иначе:
В отличие от IsDebuggerPresent, функция CheckRemoteDebuggerPresent не возвращает результат напрямую через регистр, а использует указатель, переданный в качестве аргумента. Это означает, что результат проверки записывается в память, на которую указывает второй параметр функции.
В отладчике можно увидеть начало её выполнения:
Данная инструкция сохраняет значение регистра
Дальнейшая логика функции сводится к вызову внутренних механизмов Windows, в частности NtQueryInformationProcess с параметром ProcessDebugPort или ProcessDebugFlags. Через этот системный вызов ядро возвращает информацию о том, привязан ли к процессу отладчик:
То есть логика такова:
Если 1, - то программа под отладчиком.
Если 0, - программа НЕ под отладчиком.
2. Погружаемся в бездну PEB.
А вот теперь мы познаем, как условно та же функция IsDebuggerPresent работает под капотом, как я уже писал выше. Несмотря на кажущуюся простоту, эта функция всего лишь читает один байт по фиксированному смещению в структуре PEB.
Сначала процесс получает указатель на PEB через сегментный регистр GS по смещению 0x60. Именно там Windows хранит внутренние данные о процессе, включая флаг BeingDebugged, который и указывает на наличие отладчика.
Далее байт читается и расширяется до регистра EAX, который возвращается как результат функции. Этот простой приём позволяет API скрыть детали работы с внутренними структурами и предоставить быстрый доступ к информации о состоянии процесса.
А также сразу хотелось бы рассказать про другое смещение, связанное с
Помимо поля BeingDebugged, в структуре PEB существует ещё один интересный элемент - NtGlobalFlag. Он расположен по смещению 0xBC (для x64) и используется системой для хранения различных флагов режима выполнения процесса.
При запуске программы под отладчиком некоторые биты в NtGlobalFlag автоматически устанавливаются. В частности, это флаги, связанные с отладочной кучей:
1.
2.
3.
В совокупности они дают значение 0x70, которое часто используется как индикатор наличия отладчика.
Проверка двух этих способов выглядит следующим образом:
В первом случае мы обращаемся к полю BeingDebugged и проверяем его напрямую. Если значение ненулевое - процесс находится под отладчиком.
Во втором случае читается NtGlobalFlag, после чего применяется маска>0x70, позволяющая выделить биты, связанные с отладочной кучей. Если хотя бы один из них установлен, это также указывает на возможное присутствие отладчика.
Давайте проверим программу под отладчиком:
Как видно на скриншоте, оба метода работают идеально и отоброжаются.
1.
2.
Двигаемся дальше
Если там 1 - отладчик есть, если 0 - нет.
Про NtGlobalFlag я уже чуть выше объяснял, дублировать думаю нет смысла.
3. Анти-отладка через SEH-исключение
Далее у нас идёт анти-отладка через исключение, метод довольно прост, рассматривать я его буду на x86, так как будет легче.
Суть метода в том, что программа сознательно генерирует исключение, например делением на ноль или инструкцией
Однако если процесс запущен под отладчиком, управление часто перехватывается им раньше, чем доходит до обработчиков программы. Это создаёт заметное различие в поведении: в отладчике исключение либо останавливает выполнение, либо вызывает особое поведение, которое можно зарегистрировать.
Пример:
В коде всё просто, мы вручную регистрируем обработчик исключений через цепочку SEH, после чего искусственно генерируем исключение и анализируем поведение программы.
Сначала в стек помещается адрес нашего обработчика
Далее выполняется инструкция
Если выполнение действительно попало в
Также чуть не забыл про
Однако же, если программа запущена под отладчиком, ситуация меняется. Отладчик перехватывает
Откроем отладчик и посмотрим(важно, для x86 - x32dbg.exe):
Как видно, все прекрасно работает, в отладчике выполнение останавливается на инструкции
То есть несмотря на то, что мы зарегистрировали свой SEH через
Если же запустить программу вне отладчика, ситуация будет иной. Инструкция
Заключение
В заключение хочется отметить, что рассмотренные способы - это далеко не все существующие методы анти-отладки. Их очень много, но в данной статье я показал базовые приёмы, которые должен знать каждый реверсер.
В будущих материалах, если эта статья окажется интересной для читателей, планирую рассмотреть более сложные техники, продвигаясь шаг за шагом и изучая их постепенно.
Всем удачи.
Автор: ghotycal57.
Введение
Доброго времени суток.
Анти-дебаг - или, выражаясь точнее, механизмы противодействия отладке и отладчику - применяются с целью затруднить или полностью предотвратить динамический анализ исполняемого кода. В контексте reverse engineering это означает создание условий, при которых поведение программы искажается, прерывается или становится недостоверным при попытке её трассировки.
Большинство широко известных техник анти-отладки опираются на высокоуровневые API, такие как
IsDebuggerPresent и CheckRemoteDebuggerPresent. Первая функция возвращает флаг наличия отладчика для текущего процесса, вторая позволяет проверить другой процесс через дескриптор. Однако оба подхода работают на уровне WinAPI и потому легко обходятся - достаточно перехватить вызов или подменить возвращаемое значение.Следовательно, если наша цель - не формальная проверка, а реальное обнаружение вмешательства, необходимо опускаться на уровень ниже. В данной статье мы рассмотрим два ключевых механизма: доступ к внутренним структурам процесса через PEB, где напрямую хранится информация о состоянии отладки, а также использование механизма исключений Windows (
SEH) как инструмента активного обнаружения.Механизм обработки исключений для меня вызывает больше интереса нежели два варианта выше. В нормальном режиме выполнение программы при возникновении исключения (например, int3, access violation или guard page) передаётся SEH обработчику. Однако при наличии отладчика управление может быть перехвачено раньше, ещё до передачи в обработчики процесса. Это создаёт наблюдаемый эффект: поведение программы при генерации исключения меняется в зависимости от того, присутствует ли отладчик. Вообщем, рассмотрим в дальнейшем.
Начинаем.
Спойлер: Макрос section.inc из кода
C:
macro .code {
section '.text' code readable executable
}
macro .data {
section '.data' data readable writeable
}
macro .idata {
section '.idata' import data readable writeable
}
1. IsDebuggerPresent / CheckRemoteDebuggerPresent
Первый способ реализуется непосредственно через API Windows, логика довольно проста - условно на примере
IsDebuggerPresent.Данная функция возвращает ненулевое значение, если текущий процесс выполняется под управлением отладчика. Внутренне её реализация сводится к чтению соответствующего флага из структуры PEB, а именно поля BeingDebugged. То есть это некая обертка, чтобы не делать вручную работу с PEB самим.
Простейший пример использования выглядит следующим образом:
Не удалось прогрузить медиа
Спойлер: Code
C:
format PE64 console
entry start
include 'win64a.inc'
include 'C:\FLAT\section.inc'
.data
information db "This is test programm for Debug | author: ghotycal57", 10, 0
; ----
dls dq 5
hProcess dq 0
isDbg dq 0
.code
start:
sub rsp, 28h
lea rcx, [information]
call[printf]
call[getch]
; ---
call [GetCurrentProcess]
mov [hProcess], rax
call [IsDebuggerPresent]
mov rcx, rax
call [ExitProcess]
mov rcx, [hProcess]
lea rdx, [isDbg]
call [CheckRemoteDebuggerPresent]
add rsp, 28h
ret
.idata
library kernel32, 'kernel32.dll',\
msvcrt, 'msvcrt.dll'
import kernel32,\
ExitProcess, 'ExitProcess',\
GetCurrentProcess, 'GetCurrentProcess',\
IsDebuggerPresent, 'IsDebuggerPresent',\
CheckRemoteDebuggerPresent, 'CheckRemoteDebuggerPresent'
import msvcrt,\
printf, 'printf',\
getch, '_getch'
Мы последовательно вызываем функции
GetCurrentProcess, IsDebuggerPresent и CheckRemoteDebuggerPresent, чтобы получить информацию о состоянии текущего процесса с точки зрения наличия отладчика. Сначала извлекается псевдодескриптор текущего процесса, который затем может быть использован для дальнейших проверок.Далее вызывается IsDebuggerPresent, которая возвращает результат напрямую через регистр
RAX. Это быстрый и тривиальный способ определить наличие отладчика, основанный на внутреннем флаге в PEB. Полученное значение используется как код завершения процесса, что позволяет сразу увидеть результат работы функции.После этого, в более корректной реализации, можно использовать CheckRemoteDebuggerPresent, передав ей дескриптор процесса и указатель на переменную, в которую будет записан результат проверки. В отличие от предыдущей функции, здесь используется косвенная передача результата через память.
Давайте же посмотрим что у нас получится в отладчике:
Не удалось прогрузить медиа
После открытия отладчика всё становится на свои места. Как уже упоминалось ранее, функция IsDebuggerPresent не содержит сложной логики и по сути является обёрткой над чтением структуры PEB.
C:
00007FFF368AB6C0 | 6548:8B0425 60000000 | mov rax,qword ptr gs:[60] |
00007FFF368AB6C9 | 0FB640 02 | movzx eax,byte ptr ds:[rax+2] |
00007FFF368AB6CD | C3 | ret |
mov rax, qword ptr gs:[60] получает адрес структуры PEB текущего процесса. В x64 Windows сегментный регистр GS используется для доступа к структурам, привязанным к потоку, и по смещению 0x60 как раз находится указатель на PEB.Далее инструкция
movzx eax, byte ptr [rax+2] читает байт по смещению 0x2 от начала PEB. Это и есть поле BeingDebugged. Значение расширяется до регистра EAX, который используется как возвращаемое значение функции.Если в этом байте находится 1 - процесс запущен под отладчиком. Если 0 - отладчик отсутствует.
Последняя инструкция ret завершает выполнение функции, возвращая управление вызывающему коду.
Что же касается второй функции anti-debug-а, то работает она несколько иначе:
Не удалось прогрузить медиа
В отличие от IsDebuggerPresent, функция CheckRemoteDebuggerPresent не возвращает результат напрямую через регистр, а использует указатель, переданный в качестве аргумента. Это означает, что результат проверки записывается в память, на которую указывает второй параметр функции.
В отладчике можно увидеть начало её выполнения:
C:
00007FFF368F0960 | 48:895C24 10 | mov qword ptr ss:[rsp+10],rbx |
RBX на стек. Это стандартный пролог функции - сохранение невольтильных регистров перед выполнением основной логики.Дальнейшая логика функции сводится к вызову внутренних механизмов Windows, в частности NtQueryInformationProcess с параметром ProcessDebugPort или ProcessDebugFlags. Через этот системный вызов ядро возвращает информацию о том, привязан ли к процессу отладчик:
Не удалось прогрузить медиа
То есть логика такова:
- В RCX регистр передается дескриптор процесса;
- В RDX регистр передается указатель на переменную для результата;
- Внутри функции выполняется системный вызов;
- Полученное значение интерпретируется и записываается по адресу и з RDX.
Если 1, - то программа под отладчиком.
Если 0, - программа НЕ под отладчиком.
2. Погружаемся в бездну PEB.
А вот теперь мы познаем, как условно та же функция IsDebuggerPresent работает под капотом, как я уже писал выше. Несмотря на кажущуюся простоту, эта функция всего лишь читает один байт по фиксированному смещению в структуре PEB.
Сначала процесс получает указатель на PEB через сегментный регистр GS по смещению 0x60. Именно там Windows хранит внутренние данные о процессе, включая флаг BeingDebugged, который и указывает на наличие отладчика.
Далее байт читается и расширяется до регистра EAX, который возвращается как результат функции. Этот простой приём позволяет API скрыть детали работы с внутренними структурами и предоставить быстрый доступ к информации о состоянии процесса.
А также сразу хотелось бы рассказать про другое смещение, связанное с
NtGlobalFlag.Помимо поля BeingDebugged, в структуре PEB существует ещё один интересный элемент - NtGlobalFlag. Он расположен по смещению 0xBC (для x64) и используется системой для хранения различных флагов режима выполнения процесса.
При запуске программы под отладчиком некоторые биты в NtGlobalFlag автоматически устанавливаются. В частности, это флаги, связанные с отладочной кучей:
1.
FLG_HEAP_ENABLE_TAIL_CHECK2.
FLG_HEAP_ENABLE_FREE_CHECK3.
FLG_HEAP_VALIDATE_PARAMETERSВ совокупности они дают значение 0x70, которое часто используется как индикатор наличия отладчика.
Проверка двух этих способов выглядит следующим образом:
Не удалось прогрузить медиа
Спойлер: Code
C:
format PE64 console
entry start
include 'C:\FLAT\INCLUDE\win64a.inc'
include 'C:\FLAT\section.inc'
.data
information db "This is test programm for Debug | author: ghotycal57", 10, 0
; ----
.code
start:
sub rsp, 28h
lea rcx, [information]
call[printf]
call[getch]
; ---
mov rax, [gs:60h] ; peb
movzx eax, byte [rax+2]
test eax, eax
jnz debugger_detect
; nt
mov rax, [gs:60h]
mov eax, [rax+0BCh]
and eax, 70h
test eax, eax
jnz debugger_detect
debugger_detect:
xor ecx, ecx
call[ExitProcess]
add rsp, 28h
ret
.idata
library kernel32, 'kernel32.dll',\
msvcrt, 'msvcrt.dll'
import kernel32,\
ExitProcess, 'ExitProcess'
import msvcrt,\
printf, 'printf',\
getch, '_getch'
Во втором случае читается NtGlobalFlag, после чего применяется маска>0x70, позволяющая выделить биты, связанные с отладочной кучей. Если хотя бы один из них установлен, это также указывает на возможное присутствие отладчика.
Давайте проверим программу под отладчиком:
Не удалось прогрузить медиа
Как видно на скриншоте, оба метода работают идеально и отоброжаются.
C:
00402017 | 68 60000000 | push bendix.402060
0040201C | 64:FF35 00000000 | push dword ptr fs:[0]
00402023 | 64:8925 00000000 | mov dword ptr fs:[0],esp
0040202A | CD 03 | int 3
0040202C | B8 01000000 | mov eax,1
00402031 | EB 16 | jmp bendix.402049
00402033 | B8 00000000 | mov eax,0
mov rax, qword ptr gs:[60h] - берется адрес PEB текущего потока(в x64 - GS)2.
movzx eax, byte ptr [rax+2] - читается поле BeingDebuggedДвигаемся дальше
Если там 1 - отладчик есть, если 0 - нет.
Про NtGlobalFlag я уже чуть выше объяснял, дублировать думаю нет смысла.
3. Анти-отладка через SEH-исключение
Далее у нас идёт анти-отладка через исключение, метод довольно прост, рассматривать я его буду на x86, так как будет легче.
Суть метода в том, что программа сознательно генерирует исключение, например делением на ноль или инструкцией
INT 3. В обычных условиях выполнение передаётся стандартной цепочке обработчиков исключений в самом процессе. Программа может перехватывать это событие через Structured Exception Handling (SEH) или Vectored Exception Handling (VEH) и корректно реагировать на него. Мы рассмотрим SEH.Однако если процесс запущен под отладчиком, управление часто перехватывается им раньше, чем доходит до обработчиков программы. Это создаёт заметное различие в поведении: в отладчике исключение либо останавливает выполнение, либо вызывает особое поведение, которое можно зарегистрировать.
Пример:
Не удалось прогрузить медиа
Спойлер: Code
C:
format PE console
entry start
include 'C:\FLAT\INCLUDE\win32a.inc'
include 'C:\FLAT\section.inc'
.data
information db "This is test programm for Debug | author: ghotycal57", 10, 0
.code
start:
push ebp
mov ebp, esp
push information
call [printf]
add esp, 4
call [getch]
push seh_handler
push dword [fs:0]
mov [fs:0], esp
int 3
mov eax, 1
jmp exit
seh_handler:
mov eax, 0
mov esp, [esp+8]
mov [fs:0], esp
add esp, 4
xor eax, eax
ret
exit:
push eax
call [ExitProcess]
.idata
library kernel32, 'kernel32.dll',\
msvcrt, 'msvcrt.dll'
import kernel32,\
ExitProcess, 'ExitProcess'
import msvcrt,\
printf, 'printf',\
getch, '_getch'
Сначала в стек помещается адрес нашего обработчика
seh_handler, затем сохраняется предыдущий обработчик из fs:[0]. После этого мы обновляем fs:[0], тем самым устанавливая свой обработчик в начало цепочки SEH.Далее выполняется инструкция
int 3, которая генерирует исключение типа breakpoint. В нормальном режиме, без отладчика, управление передаётся нашей функции seh_handler, так как она находится первой в цепочке обработчиков.Если выполнение действительно попало в
seh_handler, значит исключение было обработано внутри процесса. В этом случае мы корректно восстанавливаем цепочку SEH, возвращаем управление и устанавливаем результат, указывающий на отсутствие отладчика.Также чуть не забыл про
CONTEXT. При работе с SEH в x86 важно понимать, что на стеке в момент вызова обработчика лежит не только указатель на предыдущий SEH, но и структуры исключения и контекст потока.Однако же, если программа запущена под отладчиком, ситуация меняется. Отладчик перехватывает
int 3 раньше, чем управление дойдёт до нашего обработчика. В результате выполнение либо останавливается на точке останова, либо идёт по другому сценарию, и наш seh_handler не вызывается.Откроем отладчик и посмотрим(важно, для x86 - x32dbg.exe):
Не удалось прогрузить медиа
C:
00402000 | 55 | push ebp |
00402001 | 89E5 | mov ebp,esp |
00402003 | 68 00104000 | push bendix.401000 | 401000:"This is test programm for Debug | author: ghotycal57\n"
00402008 | FF15 84304000 | call dword ptr ds:[<printf>] |
0040200E | 83C4 04 | add esp,4 |
00402011 | FF15 88304000 | call dword ptr ds:[<_getch>] |
00402017 | 68 33204000 | push bendix.402033 |
0040201C | 64:FF35 00000000 | push dword ptr fs:[0] | [00000000]:&"яяяяЙ“іw"
00402023 | 64:8925 00000000 | mov dword ptr fs:[0],esp | [00000000]:&"яяяяЙ“іw"
0040202A | CD 03 | int 3 |
0040202C | B8 01000000 | mov eax,1 | eax:&"яяяяЙ“іw"
00402031 | EB 16 | jmp bendix.402049 |
00402033 | B8 00000000 | mov eax,0 | eax:&"яяяяЙ“іw"
00402038 | 8B6424 08 | mov esp,dword ptr ss:[esp+8] | [esp+08]:BaseThreadInitThunk
0040203C | 64:8925 00000000 | mov dword ptr fs:[0],esp | [00000000]:&"яяяяЙ“іw"
00402043 | 83C4 04 | add esp,4 |
00402046 | 31C0 | xor eax,eax | eax:&"яяяяЙ“іw"
00402048 | C3 | ret |
00402049 | 50 | push eax | eax:&"яяяяЙ“іw"
0040204A | FF15 60304000 | call dword ptr ds:[<ExitProcess>] |
int 3, так как она воспринимается как breakpoint. Управление в этот момент перехватывается самим отладчиком, и до нашего обработчика seh_handler выполнение не доходит.То есть несмотря на то, что мы зарегистрировали свой SEH через
fs:[0], отладчик имеет приоритет и обрабатывает исключение раньше. В результате поток выполнения идёт дальше по основной ветке, где устанавливается eax = 1 и происходит переход к завершению программы.Если же запустить программу вне отладчика, ситуация будет иной. Инструкция
int 3 вызовет исключение, которое не будет перехвачено внешним инструментом, и управление перейдёт в наш seh_handler.Заключение
В заключение хочется отметить, что рассмотренные способы - это далеко не все существующие методы анти-отладки. Их очень много, но в данной статье я показал базовые приёмы, которые должен знать каждый реверсер.
В будущих материалах, если эта статья окажется интересной для читателей, планирую рассмотреть более сложные техники, продвигаясь шаг за шагом и изучая их постепенно.
Всем удачи.
👍25
😮6
💯3
LIMB