Проблема защиты своего софта от крякеров
стоит в последнее время довольно остро.
Многие современные программы от солидных
фирм ломаются на “раз-два-три”. Чего уж тут
после этого говорить о шароварных
творениях, когда цена какой бы то ни было
защиты намного дороже десятка копий
защищаемой программы. Но процесс
изобретения защит не стоит на месте! 🙂 Всё
время появляются новые мульки для борьбы с
debugger’ами. Сейчас я расскажу как можно
обойти перехват WinApi в таких монстрах как
SoftIce (да и некоторых других дебаггерах). Для
начала надо узнать как происходит перехват
WinApi’шки или своей процедуры:

Когда мы пишем в софтайсе bpx, отладчик
ставит в процедуру вместо первой
инструкции опкод 0CCh, что соответствует int 03 (На
самом деле это, конечно, не int 03, опкод
которого 0CD03h, но все же это инструкция
вызывающая специальное прерывание и
возвращающее управление отладчику. Короче
говоря, покопайся в “Intel Hex Opcodes And Mnemonics” и
все поймешь). Т.е. когда вызывается
отслеживаемая функция, срабатывает
прерывание и управление получает SoftIce. Что ж,
давай проверим это утверждение. Для этого
поставим бряк на MessageBox (bpx MessageBox) в нашем
процессе и напишем следующий код:

function IsThereDebuggingProc(libname,
procname:string; Range:integer):integer;
var ProcAddr:Pointer;
mh,i:integer;
begin
result:=0;
mh:=LoadLibrary(Pchar(libname)); 
if mh = 0 then exit;
ProcAddr:=GetProcAddress(mh, Pchar(procname));
if ProcAddr = nil then exit;
for i:=0 to Range do
if byte(Pointer((integer(ProcAddr)+i))^) = $0CC then
inc(result);
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
if IsThereDebuggingProc('user32.dll', 'MessageBoxA',10)<>0 then
Caption:='Debugging proc present'
else
Caption:='There''s not debugging proc';
end;

Теперь почти то же, но на ассемблере:

IsThereDebuggingProc proc libname:PCHAR,
procname:PCHAR, Range: WORD
push libname
call LoadLibraryA
cmp eax, 0
je exit
push procname
push eax
call GetProcAddress
cmp eax, 0
je exit
mov cx, Range
continue:
cmp [eax], 0CCh
je UnderWatch
inc eax
loop continue
exit:
ret
UnderWatch:
mov eax, TRUE
ret
IsThereDebuggingProc endp

И на С:

BOOL IsThereDebuggingProc(char* libname, char*
procname, int Range)
{
HINSTANCE mod = LoadLibraryA(libname);
if (mod==NULL) return 0;
PROC proca = GetProcAddress(mod, procname);
if(proca==NULL) return 0;
int i;
for (i=0; i<=Range; i++) if ((BYTE)(*(BYTE*)((int)proca+i))==0xCC) return
TRUE;
return 0;
}

Далее я буду приводить код только на
Дельфе, ибо главное метод, а не реализация 🙂
Функции IsThereDebuggingProc передаются имя
библиотеки (libname), в которой расположена
проверяемая функция, имя самой функции (procname)
и количество байт, которые проверяем на
наличие 0CCh (т.е. дебаггера), ведь бряк можно
поставить и не на первую инструкцию! (например
bpx MessageBoxA+8) Главное, чтобы опкод 0CCh совпадал
с началом инструкции, иначе мы просто
изменим какую-нибудь часть инструкции и
отслеживаемая программа просто упадёт.
Также можно следить и за своими функциями и
процедурами – передавать в качестве
параметра адрес функции и количество байт
от offset’а:

function cooltest:string; // просто для
примера
begin
result:='cool';
end;

function IsThereDebuggingMyProc(offset:integer; Range:integer):integer; // проверяем
свою ( хотя можно и любую другую из
библиотеки!) функцию на наличие 0CCh

var i:integer;
begin
result:=0;
for i:=offset to offset+Range do
if byte(Pointer(i)^) = $0CC then
inc(result);
end;

procedure TForm1.Button3Click(Sender: TObject);
begin
if IsThereDebuggingMyProc(integer(@cooltest),20)<>0 then
Caption:='Debugging proc present'
else
Caption:='There''s not debugging proc';
end;

Но может возникнуть вопрос, а что если бряк
поставили на LoadLibraryA или GetProcAddress? Или даже на
обе эти функции 🙂 и вызов 

mh:=LoadLibrary(Pchar(libname)); 
ProcAddr:=GetProcAddress(mh, Pchar(procname));

приведет к раскрытию “защиты”. Для этого
мы можем сначала проверить эти две функции,
а потом уже использовать IsThereDebuggingProc. Вот
пример функции, ищущей и проверяющей
функции из kernel32.dll:

function IsThereDebuggingProcHeader(procname:string;
Range:integer):integer; 
{
Данная функция ищет
ImageBase kernel32.dll (этот метод подробно описан
здесь - http://sbvc.host.sk/articles/9.html),
затем через таблицу экспорта находи нужную
нам функцию и проверяем её на наличие
дебаггинга.
}
label l;
var
h:integer; //
здесь будет
длительное время лежать ImageBase кернела.
Также будет использоваться при поиске
кернела.

f:boolean; //
используется
как флаг при поиске ImageBase и показывает нашли
ли мы искомую функцию.
i,j:dword;
ied: PImageExportDirectory; //
указатель
на секцию описателей экспортируемых
функций. (хотя этот pointer будет
использоваться для целого таза объектов :))

begin //
самый последний Seh
будет указывать на процедуру в сердце
kernel32.dll, поэтому найти территорию кернела не
составит труда 🙂

asm
push ebx
mov ebx, fs:[0]
@l:
cmp [ebx], -1
jz @l2
mov ebx, [ebx]
jmp @l
@l2:
mov eax, [ebx+4]
mov h, eax
pop ebx
end;
h := h and $FFFF0000; //
как
известно, кернел выровнен на границу в 10000h,
так что его искать теперь ещё легче.

result:=0;
ied := pointer(h);
L:
f:=false;
try
while (PImageDosHeader(ied).e_magic<>IMAGE_DOS_SIGNATURE) and
(PImageNtHeaders(integer(ied)+PImageDosHeader(ied)._lfanew).Signature<>IMAGE_NT_SIGNATURE)
do begin
dec(integer(ied),$10000);
end;
except
dec(integer(ied),$10000);
f:=true;
end;
if f then
begin
f:=false;
goto l;
end;
h:=integer(ied); //
после того
как ImageBase найден, можно приступить к поиску
требуемой функции.

ied := pointer(PImageDosHeader(ied)._lfanew+h);
ied := pointer(h + PImageNtHeaders(ied).OptionalHeader.DataDirectory[0].VirtualAddress);
for i:=0 to ied.NumberOfNames-1 do begin
j:=(pdword(h+integer(ied.AddressOfNames)+i*4))^;
if lstrcmpi(pchar(procname),pchar(h+j))=0 then //
сравнение
я решил сделать не чувствительным к
регистру.

begin
f:=true;
break;
end;
end;
if f then //
если процедура
найдена,

begin //
то проверяем её на
наличие 0CCh.

j:=h+(pdword(h+integer(ied.AddressOfFunctions)+i*4))^;
for i:=j to j+range do
if byte(Pointer(integer(i))^) = $0CC then
inc(result);
end;
end;

procedure TForm1.Button2Click(Sender: TObject); // проверяем
GetProcAddress

begin
if IsThereDebuggingProcHeader('GetProcAddress',10)<>0 then
Caption:='Debugging proc present'
else
Caption:='There''s not debugging proc';
end;

Теперь мы умеем находить факт отладки… Но
как от неё можно уйти? Очень просто! 🙂 Раз мы
уже знаем, что SoftIce (или любой другой
отладчик) вставляет куда не попадя 0CCh, то мы
можем хранить небольшую таблицу
восстановления “повреждённых функций”.
Суть этого метода проста: мы проверим какую-нибудь
функцию на наличие отладки и восстановим её.
Т.е. int 3 не будет вызвано, а значит и “злобный
дядя крякер“ ничего “лишнего” о началу не
увидит. Для примера я выбрал MessageBoxA, потому
что именно эта функция чащё всего
сигнализирует об удачной или неудачной
регистрации приложения. Для этого нам
придётся её задизасмить:

Exported fn(): MessageBoxA - Ord:01DDh
:77D5ADD7 833DC4D3D87700 cmp dword ptr [77D8D3C4], 00000000
:77D5ADDE 0F85377E0100 jne 77D72C1B

* Referenced by a (U)nconditional or (C)onditional Jump at Addresses:
|:77D72C33(C), :77D72C43(U)
|
:77D5ADE4 6A00 push 00000000
:77D5ADE6 FF742414 push [esp+14]

Из листинга видно, что если будут ставить
бряк, то скорее всего это будет bpx MessageBoxA или
bpx MessageBoxA + 7… Значит в нашем лечащем массиве
для MessageBoxA будет 2 записи:

HealMas[0].position:=0; // позиция, от начала (первого
байта) излечиваемой функции
HealMas[0].truly:=$83; // сам опкод лечения
HealMas[1].position:=7;
HealMas[1].truly:=$0F;

Сама структура HealMas выглядит так:

Type
PHealRec = ^THealRec;
THealRec = record
position:integer;
truly:byte;
end;

А вот и сама процедура лечения:

procedure Healing(ArrPointer:PHealRec;
NumOfElems:integer; Offset:integer);
{
параметры следующие:
указатель на массив, (!!! массив надо
создавать с 0-ого элемента!!!) содержащий
количество “лечений”, количесво элементов
массива, и указатель на первый байт
излечаемой процедуры/функции
}
var i:integer;
saved,s:cardinal;
b:pbyte;
begin
for i:=0 to NumOfElems-1 do
begin
b:=pointer(offset+PHealRec(integer(ArrPointer)+i*sizeof(THealRec)).position); //

высчитываем указатель на исправляемый байт

VirtualProtect(b, 1, PAGE_READWRITE, saved); //
делаем
возможным запись в этот байт

b^:=PHealRec(integer(ArrPointer)+i*sizeof(THealRec)).truly; //
восстанавливаем
всё что попортил дебаггер

VirtualProtect(b, 1, saved, s); //
возвращаем
атрибуты памяти

end;
end;

procedure TForm1.Button4Click(Sender: TObject);
var HealMas:array[0..1] of THealRec;
h:integer;
begin
HealMas[0].position := 0; //
заполняем
массив

HealMas[0].truly := $83;
HealMas[1].position := 7;
HealMas[1].truly := $0F;
h := loadlibrary('user32.dll');
Healing(@HealMas,2,integer(GetProcAddress(h,'MessageBoxA'))); //

вызываем “лечение”

MessageBoxA(0,'Test','Am I healed?',MB_OK); //
её
никто не поймал 🙂

end;

Ну это всё было про SoftIce и WinApi. Теперь же мы
попробуем и саму Delphi обмануть! 🙂 Для этого
напишем какую-нибудь функцию и поставим в
ней BreakPoint, после чего излечим её и вызовем. В
результате выполнение программы не
прервется, а красная полоска в редакторе
так и будет одиноко стоять, символизируя
обманутые надежды: 🙂

function cooltest(i:integer):string; // подопытная
функция

begin
result := inttostr(i); // т
ут
поставим BreakPoint 🙂

result := result+' cool';
end;
procedure TForm1.Button4Click(Sender: TObject);
var HealMas:THealRec;
h:string;
begin
HealMas.position := 6;
HealMas.truly := $8b;
Healing(@HealMas,1,integer(@cooltest));
H := cooltest(5);
MessageBoxA(0, pchar(h), 'Am I healed?', MB_OK);
end;

Смещение в “6” можно увидеть из дизасма
процедуры:

* Referenced by a (U)nconditional or (C)onditional
Jump at Address:
|:0044DD12(C)
|
:0044DD64 53 push ebx
:0044DD65 56 push esi
:0044DD66 8BDA mov ebx, edx
:0044DD68 8BF0 mov esi, eax
:0044DD6A 8BD3 mov edx, ebx //
вот на
эту строчку и ставится int 03 (важно помнить,
что нумерация байт в процедуре начинается с
нуля!)

:0044DD6C 8BC6 mov eax, esi
:0044DD6E E8E59DFBFF call 00407B58
:0044DD73 8BC3 mov eax, ebx

* Possible StringData Ref from Code Obj ->" cool"
|
:0044DD75 BA8CDD4400 mov edx, 0044DD8C
:0044DD7A E85963FBFF call 004040D8
:0044DD7F 5E pop esi
:0044DD80 5B pop ebx

* Referenced by a (U)nconditional or (C)onditional Jump at Address:
|:0044DD1A(C)
|
:0044DD81 C3 ret

Что ж… Вот и всё. 🙂 Теперь наша шароварная
софтина стала немного неуязвимей и она уже
может отпугнуть какого-нибудь начинающего
крякера. 🙂 Будем надеяться, что этот метод
доставит пару приятных минут любителям
поковыряться в чужом коде.

Список литературы:

Intel Hex Opcodes And Mnemonics (взято из поставки MASM’а)
Создание эффективных WIN32-приложений с
учетом специфики 64-разрядной версии Windows,
Джеффри РИХТЕР. Эта книга заставляет
задуматься об устройстве винды и софта под
неё 🙂

Теперь thanks’ы:
Спасибо Dr.Golova & volodya за то, что предложили
идею.
Спасибо R4D][ за тестинг примеров и
содействие в написании статьи. 🙂

Check Also

Изучаем ПЛК. Краткий гайд по поиску уязвимостей в промышленных контроллерах

Если ты думаешь, что контроллеры, которые ставят в зданиях и на заводах, защищены намного …

Оставить мнение