Оригинальный DVD-ROM: eXeL@B DVD !
eXeL@B ВИДЕОКУРС !

ВИДЕОКУРС ВЗЛОМ
выпущен 12 ноября!


УЗНАТЬ БОЛЬШЕ >>
Домой | Статьи | RAR-cтатьи | FAQ | Форум | Скачать | Видеокурс
Новичку | Ссылки | Программирование | Интервью | Архив | Связь

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

Обсудить статью на форуме

Очень удобно, когда все крэкерские инструменты, книги и статьи в одном месте. Используйте сборник от EXELAB - вот тут.

Сегодня мы "придем" за 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() 1)) и сохраняется в переменной по адресу 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
     

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

  4. Ещё одно неочевидное следствие, появившееся после всех вышеописанных манипуляций - в декомпрессоре появилось место для 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).
Back
2) В общем-то нет ничего уникального в том, что ASPack сжат ASPackом. В виде аналогии такой рекурсии можно вспомнить, что компилятор GCC собирает сам себя, для сборки Perlа используется усечённая версия Perlа - miniperl. Это, правда, не означает, что все ассемблеры написаны на ассемблере (хотя это возможно), и уж тем более, что Visual Basic написан на Visual Basic.
Back



Обсуждение статьи: Исследование алгоритма работы упаковщика ASPack. >>>


Материалы находятся на сайте https://exelab.ru



Оригинальный DVD-ROM: eXeL@B DVD !


Вы находитесь на EXELAB.rU
Проект ReactOS