Версия 1.0.1
Работа программиста реализующего код защиты как правило является достаточно увлекательным занятием. Но правда сам интерес начинает проявляться только после того, как ваш продукт был в первый (или очередной) раз взломан :)
В этот момент у вас на самом деле появляется "работа", которая заключается в анализе результатов работы реверсера и построению алгоритма противодействия.
Это как война на баррикадах, сначала мы их, потом они нас и на новый круг :)
Анализируя методы взлома вашего ПО вы, во первых, учитесь и учит вас именно реверсер, за что ему, как минимум, стоит сказать спасибо. Он таким своеобразным образом делится с вами знаниями, которые вы не должны упускать.
Немножко поделюсь и я своими наработками с обоих сторон баррикад :)
А именно способом ухода от отладчика через срыв стека.
В момент анализа стороннего ПО мы работаем как правило со отладчиком. Одним из главных элементов отладки является наличие валидного стэка отлаживаемой нити. На основании стэка нити отладчик (а если ему не под силу, то дополнительные скрипты) достаточно подробно строят стэк вызовов процедур, на базе которого происходит часть анализа кода. На стэке лежат параметры вызова функций. На том-же стэке хранятся SEH фреймы. Ни один вызов функции не обходится без стэка. Собственно стэк наше все :) (
Глава 6. Базовые сведения о потоках)
В случае если вы каким либо образом обнаружили отладчик, то не стоит сразу делать TerminateProcess(), его отловят и вернут как было. Если сломать "механизм стэка", то отладка вашего приложения в большинстве случаев станет не возможна. А отловить код поломки уже на порядок сложнее, чем перехватить пресловутый Halt или TerminateProcess.
Смысл стрыва стека сводится к генерации необрабатываемого исключения PAGE_FAULT, после чего процесс можно только закрыть. Дальнейшая его отладка бесперспективна.
Здесь я приведу четыре классических варианта срыва стэка:
1. Что есть стэк с точки зрения программиста в конкретной точке кода?
Это два регистра, EBP - Base Pointer и ESP - Stack Pointer.
Изменив эти два значения на любые произвольные, мы разрушим стэк::
asm
mov ebp, 0
mov esp, 0
end;
2. Стек всегда растет вниз, в то время как сверху остаются некие данные в том числе и адреса возврата и SEH фреймы. Все эти данные доступны для чтения и модификации. Удалив их мы разрушим стэк.
procedure _FillChar(var Dest; count: Integer; Value: Byte);
asm
{ ->EAX Pointer to destination }
{ EDX count }
{ CL value }
PUSH EDI
MOV EDI,EAX { Point EDI to destination }
MOV CH,CL { Fill EAX with value repeated 4 times }
MOV EAX,ECX
SHL EAX,16
MOV AX,CX
MOV ECX,EDX
SAR ECX,2
JS @@exit
REP STOSD { Fill count DIV 4 dwords }
MOV ECX,EDX
AND ECX,3
REP STOSB { Fill count MOD 4 bytes }
@@exit:
POP EDI
end;
procedure TForm1.btnKillUpStackClick(Sender: TObject);
var
P: Pointer;
begin
_FillChar(P, MaxInt, 0);
end;
Здесь используется реализация FillChar из Delphi 7. В более старших версиях данная продцедура выполнена несколько другим способом и не дает необходимого эффекта.
3. Границы стека всегда обрамлены страницами с флагом PAGE_GUARD. Это можно наглядно увидеть
в данном примере. Механизм работы данного флага следующий, при обращении к участку памяти с данным флагом срабатывает исключение Access Violation и данный флаг снимается. После чего при повторном обращении к этому участку срабатывает PAGE_FAULT.
В данном коде используется локальный статический массив для ускорения переполнения стека.
procedure TForm1.btnKillStackOnGuardClick(Sender: TObject);
procedure T;
var
HugeBuff: array [0..10000] of DWORD;
begin
if HugeBuff[0] <> HugeBuff[10000] then
Inc(HugeBuff[0]);
T;
end;
begin
try
T;
except
T;
end;
end;
4. Срыв стека на рекурсивном вызове SEH фрейма. Логика данных фреймов проста, после установки они обрабатывают все исключения до тех пор, пока не будут сняты. В Delphi они представлены в частично обрезанном виде в качестве оберток try..finally/except. Идея заключается в том, что после установки SEH фрейма мы не производим его удаления и в нем-же генерируем ошибку, заставляя рекурсивно вызывать самого себя. В результате мы имеем переполнение стека, плавно перерастающее в PAGE_FAULT.
procedure TForm1.btnKillStackOnSEHClick(Sender: TObject);
begin
asm
lea eax, @KillStack
push eax
push dword ptr [fs:0]
mov [fs:0], esp
xor eax, eax
mov eax, [eax]
@KillStack:
mov eax, 0
call eax
end;
end;
Пошагово трассировать всю вашу программу никто никогда не будет, поэтому разместив несколько данных ловушек вы достаточно осложните жизнь реверсеру, которому придется раз за разом перезапускать процесс отладки с целью локализовать место срыва стэка.
Подсветка кода выполнена при помощи: http://highlight.hohli.com/