─ Echo From 2:5022/12.0 (2:5022/12.23) ───────────────────────────── RU.HACKER ─ Msg : 164 из 221 From : Gate :) 2:5030/84 Втр 21 Янв 97 23:00 To : All Пон 27 Янв 97 14:11 Subj : [1/2] тpактат ──────────────────────────────────────────────────────────────────────────────── Hello All! ─ Moderatorials (2:5030/84) ────────────────────────────────────────── >MODERS Msg : 441 of 449 Pvt Cfm From : Mike Belonosov 2:463/198.4 10 Jan 97 13:49:22 To : Comoderator 11 Jan 97 20:56:24 Subj : ru.hacker ─────────────────────────────────────────────────────────────────────────────── Hi, Comoderator! Покорнейше прошу Вашего высочайшего позволения кинуть в эху мой трактат, посвященный методам борьбы с отладчиками и дизассемблерами. === Cut === Режим пошагового выполнения (трассировки) программы инициируется установкой флага TF в регистре флагов. В пошаговом режиме процессор автоматически генерирует трассировочное прерывание (INT 1) после выполнения каждой команды или пары команд, если первая команда связана с изменением или пересылкой регистра SS. Процессоры 8086/8088 пропускают трассировочное прерывание после команд изменения или пересылки любых сегментных регистров. Эта особенность пошагового режима работы называется "потерей трассировочного прерывания" и может быть использована для определения работы программы под отладчиком. Обычно процедура обработки трассировочного прерывания используется программами отладки для индикации содержимого регистров и некоторых ячеек памяти. При обработке прерываний процессор сохраняет в стеке содержимое регистра флагов, адрес возврата CS:IP, а затем сбрасывает флаги IF и TF, что предотвращает пошаговое выполнение самого обработчика прерывания. Когда процедура обработки прерывания завершается, из стека извлекаются прежние состояния флагов и процессор снова переводится в пошаговый режим работы. Выполняя программу в режиме трассировки, все популярные отладчики (включая Turbo Debugger, Periscope, CodeView, AFD) отслеживают команды PUSHF и INT, и перед их выполнением сбрасывают флаг TF. Этим достигается эмуляция реального поведения трассируемой программы -- при работе под отладчиком в стек засылаются те же самые данные, что и в обычном режиме работы. Следующий фрагмент кода реализует один из возможных способов регистрации выполнения программы в режиме трассировки, использующий особенности работы процессора и отладчика в пошаговом режиме: ... pop ss ; в режиме трассировки после этой команды ; прерывание int 1 не будет вызвано pushf pop ax ; получить флаги в ax test ax,0100h ; установлен ли флаг TF ? jnz tracing ... tracing: ; работа в пошаговом режиме! ... Потеря процессором трассировочного прерывания после выполнения команды POP SS приводит к тому, что отладчик "не заметит" команду PUSHF и в стек будет занесено реальное состояние регистра флагов (с установленным битом TF). Как правило, отладчики позволяют устанавливать в коде программы контрольные точки (breakpoints). Прерывание контрольной точки вызывается командой INT 3 с кодом операции 0CCh. Однобайтовая длина команды INT 3 дает возможность установить контрольную точку в любое место программы, где нужно прервать нормальное выполнение и выполнить некоторые специальные действия. Поскольку использование прерываний 1 и 3 характерно практически для всех отладочных средств, можно сделать предварительный вывод о работе программы под отладчиком, если вектора этих прерываний не указывают на инструкцию IRET (код 0CFh). Если же в достаточном количестве разбросать по коду программы команды вызова прерывания контрольной точки, отладчик будет останавливаться на каждой инструкции INT 3, а при нормальном запуске программы ее выполнение прерываться не будет. Эффективность этого метода существенно повышается при использовании команд вызова прерывания контрольной точки внутри циклов с большим числом повторений. Естественная реакция хакера в таких случаях -- замена инструкций INT 3 на NOP. Поэтому предложенный способ желательно дополнять подсчетом контрольной суммы участков кода, в которых происходит вызов INT 3, или же поручать обработчику прерывания контрольной точки какую-нибудь полезную работу. В простейшем случае можно просто переустановить вектора этих прерываний на процедуры, вызывающие завершение выполняемой программы (например, на обработчики прерываний INT 22 (Program Termination) или INT 0 (Divide Error)), но опытного хакера этот способ надолго не остановит. еплохих результатов в плане защиты кода от трассировки можно добиться, используя обработчики отладочных прерываний для динамической модификации кода программы. В следующем примере обработчик прерывания INT 1 замещает пару команд NOP вызовом прерывания 21h, сбрасывая перед возвратом флаг TF для выхода из пошагового режима. При трассировке этой программы отладчиками CodeView и AFD наш обработчик INT 1 так и не вызывается, а Turbo Debugger, хотя и ухитряется заменить NOP'ы на INT 21, дальше выполнения первой команды PUSHF идти не желает. Во всех случаях результат один: вывод на экран сообщения msg блокируется. code segment assume cs:code,ds:code,ss:code org 100h start: mov ax,2501h ; установить обработчик int 1 mov dx,offset int1 int 21h pushf ; инициировать пошаговое pop ax ; выполнение программы or ah,1 ; TF=1 push ax popf mov dx,offset msg ; вывод сообщения mov ah,09h print: nop ; это место для int 21h nop int 20h ; выход int1: push bp ; обработчик int 1 mov bp,sp push ax mov ax,[bp+6] ; сбросить флаг TF and ah,not 1 mov [bp+6],ax mov word ptr print,021CDh ; сформировать команду int 21h pop ax pop bp iret msg db 'Normal execution! ',0Ah,0Dh,'$' code ends end start Еще один прекрасный способ сбить с толку практически любой отладчик -- назначение стека в область исполняемых кодов. Как правило, все отладчики используют стек трассируемой программы и для своих нужд, затирая при этом отлаживаемый код. В следующем примере серия команд push ax затирает команды выхода из программы, что позволяет программе продолжить выполнение и после метки stacktop. При попытке трассирования этого фрагмента обычно затираются и сами команды push ax. ... push cs ; настроить стек в область кода pop ss mov sp,offset stacktop mov ax,9090h ; nop,nop push ax ; затираем инструкции push ax push ax nop db 0B8h,00h,04Ch ; mov ax,4C00h db 0CDh,21h ; int 21h db 90h ; nop stacktop: ... ; здесь нужно восстановить указатели стека! Интересные методы, позволяющие изменить логику работы программы при ее пошаговом исполнении под отладчиком, основываются на использовании конвейерного принципа выполнения команд центральным процессором (очереди команд). CodeRipper --- DiskEdit 7.02 under OS/2 v2.2 * Origin: frq RUHACKER.ZIP - RU.HACKER FAQ. 00.00-07.00msk, 9600+ (2:5030/84) ─ Echo From 2:5022/12.0 (2:5022/12.23) ───────────────────────────── RU.HACKER ─ Msg : 165 из 221 From : Gate :) 2:5030/84 Втр 21 Янв 97 23:01 To : All Пон 27 Янв 97 14:11 Subj : [2/2] тpактат ──────────────────────────────────────────────────────────────────────────────── Hello All! <-prev msg Очередь команд представляет собой набор байтовых регистров в схеме шинного интерфейса процессора, в которые поступают коды, выбранные из программной памяти непосредственно перед их выполнением. Когда операционное устройство процессора занято выполнением команды, шинный интерфейс самостоятельно инициирует опережающую выборку кодов очередных команд из памяти, что позволяет совместить во времени фазы выборки и выполнения команд. Таким образом достигается высокая плотность загрузки шины и повышение скорости выполнения программы. При выполнении операционным устройством команд передачи управления (условные и безусловные переходы, вызовы подпрограмм и прерываний, возврат из подпрограмм и прерываний), шинный интерфейс сбрасывает очередь и начинает выборку команд по новому адресу. Отладчик, выполняя программу в пошаговом режиме, очищает очередь команд на каждом шаге, вызывая трассировочное прерывание. Механизмы противодействия трассировке могут использовать эту особенность. Пусть, например, cmd1 и cmd2 -- две последовательные команды, выбранные шинным интерфейсом в очередь для выполнения, причем cmd1 не является командой передачи управления или командой пересылки/изменения сегментных регистров. Если команда cmd1 изменит "образ" команды cmd2 в памяти на cmd2', это никак не отразится на ходе выполнения программы в нормальном режиме, поскольку процессор перейдет к исполнению следующей команды из очереди (cmd2). В пошаговом режиме после выполнения команды cmd1 произойдет вызов трассировочного прерывания. При возврате из прерывания очередь команд сбрасывается и из памяти выбирается модифицированная команда cmd2', что, как правило, изменяет логику работы программы. Следующий фрагмент кода иллюстрирует проведенные рассуждения. ... mov byte ptr _cmd2,0F9h ; 0F9h -- код операции stc _cmd2: clc jc tracing ... ; код "нормального" режима ... ; выполнения программы tracing: ... ; работа под отладчиком! В приведенном примере команда mov byte ptr _cmd2,0F9h (cmd1) осуществляет замену следующей команды clc (cmd2) на stc (cmd2'). Однако, в нормальном режиме работы программы, вместо команды stc будет выполнена "старая" команда clc, которая к этому моменту уже находится в очереди команд. При выполнении этого фрагмента в пошаговом режиме очередь команд будет сброшена и выполнится команда stc, что вызовет переход по метке tracing на соответствующий обработчик особой ситуации. В некоторых случаях удается обнаружить трассировочный режим выполнения программы, используя особенности выполнения под отладчиком системных вызовов DOS. При обработке прерывания 21h текущий указатель стека SS:SP исполняемой программы сохраняется по смещению 2Eh от начала PSP. Если INT 21h вызывается в пошаговом режиме, его обработка поручается соответствующим функциям отладчика и указатель стека, как правило, переустанавливается. Иногда это приводит к тому, что после вызова системной функции в PSP заносится указатель стека отладчика. Такая ситуация характерна, например, для отладчиков Turbo Debugger и CodeView, но AFD и Periscope на эту удочку не клюют. Программная реализация описанной ловушки может выглядеть следующим образом: ... mov ah,9 int 21h ; вызов функции вывода строки ... mov ax,ss ; сравнение стековых сегментов cmp ax,es:[30h] ; (предполагаем, что ES указывает на PSP) jne tracing ; сегменты не равны -- работа под отладчиком! ... tracing: ... ; реакция на трассировку Следующий способ обнаружения отладчика учитывает особенности инициализации регистров программы при загрузке. После загрузки программы типа COM, регистры CS,DS,ES и DS содержат сегментный адрес PSP, регистр IP равен 100h, а указатель стека адресует конец программного сегмента (обычно SP=0FFFEh, но может быть и меньше, если программе недоступен полный сегмент памяти). В регистр CX заносится длина образа программы, равная длине COM-файла. При загрузке EXE-программ, на PSP указывают регистры DS и ES, а начальное состояние регистров CS,IP,SS,SP устанавливается в соответствии со значениями в заголовке EXE-файла. В регистр CX помещается величина, определяемая следующей формулой: CX = (PageCnt*200h - HdrSize*10h + 200h - PartPag) % 0FFFFh, где PageCnt -- длина образа программы в 512-байтных страницах (2 байта по смещению +4 в заголовке EXE-файла), HdrSize -- длина заголовка в 16-байтных параграфах (2 байта по смещению +8), PartPag -- длина частично заполненной последней сраницы в байтах (2 байта, смещение +2), % -- операция взятия остатка от деления. Как EXE-, так и COM-программы инициализируют регистр AX одним из четырех значений: 00FFh, если 1-й аргумент программы начинается символами X:, где X соответствует букве несуществующего дисковода; FF00h, если 2-й аргумент программы начинается символами X:, где X соответствует букве несуществующего дисковода; FFFFh, если 1-й и 2-й аргументы программы ссылаются на несуществующие дисководы; 0000h, если 1-й и 2-й аргументы не ссылаются на несуществующие дисководы. Какими значениями должны инициализироваться регистры BX,DX,SI,DI,BP мне достоверно не известно. Похоже, регистр DI устанавливается равным SP, а все остальные всегда содержат нулевые значения. При первой загрузке программы отладчики Turbo Debugger и CodeView всегда обнуляют регистры AX,BX,CX,DX,SI,DI,BP. После повторной загрузки Turbo Debugger содержит в перечисленных регистрах мусор, оставшийся после первого прогона программы, а CodeView снова инициализирует их нулями. Таким образом, ахиллесовой пятой этих двух отладчиков можно считать регистр CX, в котором при старте программы всегда находится не то, что нужно. Кроме того, Turbo Debugger с большой вероятностью можно "поймать" и на повторной загрузке, проверяя значение в регистре AX. Отладчиком AFD при загрузке программы также обнуляются регистры AX,BX,DX,SI,DI,BP, но в регистр CX помещается корректное значение. После повторной загрузки CX снова устанавливается правильно, а остальные регистры содержат мусор. А вот Periscope v4.01, мало того, что корректно инициализирует регистр CX, к тому же еще почти всегда правильно загружает и регистр AX, за тем исключением, когда первый аргумент программы содержит символы '\' или '/' -- в этом случае AH всегда будет содержать 00h. С помощью следующего нехитрого трюка можно спрятать от некоторых "умных" отладчиков (типа TD.EXE) реально выполняемые команды. Достаточно сделать маскируемую команду частью более длинной инструкции и передавать на нее управление каким-либо способом (ну, скажем, jmp-ом). Отладчик честно дизассемблирует код, утратив мнемонику выполняемой в действительности команды, а неискушенный хакер, возможно, будет сбит с толку. В приведенном фрагменте создается иллюзия записи значения E3FFh по адресу 0000h:046Ch (счетчик тиков системного таймера), а на самом-то деле осуществляется переход на метку addr1. mov bx,offset addr1 jmp hidden ; переход в середину инструкции xor ax,ax mov ds,ax db 0C7h,06h,6Ch,04h ; здесь Turbo Debugger видит ; mov word ptr [046C],E3FF hidden: jmp bx ; переход на addr1 addr0: ... addr1: ... При проигрывании этого фрагмента под Turbo Debugger'ом инструкция jmp bx спрячется в теле команды mov word ptr [046Ch],0E3FFh. После выполнения команды jmp hidden, Turbo Debugger сначала прыгает на инструкцию по адресу addr0, после чего выполняется скрытая команда jmp bx и происходит переход по метке addr1. При этом команда jmp bx так и останется недизассемблированной. В более простых отладчиках (например, в AFD.COM), инструкция jmp bx, все-таки всплывает в момент выполнения jmp hidden. Тем не менее, описанный способ право на существование имеет и может также с успехом применяться для защиты кода от дизассемблеров. Весьма эффективное средство защиты от отладки и дизассемблирования -- использование обработчиков трассировочных прерываний для динамической модификации кода. В следующем примере код программы расшифровывается обработчиком пошагового прерывания INT 1. ... xor ax,ax mov es,ax mov ax,word ptr es:[06h] ; сохраняем вектор прерывания 01h mov cs:int01seg,ax mov ax,word ptr es:[04h] mov cs:int01off,ax mov word ptr es:[06h],cs ; устанавливаем новый обработчик mov word ptr es:[04h],offset coder mov dx,offset msg pushf pop ax or ax,0100h ; устанавливаем флаг TF push ax popf nop ; зашифрованные команды: db 2Eh,09h ; mov ah,9 db 57h,21h ; int 21h db 06h ; pushf db 0C2h ; pop ax db 0BFh,0FFh,0FEh ; and ax,0FEFFh db 0CAh ; push ax db 07h ; popf db 0Ah ; nop mov ax,word ptr cs:int01seg ; восстанавливаем вектор 01h mov word ptr es:[06h],ax mov ax,word ptr cs:int01off mov word ptr es:[04h],ax mov ax,4c00h int 21h coder: ; обработчик int 01h push bp mov bp,sp mov bp,word ptr [bp+2] xor byte ptr cs:[bp],9Ah pop bp iret int01off dw 0 int01seg dw 0 msg db 'Try to trace me!',0Dh,0Ah,'$' ... Следующий фрагмент кода иллюстрирует косвенную передачу управления на метку jmphere форсированным вызовом прерывания INT 0. Этот пример примечателен еще и тем, что отладчики, работающие в защищенном режиме (TD386.EXE и т.п.), не вызывают прерывание INT 0 для обработки деления на 0. апример, TD386 останавливает работу на инструкции div ah, выдавая сообщение "Divide by zero". ... xor ax,ax mov es,ax mov word ptr es:[0000h],offset jmphere mov word ptr es:[0002h],cs div ah ; деление на 0 ... jmphere: ... === Cut === Bye! Mike --- GoldEd 2.50 CodeRipper --- DiskEdit 7.02 under OS/2 v2.2 * Origin: frq RUHACKER.ZIP - RU.HACKER FAQ. 00.00-07.00msk, 9600+ (2:5030/84)