Исследование алгоритма работы упаковщика 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 - происходит переход по вычисленному адресу.
Результаты исследования
- Ясно, что нельзя написать универсальный 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.
|