Исследование алгоритма работы упаковщика ASPack v1.08.03

{codecitation class=»brush: pascal; gutter: false;» width=»600px»}

Компьютеры под глюч! ООО «Виру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 — происходит переход по вычисленному адресу.

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

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

Нам нужен инструмент, который позволил бы с лёгкостью редактировать PE-файлы (как заголовки, так и содержимое секций, перестраивать таблицы импорта/экспорта и т.п.) и имел при этом язык для написания скриптов (например, как IDC в IDA Pro). Такую программу я в Сети так и не смог найти (ProcDump не в счёт — практически не имеет документации, исходные тексты недоступны, и он не позволяет создавать свои сценарии). Видимо, придётся самому писать (как свободное время появится).

Возможен запуск программ, упакованных ASPackом, под отладчиком (см. выше описание механизма).

Возможно также использование 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

Далее я сделал копию участка памяти в файл с работающей программы — и вот оно работает! Правда, проблемы с ресурсами, но это уже исправляется (дизассемблер, правда, таблицу импорта так и не увидел, но программа, по крайней мере, стала запускаться).

Ещё одно неочевидное следствие, появившееся после всех вышеописанных манипуляций — в декомпрессоре появилось место для 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.

{/codecitation}

Добавить комментарий