Delphi World - это проект, являющийся сборником статей и малодокументированных возможностей  по программированию в среде Delphi. Здесь вы найдёте работы по следующим категориям: delphi, delfi, borland, bds, дельфи, делфи, дэльфи, дэлфи, programming, example, программирование, исходные коды, code, исходники, source, sources, сорцы, сорсы, soft, programs, программы, and, how, delphiworld, базы данных, графика, игры, интернет, сети, компоненты, классы, мультимедиа, ос, железо, программа, интерфейс, рабочий стол, синтаксис, технологии, файловая система...
Исследование алгоритма работы упаковщика ASPack v1.08.03
Компьютеры под глюч! ООО "ВируS"

Сегодня мы "придем" за ASPack'ом. Автора зовут Солодовников Алексей - и я уже вижу, как в меня полетят камни праведного гнева: "Разве мы не должны защищать отечественных программистов?" Конечно должны! Однако, меня интересовала не сама программа, а алгоритм ее работы. Кроме того отказ от исследований отечественных программных продуктов по морально-этическим соображениям отнюдь не означает их хорошую защищенность, а даже наоборот, может стать предпосылкой к игнорированию этого аспекта нашими программистами!

Введение

Итак, что мы имеем. Программа неким мистическим образом "ускользает" из-под SoftICE. Даже сейчас, проанализировав её код, я не смогу дать ответ на вопрос "Почему?". В самом коде я не нашёл ничего "необычного". Остаётся предположить, что программа обманывает не сам SoftICE, а его символьный загрузчик (loader32.exe) - и делает она это, вероятнее всего, вследствие хорошо поправленной структуры PE-файла. В SoftICE же мы видим примерно следующее:


NTICE: Exit32 PID=129 MOD=a
NTICE: Unload32 MOD=a
Mixer Dispatch: IOCTL_MIX_REQUEST_NOTIFY
NTICE: Load32 START=400000 SIZE=68000 KPEB=82915A80 MOD=A
NTICE: Load32 START=77F00000 SIZE=5F000 KPEB=82915A80 MOD=KERNEL32
NTICE: Load32 START=77E70000 SIZE=54000 KPEB=82915A80 MOD=USER32
NTICE: Load32 START=77ED0000 SIZE=2C000 KPEB=82915A80 MOD=GDI32
NTICE: Load32 START=77DC0000 SIZE=3F000 KPEB=82915A80 MOD=ADVAPI32
NTICE: Load32 START=77E10000 SIZE=57000 KPEB=82915A80 MOD=RPCRT4
NTICE: Load32 START=65340000 SIZE=92000 KPEB=82915A80 MOD=oleaut32
NTICE: Load32 START=77B20000 SIZE=B5000 KPEB=82915A80 MOD=OLE32
NTICE: Load32 START=77A80000 SIZE=B000 KPEB=82915A80 MOD=version
NTICE: Load32 START=77C40000 SIZE=13D000 KPEB=82915A80 MOD=SHELL32
NTICE: Load32 START=73000000 SIZE=74000 KPEB=82915A80 MOD=COMCTL32
NTICE: Load32 START=779B0000 SIZE=8000 KPEB=82915A80 MOD=lz32
NTICE: Load32 START=77D80000 SIZE=32000 KPEB=82915A80 MOD=COMDLG32
Mixer Dispatch: IOCTL_MIX_REQUEST_NOTIFY
NTICE: Load32 START=776D0000 SIZE=6000 KPEB=82915A80 MOD=indicdll
NTICE: Load32 START=77780000 SIZE=6000 KPEB=82915A80 MOD=MSIDLE

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

Также не помощник нам и ProcDump (может быть вследствие неверного использования или недопонимания, но этот инструмент бывает мне полезен примерно в одном случае из 7-8). Несмотря на то, что у него гордо прописан метод декомпрессии ASPack, программа "убегает" и от него. Правда, он честно снимает копию участка (dump) памяти с уже запущенной программы, но пользоваться им потом нельзя - ни один дизассемблер не может с уверенностью распознать, программа ли это вообще.

Ещё одна особенность - дизассемблеры ведут себя на ASPackе не лучшим образом. Скажем, IDA Pro в режиме автоанализа долго обращается к жесткому диску и выдаёт листинг, весьма отдалённо похожий на программный код, WinDasm просто зависает, у QView и HView также не могут ничего сделать. Короче, на сей раз мы имеем кое-что посложнее, чем программы типа "ставим контрольную точку на strcmp() - это и будет наш серийный номер". Однако, как говорил знаменитый Old Red Cracker (ORC+): " если программу можно запустить - её можно сломать"!

Используемые программы

Данная статья предполагает знание читателем ассемблера, языка C, Windows 32 API и общее представление о формате PE файлов, а также умение пользоваться отладчиком SoftICE и дизассемблером IDA Pro.

Вам понадобятся следующие программы:

  • Дизассемблер IDA Pro (я использовал версию 3.76);
  • Отладчик SoftICE (у меня установлен SoftICE 3.23 for Windows NT - операционная система Windows NT Workstation 4.0 with SP 4);
  • Компилятор C (подойдёт любой, поддерживающий ассемблерные вставки, я использовал урезанную до минимума версию Visual C++ 6.0 - т.е. без документации, библиотек MFC и прочего - получилось всего 64 Mb);
  • Любой шестнадцатеричный редактор.

Исследование

Советую начинать всегда с чтения прилагающейся документации. Что мы можем почерпнуть из файлов readme.txt и history.txt? Очень много, а именно:

  • написан сей шедевр на Delphi 2.0;
  • имеется небольшая защита декомпрессора;
  • имеется защита от копирования участков памяти;
  • декомпрессор добавляется в сегмент .adata.

Загрузим программу в IDA Pro, но будем держать всё под контролем, а именно - выберем пункт "Manual Load" в диалоговом окне "Load File of New Format". IDA будет спрашивать у нас подтверждение на загрузку каждого сегмента программы. Мы пропустим совершенно бесполезные в данном случае CODE, DATA, BSS, .idata, .tls, .rdata, .reloc, .rsrc, а загрузим только последние два сегмента .adata и .udata. Точка входа расположена по адресу 465000h:


00465000                 pusha
00465001                 call    $+5
00465006                 pop     ebp
00465007                 sub     ebp, 444A0Ah	; база ebp = 205FC

Замечательный пример определения адреса, по которому выполняется код. Инструкция CALL $+5 вызывает в виде функции код, следующий непосредственно за ней, но при этом помещает в стек адрес возврата, т.е. 465006h. Инструкция POP EBP извлекает его из стека - и вот мы имеем адрес, по которому расположен код. Далее вычитается некоторое смещение - в EBP на протяжении работы всей программы будет находиться смещение на данные и код (поскольку загрузчик должен работать на множестве упакованных программ, он обычно пишется с применением так называемой "относительной" адресации, т.е. когда код может быть расположен по любому адресу.


0046501A                 cmp     dword ptr [ebp+4450ACh], 0 ; 4656A8h
00465021                 mov     [ebp+444EBBh], ebx ; 4654B7h
00465027                 jnz     465544

Происходит проверка dword по адресу 4656A8h на равенство 0 - если не 0, то переход к запуску распакованной программы по адресу 465544h (я назвал его run_programm). По адресу 4654B7h записывается ранее вычисленное значение 444A0Ah + ebp - [4656ADh] = 400000h


0046502D                 lea     eax, [ebp+4450D1h] ; 4656CDh
					;  адрес строки kernel32.dll
00465033                 push    eax
00465034                 call    dword ptr [ebp+445194h] ; 465790h
					; GetModuleHandleA
0046503A                 mov     [ebp+4450CDh], eax ; 4656C9h
00465040                 mov     edi, eax
00465042                 lea     ebx, [ebp+4450DEh] ; 4656DAh
					; адрес строки VirtualAlloc
00465048                 push    ebx
00465049                 push    eax
0046504A                 call    dword ptr [ebp+445190h] ; 46578Ch
					; GetProcAddress
00465050                 mov     [ebp+4450B9h], eax ; 4656B5h
00465056                 lea     ebx, [ebp+4450EBh] ; 4656E7h
					; адрес строки VirtualFree
0046505C                 push    ebx
0046505D                 push    edi
0046505E                 call    dword ptr [ebp+445190h] ; GetProcAddress
00465064                 mov     [ebp+4450BDh], eax ; 4656B9h

У упакованной программы имеется сегмент импорта, но содержит ровно столько импортируемых функций, сколько необходимо для работы декомпрессора:


; Imports from kernal32.dll
46578C GetProcAddress		dd ?
465790 GetModuleHandleA 	dd ?
465794 LoadLibraryA		dd ?

Лаконичность поражает воображение. Все необходимые функции для работы декомпрессора загружаются динамически. Для начала извлекается описатель (handle) библиотеки "kernel32.dll" (посредством вызова функции GetModuleHandleA()) и сохраняется в переменной по адресу 4656C9h, далее с помощью функции GetProcAddress() извлекаются адреса функций VirtualAlloc() и VirtualFree(), и сохраняются по адресам 4656B5h и 4656B9h соответственно.


0046506A                 mov     eax, [ebp+444EBBh] ; 4654B7h
00465070                 mov     [ebp+4450ACh], eax ; 4656A8h

Извлекается ранее вычисленное значение 400000h из [4654B7h], и помещается по новому адресу 4656A8h. Я назвал последний base - оно используется далее как стартовый адрес для декомпрессированного кода.


00465076                 push    4
00465078                 push    1000h
0046507D                 push    49Ah
00465082                 push    0
00465084                 call    dword ptr [ebp+4450B9h] ; VirtualAlloc_
0046508A                 mov     [ebp+4450B5h], eax ; 4656B1h

Вызывается функция VirtualAlloc() (помните, что параметры передаются в обратном порядке) с аргументами (0, 049Ah, 1000h, 4). Она выделяет несколько страниц памяти в виртуальном адресном пространстве процесса. Первый аргумент - адрес, обычно 0. Второй - размер области памяти. Третий - флаг, 1000h = MEM_COMMIT, выделить физическую память для запрашиваемых страниц. Последний аргумент - атрибуты защиты для выделенной памяти, 4 = PAGE_READWRITE (я надеюсь, не нужно объяснять). Указатель на выделенную память запоминается по адресу 4656B1h.


00465090                 lea     ebx, [ebp+444ACFh] ; 4650CBh
00465096                 push    eax
00465097                 push    ebx
00465098                 call    unpack
0046509D                 mov     ecx, eax
0046509F                 lea     edi, [ebp+444ACFh] ; 4650CBh
004650A5                 mov     esi, [ebp+4450B5h] ; 4656B1h
004650AB                 sar     ecx, 2
004650AE                 repe movsd
004650B0                 mov     ecx, eax
004650B2                 and     ecx, 3
004650B5                 repe movsb

А вот это и есть обещанная защита декомпрессора - процедура декомпрессора сама сжата 2)! В EBX помещается её адрес (4650CBh), в EAX расположен адрес только что выделенного участка памяти. Сама процедура находится по адресу 465565h. Приводить её текст и комментировать его у меня нет желания - профессионалы и так разберутся, а начинающие всё равно ничего не поймут. Достаточно сказать, что это обычный (правда, очень вылизанный, что свидетельствует о его почтенном возрасте) алгоритм декомпрессии LZ, о чём можно догадаться, например, по такому коду:


00465654                 push    esi	  ; в esi адрес сжатого кода
00465655                 mov     esi, edi ; в edi - адрес в буфере
00465657                 sub     esi, eax ; вычтем смещение на уже
				; распакованный кусок
00465659                 repe movsb 	  : и запишем его по текущему адресу
0046565B                 pop     esi

Далее распакованный декомпрессор копируется из буфера по адресу 4656B1h (помните, что movsd перемещает по 4 байта, но длина распакованного кода может быть не кратна 4, поэтому мы должны позаботиться об остатке).

Итак, для дальнейших исследований мы должны распаковать декомпрессор. Я написал небольшую программу на C (точнее, две трети на ассемблере), которая декомпрессирует этот кусок кода и сохраняет его в файле unpacked. Исходный текст программы прилагается (файл as1.c). Два момента заслуживают внимания:

  • Откуда я узнал размеры исходного и выходного массивов? Довольно просто - если Вы следите за моим повествованием, Вы должны помнить, что под буфер памяти было выделено 049Ah байт. Соответственно, поскольку код сжат, то исходный должен иметь меньшую длину. Я взял с запасом - те же 049Ah байт.
  • Откуда я узнал смещение интересующего нас участка кода? Это тоже просто. В IDA Pro записываем первые несколько байт по адресу 4650CBh, и ищем их в шестнадцатеричном редакторе. Он и покажет нам искомое смещение.

Теперь мы должны как-то загрузить распакованный код обратно в IDA Pro. Для этого воспользуемся одной из уникальных возможностей этого инструмента - встроенным языком программирования IDC (документацию на него можно найти в файле помощи самой IDA Pro). Сценарий выглядит примерно так (файл unpack.idc):


static unpack_one()
{
 auto file, char_, count;
 count = 0;
 file = fopen("unpacked", "rb");
 for (count = 0; count < 1178; count++)
 {
  char_ = fgetc(file);
  if (char_ == -1)
  {
   Message("EOF detected ...");
   break;
  }
  PatchByte(0x4650CB + count, char_);
 }
}

(1178 = 049Ah). Я поместил этот script во внешний файл, загрузил его посредством команды Load File -> IDC File ... (можно просто нажать F2). Далее (нажав Shift+F2) наберём команду "unpack_one();".

Теперь мы можем продолжить. Вы можете убедиться, что сейчас мы имеем осмысленный ассемблерный листинг.


004650B7                 mov     eax, [ebp+4450B5h] ; 4656B1h
004650BD                 push    8000h
004650C2                 push    0
004650C4                 push    eax
004650C5                 call    dword ptr [ebp+4450BDh] ; VirtualFree
004650CB                 lea     eax, [ebp+444C37h] ; 465233h
004650D1                 push    eax
004650D2                 retn

По адресу 4656B1h записан указатель на ранее выделенный буфер памяти. Здесь вызывается функция VirtualFree() с аргументами (адрес_буфера, 0, 8000h). Интуитивно понятно, что происходит освобождение ранее выделенной памяти. Далее происходит переход на адрес 465233h. Он выглядит несколько странным (через стек), но мы должны помнить, что здесь не должна использоваться прямая адресация - потому что этот загрузчик универсален и код должен работать по любому (заранее неизвестному) адресу (также можно было использовать инструкцию jmp eax).


00465233                 mov     ebx, [ebp+444ADFh] ; 4650DBh
00465239                 or      ebx, ebx
0046523B                 jz      short loc_465247
0046523D                 mov     eax, [ebx]
0046523F                 xchg    eax, [ebp+444AE3h] ; 4650DFh
00465245                 mov     [ebx], eax

Малопонятное место. Проверяется dword по адресу 4650DBh, если он не 0 (в нашем случае 0), происходит копирование dword из [4650DBh], запись его в 4650DFh, а прежнее содержимое 4650DFh копируется в [4650DBh]. Далее (код я опустил - ничего интересного) происходит повторное определение адресов функций VirtualAlloc() и VirtualFree()


00465293                 lea     esi, [ebp+444AF7h] ; 4650F3h - начало таблицы
00465299                 mov     eax, [esi+4]
0046529C                 push    4
0046529E                 push    1000h
004652A3                 push    eax
004652A4                 push    0
004652A6                 call    dword ptr [ebp+4450B9h] ; 4656B5h
					; VirtualAlloc
004652AC                 mov     [ebp+4450B5h], eax ; 4656B1h
004652B2                 push    esi
004652B3                 mov     ebx, [esi]
004652B5                 add     ebx, [ebp+4450ACh] ; 4656A8h - base
004652BB                 push    eax
004652BC                 push    ebx
004652BD                 call    unpack
004652C2                 cmp     eax, [esi+4]
004652C5                 jz      short loc_4652D2
004652C7                 lea     ebx, [ebp+44515Dh] ; 465759h
				; адрес строки "Decompress error"
004652CD                 jmp     loc_465421

Происходит здесь следующее: в ESI загружается адрес начала таблицы со смещениями и размерами компрессированных блоков кода (названа мною pack_table). Далее в EAX помещается размер области памяти, выделяется виртуальная память посредством вызова VirtualAlloc() (см. пояснения выше), происходит определение адреса сжатого блока - в таблице хранится смещение относительно адреса загрузки программы (который хранится по адресу 4656A8h - base). Затем происходит декомпрессия. Функция unpack() возвращает длину декомпрессированного блока. Если эта длина не совпадает с указанной в таблице pack_table - происходит переход на адрес 465421h с сообщением "Decompress error". Там расположен код, который загружает все необходимые для своей работы функции из системных библиотек, выдаёт MessageBox с переданным в EBX сообщением, и осуществляет выход из программы (я назвал этот адрес say_BAD).


004652D2                 cmp     byte ptr [ebp+4450B0h], 0 ; 4656ACh
004652D9                 jnz     short loc_465316
004652DB                 inc     byte ptr [ebp+4450B0h] ; 4656ACh
004652E1                 push    eax
004652E2                 push    ecx
004652E3                 push    esi
004652E4                 push    ebx
004652E5                 mov     ecx, eax        ; длина распакованного кода
004652E7                 sub     ecx, 6
004652EA                 mov     esi, [ebp+4450B5h] ; 4656B1h
004652F0                 xor     ebx, ebx
004652F2 loc_4652F2:
004652F2                 or      ecx, ecx
004652F4                 jz      short loc_465312
004652F6                 js      short loc_465312
004652F8                 lodsb
004652F9                 cmp     al, 0E8h
004652FB                 jz      short loc_465305
004652FD                 cmp     al, 0E9h
004652FF                 jz      short loc_465305
00465301                 inc     ebx
00465302                 dec     ecx
00465303                 jmp     short loc_4652F2
00465305 loc_465305:
00465305                 sub     [esi], ebx
00465307                 add     ebx, 5
0046530A                 add     esi, 4
0046530D                 sub     ecx, 5
00465310                 jmp     short loc_4652F2

В этой части кода происходит расшифровка распакованного кода. Проверяется переменная по адресу 4656ACh на равенство с 0, и если там не 0 - переход на loc_465316. Иначе - значение 4656ACh увеличивается на 1, гарантируя, что последующий код исполнится только один раз. Так как начальное значение этой переменной 0, то этот код исполняется только в первом цикле.

В ECX помещается длина распакованного кода - 6, в ESI - адрес буфера в памяти с самим распакованным кодом. Далее следует цикл: пока длина (ECX) больше 0: в EAX грузится байт по адресу в ESI (при этом ESI увеличивается на 1), и если он равен E8h или E9h - из dword по адресу в ESI вычитается EBX. Далее счётчики соответствующим образом увеличиваются для следующей итерации.


00465312                 pop     ebx
00465313                 pop     esi
00465314                 pop     ecx
00465315                 pop     eax
00465316 loc_465316:
00465316                 mov     ecx, eax
00465318                 mov     edi, [esi]
0046531A                 add     edi, [ebp+4450ACh] ; 4656A8h - base
00465320                 mov     esi, [ebp+4450B5h] ; 4656B1h
00465326                 sar     ecx, 2
00465329                 repe movsd
0046532B                 mov     ecx, eax
0046532D                 and     ecx, 3
00465330                 repe movsb
00465332                 pop     esi
00465333                 mov     eax, [ebp+4450B5h] ; 4656B1h
00465339                 push    8000h
0046533E                 push    0
00465340                 push    eax
00465341                 call    dword ptr [ebp+4450BDh] ; VirtualFree()
00465347                 add     esi, 8          ; esi: 4650FBh
0046534A                 cmp     dword ptr [esi], 0
0046534D                 jnz     loc_465299
00465353                 mov     ebx, [ebp+444ADFh] ; 4650DBh
00465359                 or      ebx, ebx
0046535B                 jz      short loc_465365
0046535D                 mov     eax, [ebx]
0046535F                 xchg    eax, [ebp+444AE3h] ; 4650DFh

Распакованный код копируется обратно на своё законное место в памяти (base + смещение в таблице pack_table) (инструкции 465316h - 465330h). Затем восстанавливается в ESI текущий указатель в таблице pack_table и освобождается ранее выделенный буфер в памяти. Указатель в таблице pack_table перемещается на следующую структуру - до тех пор, пока смещение в этой таблице не примет значение 0. Далее снова происходит малопонятные манипуляции с переменными по адресам 4650DBh и 4650DFh


00465365                 mov     edx, [ebp+4450ACh] ; 4656A8h
0046536B                 mov     eax, [ebp+444ADBh] ; 4650D7h
00465371                 sub     edx, eax
00465373                 jz      short loc_4653EE

Происходит сравнение переменной base и 4650D7h (base2?), и если они равны (в нашем случае они равны), переход на 4653EEh. Я не смотрел, что происходит, если они не равны - у меня было мало времени.


004653EE                 mov     esi, [ebp+444AEBh] ; 4650E7h
004653F4                 mov     edx, [ebp+4450ACh] ; 4656A8h - base
004653FA                 add     esi, edx

Здесь вычисляется адрес таблицы импорта. В переменной 4650E7h содержится смещение на таблицу импорта относительно base.


004653FC loc_4653FC:
004653FC                 mov     eax, [esi+0Ch]
004653FF                 test    eax, eax
00465401                 jz      run_programm
00465407                 add     eax, edx
00465409                 mov     ebx, eax
0046540B                 push    eax
0046540C                 call    dword ptr [ebp+445194h] ; GetModuleHandleA()
00465412                 test    eax, eax
00465414                 jnz     short loc_46547D
00465416                 push    ebx
00465417                 call    dword ptr [ebp+445198h] ; LoadLibraryA()
0046541D                 test    eax, eax
0046541F                 jnz     short loc_46547D
00465421 say_BAD:
 ...
0046547D                 mov     dword ptr [ebx], 0 ; здесь затирается начало
		; имени .dll в таблице импорта !!!
00465483                 mov     [ebp+44516Eh], eax ; 46576Ah - implib_handle
00465489                 mov     dword ptr [ebp+445172h], 0 ; 46576Eh - import_counter
00465493 loc_465493:
00465493                 mov     edx, [ebp+4450ACh] ; 4656A8h - base
00465499                 mov     eax, [esi]
0046549B                 test    eax, eax
0046549D                 jnz     short loc_4654A2
0046549F                 mov     eax, [esi+10h]
004654A2 loc_4654A2:
004654A2                 add     eax, edx
004654A4                 add     eax, [ebp+445172h] ; implib_counter
004654AA                 mov     ebx, [eax]
004654AC                 mov     edi, [esi+10h]
004654AF                 add     edi, edx
004654B1                 add     edi, [ebp+445172h] ; implib_counter
004654B7                 test    ebx, ebx
004654B9                 jz      short loc_46552C
004654BB                 test    ebx, 80000000h
004654C1                 jnz     short loc_4654C7
004654C3                 add     ebx, edx
004654C5                 inc     ebx
004654C6                 inc     ebx
004654C7 loc_4654C7:
004654C7                 push    ebx
004654C8                 and     ebx, 7FFFFFFFh
004654CE                 push    ebx
004654CF                 push    dword ptr [ebp+44516Eh] ; implib_handle
004654D5                 call    dword ptr [ebp+445190h] ; GetProcAddress
004654DB                 test    eax, eax
004654DD                 pop     ebx
004654DE                 jnz     short loc_46551E
004654E0                 test    ebx, 80000000h
004654E6                 jz      short loc_465512
004654E8                 push    edi
004654E9                 and     ebx, 7FFFFFFFh
004654EF                 mov     edx, ebx
004654F1                 dec     edx
004654F2                 shl     edx, 2
004654F5                 mov     ebx, [ebp+44516Eh] ; implib_handle
004654FB                 mov     edi, [ebx+3Ch]
004654FE                 mov     edi, [ebx+edi+78h]
00465502                 add     ebx, [ebx+edi+1Ch]
00465506                 mov     eax, [ebx+edx]
00465509                 add     eax, [ebp+44516Eh] ; implib_handle
0046550F                 pop     edi
00465510                 jmp     short loc_46551E
00465512 loc_465512:
00465512                 lea     ebx, [ebp+445149h] ; 465745
			; строка "Can`t load function"
00465518                 push    ebx
00465519                 jmp     say_BAD
0046551E loc_46551E:
0046551E                 mov     [edi], eax
00465520                 add     dword ptr [ebp+445172h], 4 ; import_counter
00465527                 jmp     loc_465493
0046552C loc_46552C:
0046552C                 xor     eax, eax
0046552E                 mov     [esi], eax ; здесь затирается имя
00465530                 mov     [esi+0Ch], eax ; импортируемой функции!
00465533                 mov     [esi+10h], eax
00465536                 add     esi, 14h
00465539                 mov     edx, [ebp+4450ACh] ; 4656a8 - base
0046553F                 jmp     loc_4653FC

Ндаа... Без SoftICE сложно сказать, что происходит. Чтобы таки посмотреть программу под отладчиком, я применил следующий трюк: найдём смещение в шестнадцатеричном редакторе на начало декомпрессора (см. выше, как именно), и изменим один байт на CC (инструкция Int 3). Загрузим SoftICE, скажем ему i3here on, чтобы он перехватывал третье прерывание. Теперь запускаем исследуемую программу - и она прерывается в том месте, где мы поменяли команду. Ставим нужные контрольные точки и приступаем к работе. Только не забудьте восстановить исправленный байт в нашей программе и запустить её снова.

Итак, этот участок кода эмулирует работу загрузчика операционной системы - а именно, он грузит все необходимые программе функции из системных библиотек. Сначала идёт попытка получить описатель уже загруженной библиотеки вызовом функции GetModuleHandleA(), если же файл ещё не был загружен - LoadLibaryA(). Если библиотека не может быть загружена - на выход с соответствующим сообщением. Иначе описатель загруженной библиотеки помещается в переменную 46576Ah (я назвал её implib_handle), и обнуляется счётчик порядкового номера импортируемых функций - переменная 46576Eh (import_counter). Тут же располагается процедура защиты от копирования участков памяти - в dword имени библиотеки записывается 0. Далее следует цикл по всем именам функций (причём, как и в обычной таблице импорта, можно загрузить функцию как по имени, так и по номеру - в последнем случае адрес имеет установленный старший бит).


00465544 run_programm:
00465544                 mov     eax, [ebp+444AEFh] ; 4650EBh (start_addr)
0046554A                 push    eax
0046554B                 add     eax, [ebp+4450ACh] ; base
00465551                 pop     ebx
00465552                 or      ebx, ebx
00465554                 mov     [esp+1Ch], eax
00465558                 popa
00465559                 jnz     short loc_465563
0046555B                 mov     eax, 1
00465560                 retn    0Ch
00465563 loc_465563:
00465563                 push    eax
00465564                 retn

Здесь происходит запуск полностью распакованной программы. По адресу 4650EBh находится смещение точки входа относительно base. Если оно не 0 - происходит переход по вычисленному адресу.

Результаты исследования

  1. Ясно, что нельзя написать универсальный unpacker, т.к. Алексей Солодовников оказался очень плодовитым, и мне попадались программы, запакованные ASPackом более старых (притом разных) версий - они используют декомпрессор попроще, параметров поменьше.

    Нам нужен инструмент, который позволил бы с лёгкостью редактировать PE-файлы (как заголовки, так и содержимое секций, перестраивать таблицы импорта/экспорта и т.п.) и имел при этом язык для написания скриптов (например, как IDC в IDA Pro). Такую программу я в Сети так и не смог найти (ProcDump не в счёт - практически не имеет документации, исходные тексты недоступны, и он не позволяет создавать свои сценарии). Видимо, придётся самому писать (как свободное время появится).
  2. Возможен запуск программ, упакованных ASPackом, под отладчиком (см. выше описание механизма).
  3. Возможно также использование ProcDump. Нам нужно модифицировать место, где затирается имя загружаемой .dll. Этого можно добиться так: поскольку уже есть программа, распаковывающая декомпрессор, она может записать его в тот же файл на прежнее место. Но это не всё! Дело в том, что (видимо, преднамеренно) используется dword по адресу ebp+444EBBh = 4654B7h, т.е. на месте нашего вручную распакованного декомпрессора. Я сделал следующие изменения:

Offset 26876
465076: EB 53	jmp short 4650CB

Offset 26821
465021: 89 9D 7C 4A 44 00	mov [ebp+444A7C], ebx ; используется 465078р

Offset 2686A
46506A: 8B 85 7C 4A 44 00	mov eax, [ebp+444A7C]

Offset 26C7D
46547D: EB 04	jmp short 46547D

Далее я сделал копию участка памяти в файл с работающей программы - и вот оно работает! Правда, проблемы с ресурсами, но это уже исправляется (дизассемблер, правда, таблицу импорта так и не увидел, но программа, по крайней мере, стала запускаться).
  1. Ещё одно неочевидное следствие, появившееся после всех вышеописанных манипуляций - в декомпрессоре появилось место для memory patch. Мы имеем минимум 53h - 4 (на dword по адресу ebp+444A7Ch=465078h) = 4Fh байт! Этого будет достаточно для большинства обычных программ. Если же места не будет хватать, можно применить ещё один приём - загрузить внешнюю .dll. Декомпрессор уже имеет загруженную библиотеку kernel32.dll (её описатель хранится в переменной ebp+4450CDh, в данном случае, по адресу 4656C9h), также известны адреса функций LoadLibrary() и GetProcAddress() (из таблицы импорта) - у нас есть всё необходимое. Внешняя же .dll может быть написана уже не на скорую руку в шестнадцатеричном редакторе, а в нормальных условиях, на "любимом" Visual Basic, и делать она может всё, что душа пожелает. Я пожелал сделать копию всех запакованных сегментов. Для этого была написана маленькая и непритязательная .dll на C (файлы dump.c и dump.h), а в саму программу были добавлены ещё несколько изменений:

Offset 2691B
46511b: 64 75 6D 70 2E 64 6C 6C 00 66 6E 44 75 6D 70 00

В первую же свободную (помните, что, поскольку признаком окончания таблицы repack_table считается нулевая величина в поле offset, то первые два dword со значениями 0 в конце таблицы нужно считать её продолжением) ячейку таблицы repack_table я поместил две строки "dump.dll" (адрес 46511Bh - имя библиотеки) и "fnDump" (адрес 465124h - имя экспортируемой из библиотеки функции). Функция эта имеет такой прототип:

#pragma pack(1)
struct pack_table_cell
{
	unsigned long offset;
	unsigned long size;
};

DUMP_API int fnDump(void *, struct pack_table_cell *);

Первый параметр - базовый адрес (base, хранится, как мы помним, по адресу 4656A8h), второй - адрес первого элемента таблицы repack_table (её структура приведена над описанием функции).

Offset 26876
46507D: 8D 05 1B 51 46 00	lea eax, 46511Bh ; "dump.dll"
465083: 50			push eax
465084: FF 95 98 51 44 00	call dword ptr [ebp+445198] ; 465795h,
                                                          ; LoadLibraryA()
46508A: 09 C0			or eax,eax	; проверим результат
46508C: 75 0B			jnz loc_465099  ; dll loaded successfully
46508E: 8D 1D 32 57 46 00	lea ebx, 465732 ; Can`t load library
465094: E9 88 03 00 00		jmp loc_465421  ; say_BAD
loc_465099:
465099: 8D 1D 24 51 46 00	lea ebx, 465124 ; "fnDump"
46509F: 53			push ebx        ; сначала имя функции
4650A0: 50			push eax 	; затем описатель .dll
4650A1: FF 95 90 51 44 00	call dword ptr [ebp+445190h] ; 46578c,
                                                           ; GetProcAddress()
4650A7: 09 C0			or eax,eax	; проверим результат
4650A9: 75 0B			jnz loc_4650B6
4650AB: 8D 1D 45 57 46 00	lea ebx, 465745h ; Can`t load function
4650B1: E9 6B 03 00 00		jmp loc_465421 ; say_BAD
loc_4650B6:
4650B6: 8D B5 F7 4A 44 00	lea esi, [ebp+444AF7] ; repack_table
4650BC: 56			push esi
4650BD: FF B5 AC 50 44 00	push dword ptr [ebp+4450ACh]; base
4650C3: FF D0			call eax
4650C5: 58			pop eax		; восстановим стек
4650C6: 58			pop eax
4650C7: 61			popa		; как в оригинальном
4650C8: 50			push eax	; запуске программы
4650C9: C3			retn

Я надеюсь, всё понятно из комментариев. Я использовал для обработки ошибок оригинальный код декомпрессора (инкапсуляция на уровне ассемблера) по адресу say_BAD (см. описание выше). Последний участок, передающий управление оригинальной точке входа, скопирован полностью. Это не относительный код, он специфичен для данной конкретной программы, но Вы можете использовать его, поменяв адреса в инструкциях загрузки адресов строк. Можно переписать его, чтобы он также был относительным, но в таком случае нам придётся задействовать память за нашими строками (с адреса 46512Ch) - как мы помним, следующий нужный код начинается с адреса 4650CBh, а последняя инструкция в ранее добавленном коде располагается по адресу 4650C9h - едва поместилось.

И, наконец, чтобы наш код получил управление после полной распаковки программы, модифицируем ещё одно место (где программа передаёт управление на оригинальную точку входа):

Offset 26D58
465558: E9 20 FB FF FF	jmp loc_46507D

В самой же функции Вы вольны делать что угодно! Например, модифицировать память, сохранить в файле содержимое сегментов и т.д. И всё это не создавая VxD и не задействуя нулевого кольца процессора!

Приложение

Список созданных мною в процессе исследования файлов:

  • as1.c - программа для распаковки "защищённого" декомпрессора
  • dump.c и dump.h - исходные тексты "внедрённой" DLL для копирования участка памяти в файл с полностью распакованной программы"
  • iaspack.idb - прокомменированный мною ассемблерный листинг загрузчика ASPack для IDA Pro.

1) Если Вы не знаете, что делает функция GetModuleHandleA() (или любая другая), советую найти хорошую документацию по Win32 API (скажем, с Visual C++ поставляется достаточно хорошая), или подписаться на MSDN. Я не вижу ничего предосудительного в том, чтобы изучать Windows API (равно как и любую программистскую технологию или приёмы защиты программ от любой фирмы, включая Microsoft) - Вы должны уважать своих врагов, внимательно изучать их, и брать от них самое лучшее. Иначе Вы никогда не сможете победить.

Возвращаясь же к нашей теме: все функции Win API возвращают результат в регистре EAX, параметры передаются им в обратном порядке, и они сами чистят за собой стек (так называемое соглашение о вызовах функций stdcall).

2) В общем-то нет ничего уникального в том, что ASPack сжат ASPackом. В виде аналогии такой рекурсии можно вспомнить, что компилятор GCC собирает сам себя, для сборки Perlа используется усечённая версия Perlа - miniperl. Это, правда, не означает, что все ассемблеры написаны на ассемблере (хотя это возможно), и уж тем более, что Visual Basic написан на Visual Basic.

Проект Delphi World © Выпуск 2002 - 2004
Автор проекта: ___Nikolay