Бытует мнение, что отладчики получили своё название благодаря обычному жуку (англ. Bug), который пролез в первый компьютер эпохи неолита 1940-50х годов, и нарушил его работу, застряв между ферритовыми кольцами
Оглавление:
1. Системная поддержка отладки
Процессоры архитектуры х86 имеют аппаратную поддержку отладки, для чего предусмотрен «TrapFlag» TF = бит 8 в регистре флагов
Поскольку аппаратная поддержка требовательна к ресурсам, имеется и программная реализация пошагового исполнения, которая базируется на том-же флаге трассировке TF. Когда этот флаг взведён, процессор останавливается после исполнения каждой инструкции, передавая управление обработчику прерывания
Что касается защищённого режима ЦП в Windows, то для установки брейков (точек останова, далее ВР) здесь используется опкод со-значением
На уровне пользовательского режима, отладка поддерживается специальным классом
2. Программная реализация точек останова
Прежде чем что-то ломать, необходимо чётко представлять себе архитектурные особенности и программную реализацию клиента, а потому проведём небольшие эксперименты на практике, загрузив подопытное приложение в отладчик, и установив точку-останова, например, на повсеместно используемую функцию вывода сообщений в окно MessageBox().
Теперь запустим отладчик x64Dbg и посмотрим, как он отреагирует на команду установки брейка
Дело в том, что у страниц памяти системных библиотек всегда стоят атрибуты Execute-Read, что потенциально предотвращает возможность их модификации Write (а как иначе записать туда опкод 0xCC). Поэтому отладчики в своих тушках имеют ассоциативные списки учёта точек-останова, где каждая запись имеет формат
Таким образом опкод
3. Способы противодействия точкам останова
От сюда следует, что если мы хотим избавиться в своём коде от брейков на WinAPI, то достаточно функцией VirtualProtect() открыть на запись страницу требуемой библиотеки (в данном случае user32.dll), после чего восстановить оригинальный/первый байт точки-входа в брякнутую функцию. Но проблема здесь в том, что для антивирусов подобная тактика неприемлема, и при обращении к системным DLL с попыткой записи, они тут-же поднимают тревогу, незаслуженно отправляя нашу софтину в блэк-лист. Поэтому малварь и прочие хитрые зверки поступают иначе..
А что, если в своём коде мы будет вызывать API-функцию не с первого байта, а например с восьмого? Тогда установленный отладчиком на точке-входа в функцию брейк уже не сработает, поскольку мы тупо «перепрыгнем» его, и он вообще не получит управления. Звучит заманчиво, но здесь мы сталкиваемся с некоторыми проблемами, которые в принципе решить можно, хотя и с некоторыми исключениями. В общем будем преследовать следующий алгоритм:
На скринах ниже представлены две исследованные мною функции – это наша подопытная MessageBox(), и для разнообразия printf() из либы рантайма msvcrt.dll. Как видим, на системах Win10 и Win7 содержимое первой функции буквально идентично как под копирку, и для реализации задуманного нам будет достаточно скопировать в свой буф первые 7-байт, которые я выделили в блок. Соответственно управление джампом буду передавать на следующий за этим прологом адрес, вычислив его динамически в регистре
А вот состав инструкций функции printf() уже немного отличается на разных осках, хотя и здесь достаточно идентичных байт в прологе. Аналогичную картину можно наблюдать и во-всех остальных WinAPI, а если нет, то лучше сразу отказаться от рассматриваемой нами техники анти-брейкпоинтов.
4. Практика
Ну и под занавес напишем приложение, которое соберёт под один капот всё вышеизложенное.
Чтобы суть всплыла наружу, я оставлю только самое необходимое, хотя для маскировки непосредственно факта копирования пролога, имеет смысл озадачить этим секцию TLS – если реверсер «недавно встал с горшка», авось не додумается включить в отладчике опцию «Останов на секции TLS». А в остальном всё описано в комментах:
5. Заключение
Описанный в данной статье способ противодействия точкам-останова на Win32API отлично справляется со своими обязанностями будучи загруженным на любой отладчик, включая WinDbg, x64Dbg, IDA-Pro, Immunity, и всей их братии. Конечно-же в данном примере все карты раскрыты и обнаружить секрет не составит особого труда. Однако в реальных программах сотни тысяч строк кода, и если запустить его в миксер, то реверсер будет долго недоумевать, почему-же не срабатывает установленная им точка-останова. В общем техника имеет право на существование, а использовать её или нет уже решать вам. В скрепке лежит исполняемый файл для тестов – попробуйте в отладчике установить брейк на MessageBoxA() и протестите фишку. Всем удачи, пока!
Ссылка скрыта от гостей
. Так появился термин «Debugger», что подразумевает борьба с ошибками. За свой почтенный возраст более чем 80 лет, из гадкого утёнка отладчик превратился в прекрасного лебедя – основной инструмент реверс-инженеров, без которого поиск багов в софте просто невозможен. Такие монстры как дизассемблер IDA-Pro, отладчик ядра ОС WinDbg, и x64Dbg пользовательского уровня, не оставляют никаких шансов на выживание малвари, а немаловажную роль играет здесь способность дебагеров устанавливать точки останова на произвольные участки кода «BreakPoint». В данной статье приводятся несколько способов обхода «бряков» на Win32API в надежде, что вы найдёте им применение на практике.Оглавление:
- Системная поддержка отладки
- Программная реализация точек останова
- Способы противодействия
- Практика
- Заключение.
1. Системная поддержка отладки
Процессоры архитектуры х86 имеют аппаратную поддержку отладки, для чего предусмотрен «TrapFlag» TF = бит 8 в регистре флагов
EFLAGS
(пошаговая трассировка), а так-же 8 регистров спец.назначения DR0-DR7
. Более того, начиная с Pentium был добавлен и модельно-специфичный регистр управления MSR.IA32_DEBUGCTL=0x01D9
, который позволяет более тонко контролировать процессы ветвления в коде отладки, например заходить внутрь функции клавишей F7, или пропускать циклы по F4.Поскольку аппаратная поддержка требовательна к ресурсам, имеется и программная реализация пошагового исполнения, которая базируется на том-же флаге трассировке TF. Когда этот флаг взведён, процессор останавливается после исполнения каждой инструкции, передавая управление обработчику прерывания
INT-1h
. По сути номер этого «инта» можно переназначить на произвольный (путём перепрограммирования контроллёра PIC/APIC), но так повелось, что при инициализации BIOS привязывает к Trap’у именно INT-1h
. Чуть позже появилось и прерывание INT-3h
под кличкой «BreakpointTrap», которое позволяет взводить флаг TF по значению регистра-указателя команд EIP
(Instruction Pointer), что равносильно достижении процессором произвольно взятого адреса в памяти.Что касается защищённого режима ЦП в Windows, то для установки брейков (точек останова, далее ВР) здесь используется опкод со-значением
0xCC
, который перехватывается системой для дальнейшей передачи управления на всё тем-же обработчикам INT-1/3h. Если отладчиком WinDbg заглянуть в таблицу диспетчеризации прерываний командой !idt
, то в её логе можно обнаружить указатели на обработчики этих прерываний, и соответственно дизассемблировать их по uf
. Поскольку листинги получаются весьма объёмными, я сократил их ограничившись ключом /i
(возвращает кол-во инструкций), и ключом /с
(только вызовы процедур call без дизасма). Как видим, на обработку брейков ЦП тратит вдвое больше инструкций =833, чем на обычный трап =492:
Код:
0: kd> !idt <------ Interrupt Descriptor Table
Dumping IDT:
00: fffff800028ec900 nt!KiDivideErrorFault
01: fffff800028ecc00 nt!KiDebugTrapOrFault <------- Trap = Step | INT-01
02: fffff800028ed080 nt!KiNmiInterrupt
03: fffff800028ed480 nt!KiBreakpointTrap <------- Breakpoint | INT-03
04: fffff800028ed780 nt!KiOverflowTrap
05: fffff800028eda80 nt!KiBoundFault
06: fffff800028edd80 nt!KiInvalidOpcodeFault
07: fffff800028ee380 nt!KiNpxNotAvailableFault
08: fffff800028ee640 nt!KiDoubleFaultAbort
............
0: kd> uf /i /c fffff800028ecc00
nt!KiDebugTrapOrFault (fffff800`028ecc00), 492 instructions <---------------
call to nt!KiBugCheckDispatch (fffff800`028f20c0)
call to nt!KiCheckForSListAddress (fffff800`0286ed00)
call to nt!KiExceptionDispatch (fffff800`028f2140)
call to nt!KiFlushBhbDuringTrapEntryOrExit (fffff800`028f2f00)
call to nt!KiProcessNMI (fffff800`02957fa0)
call to nt!KiRestoreDebugRegisterState (fffff800`028e2900)
call to nt!KiSaveDebugRegisterState (fffff800`028e2970)
call to nt!KiSaveProcessorState (fffff800`0293f9a0)
call to nt!KiSetSpecCtrlNmi (fffff800`028e6520)
call to nt!KxDebugTrapOrFault+0x202 (fffff800`028ecec2)
call to nt!KxNmiInterrupt (fffff800`028ed380)
0: kd> uf /i /c fffff800028ed480
nt!KiBreakpointTrap (fffff800`028ed480), 833 instructions <-----------------
call to nt!KiBoundFault+0x20b (fffff800`028edc8b)
call to nt!KiBreakpointTrap+0x202 (fffff800`028ed682)
call to nt!KiCopyCounters (fffff800`0293cd00)
call to nt!KiExceptionDispatch (fffff800`028f2140)
call to nt!KiFlushBhbDuringTrapEntryOrExit (fffff800`028f2f00)
call to nt!KiInitiateUserApc (fffff800`028e7ae0)
call to nt!KiInvalidOpcodeFault+0x20b (fffff800`028edf8b)
call to nt!KiOverflowTrap+0x202 (fffff800`028ed982)
call to nt!KiPreprocessInvalidOpcodeFault (fffff800`028cb900)
call to nt!KiRestoreDebugRegisterState (fffff800`028e2900)
call to nt!KiSaveDebugRegisterState (fffff800`028e2970)
call to nt!KiUmsExit (fffff800`028f2d80)
call to nt!KiUmsTrapEntry (fffff800`028f2900)
0: kd>
На уровне пользовательского режима, отладка поддерживается специальным классом
Ссылка скрыта от гостей
из либы Kernel32.dll. Все отладчики третьего кольца (такие как x64Dbg, OllyDbg, и прочие) используют функции именно из этой библиотеки, а для аппаратных точек на чтение/записи/исполнение памяти и обращения к портам ввода-вывода, обращаются к упомянутым регистрам DR0-DR7
. Детали устройства этих регистров уже обсуждались в статье «Скрытый потенциал регистров отладки», так-что не буду здесь повторяться.2. Программная реализация точек останова
Прежде чем что-то ломать, необходимо чётко представлять себе архитектурные особенности и программную реализацию клиента, а потому проведём небольшие эксперименты на практике, загрузив подопытное приложение в отладчик, и установив точку-останова, например, на повсеместно используемую функцию вывода сообщений в окно MessageBox().
Теперь запустим отладчик x64Dbg и посмотрим, как он отреагирует на команду установки брейка
bp MessageBoxA
в ком.строке, и запуске приложения на исполнение по F9. Здесь видим, что ловушка Trap сработала и мы оказались в нёдрах библиотеки User32.dll, однако опкода 0xCC
не видно ни в окне кода, ни в окне дампа памяти, которому мы задали адрес функции MessageBox() = 0x77AF1A20
– это продемонстрировано на скрине ниже. По идее опкод ВР должен был заменить первый-же байт функции со-значением (в данном случае) 0x48
, играющего роль префикса 64-битной инструкции. Тогда каким-же образом сработал пошаговый механизм отладчика?Дело в том, что у страниц памяти системных библиотек всегда стоят атрибуты Execute-Read, что потенциально предотвращает возможность их модификации Write (а как иначе записать туда опкод 0xCC). Поэтому отладчики в своих тушках имеют ассоциативные списки учёта точек-останова, где каждая запись имеет формат
Address:Type:Status
(адрес, тип брейка, и его состояние вкл/выкл), которые и проверяются при исполнении отлаживаемого кода. Например вот лог WinDbg, в котором я открыл это-же приложение, и установив брейк на MessageBox(), после его срабатывания проверил флаги состояния памяти – как видим это PAGE_EXECUTE_READ
без доступа на запись:
Код:
0:000> bp @$exentry <-------- WinDbg встаёт на системной точке, поэтому меняем её на ЕР софта
0:000> bl
0 e 00000000`00402000 0001 (0001) 0:**** image00000000_00400000+0x2000 <--- Правильная точка входа
0:000> g
Breakpoint 0 hit
image00000000_00400000+0x2000:
00000000`00402000 4883ec08 sub rsp,8
0:000> bp user32!MessageBoxA <----- Ставим брейк на WinAPI и проверяем лист (е = Enable)
0:000> bl
0 e 00000000`00402000 0001 (0001) 0:**** image00000000_00400000+0x2000
1 e 00000000`77af1a20 0001 (0001) 0:**** user32!MessageBoxA
0:000> g
Breakpoint 1 hit
user32!MessageBoxA:
00000000`77af1a20 4883ec38 sub rsp,38h
0:000> !address user32!MessageBoxA <------------- Сбор информации об адресе
Usage: Image
Allocation Base: 00000000`77a80000
Base Address: 00000000`77a81000
End Address: 00000000`77b02000
Region Size: 00000000`00081000
Type: 01000000 MEM_IMAGE
State: 00001000 MEM_COMMIT
Protect: 00000020 PAGE_EXECUTE_READ <---- Упс: Запрет на запись!
More info: lmv m user32
More info: !lmi user32
More info: ln 0x77af1a20
Таким образом опкод
0xCC
прерывания INT-3
на самом деле существует, просто все инструменты отладки маскируют его в своих списках учёта брейков хотя-бы потому, что иных способов заставить ЦП остановиться на указанном адресе просто нет. Отладчик – это далеко не та сущность, которой следует доверять, поскольку он функционирует только в своих интересах. Разработчики стараются по максимуму приблизить обстановку к реальному положению дел, в результате чего многое приходится скрывать и эмулировать. В этом легко убедиться, если вставить в код небольшую процедуру проверки первого байта API-функции на опкод 0xCC
например так:
C-подобный:
format pe64 console ;// микс консольного приложения с гуем
include 'win64ax.inc'
entry start
;//-----------
section '.data' data readable writeable
buff db 0
;//-----------
section '.text' code readable executable writeable
start: sub rsp,8
mov rax,[MessageBox]
push rax ;// запомнить адрес MessageBox()
cinvoke wsprintf,buff,<'Адрес MessageBoxA: 0x%016I64X',0>,rax ;// заполнить буф строкой
pop rsi
cmp byte[rsi],0xCC ;//<----------- проверить первый байт на опкод 0xCC = INT-3h
jnz @f ;// пропустить, если нет
cinvoke printf,<10,' Warning! Breakpoint in MessageBoxA',0> ;// иначе вкл сирену!
@@: invoke MessageBox,0,buff,<'Anti-Breakpoint',0>,0 ;// просто форточка с мессагой
@exit: invoke ExitProcess, 0
;//-----------
section '.idata' import data readable writeable
library kernel32,'kernel32.dll',user32,'user32.dll',msvcrt,'msvcrt.dll'
include 'api\kernel32.inc'
include 'api\user32.inc'
include 'api\msvcrt.inc'
3. Способы противодействия точкам останова
От сюда следует, что если мы хотим избавиться в своём коде от брейков на WinAPI, то достаточно функцией VirtualProtect() открыть на запись страницу требуемой библиотеки (в данном случае user32.dll), после чего восстановить оригинальный/первый байт точки-входа в брякнутую функцию. Но проблема здесь в том, что для антивирусов подобная тактика неприемлема, и при обращении к системным DLL с попыткой записи, они тут-же поднимают тревогу, незаслуженно отправляя нашу софтину в блэк-лист. Поэтому малварь и прочие хитрые зверки поступают иначе..
А что, если в своём коде мы будет вызывать API-функцию не с первого байта, а например с восьмого? Тогда установленный отладчиком на точке-входа в функцию брейк уже не сработает, поскольку мы тупо «перепрыгнем» его, и он вообще не получит управления. Звучит заманчиво, но здесь мы сталкиваемся с некоторыми проблемами, которые в принципе решить можно, хотя и с некоторыми исключениями. В общем будем преследовать следующий алгоритм:
1. Копируем пролог функции в несколько байт в специально выделенный для этих целей свой буфер. Он обязательно должен иметь атрибуты
Execute-Read-Write
. Можно создать отдельную секцию в программе, или-же просто открыть на запись уже имеющуюся секцию-кода. В силу того, что у всех упаковщиков типа UPX код всегда открыт на запись, аверы легко пропускают его между ног, что играет нам только на руку.
2. В архитектуре х86 размер инструкций носит переменный характер, и варьируется в широком диапазоне от 1 до 16-байт. Поэтому нам нужно в качестве копируемого пролога вычислить такое кол-во байт, чтобы не «разорвать» инструкцию пополам. Это основная проблема, к которой нужно отнестись серьёзно. К счастью содержимое большинства функций WinAPI переходит от версии к версии ОС практически без изменений, при условии одинаковой их разрядности х32 или х64.
3. Определившись с размером пролога, мы должны будем джампом
JMP
передать управление на следующий за прологом байт, а значит нужно сохранить и его адрес. При этом прыгать обязательно нужно по абсолютному, а не относительному адресу, т.к. последний представляет собой смещение от текущего. Достаточно в качестве аргумента инструкции JMP
использовать любой из регистров ЦП.На скринах ниже представлены две исследованные мною функции – это наша подопытная MessageBox(), и для разнообразия printf() из либы рантайма msvcrt.dll. Как видим, на системах Win10 и Win7 содержимое первой функции буквально идентично как под копирку, и для реализации задуманного нам будет достаточно скопировать в свой буф первые 7-байт, которые я выделили в блок. Соответственно управление джампом буду передавать на следующий за этим прологом адрес, вычислив его динамически в регистре
RSI
инструкцией копирования rep movsb
. При желании можно прихватить N-нное число и следующих байт, просто в этом нет особого смысла:А вот состав инструкций функции printf() уже немного отличается на разных осках, хотя и здесь достаточно идентичных байт в прологе. Аналогичную картину можно наблюдать и во-всех остальных WinAPI, а если нет, то лучше сразу отказаться от рассматриваемой нами техники анти-брейкпоинтов.
4. Практика
Ну и под занавес напишем приложение, которое соберёт под один капот всё вышеизложенное.
Чтобы суть всплыла наружу, я оставлю только самое необходимое, хотя для маскировки непосредственно факта копирования пролога, имеет смысл озадачить этим секцию TLS – если реверсер «недавно встал с горшка», авось не додумается включить в отладчике опцию «Останов на секции TLS». А в остальном всё описано в комментах:
C-подобный:
format pe64 console
include 'win64ax.inc'
entry start
;//-----------
section '.data' data readable writeable
MsgBoxEntry dq 0
msg db 'Установи на меня Breakpoint!',0
buff db 0
;//-----------
section '.text' code readable executable writeable ;//<--- доступ на запись
;//----- TLS callback --------------------
proc DeleteBPoint
mov rsi,[MessageBox] ;// взять адрес функции
mov edi,MyMBox ;// приёмный буфер
mov ecx,7 ;// байт для копирования
rep movsb ;// перегнать из RSI в RDI
add byte[MyMBox+3],8 ;// сбить сигнатуру пролога в буфере
mov [MsgBoxEntry],rsi ;// запомнить указатель для JMP
ret
endp
align 8
start: sub rsp,8 ;//<----------- Точка входа!
frame
invoke GetCurrentThread ;// здесь основной код программы..
invoke Wow64GetThreadSelectorEntry,rax,10h,buff
endf
cinvoke printf,<10,' **** Hello Hacker! ****',0> ;// напечатаем что-нибудь
cmp byte[MyMBox],0xCC ;// чекнуть первый байт на INT-3
jnz @f ;// пропустить, если ложь
dec ebp ;// иначе: мусор
mov edx,ebp ;//
mov byte[MyMBox],0x48 ;// ...и восстановить оригинал!
@@: push 0 0 0 ;// вызов API через свой буф
pop rcx r8 r9 ;// аргументы MessageBox()
mov rdx,msg ;//
mov rsi,[MsgBoxEntry] ;// RSI = адрес после пролога
call MyMBox ;// передать на него управление!!!
@exit: cinvoke _getch ;// ждём клавишу..
cinvoke exit, 0 ;// на выход
;//*************** Буфер для копирования пролога API ***************
MyMBox: rb 7 ;// резерв для первых 7 байт *
add rsp,8 ;// восстановить сигнатуру пролога *
jmp rsi ;// вызвать API не с первого байта! *
;//*****************************************************************
section '.idata' import data readable writeable
library msvcrt,'msvcrt.dll',kernel32,'kernel32.dll',user32,'user32.dll'
include 'api\msvcrt.inc'
include 'api\kernel32.inc'
include 'api\user32.inc'
;//--------------------------- Секция TLS ----------------------------------------
data 9
dq @tls,@tls,@tls ;// линк на начало/конец лок.памяти, и индекс треда
dq cback ;// линк на Callback цепочку
;//******************** Статическая память TLS ***********************************
@tls dq 0 ;// будет индексом треда (заполняет загрузчик) *
cback dq DeleteBPoint,0 ;// цепочка указателей на Callback (нультерминал) *
;//*******************************************************************************
end data
5. Заключение
Описанный в данной статье способ противодействия точкам-останова на Win32API отлично справляется со своими обязанностями будучи загруженным на любой отладчик, включая WinDbg, x64Dbg, IDA-Pro, Immunity, и всей их братии. Конечно-же в данном примере все карты раскрыты и обнаружить секрет не составит особого труда. Однако в реальных программах сотни тысяч строк кода, и если запустить его в миксер, то реверсер будет долго недоумевать, почему-же не срабатывает установленная им точка-останова. В общем техника имеет право на существование, а использовать её или нет уже решать вам. В скрепке лежит исполняемый файл для тестов – попробуйте в отладчике установить брейк на MessageBoxA() и протестите фишку. Всем удачи, пока!