Когда-то собирал материал по отладчикам реального режима в надежде понять 'как это работает', и остался черновик, который я чуть-причесал и решил выложить сюда. Уточню, что всё ниже-изложенное представляет из-себя исключительно моё мнение по данному вопросу. Приходилось разбираться в тонкостях самому.., поэтому некоторые моменты могут быть ошибочны. Тему создал в разделе фасма, т.к. планирую приводить примеры именно на этом диалекте ассемблера. Сабж расчитан на юзеров первого звена, и только\начинающих познавать мир асма - любителей. Постараюсь без соли.. ----------------------------- ВВОДНАЯ ЧАСТЬ Первые отладчики появились более 30-ти лет назад. За это время поколение их разрослось, но прогресса в алгоритмах не наблюдается. Виной тому архитектура самого ЦП, где отладочные средства сведены к минимуму - это бит(TF) в регистре флагов, да-пара прерываний BIOS: 01h (останов при TF=1), и 03h (программный брэйк-поинт). Позже, у 80386+ появилось 8 специальных\отладочных регистров DR0-DR7, которые значительно расширили возможности отладки. Теперь, в добавок к программным.., можно ставить ещё и аппаратные брэйки, число которых ограничено четырмя. Нужно отдать должное разработчикам дебагеров, которые умудрились создать продукты находясь в условиях, с такими\жёсткими квотами. В отладчиках реального режима имеется столько дыр, что можно рассматривать их как одну\сплошную дыру, прикрытую крупной сеткой полезных алгоритмов. Они пытаются хоть как-то скрыть от исследуемой программы своё присутствие перехватывая прерывания, обращения к 'опасным' (на их взгляд) инструкциям, подсовывая левый флаг(TF) - но всё напрасно. Как оказалось, заштопать все лазейки не возможно, ..как и невозможно описать их в одной статье. -------------------------- Эволюция разделила все отладчики на 4 вида: отладчики режимов реального\защищённого, и отладчики эмуляторы\унпакеры. В свою очередь отладчики защищённого режима делятся ещё на 2 (под)вида - прикладного уровня (OllyDBG), и уровня ядра (S-Ice\Syser). Отладчики реального режима: SoftIce v.2+ (Nu-Mega DOS), AVPUtil (by E.Kaspersky), Code View (Microsoft), Turbo Debugger (Borland), AFDPro (by H.Puttkamer), GRDB (LADsoft / text-mode), DEBUG (Microsoft / text-mode) GameTools, Periscope, WatcomDebugger, Quaid Analyzer, etc. Отладчики защищённого режима: SoftIce v.4+ (Nu-Mega core/Win), Syser (by LoyTheCjw core/Win), OllyDbg (by Oleh Yuschuk) DeGlucker (VAGSoft), etc. Эмулирующие отладчики: Soft Debugger (SDB), EDB (by Serge Pachkovsky), SD-2 (by Dmitry Groshev), etc. EMU \ авто-распаковщики: cup386 (Cyberware) UNP (by B.Castricum) Intruder (by Creat0r) SnapShot (Dale Co.) AutoHack (BCP group) TRON, TSUP, etc. Как видим - список приличный, и у каждого свои преимущества и недостатки. Если один разработчик закрывает дыру(x), то в другом продукте она остаётся открыта. Поэтому свои защитные механизмы разумно тестить под разными отладчиками, или-же вставлять в код сразу несколько вариантов защиты. Авось какой-нить вариант да-сработает.
КАК РАБОТАЕТ ОТЛАДЧИК ..:: #1. Инстинкт самосохранения у отладчиков реального режима отсутствует напрочь. Первое, что они делают при запуске, это делят с клиентом стек. Вернее стек они не делят, а делают его общим, что приводит к гормональным сбоям с плачевными последствиями. Общий стек позволяет исследуемой программе захватить ресурсы самого отладчика, и ещё не ясно, кто-кого 'поведёт' по трассе. Например, если начать код с инструкции NEG_SP, то при дефолтном значении указатель(SP) = FFFEh, он обратится в 0002h, на что проц тут-же отреагирует исключением(#SS) - ошибка стека. Если это прерывание не перехватывается отладчиком, то он просто зависнет, или вылетит даже не успев попрощаться: Код (ASM): start: neg sp nop neg sp ;... На этом фоне выделяются такие дебагеры как: TD, AVPutil, CodeView, ..а вот разработчики AFD, Debug, GRDB оказались продумАноми с нетрадиционной ориентацией. Кто-то выделяет себе отдельную область для стека, а кто-то перехватывает стековые инструкции. Но то-что эти отладчики уверенно продолжают трассировку даже при сброшеным в зеро указателем(SP) - остаётся фактом. Список исключений реального режима: ----------------------------------- INT 00h (#DE) - Ловушка деления на ноль. INT 01h (#DB) - Ловушка отладочного прерывания при TF=1. INT 02 - NMI-прерывание (не маскируемое). INT 03h (#BP) - Ловушка точки останова. INT 04h (#OF) - Ловушка переполнения регистра. INT 05h (#ВС) - Переполнение при BOUND (выход операнда за границы). INT 06h (#UD) - Недопустимая инструкция (ud2). INT 07h (#NM) - Нет сопра (вызов FPU, когда в CR0 бит(2)=1). INT 0Ch (#SS) - Исчерпание стека. INT 0Dh (#GP) - Общая ошибка защиты. ..:: #2. На сл.этапе, отладчик готовится к маскировке. Разработчики быстро поняли, что находиться безоружным в агрессивной среде, по меньшей мере глупо. Чтобы выжить, нужны механизмы отличные от тяжёлой артилерии, т.к. её применение противоречит самой логике отладки. Если при малейшем подозрении сразу-же прибивать клиента, кого тогда отлаживать? Необходимо позволить отлаживаемой программе вести себя вольготно, и в тоже-время как-то ограничивать её свободу. Компромисс был найден в виде затенения трассировочного флага(TF). Этот флаг не отображает в своём окне ни один отладчик, дабы не спровоцировать его сброс. Так-же нельзя получить его и прямой операцией чтения регистра(FLAGS), типа PUSHF\POP. Бит(TF) всегда в нём будет сброшен. Как получить реальное значение регистра флагов, будет рассказано ниже. ..:: #3. На заключительной стадии настройки среды, отладчики перехватывают прерывания: 1/3/21h (а некоторые ещё кучу), и взводят флаг(TF) в регистре FLAGS. С этого момента проц переходит в пошаговый режим выполнения инструкций, дёргая на каждом Step'e перехваченный INT-1h. ЦП не приступит к выполнению очередной инструкции, пока не дождётся от обработчика INT-1h команды IRET, после которой выполнит ещё одну.., которая так-же сгенерит INT-1h. Круг - замыкается. Внутри обработчика инта(1), дебагеру ни что не мешает спокойно 'рисовать' на экране состояние регистров\памяти, и по окончании - вставить (до IRET) функцию ожидания клавиши(F2\F8) для следующего стэпа. Алгоритм работы процессора с установленным флагом(TF) поддерживается на аппаратном уровне. Это просто вызов INT-1h, стандартный обработчик которого представляет из себя чуть-навороченную заглушку (типа: зашёл-вышел), поэтому в реальных условиях от первого прерывания прогам не-холодно-не-жарко. При вызове любого прерывания, ЦП сначала помещает в стек флаги, CS:IP (как адрес возврата), и если это трейс в отладчике без входа в INT\CALL, то дебагер сбрасывает ещё и флаг(TF), чтобы обработчик прерывания сделал свою работу на одном дыхании, без остановок и тормозов. При трассировке со-входом в INT, флаг(TF) остаётся взведённым, предоставляя нам возможность зайти внутрь обработчика, и ознакомиться с его содержимым. Посмотрим на таблицу векторов прерываний, и на стандартный код ловушки INT-01h: Код (Text): GRDB version 1.7 Copyright (c) LADsoft History enabled |<-------------- Векторы прерываний---------------->| ->d 0:0 | -0- -1- -2- -3- | 0000:0000 68 10 A7 00 - 8B 01 70 00 - 16 00 91 03 - 8B 01 70 00 0000:0010 8B 01 70 00 - B9 06 0C 02 - 40 07 0C 02 - FF 03 0C 02 0000:0020 46 07 0C 02 - 0A 04 0C 02 - 3A 00 91 03 - 54 00 91 03 0000:0030 6E 00 91 03 - 88 00 91 03 - A2 00 91 03 - FF 03 0C 02 -> ->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 0070:0198 754F JNZ 01E9 ;......... 0070:01E9 58 POP AX 0070:01EA 1F POP DS 0070:01EB CF IRET Здесь видно, что обработчики прерываний 1 и 3 лежат по одинаковым адресам 0070:018Bh, который я U'нассемблировал. Но на этот-же адрес указывает и вектор(4) по адресу(10h), который редко перехватывается отладчиками, и может послужить флагом его присутствия. Достаточно сравнить оффсеты векторов 1 и 4, как отладчик всплывёт наружу: Код (ASM): start: push ds 0 ; pop si ; подготовка mov di,4 ; ..к подмене вектора(1), mov cx,di ; ..на вектор(0) push 0 0 ; DS:SI = 0000:0000 pop ds es ; ES:DI = 0000:0004 (СХ=4 для REP) mov ax,[4] ; сравниваем адреса обработчиков cmp ax,[10h] ; ..int'1 и int'4 je @okey ; нет отладчика! rep movsb ; иначе: ставим на int'1 вектор(0) @okey: pop ds ; и пусть отладчик стэпит дальше.. nop ; ;..... Если мы под отладчиком, то после этих манипуляций ЦП будет генерить на каждом шаге уже не INT'1, а исключение(#DB) предполагая, что очередная инструкция делит на нуль, вне зависимости от её опкода. TurboDebugger (и не только он) не перехватывает вектор(4), поэтому глотает эту наживку и давится. Хоть инт(4) и перехвачен, то всё-равно его обработчик будет лежать в другой области памяти, т.к. должен обрабатывать ситуацию своеобразно, а не как отладочное прерывание(1). После всех телодвижений отладчик готов принять клиента на борт, загрузив его в начало сегмента и настроив указатель(IP) согласно адресу из PSP. Дальнейшие его действия поверхностно описаны выше, хотя на самом-деле там всё довольно сложней.
ОТЛАДОЧНЫЕ РЕГИСТРЫ ПРОЦЕССОРОВ 386+ У процессоров до 80386 весь механизм отладки держался на единственном бите(TF) регистра FLAGS. Если он взведён, то проц входит в режим шаговой трассировки, генерируя на каждой инструкции INT-01h. Это прерывание перехватывает дебагер, показывая текущее состояние регистров, чего за-глаза хватало исследователям кода. Но с появлением процессора 80386 всё изменилось. Взору программистов предстали новые регистры, в том числе и 8 отладочных (DR7-DR0), 2 из которых (DR5-DR4) забрал ЦП для своих нужд. В отличии от предыдущих процессоров с 'Программными точками останова' (коих может быть сколько угодно), теперь ЦП поддерживает и 4 аппаратных брэйка. Программный Breakpoint представляет собой 1-байтный код CCh. Если поместить этот байт перед какой-нибудь инструкцией, то дойдя до неё ЦП сгенерит исключение(#BP), которое дёрнет INT-3. Отлаживаемой программе достаточно подсчитать свою контрольную сумму, чтобы выяснить, вставил-ли отладчик байт(ССh) как бряк, обнаружив тем-самым процесс отладки. При установке 'аппаратных точек', всё-что требуется от программиста - это лишь определить ситуацию, на которую должен отреагировать проц прерыванием(3). Всего существует 4 различных условия - отсюда и 4 точки: прерывание при выполнении команды (Exec), прерывание при модификации ячейки памяти (Write), прерывание при модификации или чтении ячейки (Read/Write), прерывание при обращении к портам ввода-вывода (I/O). Условия для них задаются через отладочные регистры (DR7-DR0), доступ к которым возможен только из реального режима работы ЦП (или нулевого кольца виндозы), и исключительно инструкцией (MOV). Никакие другие инструкции - не поддерживаются. Самым информативным является "Регистр управления отладкой" (DR7). Регистр(DR6) - это регистр статуса. Он заполняется процессором при каждом срабатывании брэк\поинта. Взведённые биты (DR6) информируют о том, какая именно точка сработала, и по какой причине. Четыре регистра (DR0-DR3) хранят физические адреса установленных точек останова. Регистры доступны для чтения и записи, что позволяет легко съэмулировать любой порт, устанавить бряк на адрес памяти, или на выполнение инструкции. Поистине прорыв! Посмотрим на битовую карту отладочных регистров(DR7-DR0): Код (Text): DR7: Управление отладкой. ------------------------- биты(31–30): поле LEN для точки останова(3). Размер точки в байтах. 00=1, 01=2, 10=0 (eхec), 11=4. биты(29–28): поле R/W для точки останова(3). Тип точки. 00=EXEC, 01=R, 10=порт (если #DE в CR4=1), 11=R/W биты(27–26): поле LEN для точки(2) биты(25–24): поле R/W для точки(2) биты(23–22): поле LEN для точки(1) биты(21–20): поле R/W для точки(1) биты(19–18): поле LEN для точки(0) биты(17–16): поле R/W для точки(0) бит(13)GD: запрет на обращение к отладочным регистрам (исключение #DB). бит(9)GE: глобальный BreakPoint бит(8)LE: локальный BreakPoint бит(7)G3: точка(3) включена (глобальная) бит(6)L3: точка(3) включена (локальная) бит(5)G2: точка(2) бит(4)L2: точка(2) бит(3)G1: точка(1) бит(2)L1: точка(1) бит(1)G0: точка(0) бит(0)L0: точка(0) DR6: Регистр состояния отладки. ------------------------------- бит(14)BS: причина прерывания - флаг(ТF) из регистра FLAGS. бит(13)BD: причина прерывания - сл.команда обращается к DR0-7. бит(3)B3: сработала точка(3) бит(2)B2: сработала точка(2) бит(1)B1: сработала точка(1) бит(0)B0: сработала точка(0) DR5–DR4: Используются самим ЦП и программисту не доступны --------------------------------------------------------- DR3–DR0: Регистры адреса. | Физические адреса 4-х точек останова. CR4: Новые возможности. | Бит(3)DE - запрет на аппартные прерывания к портам. В реальном режиме, определения типа 'локальная\глобальная точка' смысла лишены. Глобальная введена для защищённого режима Win. Если мы под досом, то можно выставлять любой из битов LE/GE (или-же оба сразу), чтобы ЦП понял наши намерения обозначить бряк. Попробуем вручную выставить BreakPoint(3). Ставить будем на попытку чтения\записи слова-памяти по физ.адресу(200h). Нужно сказать, что обращение должно быть именно к слову, а не к двойному слову. Как говорится - размер имеет значение: Код (Text): 32-битный\отладочный регистр DR7: --------------------------------- BP3 BP2 BP1 BP0 3 2 1 0 <-- точки останова (2-бита на точку) | | | | | | | | 0111 0000 0000 0000 0010 0001 1100 0000 ^^^^ | | ^^ | | | | +--- (G\L3) Включить точку(3) (в R-mode любой из битов). | | | +------ (LE) Локальный бряк. | | +------------ (GD) Закрываем доступ к DR0-DR7. | | | +------ (R/W) Тип точки останова: 11 = Memory R/W. +-------- (LEN) Размер инструкции : 01 = 2 байта. PS\\: адрес точки нужно указать в регистре(DR3). ;----------------------------------------------------------- mov eax,01110000000000000010000111000000b ;0x700021C0 mov ebx,200h mov dr7,eax ; задаём условие mov dr3,ebx ; указываем физ.адрес ;0x00000200 Кстати, вопреки мнению, что регистр(DR7) является 'Регистром управления' может послужить тот факт, что под отладчиком, ЦП взводит бит(LE) регистра(DR7) автоматически, в независимости от того, установлен какой-нить бряк или нет. То есть он может играть роль 'Регистра статуса'. Это касается и самого 'Регистра статуса' DR6, который так-же выставляет под отладчиком свой бит(BS), паля взведённый флаг(TF) в регистре флагов.
ОБЗОР ВОЗМОЖНОСТЕЙ ОТЛАДЧИКОВ Первые отладчики появились задолго до рождения процессора 80386 с отладочными регистрами DR0-DR7, поэтому физически не в силах отследить обращения к ним. На скамейку запасных отправляются: Debug, TurboDebugger, AFDPro, AVPUtil и вся их братия. Этому есть простое объяснение: все\они 16-битные, а отладочные регистры 32-бит. Но картина не такая-уж и мрачная. Разработчики, которые поддерживали свои продукты 'на плаву' (SoftICE, TurboDebugger, CodeView), в последующих версиях добавляли всё новый функционал, в результате чего работа с отладочными регистрами становится всё-же возможной, но и то с большой натяжкой. Алгоритмы реализованы криво.., видно что левой рукой, через правое ухо. Посмотрим, как подопытные отладчики дизассемблируют чтение отладочных регистров: Код (ASM): ;fasm-code ;------------------- org 100h mov eax,dr7 nop mov ebx,dr6 ret Дизассемблеры IDA, HIEW и HDasm справляются с такой задачей нормально, но 16-битные дебагеры дымят не понимая, куда следует впихнуть первый байт, считая его лишней инструкцией: Код (Text): = CodeView = мелкософт здесь рулит и отображает REG32: ------------------------------------------ 0A2D:0100 0F21F8 MOV EAX,DR7 0A2D:0103 90 NOP 0A2D:0104 0F21F3 MOV EBX,DR6 0A2D:0107 C3 RET = TurboDebugger = всё ОК, только состояние в REG16, а не 32: ------------------------------------------- 00000000: 0F21F8 mov eax,dr7 00000003: 90 nop 00000004: 0F21F3 mov ebx,dr6 00000007: C3 retn = AVPUtil = дешифрует не правильно. состояние в REG16: ------------------------------------------- 17F4:0100 0F21F8 MOV EDI,DR0 17F4:0103 90 NOP 17F4:0104 0F21F3 MOV ESI,DR3 17F4:0107 C3 RET = AFDPro = в упор не видит 32-битные регистры: ------------------------------------------- 1E09:0100 0F DB 0F 1E09:0101 21F8 AND AX,DI 1E09:0103 90 NOP 1E09:0104 0F DB 0F 1E09:0105 21F3 AND BX,SI 1E09:0107 C3 RET Очевидно, что для экспериментов с отладочными регистрами понадобится инструмент с более современным ядром, который при виде 32-бит не впадал-бы в ступор, и имел мало-мальски 'прямые руки'. На эту роль подошёл-бы SoftICE, но он очень привередлив на платформах выше Win2000, поэтому я остановил свой выбор на консольном дизассемблере\отладчике GRDB, который можно скачать с сайта разработчика, или вместе с исходниками от сюда: http://exmortis.narod.ru Код (Text): GRDB version 1.7 Copyright (c) LADsoft History enabled eax:00000000 ebx:00000000 ecx:00000000 edx:00000000 esi:00000000 edi:00000000 ebp:00000000 esp:000BFFEE eip:00000100 eflags:000B0202 NV UP EI PL NZ NA PO NC ds: 0E80 es:0E80 fs:0E80 gs:0E80 ss:0E80 cs:0E80 ->?o ; опции -------------------- WR - wide registers enabled FR - flat real commands disabled 32 - 32 bit disassembly enabled ZR - divide by zero trap enabled BK - ctrl-break trap enabled NV - native video enabled FI - flat real autoinit enabled F0 - flat real 0 default disabled SO - signed immediates disabled HI - command history enabled MD - msdos I/O disabled Logging disabled Тулза конечно не шедевр, но главное удовлетворяет потребностям, которые в пределах этого топика весьма скромны. С помощью GRDB можно считать состояние любого из 32-битных регистров, изменить пару бит и запихнуть изменённый регистр обратно. Нужно сказать, что данный отладчик по отношению к своим братьям ещё и более 'честный'. Ничего не маскирует и показывает всё как-есть, не вводя исследователя в заблуждение. Приведу простой пример.. Известный факт, что все отладчики взводят трассировочный бит(TF) в регистре флагов, что переводит ЦП в шаговый режим. Чтобы тестируемая программа не обнаружила процесс отладки, дебаггер маскирует этот факт, представляя в своём окне бит(TF) как сброшеный. Не помогает даже прямое чтение FLAGS: Код (Text): AX 3202 CX 00FF DS:SI 17F4:0100 CS:IP 17F4:0102 BP 0000 ODITSZAPC | Stack BX 0000 DX 17F4 ES:DI 17F4:FFFE SS:SP 17F4:FFFE FL 3202 001000000 |-08 17F4 -----------------------------------------------------------------------|-06 0102 17F4:0100| 9C PUSHF |-04 17F4 17F4:0101| 58 POP AX |-02 3202 -----------------------------------------------------------------------|>00 0000 Здесь видно, что флаг(T) сброшен в нуль, а взведён только 'INT' разрешая нам пользоваться прерываниями. В регистре(AX) лежат флаги со-значением 3202h. Посмотрим, в каком состоянии находится процессор с такими флагами: Код (Text): Регистр FLAGS ----------------------- биты 1.5.13.15 - резерв 0011 0010 0000 0010 = 3202h | | |||| || | | | | | |||| || | | +--- бит(0) = CF (Cary) перенос | | |||| || | +----- бит(2) = PF (Parity) чётность | | |||| || +-------- бит(4) = AF (Auxiliary) всп.перенос | | |||| |+---------- бит(6) = ZF (Zero) нуль | | |||| +----------- бит(7) = SF (Sign) знак | | |||+------------- бит(8) = TF (Trap) трассировка <---<----// | | ||+-------------- бит(9) = IF (Interrupt) прерывания | | |+--------------- бит(10) = DF (Direction) направление | | +---------------- бит(11) = OF (Overflow) переполнение | +------------------ бит(12) = IOPL (2-бита) I/O Privilege Level +-------------------- бит(14) = NT (Nested Task) вложенность задачи У-гу.. Бит(8) сброшенный, ..хотя мы знаем, что это не так. Нужно сказать, что все\подопытные отладчики ведут себя в данном случае одинаково. Один плюс ушёл в копилку разработчиков, которые хоть как-то попытались замаскировать своё присутствие. Но если вспомнить глюк всех отладчиков с тасованием регистра(SS), то оказывается что 'рано пить Боржоми'. Их маскировка напоминает страуса с зарытой головой, и вот почему.. Под отладчиком, проц генерит INT-1 после каждой инструкции. Но при стековых манипуляциях с регистром(SS), INT-1 вызывается уже не после одной, а после 2-х инструкций, что позволяет нам вытащить реальные флаги контрабандным путём: Код (Text): AX 3302 CX 00FF DS:SI 17F4:0100 CS:IP 17F4:0105 BP 0000 ODITSZAPC | Stack BX 0000 DX 17F4 ES:DI 17F4:FFFE SS:SP 17F4:FFFE FL 3202 001000000 |-08 17F4 -----------------------------------------------------------------------|-06 0105 17F4:0101| 16 PUSH SS |-04 17F4 17F4:0102| 17 POP SS |-02 3202 17F4:0103| 9C PUSHF |>00 0000 17F4:0104| 58 POP AX |+02 20CD Стоило только снять со-стека регистр(SS), как отладчик пропустил между ног следующую инструкцию(PUSHF). Теперь он не смог отследить обращение к флаговому регистру, и выдал в регистр(АХ) действительное его значение(3302h), хотя в окне и стеке продолжает красоваться 3202h. AH=33h - число не чётное (мл.бит=1), значит в АХ трассировный бит(TF) на самом деле взведён. Минусуем с копилки жуликов. Теперь наш код может легко обнаружить факт отладки, и уйти из-под неё. Эту особенность шагового режима называют "Потерей трассировочного прерывания". Процесс обработки прерываний отладчиками выглядит так.. Сначала ЦП сохраняет в стеке регистр флагов, и CS:IP как адрес возврата в программу. Затем отладчик сбрасывает флаг(TF), что предотвращает по-шаговое выполнение самого обработчика прерывания. IRET (на выходе из обработчика) извлекает со-стека прежние состояния флагов и CS:IP, снова переводя ЦП в режим по-шаговой трассировки. Это означает, что бесполезно искать реальный флаг(TF) внутри обработчика прерывания, т.к. он будет там сброшен. Нужно поймать его снаружи подручными средствами. Нужно отдать должное разработчикам 'AFDPro', которые предусмотрели финт с регистром(SS). Он продолжает трассировку, в результате чего выдаёт подложный бит(TF). Зато 'GRDB' вообще его не контролирует. Имеется лишь вялая попытка маскировки в окне отладчика, но при обычном чтении FLAGS, GRDB сразу-же выдаёт чистое значение - 3302h. Глюк(SS) по-прежнему не обрабатывается. Видимо компания 'LADsoft' придерживается мнения: "Не хотите чтобы вас отлаживали? Да никто и не настаивает". Продолжение следует...
ПРАКТИЧЕСКАЯ ЧАСТЬ. ANTI-DEBUG Достоинства процесса отладки трудно переоценить, когда мы пытаемся выловить блох в коде своего защитного механизма. Но ключевое слово здесь "МЫ", т.к. это-же может проделать и злоумышленник, чтобы нейтрализовать нашу защиту. Вот тут-то и приходится бороться с отладчиками, дабы спрятать ключ от потайной двери. Рассмотрим несколько способов противодействия отладке.. #0. Первое, что приходит на ум - это отправить клаву к праотцам. Достаточно просто отключить.., и сразу-же включить её обратно. Ни один из дебагеров не перенесёт такой изврат и наглухо зависнет на INC_AL. (правда эмулирующие отладчики могут заблокировать обращения к портам клавиатуры, но дебаг под ними скорей исключение, чем правило): Код (ASM): ; вкл/откл клавиатуры bye_Keyb: mov cx,2 mov al,0ADh ; выключить @1: out 64h,al inc al ; включить dec cx jnz @1 #1. На сл.месте знаменитый трюк с инверсией регистра(SP). Большинство отладчиков видят это действо в своём страшном сне. Любой дебагер (да и не только он) сильно привязан к стеку, поэтому установка (SP) ниже плинтуса воспринимается им как форс-мажорное обстоятельство, на которое каждый из отладчиков реагирует по-своему. Кто-то перехватывает эту инструкцию, а кто-то забивает на неё, полагаясь на совесть кодера. Только не понятно, почему разработчики не учитывали эту особенность? Если стек для тебя так критичен, почему-бы на старте не отхватить от него кусок по-больше: [SUB_SP,40h]. Тогда (при инверсии) в заначке останется как-минимум 64-байт, с которыми можно выехать из сложившийся ситуации на подсосе. Посмотрим, на стартовые значения(SP) тестируемых отладчиков: Код (ASM): ; инверсия указателя SP bye_Debug: neg sp ; если SP был fffeh, то станет 0002h nop ; если SP был ffeeh, то станет 0012h neg sp ;.... ;//----------------------------------------------------- TD : SP = fffeh - не жилец AVPutil : SP = fffeh - вылетает со-свистом CodeView: SP = fffeh - в нокауте AFDPro : SP = fffeh - ОК! имеет свой стек. GRDB : SP = ffeeh - ОК! перехватывает SP. Такой-же эффект можно получить, если при выровненном стеке снять с него слово. При указателе (SP=FFFEh) он примет значение нуль: Код (ASM): start: ; SP = FFFEh pop ax ; SP = 0000h ;... #2. На очередной позиции прочно обосновался финт с проверкой флага(TF). Этот вариант никогда не даёт осечек. Ясно, что отладчик начнёт тут мухлевать, подсовывая нам левый флаг, поэтому вспомним про ошибку с потерей трассировочной инструкции через регистр(SS). Способов реализации самих проверок можно насчитать сотни, и вот только некоторые из них: Код (ASM): ;// Вариант(#0). Типичная проверка регистра ;------------------------------------------ xor ax,ax ; АХ = 0 push ss ; насилуем регистр(SS) pop ss ; pushf ; (!)отладчик пропустит эту инструкцию pop bx ; реальный FLAGS в ВХ test bh,1 ; проверить бит(TF) jz @okey ; если он сброшен... ----->----+ jmp ax ; иначе: INT-20h (на выход) | @okey: nop ; <--------------<-------------+ ;..... ;// Вариант(#1). Манипулируем с TF прямо в стеке ;// Вместо push\pop_SS можно предварять pushf префиксами сегментов. ;// ES: = 26h, SS: = 36h, CS: = 2Eh, DS: = 3Eh ;------------------------------------------------------- mov bx,sp ; BХ = дно стека sub bx,2 ; ..(корректируем прицел) db 26h ; сбиваем трассировку сл.инструкции (префикс ES:) pushf ; флаги в стек and word[bx],100h ; оставляем в стеке только бит(TF) jz @okey ; если он сброшен.. ---------->---------+ in ax,40h ; иначе: берём рандом в AX | shrd [bx],ax,16 ; задвигаем его в стек вместо флагов, | ret ; ..и уходим по рандому в космос. | @okey: add sp,2 ; выравниваем стек от PUSHF <-----<-----+ ;..... ;// Вариант(#2). Тест отладочного регистра(DR6) ;// всё-что от нас требуется, это взвести биты(10-9-8) в DR7, ;// ..а потом проверить бит(14) в DR6. ;// ЦП взводит его при активном флаге трассировки(TF) ;--------------------------------------------------------------- mov eax,700h ; заряжаем DR7 битами(10-9-8) mov dr7,eax ; db 90h ; NOP mov eax,dr6 ; читаем регистр статуса(DR6) db 90h ; and ax,4000h ; выделяем в нём трассировочный бит(14) jz @okey ; OK! если сброшен ----------------+ div al ; иначе: ошибка деления на нуль! | @okey: nop ; <----------------<---------------+ ;..... #3. Время выполнения инструкций так-же оказывает отладчикам медвежью услугу. При трассировке, после каждого шага в дело вступает обработчик int'1, который выполняется какое-то время. За этот промежуток времени таймер системы успеет несколько раз тикнуть. Стандартный интервал системных тиков составляет 18.2 раза в секунду, чего вполне хватает для вычисления разницы между исполнением соседних инструкций. Вот пример реализации подобной проверки: Код (ASM): xor cx,cx ; СХ = 0 (счётчик для LOOP) mov es,cx ; сегмент BIOS mov ax,[es:046Ch] ; читаем системные тики nop ; cmp ax,[es:046Ch] ; время выполнения NOP je @okey ; нет тормозов.. -------->------+ @00: push ax ; иначе: переполняем стек | loop @00 ; | @okey: nop ; <--------------<--------------+ ;..... В реальных условиях однобайтная инструкция NOP выполнится гораздо быстрее, чем таймер успеет тикнуть 1 раз, поэтому такая проверка под отладчиком имеет место быть. Необычный здесь и обработчик события 'иначе', который тупо пихает в стек 65535 слов, в результате чего все дебагеры предпочитают сделать себе хара-кири, чем решать этот вопрос на программном уровне. #4. Ну и почётное место на пьедестале занимает перехват векторов(1\3). Этим трюком мы забираем у отладчика все\его ресурсы, и он полностью теряет потенцию к дальнейшему трейсу. Получив таким образом управление, можно заставить дебагер трассировать самого себя, пере\направить CS:IP на системную область, и т.д. и т.п. Тут всё зависит от фантазии, ..которую я планирую включить в следующий пост.. Продолжение следует..
..значит перехват прерываний отладчика. Как уже говорилось, весь алгоритм работы дебагеров зашит в перехваченых интах 1 и 3. Если первый - это стэп, то третий - это бряк, перехват которого редко оправдывает ожидания. Кодо\копатель может вообще не использовать точки останова, тогда наш обработчик(3) останется не у дел. Другое дело трейс-прерывание(1), без участия которого не обходится ни один стэп. Доступ к вектору(1) эмулируют все\известные отладчики, и это логично. Никто в здравом уме не позволит срубить сук, на котором сидит. Но что интересно, дебагеры отлавливают перехват инта(1) только стандартными методами, типа fn.(25h) сервиса DOS. Попробуем подменить его и посмотрим, что из этого выйдет: Код (ASM): start: db 2 dup(90h) ; два NOP'a на старте mov ax,2501h ; забираем вектор(1) у отладчика mov dx,@byeDebug ; ставим на него свой обработчик int 21h ; jmp begin ; к началу полезного кода. @byeDebug: ; новый обработчик(1) xor ax,ax ; nop ; iret ; выход из обработчика! begin: nop ; ;.... Только 'TurboDebugger' аплодирует стоя такому приёму, и жертвует собой в пользу науки. Но все\остальные отладчики спокойно продолжают делать свою работу, полностью игнорируя событие, на которое мы так расчитывали. Если (после перехвата) заглянуть в таблицу векторов прерываний, то можно обнаружить, что ни один тушканчик там не пострадал, и что вектор(1) ничуть не изменился. Делаем вывод, что для перехвата INT-1h нужны альтернативные методы, которыми природа нас щедро одарила. Достаточно произвести запись в сегмент(0), плюс номер инта умноженый на 4, как вектор можно считать перехваченным. Формат векторов - Offset:Segment, т.е. сперва указывается смещение обработчика, а потом его сегмент. Так-как никто кроме отладчиков не пользуется прерыванием(1), то сохранять старый его вектор не обязательно. Если юзеру сильно приспичит, то пусть он ребутнёт машину, и BIOS создаст девственную таблицу векторов IVT. Примеры ниже демонстрируют варианты перехвата вектора(1) в обход сторожевых псов отладчика. Подменять весь вектор не обязательно. Вполне хватит сменить что-то одно: или сегмент, или смещение вектора. Отладчики 'AFDPro и AVPutil' органически не переваривают присутствия в коде сегментных регистров FS и GS, поэтому резонно задействовать именно эти регистры: Код (ASM): ;// Вариант(#1). Прямая запись 'stack-2-vector'. ;//---------------------------------------------- start: pushd 0 ; в стеке 4 байта нулей pop fs ; FS = 0000h (сегмент таблицы прерываний) pop word[fs:6] ; забиваем нулями сегмент обработчика(1) nop ; ;.... ;// Вариант(#2). Перехват прерываний через регистры. ;// Ставим на вектор(01h), вектор(19h). ;// Обработчик INT-19h под чистым досом перезагружает машину ;------------------------------------------------------------ start: xor ax,ax ; АХ = 0 mov gs,ax ; сегмент таблицы векторов прерываний lfs si,[gs:19h*4] ; FS:SI = адрес обработчика(19h) mov word[gs:4],si ; копируем в инт(1) смещение(19h) mov word[gs:6],fs ; копируем в инт(1) сегмент(19h) nop ;[....] ;// Вариант(#3). Вешаем на инт(1) свой обработчик 'EXIT'. ;//------------------------------------------------------ start: push ds 0 ; pop ds ; mov word[4],@exit ; оффсет вектора(1) указывает на наш 'EXIT' mov word[6],cs ; сегмент тоже нашей программы pop ds ; nop ; ;[..........] ;... здесь вся программа... ;[..........] @exit: mov ax,4C00h ; завершить программу int 21h ; выход в DOS !!! Нужно сказать, что в большинстве случаях перехватывать вектор(1) даже не обязательно (пусть он останется таким, как 'мать-дзебуг' родила), а модифицировать уже сами инструкции внутри перехваченного отладчиком обработчика(1). В этом случае до трассировки дело вообще не дойдёт, и любопытный юзверь будет только недоумевать, почему отладчик крэшится. Посмотрим на код ниже, который расскажет больше: Код (ASM): start: push ds 0 ; pop ds ; DS - сегмент IVT lds si,[4] ; DS:SI = адрес обработчика(1) mov byte[si],0Fh ; вставим первой инструкцией ошибку 'ud2' pop ds ; дело сделали. вернёмся на-родину.. nop ; ;[....] Здесь мы получаем в DS:SI сегментный адрес, где находится (в памяти дебагера) обработчик прерывания(01h). Именно на этот адрес отладчик передаёт управление на каждом стэпе, для вывода на экран содержимого регистров\памяти. Эта 'святыня' никак не охраняется, что позволяет нам вертеть дебагами как вздумается. Что делает инструкция: MOV_BYTE[SI],0Fh ??? Она записывает в первый байт обработчика исключение #UD2, опкодом которой является байт(0Fh). Для наглядности я записал только 1 байт, хотя это не табу. Рядом по смыслу лежат следующие опкоды: Код (ASM): mov byte[si],00Fh ; ud2 - недопустимый опкод mov byte[si],0CCh ; int3 - точка останова (бряк) mov byte[si],0CFh ; iret - выход из прерывания mov byte[si],0F0h ; lock - захват шины mov byte[si],0F4h ; halt - останов процессора Ничто не мешает нам вставить в начало обработчика хоть целую цепочку инструкций, но толку от этого нуль, т.к. дальше первой.. трейс в отладчике всё-равно не пройдёт, и наткнувшись на неё отладчик рухнет замертво, подняв в небеса гору пыли. Продолжение следует..
Спасибо за статью А то, понимаешь, обмажутся своим дотнетом.. Целая пачка антиотладочных приемов была в Night $pirit Universal Polymorphic Device, http://vxheaven.org/vx.php?id=es19 Есть еще вариант - прямой перезаписью вектора int 21h (любое важное прерывание) таблицы прерываний, установить на него свой обработчик, который например что-то расшифровывает (и только; нет возврата управлению оригинальному обработчику), и наполнить такими наномитами свой код mov ax,0xbaba int 0x21 mov ax,0xfacc int 0x21 Еще вариант - в bios data area, туда, куда обработчик int 08 пишет обновляемое каждый тик таймера значение, "сохронить" что-то важное, скажем промежуточное значение какого-то вычисления. Перед этим можно (хотя учитывая что таймер медленный, можно и без запрета прерываний обойтись) запретить прерывания, после завершения работы с этой "переменной" - разрешить. Распаковка в видеопамять - еще со времен Спектрума антиотладочный трюк. В реальном режиме таблицу прерываний можно сдвинуть через LIDT/SIDT 386+, а исконную затереть своими данными; интересно как отладчики, расчитывающие на расположение таблицы интов на 0:0, к этому отнесутся? В ветхозаветном Antidebugging Tricks by Inbar Raz приводился алгоритм Running Line от Сергея Пачковского, пример мощной антиотладки.
_edge, помню ветхозавет.. Прикольный материал был. А насчёт LIDT/SIDT - надо попробовать. Хорошая идея!
ИГРЫ СО СТЕКОМ. Стек - это золотое дно, и если его пошурудить, то наружу всплывёт много интересного. Уделим немного внимания механизму его работы и посмотрим, что из этого можно будет выгадать. Вполне реально создать программу без использования стековой памяти. Например, запоминать промежуточные данные не в стеке, а в регистрах\переменных, читать клаву с портов, и выводить данные сразу в видеобуфер. Но если мы задействуем в программе хоть одно прерывание, то ЦП без нашего согласия поместит в стек как-минимум регистр флагов и адрес возврата (CS:IP). Если при этом в стеке не будет свободного места (или он будет не определён) - крах приложения неизбежен! Стек растёт снизу-вверх и определяется состоянием регистровой пары SS:SP. Если регистр(SS) указывает на сегмент стека, то регистр(SP) является указателем на его дно. Кстати, в разной литературе его представляют по-разному: одни называют SP указателем на макушку, а другие - указателем на дно. В принципе оба определения одинаковы, просто с какой стороны рассматривать рост адресов: если снизу-вверх, то SP - макушка; если сверху-вниз, то SP - указывает на дно. Так-как отладчики отображают рост адресов сверху-вниз, то мы будем считать SP указателем на дно стека: Код (Text): CS:IP = 17F6:0100 BP = 0000 SS:SP = 17F6:FFFE FL = 7202 ------------------------------------------------------------- CS:0100 | 43 4F 4D 4D 4F 4E 50 52 4F 47 52 41 4D 46 49 4C <<- Полезный CS:0110 | 45 53 3D 43 3A 5C 50 52 4F 47 52 41 7E 31 5C 43 код. CS:0120 | 4F 4D 4D 4F 4E 7E 31 00 43 4F 4D 50 55 54 45 52 CS:0130 | 4E 41 4D 45 3D 53 41 4D 4C 41 42 00 44 45 56 4D ;.~.~.~.~.~.~ SS:FFC0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 <<- Стековая SS:FFD0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 память. SS:FFE0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 SS:FFF0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | SP = FFFEh (дно стека) ------+ Указатель(SP) можно перемещать резервируя\освобождая таким образом стековую память. Равнодушен проц и к регистру(SS), который так-же можно выставлять на любую область памяти. В глазах 'политики безопасности' (которая в дос отсутствует) это конечно рискованный трюк, хотя отладчики почему-то так не считают, ратуя нам полную свободу действий. Посмотрим на схему ниже: Код (Text): ;//--- PUSH 1234h ---------------------------------------------------------------- SS:FFE0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 SS:FFF0 | 00 00 00 00 00 00 00 00 00 00 00 00 34 12 00 00 | SP=FFFСh ---+ ;//--- POP AX ---------------------------------------------------------------- SS:FFE0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 SS:FFF0 | 00 00 00 00 00 00 00 00 00 00 00 00 34 12 00 00 | SP=FFFEh ---+ Инструкция PUSH кидает на дно стека значение 1234h (по-соглашению Intel в обратном порядке), и сдвигает указатель на 2 влево. Но обратим внимание на действие команды POP. Она вынимает значение из стека, и просто сдвигает указатель(SP) вправо, оставляя сами данные болтаться там мусором. Следующий PUSH затерёт их.. Аналогичная картина наблюдается и при вызове прерываний INT лишь с тем отличием, что в стек помещается уже 6-байтная конструкция вида: флаги\CS\IP, а указатель(SP) смещается на 6 влево. После этого, CS:IP переустанавливается новыми значениями из таблицы векторов IVT, и проц приступает к выполнению обработчика прерывания. IRET (которым заканчиваются все обработчики) опять растасовывает по своим регистрам прежние значения из стека, и выравнивает его, сдвинув SP на 6 вправо. Напомню, что данные при этом остаются на том-же месте, а значит мы можем использовать их повторно. Не плохая идея.. Код (Text): ;//--- INT 21h (стек при вызове) ---------------------------------------------------------------- SS:FFE0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 SS:FFF0 | 00 00 00 00 00 00 00 00 27 01 F6 17 02 32 00 00 | | | | ^^^^^------ флаги SP=FFF8h ---+ | ^^^^^------------- CS: ^^^^^------------------- IP ;//--- IRET (стек на выходе) ---------------------------------------------------------------- SS:FFE0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 SS:FFF0 | 00 00 00 00 00 00 00 00 27 01 F6 17 02 32 00 00 | SP=FFFEh ---+ Если дать возможность отладчику сделать 1 стэп, то он дёрнет INT-1, а тот в свою очередь сохранит в стеке адрес возврата. В это время мы смещаем указатель(SP) на 6 влево, и запоминаем его в промежуточном регистре(BP). AVPutil хорошо отображает стек: Код (Text): CS:IP 17F6:0100 BP 0000 ODITSZAPC | Stack SS:SP 17F6:FFFE FL 7202 001000000 | -08 0000 _ ------------------------------------| -06 0000 \ Содержимое стека JMP Short 0102 | -04 0000 > при загрузке клиента NOP | -02 0000 _/ CS:IP = 17F6:0100 SUB SP,0006 |-> 00 0000 MOV BP,SP | +02 20CD PUSH DS | +04 9FFF PUSH 0000 | +06 9A00 POP DS | +08 FEF0 LES BX,DWord ptr [0004] | +0A F01D ;................. CS:IP 17F6:0102 BP 0000 ODITSZAPC | Stack SS:SP 17F6:FFFE FL 3202 001000000 | -08 17F6 _ ------------------------------------| -06 0102 \ Фрейм INT-01h NOP | -04 17F6 > после первого стэпа SUB SP,0006 | -02 3202 _/ CS:IP = 17F6:0102 MOV BP,SP |-> 00 0000 PUSH DS | +02 20CD PUSH 0000 | +04 9FFF POP DS | +06 9A00 LES BX,DWord ptr [0004] | +08 FEF0 MOV Word ptr CS:[012A],BX | +0A F01D Теперь, если перехватить отладочное прерывание(1) и дописать в его начало MOV_SP,BP, то IRET на каждом шаге будет возвращать отладчик в прошлое, заставляя его маршировать на одном месте. Реализовать это достаточно просто, и эффект на выходе получаем прикольный: Код (ASM): start: nop ; получили адрес возврата sub sp,6 ; запоминаем его справа от SP mov bp,sp ; BP = указатель на предыдущий фрейм push ds 0 ; pop ds ; les bx,[4] ; ES:BX = адрес обработчика(1) mov [cs:bug+0],bx ; копируем его в переменную mov [cs:bug+2],es ; ^^^ push cs @back ; pop eax ; EAX = адрес нового обработчика mov dword[4],eax ; перехватываем прерывание(1) pop ds ; jmp @begin ; уходим на начало полезного кода.. ;----------------------------------------------------------------- ;// Новый обработчик INT-1 ;// Если мы здесь, значит SP сместился ещё на 6 влево от BP @back: mov sp,bp ; возвращаем SP в прошлое db 0EAh ; JMP FAR bug dw 0,0 ; ..на обработчик отладчика ;----------------------------------------------------------------- @begin: nop ;[......] С этого момента участь отладчика предрешена! Адрес возврата в стеке замёрзнет на одной позиции, и последствия не заставят себя ждать. На работу программы в обычном режиме это не окажет никакого влияния, т.к. перехватывается тут только отладочное прерывание(1). Без отладчика, джумп в хвосте тупо прыгнет на метку '@begin:' и программа продолжит работу без каких-либо эксцезов.
Неплохие возможности предоставлет нам и 'Префикс программного сегмента' - PSP. В нём зарыто много интересных полей, на которые можно направлять(SP) и передавать управление через RET_FAR. Отладчик начнёт трассировать не нашу программу, а например сам-себя, или вообще command.com. Наиболее интересные поля PSP описаны ниже: Код (Text): 0 1 2 3 4 5 6 7 8 9 A B C D E F -------------------------------------------------- 17F6:0000 | CD 20 FF 9F 00 9A F0 FE 1D F0 6D BD 6B 05 4B 01 17F6:0010 | 12 04 51 BE 6B 05 6B 05 01 01 01 00 02 FF FF FF 17F6:0020 | FF FF FF FF FF FF FF FF FF FF FF FF B0 17 FE FF 17F6:0030 | F6 17 14 00 18 00 F6 17 FF FF FF FF 00 00 00 00 17F6:0040 | 05 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 17F6:0050 | CD 21 CB 00 00 00 00 00 00 00 00 00 00 20 20 20 17F6:0060 | 20 20 20 20 20 20 20 20 00 00 00 00 00 20 20 20 17F6:0070 | 20 20 20 20 20 20 20 20 00 00 00 00 00 00 00 00 17F6:0080 | 00 0D 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ;...... Addr Size Name ----------------------------- 00h 2 Инструкция INT 20h 02h 2 Сегмент первого\свободного байта за пределами программы = 9FFF: 05h 5 CALL_FAR на диспетчер DOS = CALL F01D:FEF0 0Ah 4 Адрес входа в Command.com = 056B:BD6D 0Eh 4 Адрес обработчика Int-23h = 0412:014B 12h 4 Адрес обработчика Int-24h = 056B:BE51 16h 2 Сегмент PSP родителя = 056B: 2Ch 2 Сегмент блока окружения = 17B0: 2Eh 4 Адрес стека программы SS:SP = 17F6:FFFE При чтении данных из PSP нужно иметь в виду, что поля её заполняются правильными значениями только после вызова сервиса DOS. В противном случае адреса будут указывать в космос, и наш план потерпит фиаско. Для этого можно использовать любую функцию DOS с приставкой(GET), которую мы тупо проигнорируем - например AH=30h 'Get_DOS_Ver'. Если у вас дося версии(5), то вообще шик.. и можно сразу отправить отладчик дебажить 'Диспетчер DOS', ниточка к которому торчит в PSP по смещению(5) от его начала: Код (ASM): ;//-- Вариант(#1). Экскурсия в досовский код ;------------------------------------------- start: push ss ; pop ss ; pushf ; pop bx ; test bh,1 ; проверяем бит(TF) je @okey ; mov ah,30h ; заполняем PSP 'правильными' значениями int 21h ; АХ = 5, если версия дос 5.00 jmp ax ; CALL_FAR на диспетчер DOS @okey: nop ; нет отладчика.. ;... ;//-- Вариант(#2). Подмена адреса возврата ;//-- отладчик трэйсит сам-себя. ;------------------------------------------- start: mov ah,54h ; int 21h ; правим PSP mov bx,12h ; ВХ = поле в PSP (вектор int24h) push ss ; pop ss ; pushf ; pop ax ; test ah,1 ; проверяем бит(TF) je @okey ; mov sp,bx ; подменяем адрес возврата retf ; уходим по нему! @okey: nop ; нет отладчика.. ;... Продолжение следует..
ЗАЩИТА ДЛЯ ЗАЩИТЫ Тему проверки бита(TF) можно продолжать бесконечно, но есть-ли в этом смысл? Реверсеру достаточно забить NOP'ами обработчик 'иначе', как защита разлетается в пух-и-прах. Не завидная перспектива.. От сюда следует, что для создания охранной системы одной сигнализации недостаточно, а нужен ещё и питбуль, который примчится на первый-же свист. В теории всё просто, а как-же на практике защитить свою тушку от обезвоживания? NOP идеально подходит для кастрации критических узлов кода, но только в клинических случаях, - когда кодер совсем не ценит свой труд и время. Как правило, чтобы противостоять забою нопом, тушку шифруют или переодически сверяют её контрольную сумму. Если копнуть исходник любого BIOS, то можно найти в нём типичный алгоритм проверки целостности кода. Назовём его (CRC-8): Код (ASM): Microsoft (R) MASM version 5.00 page, 120 TITLE ROM_CHECKSUM ;============================================================; ; DESCRIPTION: This routine checksums ROM modules and writes ; ; down the complementary result in the last ROM byte. ; ;============================================================; 0000 B9 FFFF start: mov cx,0ffffh ;number of bytes 0003 32 C0 xor al,al 0005 BB 0000 mov bx,0 0008 02 07 lp: add al,[bx] ;checksum --> AL 000A 43 inc bx 000B E2 FB loop lp 000D 32 D2 xor dl,dl 000F 2A D0 sub dl,al ;the complementary result 0011 88 17 mov [bx],dl Но что это меняет? Опять участок кода, который точно-также подвержен NOP-инъекции. В добавок, на выходе нужна ещё и проверка(JZ), которую легко обратить на JNZ всего одним битом. Отложим в сторону кроссворд и подумаем, как можно закрыть доступ к модулю "CheckSum". Так что-же творит отладчик внутри перехваченного INT-1? Откроем любую прогу в TD, сделаем 1 шаг, перейдём табом в окно его дампа, и комбинацией [CTRL+G] введём [0:0], чтобы запелинговать обработчик INT-1h. Здесь видно, что отладчиком перехвачены инты(0,1,3) в сегменте 199Eh, а интересующий нас обработчик(1) лежит по смещению [199E:01CEh]: Код (Text): [/COLOR][/COLOR] [COLOR=#0000ff][COLOR=#000000] |<---0-------------1---------------------------3--->| | | fs:0000 54 02 9E 19 - CE 01 9E 19 - 16 00 93 03 - D7 01 9E 19 fs:0010 8B 01 70 00 - B9 06 0E 02 - 40 07 0E 02 - FF 03 0E 02 Ясно.. Значит чтобы попасть в сердце отладчика достаточно установить бряк на этот адрес, как мы окажемся внутри святыни. Если отладчик позволит трэйсить свой-же обработчик, значит его механизм защиты для наших целей не подойдёт, и нужно искать другие пути. Переходим табом в окно кода и через [ALT+F2] ставим точку останова на адрес 199Eh:01CEh. Всё готово.. Теперь дебагер должен ткнуть нас носом прямо в обработчик(1), что и происходит при первом-же стэпе. Упс.. Пара CS:IP в окне регистров сразу-же сменилась, и мы наблюдаем два одинаковых входа в обработчики(1,3) с адресами 01CEh\01D7h соответственно. Тут даже одна функция(046Fh) на двоих, просто аргумент в AL разный: Код (Text): 199E:01CE 50 push ax[/COLOR][/COLOR] [COLOR=#0000ff][COLOR=#000000]199E:01CF B002 mov al,02 199E:01D1 90 nop 199E:01D2 0E push cs 199E:01D3 E89902 call 046F 199E:01D6 CB retf 199E:01D7 50 push ax 199E:01D8 B003 mov al,03 199E:01DA 90 nop 199E:01DB 0E push cs 199E:01DC E89002 call 046F 199E:01DF CB retf ;// == Снимем бряк по [F2], и заглянем внутрь функции(046F) клавишей [F7]: cs:046F->55 push bp cs:0470 8BEC mov bp,sp cs:0472 56 push si cs:0473 57 push di cs:0474 50 push ax cs:0475 E421 in al,21 ; читается маска IRQ cs:0477 2EA26F00 mov cs:[006F],al ; cs:047B 0C1B or al,1B ; ставятся в ней биты 1Bh = 0001 1011b cs:047D E621 out 21,al ; ...маскируются IRQ: 4 3 10 cs:047F 58 pop ax ; ------------------------------------ cs:0480 FB sti ; ^^^ это инты: 08h,09h,0Bh,0Ch cs:0481 5F pop di cs:0482 5E pop si cs:0483 5D pop bp ;//........... cs:049C B020 mov al,20 ; посылка EOI в APIC cs:049E E620 out 20,al ; ;//........... Главный облом в том, что все\подопытные дебагеры позволяют трассировать себя, а значит защита от инопланетного вторжения в них не предусмотрена. Может это и к лучшему? Есть повод активировать своё\серое вещество.
Пересчёт контрольной суммы кода Ламерский алгоритм CRC-8 приведён выше, и подразумевает сложение всех байт критического участка (отбрасывая перенос), и вычитания полученной суммы из константы 100h. Следующий код демонстрирует пример подсчёта контрольной суммы. Саму функцию "CheckSum" я вклеил в обработчик INT-1h и закинул в хвост сегмента, чтобы она не маячила в отладчике. Для опытных крэкеров это конечно булочка-с-маслом, но если учесть что ломкой страдают не только проф\пригодные взломщики, но и огромная армия пионеров, то шансы на спасение тушки всё-же остаются. Несколько слов о самой реализации, чтобы программа стала дееспособной.. 1. Критический участок оформлен в виде запроса пассворда, с которого считается 1-байтный хэш в регистр(BL). Хэш исходного пасса нужно расчитать до компиляции, и сравнивать его с регистром(BL). Я задал пароль "Коцит" и получил его хэш в виде байта(03h). Рассеивание никуда-не-годное, но это всего-лишь пример: Код (ASM): ;.. @stop: mov ah,9 ; mov dx,ok ; cmp bl,3 ; pass:? je @okey ; OK! dec byte[$+5] ; меняем int21h на int20h @okey: int 21h ; ;.. 2. Контрольная сумма расчитывается только с критического блока, поэтому её тоже нужно расчитать заранее и сохранить где-нибудь в потоке кода, или в хвосте программы. Я выбрал последнее.. Для расчёта валидной CRC(8) можно задействовать тестовую функцию, которую потом изъять из кода. Эта функция обязательно должа находиться ПОСЛЕ блока с которого собираемся считать сумму, иначе (после её кастрации) все адреса поменяются, что приведёт к искажению CRC. 3. В модуле "CheckSumm" (внутри обработчика) нет ничего нового, кроме фиктивного байта(0EAh), который представляет из-себя опкод инструкции [JMP_FAR]. Если вставить этот байт перед какой-нибудь инструкцией, а потом перепрыгнуть его обычным джумпом, то в дизассемблерном листинге получится каша, т.к. дизассемблер посчитает его реальной инструкцией и добавит последующие байты как сегмент:смещение к JMP_FAR. Вот отрывок этого участка, и как он дизассемблируется отладчиком: Код (ASM): ;// участок кода ;---------------------------------------------- and ah,0 ; jmp @crc ; db 0EAh ; jmp far @crc: lodsb ; add ah,al ; считаем сумму.. loop @crc ; shr ax,8 ; neg al ; 100h - AL = CRC(8) ;// и его дизассемблерный листинг ;---------------------------------------------- cs:0100->80E400 and ah,00 cs:0103 EB01 jmp 0106 cs:0105 EAAC00C4E2 jmp E2C4:00AC cs:010A FB sti cs:010B C1E808 shr ax,08 cs:010E F6D8 neg al После NEG_AL идёт сравнение контрольной суммы, с зашитой в программу константой. Разница должна быть нуль! Если внутри контролируемого участка прибить нопом хоть-одного тушканчика (или обратить проверку на JNZ), то CRC блока сразу-же изменится, а обработчик(1) отреагирует на это подменой адреса возврата. Последствия не заставят себя ждать.. Уязвимым участком остаётся сам обработчик, в котором и хранится игла кащея. В идеале его нужно было зашифровать, но я просто избавился от него, забросив подальше от конца программы. Остальное всё в комментариях: Код (ASM): ;fasm-code.. ;------------ org 100h jmp start mes0 db 'TurboDebuger MustDie!',13,10 db 'Type pass: $' ok db ' <---OK!$' ;============= ТОЧКА ВХОДА ===================== ;// перехватываем прерывание(1) ; На старте - в стеке нуль start: pop es ; снимаем его в ES push es ; ^^..(баланс) pushd [es:4] ; в стек вектор(1) popd [cs:old] ; запомнить его в своём теле push cs vec1 ; popd [es:4] ; ставим свой обработчик! push $+4 ; ret ; Begin... ;=======8<=============8<==============8<======= ;// Критический участка кода ------------------- ;// с которой снимается CRC(8) ----------------- begin: mov dx,mes0 ; mov ah,9 ; int 21h ; Type Password! (Коцит) sub bl,bl ; хэш пароля @in: xor ah,ah ; key int 16h ; cmp al,13 ; Enter? je @stop ; add bl,al ; считаем хэш.. mov al,1 ; int 29h ; выводим смайлик jmp @in ; @stop: mov ah,9 ; mov dx,ok ; cmp bl,3 ; pass:? je @okey ; OK! dec byte[$+5] ; int21h -1 @okey: int 21h ; endCRC: ; ;=======8<=============8<==============8<======= ;// Вспомогательная функция расчёта CRC(8) ----- ;// Закомментировать в обработчике "JNZ NEXT",-- ;// и полученное значение(AH) вписать в самый хвост, ;// по адресу "check". ------------------------- ; mov cx,endCRC ; mov si,begin ; sub cx,si ; xor ah,ah ;@lp: lodsb ; add ah,al ; loop @lp ; neg ah ;<-----; CRC(8) ;============ Конец программы ================== ;----------------------------------------------- @exit: xor ax,ax ; int 16h ; mov ax,4C00h ; Game Ower! int 21h ; ;// Резервируем область, чтобы обработчик не мозолил глаза.. space rb 0FFFh ;------------ ОБРАБОТЧИК INT-1 ----------------- ; Считает CRC-8 критического участка кода,------ ; и передаёт управление по цепочке обработчиков. vec1: pusha ; pushf ; mov cx,endCRC ; CX = хвост блока mov si,begin ; SI = голова sub cx,si ; CX = длина блока and ah,0 ; AH = 0 jmp @crc ; db 0EAh ;<--------; (!)собьём листинг дизасма @crc: lodsb ; add ah,al ; ..считаем сумму.. loop @crc ; shr ax,8 ; AL = контрольная сумма neg al ; 100h - AL = CRC(8) sub al,[check] ; ^^ должно быть нуль! jnz next ;<--------; закоментировать при тесте popf ; popa ; next: db 0EAh ; передаём управление по цепочке old: dd 0 ; ..на вектор отладчика check db 90h ;<--------; валидная контрольная сумма! ; ..у меня вышла 90h. На этом повествование можно было и закончить, но хотелось-бы сказать, что для сокрытия следов своей деятельности лучше проверять CRC не сразу, а откладывать проверку до лучших времён. Например, пересчитав контрольную сумму сохранить её в надёжном месте и в момент, когда реверсер совсем этого не ожидает - проверить её. Ясно, что хранить CRC в переменных или в стеке не самая лучшая идея, но кроме них есть ещё и порты! Если записать значение в неиспользуемый порт, то оно будет храниться там, пока юзверь не ребутнёт машину. Только нужно выбирать порт, который доступен на чтение и запись, иначе ничего не выйдет. Как-нельзя лучше подходят на эту роль порты LPT с номерами 378h\278h. Это порты данных, которые как-раз доступны на R\W. Присутствие самого девайса при этом совсем не обязательно, - лишь-бы бивис поддерживал порт. Вот модернезированный вариант пересчёта контрольной суммы, с сохранением результата в порту(378h): Код (ASM): portB dw 378h ; где-нибудь.. ;//------------------------ ;[.....] mov dx,[portB] ; номер порта sub ah,ah ; @crc: lodsb ; add ah,al ; ..считаем сумму.. loop @crc ; neg ah ; xchg al,ah ; AL = CRC(8) out dx,al ; посылка до востребования! ;[.....] ;// проверка в любое время ;//------------------------ mov dx,[portB] ; imul ax,0 ; AX=0 push @okey ; mov bp,sp ; BP = адрес перехода in al,dx ; берём CRC xor al,[check] ; фэйс-контроль (впускают только лысых) add [bp],ax ; прибавим АХ к адресу перехода retn ; если CRC=0, то ничего не изменится @okey: nop ; иначе: в космос.. Как говорилось в начале сабжа - невозможно раскрыть эту тему до конца в пределах одного топика, поэтому ставлю здесь жирную точку, и желаю Всем удачи! PS//:::::: ------------------ 1. Материалы взяты из свободных источников. 2. Все эксперементы проводились лично и только на своей машине. 3. Программный код ни одного из отладчиков модификации не подлежал, а значит можно спать спокойно. The end..
Коцит, Это же дос. Эти методы принципиально не портабельны на нт. Или под иную ось, которая использует IA архитектуру. И асм у вас кривой: 1. cs:[old] должно быть, так как такова архитектура и адресация - селектор:смещение. Смещение задаётся в квадратных скобках, таким образом адрес отличается от константы, загружаемой в регистры. 2. Такой инструкции не существует.
Уважаемый, это синтаксис фасма (поэтому и топик тут), а у него сегментный регистр указывается в скобках, т.к. по сути является частью адреса