┌──┌─┐┌──
──┘├─┘──┘ Presents
┐  ┌┐┐┌─┤ VMag, Issue 1, 1 June 1997
└─┘┘ ┘└─┘ ──────────────────────────

          Методы борьбы с отладчиками и дизассемблерами.

        Режим  пошагового  выполнения   (трассировки)   программы
инициируется установкой флага 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:
    ...                ; здесь нужно восстановить указатели стека!

        Интересные методы,  позволяющие  изменить  логику  работы
программы   при   ее   пошаговом   исполнении   под   отладчиком,
основываются на использовании  конвейерного  принципа  выполнения
команд центральным процессором (очереди команд).

        Очередь  команд   представляет   собой   набор   байтовых
регистров  в  схеме  шинного  интерфейса  процессора,  в  которые
поступают коды, выбранные из программной  памяти  непосредственно
перед их выполнением. Когда  операционное  устройство  процессора
занято  выполнением  команды,  шинный  интерфейс   самостоятельно
инициирует опережающую выборку кодов очередных команд из  памяти,
что позволяет совместить во времени  фазы  выборки  и  выполнения
команд. Таким образом достигается высокая плотность загрузки шины
и  повышение  скорости  выполнения  программы.   При   выполнении
операционным устройством команд передачи управления  (условные  и
безусловные переходы, вызовы подпрограмм и прерываний, возврат из
подпрограмм и прерываний), шинный интерфейс сбрасывает очередь  и
начинает выборку команд  по  новому  адресу.  Отладчик,  выполняя
программу в пошаговом режиме, очищает очередь  команд  на  каждом
шаге,    вызывая     трассировочное     прерывание.     Механизмы
противодействия трассировке могут использовать  эту  особенность.
Пусть, например, 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:
    ...

        (c) Mike Belonosov
            ■ 2:463/198.4@FidoNet