Цей документ описує деякі аспекти роботи з компонентами Дельфі, а саме роботу з
вже скомпільованим кодом компонента у файлі dcu для внесення до нього деяких корисних змін.

Ні для кого не секрет, що багато хороші компоненти, що надають зручну функціональність, часто мають один загальний істотний недолік — за них треба платити. Зазвичай це виражається в різних попереджувальних написах і пропозиціях покупки. Тут будуть розглянуті варіанти організації такого захисту і способи її подолання.

Для роботи вам знадобиться: відладчик(бажано SoftIce, але можна обійтися і без нього, або хоча б OllyDebugger — дуже хороший відладчик користувацького рівня), hex — редактор(я використовую WinHex — дуже потужна програма), мінімальне розуміння winapi і спільної роботи Windows, а також хоча б уявлення про мову програмування асемблер(не завадить будь-якої асемблер — я особисто віддаю перевагу tasm).

Отже, приступимо. Випадок перший, симптоми: попередження при запуску програми у випадку незапущенного IDE. Ясно що компонент при запуску програми перевіряє наявність запущеного IDE — якщо немає -отримуємо сюрприз. Самий распростроненный спосіб — перевірка наявності в системі вікон певного класу, які створює середовище розробки. Цей пошук здійснюється функцією FindWindow, дивимося її опис в Win SDK:

HWND FindWindow(
LPCTSTR lpClassName, // pointer to class name
LPCTSTR lpWindowName // pointer to window name
);

lpClassName — вказівник на ім’я класу, наприклад TAppBuilder
lpWindowName — вказівник на ім’я вікна — зазвичай порожній, тобто пошук всіх вікон зазначеного класу

В разі успіху — хендл знайденого вікна, інакше 0;
Методика роботи:
Створюємо додаток з цікавлять нас компонентом — підключеним dcu.
Запускаємо SoftIce і ставимо на бряк FindWindow(хто не знає — bpx FindWindowA ну або FindWindowExA або дивіться exp FindWindow) — айс запуститься в момент виклику цієї функції.
Запускаємо програму.
Потрапляємо в айс — проматываем F10 до виходу з FindWindow і дізнаємося звідки була викликана ця функція (адреса повернення звичайно можна витягнути і з стека). Що ми бачимо в налагоджувач(компонент NativeExcel):
push 00h // 0 — порожній покажчик на ім’я вікна
push 0005F6498 // узакатель на клас вікна
call USER32!FindWindowA
mov ebx,eax // збереження результату
push 00h
push 005F64A8
call USER32!FindWindowA
mov esi,eax
push 00h
push 005F64B8
call USER32!FindWindowA
mov edi,eax
push 00h
push 5F64CC
call USER32!FindWindowA
test ebx,ebx // перевірка результату — видно якщо вікна немає — стрибаємо кудись
jz 005E136D // як раз на повідомлення
test esi,esi
jz 005E136D
test edi,edi
jz 005E136D
test eax,eax
jz 005E136D
jmp кудись на вихід // треба дістатися сюди

За адресами передаваних у функцію FindWindow в даному випадку знаходяться TApplication, TAlignPalette, TPropertyInspector, TAppBuilder. До речі важливе зауваження — практично всі функції WinApi повертають результат в регістр eax — тобто в нашому випадку в eax буде міститися хендл вікна або 0. Наприклад Ems QuickPDF повністю аналогічний код — правда перевірок менше — і заспокоюється у разі якщо хоча б одне вікно є в системі.
Так що з цим робити? Відповідь проста — саме правильне знайти цей код у файлі dcu використовуючи hex-редактор і трохи його виправити. Якщо використовується WinHex — просто забиваємо код в шаблон і шукаємо(до речі call виглядає як E800000000 — нулі це адреса який проставить PE-завантажувач при завантаженні файлу).
Замінювати інструкцію call можна — так як завантажувач пропатчивая адресу виклику знесе все, що було вами туди записано — в результатеполучится випадкова інструкція, зазвичай призводить до помилки пам’яті. Найпростіше рішення в даному випадку — замінити 2-х байтовую команду test наприклад на 2 однобайтові команди inc — хто не знає — ця команда збільшує операнд на 1, ось деякі опкоды — ви можете самі подивитися їх створивши процедуру або програму на асемблері і подивившись її в налагоджувач:

inc eax 40h
inc ebx 43h
inc esi 46h
inc edi 47h

Кому не в лом, може подивитися правила формування команд процесора. Отже, знайшовши в dcu потрібний код, міняємо інструкцію 84С0 на 4040 в результаті отримуємо: — тепер компонент думає що ide запущено незважаючи ні на що.

Таким чином, розібраний перший випадок, переходимо до другого. Симптоми: c першого погляду ті ж — але повідомлення з’являється поза зависимотсти від наявності в системі ide. В чому справа — випадково здогадуємося що швидше за все справа в налагоджувач, тобто програма перевіряє наявність відладчика — і якщо його немає — ми маємо жалюгідний результат. Як це можна визначити — дивимося sdk:

The IsDebuggerPresent function indicates whether the calling process is running under
the context of a debugger.
BOOL IsDebuggerPresent(VOID)

Ця функція повертає 0, якщо поточний процес запущений не з під відладчика і не 0 в іншому випадку. Далі технологія подібна до описаної вище. Цей спосіб представлений в пакеті AlphaControls — великий набір дуже гарних контролів. Правда розробники цього пакету вчинили хитро, напхавши перевірок в різний молули (захист проявляється послідовно при додаванні нових компонентів в проект і перевірки знаходяться у файлах sStyleSimple.dcu, sCommonData.dcu, sStypePassive.dcu) — тут проявилося дуже важлива властивість WinHex — пошук в декількох файлах і пошук з довільними символами. Загалом методика повністю аналогічна.

На останок кілька порад:
Пошук потрібного ділянки коду можна виконати, знайшовши текст, що виводиться компонентом (визначивши його адресу в модулі і знайшовши посилання на цей код — це швидше відноситься до OllyDbg)
може виникнути необхідність перестрибнути деякий ділянку коду — коли нічого змінити(так було в NativeExcel який писав у клітинку A1:A1 інфу про те, що це демо). Просто треба ассемблировать наступний код(tasm)
jmp short cs:6 + 2; 6 — це вихід на слід інструкцію після jmp — 2 скільки байт треба перестрибнути

команда займає 2 байти з опкодом EBXX — де хх — скільки байт треба перестрибнути
. Якщо знайшли в dcu потрібну ділянку — не поспішайте відразу міняти його — таких ділянок може бути кілька — заміна не того може призвести до помилок!
Після того, як зміни збережені, проект треба закрити і відкрити заново, ну і природно перекомпилять 🙂 — тоді зміни вступлять в силу.
Я ні в якому разі не закликаю ламати все відчайдушно — все таки розробник теж людина :), що витратив на створення деякий час і обґрунтовано вважає себе в праві отримати деяку винагороду за свою працю. Водночас легкість, з якою можна переробити практично будь-який компонент, підкуповує :). Так що як чинити — ваша справа.

Ну і для тих хто знає асемблер — приклад невеликої програми-крякалки, її завдання полягає якраз у пропатчивании потрібних файлів(tasm, усі константи взяті з файлу Windows.pas), виконана у вигляді консольного додатку. Можна подивитися що таке файловий меппінг якщо ви не в курсі:

.386
includelib import32.lib
include const32.inc
extern ExitProcess: proc
extern GetStdHandle: proc
extern WriteConsoleA: proc
extern CreateFileA: proc
extern CreateFileMappingA: proc
extern MapViewOfFile: proc
extern CloseHandle: proc
extern UnmapViewOfFile: proc
extern MessageBoxA: proc
extern FormatMessageA: proc
extern GetLastError: proc
extern LocalFree: proc
.model flat
.data
SHandle dd?
data db ‘1234343’,0Ah,0Dh,0
Result dd ?
w32_f_d _WIN32_FIND_DATAA
old_str db 0C0h,084h
new_str db 040h,040h

m_title db ‘title’,0

FileHandle dd ?
FileMap dd ?
MemBase dd?

FileName db ‘data.txt’,0
my_map_name db ‘my_map11’,0

buf_str dd?

Enter db 0Ah,0Dh,00h;
file1 db ‘dlg1.res’,0 ;
file2 db ‘dll.bat’,0 ;
file3 db ‘dll.asm’,0 ;
file_names dd offset file1, offset file2,offset file3; імена файлів, які треба патчити
file_lengths dd 08,07,07; довжини імен — для виводу на консоль
file_offsets dd 00h,00h,00h; зміщення потрібного коду
num dd 2
.code
Start:
WriteC macro Text,len
push 0
push offset Result
push len
push Text
push SHandle
call WriteConsoleA

push 0
push offset Result
push dword ptr 2
push offset Enter
push SHandle
call WriteConsoleA
endm
; отримуємо консоль
push STD_OUTPUT_HANDLE
call GetStdHandle
mov SHandle,eax
test eax,eax
jz on_error

; начанаем безпосередньо крякати 🙂
start_crack:
mov ecx,num
xor ebx,ebx
push ebx
push FILE_ATTRIBUTE_NORMAL
push OPEN_EXISTING
push ebx
inc ebx
push ebx
xor ebx,ebx
push 80000000h or 40000000h
push dword ptr file_names[ecx*4]
call CreateFileA; відкриваємо файл
inc eax
test eax,eax
jz on_error; якщо помилка — виходимо
dec eax
mov FileHandle,eax

xor ebx,ebx
;———— створюємо карту файлу
push offset my_map_name
push ebx
push ebx
push PAGE_READWRITE
push ebx
push eax
call CreateFileMappingA
test eax,eax; якщо помилка — на вихід
jz on_error
mov FileMap,eax

xor ebx,ebx

;———— мэппируем файл в адресний простір нашого процесу
push ebx
push ebx
push ebx
push 00000002h
push eax
call MapViewOfFile
test eax,eax; якщо помилка — на вихід
jz on_error
mov MemBase,eax

mov ecx,num
mov edi,eax
add edi,dword ptr file_offsets[ecx]
mov esi,offset old_str
push edi
cmpsw; порівнюємо байти по зсуву з шаблоном
jne on_incorrect_file; якщо щось не те — виходимо

pop edi
mov esi,offset new_str
movsw
jmp on_free_resource

on_incorrect_file:
pop edi

on_free_resource:; звільняємо ресурси
push MemBase
call UnmapViewOfFile

push FileMap
call CloseHandle

push FileHandle
call CloseHandle

;=== inc

mov ecx,num
WriteC file_names[ecx*4],file_lengths[ecx*4]

dec num
jns start_crack

jmp on_close
on_error:; повідомлення про помилку через FormatMessage
call GetLastError
push 0
push 100h
push offset buf_str
push 0
push eax
push FORMAT_MESSAGE_FROM_HMODULE
push FORMAT_MESSAGE_ALLOCATE_BUFFER or FORMAT_MESSAGE_FROM_SYSTEM
call FormatMessageA
test eax,eax
jz on_close
push 0
push offset m_title
push buf_str
push 0
call MessageBoxA
push buf_str
call LocalFree

on_close:
push 00h
call ExitProcess
end Start