Как и всё, что работает под управлением центрального процессора, операционная система – это тоже программа, которая на этапе тестирования требовала отладки. Поскольку системное пространство памяти в 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
Пусть вас не смущает здесь устаревший термин BIOS.. для обратной совместимости он эмулируется всеми современными EFI. Кстати, если запустить виндовый debug, то перед нами предстанет совсем иная картина. Дело в том, что Win32 эмулирует DOS в режиме процессора V86 (virtual), и соответственно обработчики у него свои, а не чисто-досовские. Например на 32-битных системах можно запустить debug.com прямо из виндовой консоли и командой
Таким образом, если мы хотим написать свой отладчик реального режима, то должны перехватить прерывание 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:
Отладчик ядерного уровня WinDbg, на команду
Во-всей этой кухне, важным для нас моментом является то, что отладчики пользовательского уровня вообще не используют аппаратный флаг TF для своей работы, соответственно и прерывание INT-1 остаётся не при делах. По текущему указателю регистра EIP они вставляют только программные бряки INT-3 с однобайтным опкодом
Самотрассировка – идея фикс..
В общем случае мы пришли к тому, что поведение программы со-взведённым флагом TF напрямую зависит от окружающей обстановки. Отладчики любого цвета и ориентации маскируют TF, перехватывая обращения к нему с любого уровня. Зато в реальной ситуации и без отладчика, при взведённом TF процессор моментально генерит исключение #DB и если оно не обрабатывается пользовательским SEH-фреймом, то диспетчер-исключений тупо прибивает процесс, посчитав его глючным.
В 32-битном регистре флагов EFLAGS, бит TF занимает позицию (8), однако мы не можем модифицировать его напрямую и приходиться осуществлять это действие через стек в три этапа – запомнить, чекнуть, восстановить. Вот несколько нехитрых способов установки флага TF в единицу:
Если в этот момент наш процесс не отлаживается, то после выполнения следующей за
Если у кого-то это окно и вызывает отвращение, то нам оно только радует глаз. Самотрассировка - self-Debug - подразумевает установку SEH-обработчика на это исключение, с последующей модификацией флага TF. Внутри обработчика мы можем вытворять со-своим кодом всё-что пожелает наша душа, и это будет абсолютно прозрачно для отладчика! Поскольку он постоянно фиксит флаг TF и обнаружив сразу-же сбрасывает его в нуль, то соответственно и исключения Single-Step под отладчиком не возникает. В результате, пользовательский SEH не получит уже управления, и Оля пойдёт топтать совсем не ту тропинку,.
Нужно сказать, что Win32-отладчики поглащают только отладочное исключение #DB, а остальные исправно отлавливают, передавая управление SEH-фрейму юзера. Так-что представленная на суд идея имеет право на жизнь только при взведённом флаге TF с прерыванием INT-1:
Оригинальное мнение на счёт взведённого флага TF имеет первая версия отладчика OllyDbg, которая напрочь отказывается трассировать весь последующий код, и позорно капитулирует сообщением типа: "Я не в курсе, как реагировать на команду по этому адресу. Попробуйте установить точку-останова на следующей команде". Если внять совету отладчика и установить по F2 бряк (в данном случае) на адрес
Детали реализации кода
Чтобы воплотить идею в жизнь, рассмотрим некоторые её нюансы..
Значит установка пользовательского SEH-обработчика и флага TF в единицу, позволяют перехватить исключение #DB в неотлаживаемом процессе и получить контекст всех регистров CPU. (теме SEH была посвящена отдельная статья). Чтобы лишним стековым SEH-фреймом не привлекать внимание взломщика, хорошей идеей будет не расширять цепочку обработчиков, а модифицировать уже имеющийся системный фрейм с маркером -1, просто подсунув ему указатель на свой обработчик (мы ведь не планируем финализировать свою прожку). В этом случае, в стеке будут маячить уже не два фрейма, а дефолтный один. Пример такой махинации представлен ниже:
Опытный глаз конечно-же сразу обнаружит подвох с SEH-фреймом, ведь финальный обработчик системы с терминальным
На рисунке так-же видно, что вторая версия отладчика OllyDbg категорически не хочет выставлять флаг TF в единицу, и комбинация перезаписи регистра EFLAGS инструкциями
Ещё следует обратить внимание на то, что в отличии от реального режима, в защищённом система сбрасывает флаг TF после каждого стэпа, поэтому внутри обработчика его нужно взводить по-новой, чтобы после исполнения очередной инструкции SEH-обработчик опять получил управление. Отметим, что исключение #DB процессор генерит не НА текущей инструкции, а уже ПОСЛЕ её исполнения. Такой алгоритм процессора освобождает нас от постоянной коррекции регистра EIP внутри обработчика – достаточно только взводить TF на каждом шаге, а EIP сам будет скакать по нужным инструкциям.
Чтобы убедиться в этом, можно написать тестовый код, который взведёт флаг TF и из регистрового контекста SEH, выведет на экран состояние регистров
Практическая часть
Приведённый ниже пример демонстрирует эту технику. Я старался сделать его максимально понятным, и убрал из него всё лишнее. Здесь обычная проверка пароля, код которой изначально находится в секции-данных программы, от куда потом копируется в выделенную память SEH-обработчиком. По окончанию, на этот код SEH сразу-же передаёт управление. Алгоритм будет работать только в неотлаживаемом процессе, при взведённом флаге TF. Пароль на валидность проверяется по его хэш-сумме, инклуд "except.inc" предоставляет доступ к структуре CONTEXT процессора, и прикреплён скрепкой в подвале темы (нужно положить его в дир "fasm\include\.."):
Для наглядности, здесь я упростил задачу до максимума, и внутри SEH сбрасываю в выделенную память весь код по одному/единственному исключению #DB. Но гораздо интересней залезть под капот алгоритма и исполнять код-проверки буквально на каждом шаге процессора, увеличив тем-самым скважность исключений #DB. Во-первых это на порядок усложнит взлом, а во-вторых – добавит нам скилла.
Организовать это дело совсем не сложно и если кого заинтересует этот приём, мы можем осуществить его вместе. Нужно всего-то добавить в процедуру проверки-пароля "флаг окончания", и внутри SEH-обработчика проверять его на единицу. Если индикатор = нуль, значит взводим TF в регистровом контексте и продолжаем трэйс (менять больше ничего не надо). Соответственно, если "флаг окончания теста" =1, значит оставляем TF сброшенным и заканчиваем трэйс.
Заключение (в хорошем смысле слова)
Здесь мы рассмотрели только основную идею самотрассировки в надежде, что это послужит генератором идей. В этом направлении, наши возможности ограничивает только фантазия – например, можно озадачить SEH распаковкой или декриптором зашифрованного кода, жонглировать контекстом любых регистров, вплоть до отладочных DR0-DR7 (которые для юзера считаются привилегированными), и многое другое. А всё потому, что в брачном периоде, парнями из Microsoft была заброшена капсула, которая со-временем сыграла злую шутку с отладчиками прикладного уровня – они доверяли флагу TF на все 100%, а он оказался хитрым на все 200. До скорого..
Это касается таких механизмов как 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):Пусть вас не смущает здесь устаревший термин 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'
Отладчик ядерного уровня WinDbg, на команду
!idt –a
отзывается более информативным сообщением. Отметим, что системы класса Win обрабатывают не все 256 прерываний INT 0x00..0xFF
– большая часть из них вообще необрабатываемые "Unexpected_Interrupt" и попадают они прямиком в цепкие лапы диспетчера исключений, который тупо проглатывает их, даже не оповещая об этом юзера:Во-всей этой кухне, важным для нас моментом является то, что отладчики пользовательского уровня вообще не используют аппаратный флаг 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, просто обрабатывать его не видит причин – кому надо, тот пусть и занимается этим:Если у кого-то это окно и вызывает отвращение, то нам оно только радует глаз. Самотрассировка - self-Debug - подразумевает установку SEH-обработчика на это исключение, с последующей модификацией флага TF. Внутри обработчика мы можем вытворять со-своим кодом всё-что пожелает наша душа, и это будет абсолютно прозрачно для отладчика! Поскольку он постоянно фиксит флаг TF и обнаружив сразу-же сбрасывает его в нуль, то соответственно и исключения Single-Step под отладчиком не возникает. В результате, пользовательский SEH не получит уже управления, и Оля пойдёт топтать совсем не ту тропинку,.
Нужно сказать, что Win32-отладчики поглащают только отладочное исключение #DB, а остальные исправно отлавливают, передавая управление SEH-фрейму юзера. Так-что представленная на суд идея имеет право на жизнь только при взведённом флаге TF с прерыванием INT-1:
Оригинальное мнение на счёт взведённого флага TF имеет первая версия отладчика OllyDbg, которая напрочь отказывается трассировать весь последующий код, и позорно капитулирует сообщением типа: "Я не в курсе, как реагировать на команду по этому адресу. Попробуйте установить точку-останова на следующей команде". Если внять совету отладчика и установить по F2 бряк (в данном случае) на адрес
0х0040204В
, то ситуацию это всё-равно не спасет и исключения #DB не произойдёт в любом случае, а значит и наш SEH не получит управления. Вот и попробуй разберись с этой мутью..Детали реализации кода
Чтобы воплотить идею в жизнь, рассмотрим некоторые её нюансы..
Значит установка пользовательского 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 ;// записать туда свой адрес!
Опытный глаз конечно-же сразу обнаружит подвох с 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 оказался опять сброшен:Практическая часть
Приведённый ниже пример демонстрирует эту технику. Я старался сделать его максимально понятным, и убрал из него всё лишнее. Здесь обычная проверка пароля, код которой изначально находится в секции-данных программы, от куда потом копируется в выделенную память 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. До скорого..
Вложения
Последнее редактирование: