Блог → Защита программ от дисассемблирования и отладки

Итак, если вы читали мои предыдущие заметки о методах защиты программ от копирования, то вы вполне готовы к продолжению. Сегодня мы коснёмся темы защиты от "вскрытия" программ с помощью специальных программных средств для отладки и дисассемблирования, и полагаю, что эта тема тоже окажется интересной для широкого круга читателей. Мы рассмотрим некоторые методы распознавания совместной работы программ с отладочными средствами и защиты от просмотра программ стандартными программными отладчиками, и по мере продвижения, будем углубляться в тонкости этих методик, а описанные методы я постарался сгруппировать по степени их "понятности", от простых к более сложным.

Итак, насчём с построения антитрассировочных средств на основе учета аппаратных особенностей микропроцессоров Intel семейства x86. Микропроцессоры семейства Intel x86 относятся к микропроцессорам с конвейерной архитектурой, и это означает, что из всей последовательности команд, управляющее устройство микропроцессора выбирает блок из нескольких байт, и образует из них очередь или конвейер. По мере выборки команд для исполнения следующие байты сдвигаются к началу очереди, а очередь с конца пополняется новыми байтами так, что длина очереди постоянна.

Длина очереди составляет:
- для процессора Intel 8086 — 4 байта;
- для процессора Intel 80286 — 8 байт;
- для процессора Intel 80386 — 16 байт.

Периодически в процессоре происходит сброс очереди. Это означает, что при выполнении некоторых команд следующие байты, уже загруженные в очередь, игнорируются, а очередь загружается сначала с указанного в предыдущей команде адреса. Сброс очереди производится следующими командами:
INT - вызов прерывания;
IRET - возврат из прерывания;
CALL - переход к подпрограмме;
RET - возврат из подпрограммы;
JMP, JE, … - команды передачи управления.

Команды, занесенные в очередь, не могут быть модифицированы, поскольку очередь недоступна программным средствам. Отладочные средства при каждом шаге отладки сбрасывают очередь, т. е. в этом случае очередь содержит не более одной машинной команды. Данный эффект служит основой для построения надёжного теста на определение совместной работы с отладочными средствами.

Допустим, у нас имеется такая последовательность команд:

jmp metl ; сброс очереди
; запись в индексный регистр адреса метки
001 metl:
mov bx,offset met2

; запись по адресу метки кода СЗ - возврат
002 mov cs:[bx],0C3h
003 met2: nop ;старое содержание - "нет операции"


В таком случае, при нормальной работе фрагмента программы команды 001—003 будут помещены в очередь, и пересылка байта не окажет влияние на адрес по метке met2 и будет выполнена команда nop (no operation). При работе отладочных средств или программных эмуляторов очередь будет последовательно очищаться после исполнения каждой команды, и по адресу метки met2 будет послан байт С3, и в точке 003 выполнится команда RET.

Очевидно, что "напрямую" данный метод применяться не может, поскольку легко может быть обойден путем выполнения старой команды. Поэтому, его надо применять в сочетании с другими методами, в частности, с методом динамического изменения кода программы во время ее выполнения при котором изменения кода программы не так заметны.

Теперь рассмотрим противодействие работе программных отладчиков, использующих прерывания ONE STEP и BREAK POINT. В оперативной памяти компьютера имеется область векторов прерываний, которая представляет собой таблицу вида:



где: offN - смещение первой команды N-го прерывания;
segN - сегмент первой команды N-ro прерывания;
N = 0, 1, 2, …, 255.

Прерывания 1 и 3 являются отладочными и работают следующим образом:
- прерывание 1 (ONE STEP) вызывает очистку очереди команд, загрузку одной команды в очередь и ее выполнение;
- прерывание 3 (BREAK POINT) останавливает работу микропроцессора в заданной точке.

Выполнение прерываний происходит путем перехода к адресу segN:offN с сохранением старых значений регистра флагов, CS и IP в стеке. Записав в ячейки памяти некоторый адрес, при вызове прерывания можно обеспечить переход именно на данный адрес. Надо отметить, что некоторые отладочные средства при выполнении каждого прерывания 1 восстанавливают адреса прерывания 3 так, чтобы они указывали на собственный обработчик отладчика. Таким образом, при работе под отладочными средствами и определении прерывания 3 под свои собственные функции будет, тем не менее, выполняться обработчик отладчика. Данный факт также можно использовать для идентификации работы программы совместно с отладочными средствами.

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

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

Первый способ заключается в том, что по некоторому адресу в коде программы располагается, например, побайтная разность между реальными командами программы и некоторой хаотической информацией, которая располагается в области данных. Непосредственно перед выполнением данного участка программы происходит суммирование хаотической информации с содержанием области кода и в ней образуются реальные команды.

Этот способ подробно рассмотрен в ранее опубликованных материалах, и в основном рекомендуется для защиты от дисассемблирования. Для эффективной защиты от отладчика данный метод можно совместить с методами 2.1 или 2.2 - производить преобразование из хаотического кода к реальным командам только при условии отсутствия отладчика.

Второй способ состоит в перемещении фрагментов кода программы в определенное место или наложении их на уже выполненные команды при помощи стековых операций. Если же отладчик использует стек защищенной программы, то соответствующее определение стека будет приводить к затиранию кода защищенной программы под отладчиком. Этот способ также рассмотрен в отдельной заметке, так что мы не будем на нем останавливаться.

Третий способ служит для защиты от модификации кода программы и определения точек останова в программе. Он состоит в том, что преобразование следующего фрагмента кода происходит на основе функции, существенно зависящей от каждого байта или слова предыдущего фрагмента или нескольких фрагментов кода программы. Такую функцию называют обычно контрольной суммой участка кода программы. Особенностью данного способа является то, что процесс преобразования должен существенно зависеть от посчитанной контрольной суммы и не должен содержать операций сравнения.

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

Пятый способ состоит в том, что вместо адресов отладочных прерываний помещается ссылка на процедуру преобразования кода программы. При этом либо блокируется работа отладчика, либо неверно преобразуется в исполняемые команды код программы.

Приведу пример кода. Будучи откомпилирована и запущена на выполнение эта программа выдаст на экран сообщение: "Работа без воздействия отладочных средств". Если же вы захотите проанализировать работу программы, например, при помощи программы Turbo Debugger, то сообщение выдано не будет.

.MODEL TINY
. CODE

org 0
s:
; ES = 0
mov ax,0
mov es,ax

; DS = ES
push cs
pop ds
cli

; установка в поле смещения прерывания 3 смещение функции по метке f1
mov bx,12
mov cx,offset fl
mov es:[bx],cx

; установка в поле сегмента прерывания 3 реального сегмента
; данной программы - регистра CS
add bx, 2
mov dx,cs
mov es:[bx],dx
st i

; установка в BX адреса участка по метке ml: 20 команд nop
mov bx,offset ml
m CO и О CO
push cs
pop es

; обнуление регистра CX
xor cx,cx

; вызов прерывания 3
int 3h

; участок команд nop
ml: db 20 dup (090h)

; сравнение регистра CX
cmp cx,3
jne m2

; выдача сообщения
mes mov ah,009h
mov dx,offset
mes int 21h

; завершение программы
m2: mov ax,04C00h
int 21h

; фрагмент кода, который заместит прерывание 3
fl: mov es:[bx+2],09041h
mov es:[bx+4],09041h
inc cx iret

mes db "Работа без воздействия отладочных средств $"
end s


В данном примере функция, которая заместит прерывание 3 (она находится по метке fl), перешлет в участок команд nop по метке ml две команды inc сх - код 41h. Таким образом, если прерывание 3 будет переопределено какой-либо программой на себя (что и происходит — на каждом шаге отладки прерывание 3 определяется на обработчик Turbo Debugger), то функция по метке f1 не выполнится, и код на участке от метки ml не модифицируется. Следовательно, содержимое регистра сх после выполнения прерывания 3 не изменится и дальнейшее выполнение программы приведет к неверному результату.

Часто с отладочными прерываниями бывают совмещены более сложные алгоритмы преобразования кода программы, которые при выполнении каждой команды производят преобразование последующей в исполняемую, а предыдущей в хаотический код. Такой метод получил название "бегущая строка". Рассматривая данный пример, надо отметить, что для успешного применения данного метода обязательно выполнение следующих условий:
- невозможность переопределения функции, замещающей отладочное прерывание и выполняющее преобразование кода программы, на неиспользуемое прерывание (так, в примере достаточно добавить команды mov bx, 180h и int 60h, чтобы избавиться от противодействия отладчику);
- отсутствие операций сравнения для определения факта работы под отладчиком (в примере в команде cmp видно, что достаточно исправить содержимое регистра cx, чтобы выполнение программы пошло по правильному пути).

Выполнить первое условие возможно, например, просчитывая контрольные суммы пройденного участка кода, либо в функциях, замещающих отладочные прерывания, необходимо вводить существенную зависимость от положения их в таблице прерываний. Что же касается операций сравнения, то, если их избежать нельзя (например, чтобы нелегальная копия программы не "зависала"), то желательно сравнивать не контрольные числовые значения, а некоторую необратимую или сложно обратимую функцию от них. Так, для идентификации неверного пароля, можно считать арифметическую сумму его байт и проводить сравнение по ней. Ясно, что по сумме сложно восстановить искомый пароль. При этом для преобразования должен использоваться весь пароль.