Статья Самотрассировка, или марш-бросок по периметру отладчика

Marylin

Marylin

Mod.Assembler
Red Team
05.06.2019
148
455
Как и всё, что работает под управлением центрального процессора, операционная система – это тоже программа, которая на этапе тестирования требовала отладки. Поскольку системное пространство памяти в Win логически разделено на два региона, инженерам требовались технические люки из юзера в кернел. После того-как всё было (типа) настроено, окончательно избавляться от этих туннелей было уже поздно – пришлось-бы опять переписывать львиную долю кода, что совсем не вдохновляло разработчиков. Поэтому лазейки просто прикрыли фиговым листом в надежде "авось прокатит"..

Это касается таких механизмов как TRAP-бит в регистре флагов Eflags, привелегия Debug в пользовательском режиме, регистры отладки DR0-DR7 и несколько модельно-специфичных регистров MSR. В результате не только админ, но и обычный юзер получил в награду аппаратные рычаги дебага, не воспользоваться которыми было-бы грех – нужно просто отключив шаблонное мышление обратить взор чуть дальше собственного носа.

В статье рассматриваются несколько простых фишек, которые помогут нам применить в своих программах довольно могучий защитный приём, с убойным радиусом поражения – это код, который будет трассировать сам-себя, но не будет дебажиться в отладчике.
----------------------------------------


Debug в реальном режиме

Начнём с того, что отладка – неотъемлемая часть программирования, и фактически поддерживается на аппаратном уровне через флаг трассировки процессора TRAP-Flag. Манипулируя битами регистра EFLAGS, программист может оперировать флагом TF как в реальном, так и в защищённом режиме работы процессора, и в обоих случаях на единичное его состояние CPU реагирует исключением #DB – Debug Breakpoint. Более того, в независимости от текущего режима, за обработку этого исключения отвечает прерывание INT-1.

Обработчик INT-1 в реальном режиме представляет из себя шунт-заглушку IRET (Interrupt Return), по типу зашёл/вышел без каких-либо действий. По этой причине, без отладчика от взведённого флага TF программам реального режима "не холодно, не жарко". В качестве демонстрации я загрузился под чистым DOS, и в отладчике авера KAV дизассемблировал INT-1 (см.в меню Alt+U):

avpUtil.png


Пусть вас не смущает здесь устаревший термин BIOS.. для обратной совместимости он эмулируется всеми современными EFI. Кстати, если запустить виндовый debug, то перед нами предстанет совсем иная картина. Дело в том, что Win32 эмулирует DOS в режиме процессора V86 (virtual), и соответственно обработчики у него свои, а не чисто-досовские. Например на 32-битных системах можно запустить debug.com прямо из виндовой консоли и командой –d 0:0 просмотреть таблицу векторов прерываний IVT (Interrupt Vector Table) – дворд по смещению (4) и будет вектором INT-1 (после нулевого). Теперь командой –u 0070:018b дизассемблируем этот адрес и получим такой код:

Код:
C:\> debug  ;// векторы:   INT-1                     INT-3
-d 0:0      ;//         -----------               -----------
0000:0000   68 10 A7 00 8B 01 70 00 - 16 00 98 03 8B 01 70 00   h.....p.......p.
0000:0010   8B 01 70 00 B9 06 0E 02 - 40 07 0E 02 FF 03 0E 02   ..p.....@.......
0000:0020   46 07 0E 02 0A 04 0E 02 - 3A 00 98 03 54 00 98 03   F.......:...T...
0000:0030   6E 00 98 03 88 00 98 03 - A2 00 98 03 FF 03 0E 02   n...............
-
-u 0070:018b
0070:018B   1E            PUSH    DS
0070:018C   50            PUSH    AX
0070:018D   B84000        MOV     AX,0040
0070:0190   8ED8          MOV     DS,AX
0070:0192   F70614030024  TEST    WORD PTR [0314],2400   ;// тест поля 0040:0314 на режим V86
0070:0198   754F          JNZ     01E9                   ;// выйти если нет!
0070:019A   55            PUSH    BP
0070:019B   8BEC          MOV     BP,SP
0070:019D   8B460A        MOV     AX,[BP+0A]      ;// иначе: AX = значение регистра FLAGS
0070:01A0   5D            POP     BP
0070:01A1   A90001        TEST    AX,0100         ;// проверить в нём бит(8) Trap-flag
0070:01A4   7543          JNZ     01E9            ;// выйти, если TF=1 (не нуль)
;//~~~~~~~~~~~~~~
0070:01E9   58            POP     AX
0070:01EA   1F            POP     DS
0070:01EB   CF            IRET                    ;// Interrupt Return
Таким образом, если мы хотим написать свой отладчик реального режима, то должны перехватить прерывание INT-1, и в своём обработчике организовать вывод на экран состояния регистров процессора и прочую служебную информацию. Процессор будет ждать от нашего обработчика инструкцию IRET, после чего наткнётся на следующую инструкцию в коде и опять сгенерит исключение #DB – круг замкнётся.. Так будет продолжаться до тех пор, пока мы принудительно не сбросим флаг TF в нулевое состояние.


Флаг трассировки в защищённом режиме процессора

После того как мастдай получает управление от загрузчика (NTLDR в win-xp, или BOOTMGR в win7+) и отправляет на покой реальный режим, картина координально меняется. Теперь уже нету таблицы векторов-прерываний IVT в том смысле, который вкладывает в неё DOS – ей на замену Win выстраивает свою таблицу IDT – Interrupt Dispatch Table (таблица диспетчеризации прерываний) со-своими обработчиками. В защищённом режиме, термин "обработчик" переименовали в ISR – Interrupt Service Routine (подпрограммы сервисов) и делятся они на два типа – "ловушка" (trap) для отлова исключений, и "шлюзы" процедур обработки прерываний.

Размер и адрес таблицы IDT хранится в регистре IDTR текущего процессора – с пользовательского уровня этот регистр доступен только для чтения инструкцией SIDT (store IDT). В отличии от остальных регистров, размер его 6-байт, где первые 2-байта определяют размер (лимит) таблицы, а следующие 4-байта – базу таблицы в ядерной памяти. Сама таблица IDT занимает один сегмент виртуальной памяти и состоит из 256 8-байтных дескрипторов – в каждом дескрипторе хранятся флаги доступа и указатель на ISR конкретного прерывания. Код ниже демонстрирует вариант чтения и вывода на консоль значения регистра IDTR:

C-подобный:
format   pe console
include 'win32ax.inc'
entry    start
;-----
.data
capt     db  13,10,' IDT info v0.1'
         db  13,10,' *********************'
         db  13,10,' Base...: 0x%08X'
         db  13,10,' Limit..: 0x%04X',0
value    dd  0,0
frmt     db  '%s',0
;-----
.code
start:   mov     ebx,value             ;// EBX = адрес приёмника
         sidt    [ebx]                 ;// Store_IDTR по адресу

         movzx   eax, word[value]      ;// EAX = 2-байтный лимит таблицы
         mov     ebx,dword[value+2]    ;// EBX = сл.4-байта = адрес таблицы
        cinvoke  printf,capt,ebx,eax   ;// выводим на консоль!

@exit:  cinvoke  scanf,frmt,frmt+2
        cinvoke  exit,0
;//xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
section  '.idata' import data readable
library  msvcrt,'msvcrt.dll'
import   msvcrt, printf,'printf',scanf,'scanf',exit,'exit'
idt_base.png


Отладчик ядерного уровня WinDbg, на команду !idt –a отзывается более информативным сообщением. Отметим, что системы класса Win обрабатывают не все 256 прерываний INT 0x00..0xFF – большая часть из них вообще необрабатываемые "Unexpected_Interrupt" и попадают они прямиком в цепкие лапы диспетчера исключений, который тупо проглатывает их, даже не оповещая об этом юзера:

wdb_idt.png


Во-всей этой кухне, важным для нас моментом является то, что отладчики пользовательского уровня вообще не используют аппаратный флаг TF для своей работы, соответственно и прерывание INT-1 остаётся не при делах. По текущему указателю регистра EIP они вставляют только программные бряки INT-3 с однобайтным опкодом 0xCC. То-есть такие отладчики как "OllyDbg", на каждом шаге запоминают байт по указателю EIP, и на его место записывают INT-3. Так-что обычной проверкой своей контрольной суммы можно обнаружить факт отладки приложения.


Самотрассировка – идея фикс..

В общем случае мы пришли к тому, что поведение программы со-взведённым флагом TF напрямую зависит от окружающей обстановки. Отладчики любого цвета и ориентации маскируют TF, перехватывая обращения к нему с любого уровня. Зато в реальной ситуации и без отладчика, при взведённом TF процессор моментально генерит исключение #DB и если оно не обрабатывается пользовательским SEH-фреймом, то диспетчер-исключений тупо прибивает процесс, посчитав его глючным.

В 32-битном регистре флагов EFLAGS, бит TF занимает позицию (8), однако мы не можем модифицировать его напрямую и приходиться осуществлять это действие через стек в три этапа – запомнить, чекнуть, восстановить. Вот несколько нехитрых способов установки флага TF в единицу:

C-подобный:
;// (1) Модификация через EAX и BTS (bit select)
;//---------------------------------------------
  pushfd              ;// запомнить регистр EFLAGS в стеке
  pop     eax         ;// снять его в регистр EAX
  bts     eax,8       ;// взвести в EAX бит 8 = TF
  push    eax         ;// затолкать его опять в стек
  popfd               ;// перезаписать EFLAGS

;// (2) Взводим TF через OR AH
;//-----------------------------------------
  pushfd              ;// EFLAGS в стек
  pop     eax         ;// EAX = EFLAGS
  or      ah,1        ;// взвести в AH бит 1 (AH биты 15-8, AL биты 7-0)
  push    eax         ;// затолкать его опять в стек
  popfd               ;// перезаписать EFLAGS значением EAX

;// (3) Прямой мод в стеке
;//-------------------------
  pushfd
  or      dword[esp],0x100   ;// взвести бит 8 по адресу ESP
  popfd
Если в этот момент наш процесс не отлаживается, то после выполнения следующей за POPFD инструкции, системный инквизитор тут-же выстрелит в нас окном где сказано, что это необрабатываемое "Unknown" исключение, и что наше приложение больше не жилец. На самом деле система знает, что это Single-Step, просто обрабатывать его не видит причин – кому надо, тот пусть и занимается этим:

exept_single.png


Если у кого-то это окно и вызывает отвращение, то нам оно только радует глаз. Самотрассировка - self-Debug - подразумевает установку SEH-обработчика на это исключение, с последующей модификацией флага TF. Внутри обработчика мы можем вытворять со-своим кодом всё-что пожелает наша душа, и это будет абсолютно прозрачно для отладчика! Поскольку он постоянно фиксит флаг TF и обнаружив сразу-же сбрасывает его в нуль, то соответственно и исключения Single-Step под отладчиком не возникает. В результате, пользовательский SEH не получит уже управления, и Оля пойдёт топтать совсем не ту тропинку,.

Нужно сказать, что Win32-отладчики поглащают только отладочное исключение #DB, а остальные исправно отлавливают, передавая управление SEH-фрейму юзера. Так-что представленная на суд идея имеет право на жизнь только при взведённом флаге TF с прерыванием INT-1:

pm_exept.png


Оригинальное мнение на счёт взведённого флага TF имеет первая версия отладчика OllyDbg, которая напрочь отказывается трассировать весь последующий код, и позорно капитулирует сообщением типа: "Я не в курсе, как реагировать на команду по этому адресу. Попробуйте установить точку-останова на следующей команде". Если внять совету отладчика и установить по F2 бряк (в данном случае) на адрес 0х0040204В, то ситуацию это всё-равно не спасет и исключения #DB не произойдёт в любом случае, а значит и наш SEH не получит управления. Вот и попробуй разберись с этой мутью..

olly1_tf.png



Детали реализации кода

Чтобы воплотить идею в жизнь, рассмотрим некоторые её нюансы..
Значит установка пользовательского SEH-обработчика и флага TF в единицу, позволяют перехватить исключение #DB в неотлаживаемом процессе и получить контекст всех регистров CPU. (теме SEH была посвящена отдельная статья). Чтобы лишним стековым SEH-фреймом не привлекать внимание взломщика, хорошей идеей будет не расширять цепочку обработчиков, а модифицировать уже имеющийся системный фрейм с маркером -1, просто подсунув ему указатель на свой обработчик (мы ведь не планируем финализировать свою прожку). В этом случае, в стеке будут маячить уже не два фрейма, а дефолтный один. Пример такой махинации представлен ниже:

C-подобный:
;// Вставляем в системный SEH-фрейм свой обработчик
;//------------------------------------------------
  sub     ebx,ebx               ;// EBX = 0
  push    dword[fs:ebx]         ;// указатель на SEH в стек (fs:0)
  pop     ebp                   ;// снимаем его в EBP
  add     ebp,4                 ;// смещаемся к обработчику в SEH-фрейме
  mov     dword[ebp],new_Seh    ;// записать туда свой адрес!
olly2_tf.png


Опытный глаз конечно-же сразу обнаружит подвох с SEH-фреймом, ведь финальный обработчик системы с терминальным 0хFFFFFFFF находится в kernel32.dll и не может указывать на наш код. Но задурманить таким приёмом мозги пионерам–крэкерам вполне возможно. Причём в данном случае я поместил в стек указатель на фрейм в начале кода, а осуществляю подмену чуть позже (желательно делать это вообще в другой ветке).

На рисунке так-же видно, что вторая версия отладчика OllyDbg категорически не хочет выставлять флаг TF в единицу, и комбинация перезаписи регистра EFLAGS инструкциями push eax/popfd здесь не срабатывает – флаги как были 0х206, так и остались.

Ещё следует обратить внимание на то, что в отличии от реального режима, в защищённом система сбрасывает флаг TF после каждого стэпа, поэтому внутри обработчика его нужно взводить по-новой, чтобы после исполнения очередной инструкции SEH-обработчик опять получил управление. Отметим, что исключение #DB процессор генерит не НА текущей инструкции, а уже ПОСЛЕ её исполнения. Такой алгоритм процессора освобождает нас от постоянной коррекции регистра EIP внутри обработчика – достаточно только взводить TF на каждом шаге, а EIP сам будет скакать по нужным инструкциям.

Чтобы убедиться в этом, можно написать тестовый код, который взведёт флаг TF и из регистрового контекста SEH, выведет на экран состояние регистров EIP и EFLAGS. На рисунке ниже, окно переднего плана было запущено под реальным процессором без отладчика, а в отладчике – я тупо просмотрел, куда указывает регистр EIP. Здесь видно, что если SEH-обработчик не спит и из его трубы идёт дым, то он получает управление уже после выполнения инструкции JMP_SHORT, и флаг TF в структуре CONTEXT оказался опять сброшен:

trap_Test.png



Практическая часть

Приведённый ниже пример демонстрирует эту технику. Я старался сделать его максимально понятным, и убрал из него всё лишнее. Здесь обычная проверка пароля, код которой изначально находится в секции-данных программы, от куда потом копируется в выделенную память SEH-обработчиком. По окончанию, на этот код SEH сразу-же передаёт управление. Алгоритм будет работать только в неотлаживаемом процессе, при взведённом флаге TF. Пароль на валидность проверяется по его хэш-сумме, инклуд "except.inc" предоставляет доступ к структуре CONTEXT процессора, и прикреплён скрепкой в подвале темы (нужно положить его в дир "fasm\include\.."):

C-подобный:
format   pe console
include 'win32ax.inc'
include 'except.inc'       ;//<--- смотри скрепку
entry    start
;//-----
.data
capt     db  13,10,' TRAP_Crackme v0.1'
         db  13,10,' *************************'
         db  13,10,' Type pass: ',0
okey     db  13,10,' Pass OK!',0
wrong    db  13,10,' Pass WRONG!',0
pass     dw  'T'+'R'+'A'+'P'+'f'+'l'+'a'+'g'   ;// хэш валидного пароля (0x02d1)
frmt     db  '%s',0
len      =   endCheckPass - CheckPass          ;// длина продуры проверки

;//=== Процедура проверки пароля, по его хэшу =============
;// позже скопируем её в выделенную область памяти. =======
CheckPass:                     ;//<--- маркер начала.
         mov     esi,buff        ;// ESI = адрес введённого юзером пароля
         and     ebx,0           ;// EBX =0
         mov     eax,ebx         ;// EAX =0
@01:     lodsb                   ;//  AL = очередной байт из ESI
         add     bx,ax           ;// считать сумму в регистр ВХ
         or      al,al           ;// проверка AL на нуль
         jnz     @01             ;// повторить, если не нуль..
         mov     eax,okey        ;// иначе: EAX = адрес мессаги "OK!"
         cmp     bx,[pass]       ;// сравнить хэш юзера с валидным хэшем.
         je      @prn            ;// если равно..
         mov     eax,wrong       ;// иначе: заменить EAX на "Wrong!"
@prn:   cinvoke  printf,eax      ;// мессагу на консоль
         push    @exit           ;// дальний (far) адрес перехода
         ret                     ;// перейти на него!!!
endCheckPass:                  ;//<--- маркер конца блока.
;//========================================================
buff     db  ?     ;// буфер для юзерского пароля,
                   ;// простилается до конца секции-данных,
                   ;// так-что переполнение scanf() нам не грозит.

;//-----
.code
start:   pushfd                       ;// флаги в стек
         sub     ebx,ebx              ;// EBX =0
         push    dword[fs:ebx]        ;// запомнить указатель на системный SEH

        cinvoke  printf,capt          ;// запрос на ввод пароля
        cinvoke  scanf,frmt,buff      ;//  .........^^^

;// Вставляем в системный SEH-фрейм свой обработчик
         pop     ebp                  ;// EBP = линк на него
         add     ebp,4                ;// смещаемся к адресу обработчика
         mov     dword[ebp],new_Seh   ;// мод системного SEH-фрейма!

;// Взводим флаг TF, и если процесс не отлаживается,
;// то управление примет наш обработчик исключений SEH.
         or      dword[esp],0x100     ;// взвести бит (8) в стеке
         popfd                        ;// перезапись регистра EFLAGS
         nop                          ;//
         xchg    eax,eax              ;// NOP, чтоб сгенерить исключение #DB
        cinvoke  printf,wrong         ;// если под отладчиком, то "Wrong_Pass"

@exit:  cinvoke  scanf,frmt,frmt+2    ;// конец программы!
        cinvoke  exit,0               ;//

;//~~~~~~ Пользовательский SEH-обработчик ~~~~~~~~~
;// Выделяет память с атрибутами RWEx,
;// копирует туда процедуру проверки пароля из секции-данных,
;// и передаёт на неё управление. ~~~~~~~~~~~~~~~~~
align   256                           ;// отделить SEH от полезного кода
new_Seh: invoke  VirtualAlloc,0,0x1000,0x3000,0x40  ;// выделить Execute-память
         push    eax                  ;// запомнить указатель на неё
         mov     edi,eax              ;// EDI = приёмник (выделенная память)
         mov     esi,CheckPass        ;// ESI = источник
         mov     ecx,len              ;// ECX = длина копируемого блока
         rep     movsb                ;// скопировать из ESI в EDI !!!

         pop     eax                  ;// EAX = указатель на начало кода
         mov     esi,[esp+12]         ;// ESI = линк на контекст регистров
         mov    [esi+CONTEXT.regEip],eax   ;// ставим EIP на начало кода-проверки
         xor     eax,eax              ;// команда "ребут контекста" диспетчеру
         ret                          ;// выйти из SEH-обработчика!!!
;xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
section  '.idata' import data readable
library  msvcrt,'msvcrt.dll', kernel,'kernel32.dll'
import   msvcrt, printf,'printf',scanf,'scanf',exit,'exit'
import   kernel, VirtualAlloc,'VirtualAlloc'
Для наглядности, здесь я упростил задачу до максимума, и внутри SEH сбрасываю в выделенную память весь код по одному/единственному исключению #DB. Но гораздо интересней залезть под капот алгоритма и исполнять код-проверки буквально на каждом шаге процессора, увеличив тем-самым скважность исключений #DB. Во-первых это на порядок усложнит взлом, а во-вторых – добавит нам скилла.

Организовать это дело совсем не сложно и если кого заинтересует этот приём, мы можем осуществить его вместе. Нужно всего-то добавить в процедуру проверки-пароля "флаг окончания", и внутри SEH-обработчика проверять его на единицу. Если индикатор = нуль, значит взводим TF в регистровом контексте и продолжаем трэйс (менять больше ничего не надо). Соответственно, если "флаг окончания теста" =1, значит оставляем TF сброшенным и заканчиваем трэйс.


Заключение (в хорошем смысле слова)

Здесь мы рассмотрели только основную идею самотрассировки в надежде, что это послужит генератором идей. В этом направлении, наши возможности ограничивает только фантазия – например, можно озадачить SEH распаковкой или декриптором зашифрованного кода, жонглировать контекстом любых регистров, вплоть до отладочных DR0-DR7 (которые для юзера считаются привилегированными), и многое другое. А всё потому, что в брачном периоде, парнями из Microsoft была заброшена капсула, которая со-временем сыграла злую шутку с отладчиками прикладного уровня – они доверяли флагу TF на все 100%, а он оказался хитрым на все 200. До скорого..
 

Вложения

Последнее редактирование:
Hardreversengineer

Hardreversengineer

Member
20.08.2019
24
5
Я аж всплакнул, думал не дождусь.
Как же мне нравятся вставки кода в статье)
 
Aleks Binakril

Aleks Binakril

Active member
04.12.2019
35
4
Увлекательная статья, автор подскажите а у вас есть такие же идеи по написанию статей по анпакингу
 
Marylin

Marylin

Mod.Assembler
Red Team
05.06.2019
148
455
@Aleks Binakril просьба в комментах оставлять только то, что касается данной темы - остальные вопросы в личку.
 
Mikl___

Mikl___

New member
01.09.2019
4
7
Marylin,
большое спасибо за статью!
 
Последнее редактирование:
  • Нравится
Реакции: Marylin
Мы в соцсетях: