Взлом компонентов Delphi

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

Для работы вам понадобится: отладчик(желательно 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
   );
В случае успеха - хэндл найденного окна иначе 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
        z    005E136D
        jmp    куда-то на выход    // надо добраться сюдa

По адресам передаваемым в функцию 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 - поиск в нескольких файлах и поиск с произвольными символами. В общем методика полностью аналогична. 
    На последок несколько советов:
  1. Поиск нужного участка кода можно выполнить найдя текст выводимый компонентом (определив его адрес в модуле и найдя ссылку на этот код - это скорее относится к OllyDbg)
  2. может возникнуть необходимость перепрыгнуть некоторый участок кода - когда нечего изменить(так было в NativeExcel который писал в ячейку A1:A1 инфу о том что это демо). Просто надо ассемблировать следующий код(tasm)
    jmp short	cs:6 + 2   ; 6 - это выход на след инструкцию после jmp - 2 сколько байт надо перепрыгнуть
    команда занимает 2 байта с опкодом EBXX - где хх - сколько байт надо перепрыгнуть
  3. . Если нашли в dcu нужный участок - не торопитесь сразу менять его - таких участков может быть нескотлько - замена не того может привети к ошибкам
  4. После того как изменения сохранены - проект надо закрыть и открыть ну и естественно перекомпилять :) - тогда изменения вступят в силу.
  5. Я ни в коем случае не призываю ломать все напропалую - всетаки разработчик тоже человек :), потративший на создание некоторое время и обоснованно считающий себя в праве получить некоторой вознаграждение за свой труд. В тоже время легкость с которой можно переделать практически любой компонент подкупает :). Так что как поступать - ваше дело.
Ну и для тех кто знает ассемблер - пример небольшой програмы-крякалки, ее задача заключается как раз в пропатчивании нужных файлов(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 <0,0,0,0,0,0,0,0,0,0>   
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