Исследование и распаковка Armadillo 3.10

Вот и ещё одна статья про распаковку... Таких статей уже полно, но надо отметить, что про Armadillo написано очень мало, видимо вследствии того, что его не часто встретишь, ASProtect гораздо распространённее. Так же на этом могло сказаться, что цена профессиональной версии Armadillo повыше будет, но по-моему всё же не завышена, т.е. соответсвует возможностям.

Теперь же рассмотрим протектор по-подробнее, что же он всё-таки из себя представляет? Armadillo активно использует отладочные средства Win32, т.е. Debug API. То что мы запускаем - является по сути отладчиком, который как бы исправляет баги в отлаживаемой программе, которую он запускает сам, т.е. получает командную строку функцией GetCommandLine, и вызывает CreateProcess для запуска процесса. Причём в NT системах используются UNICODE функции - GetCommandLineW и CreateProcessW, просто на каждом шагу видно стремление разработчиков затруднить исследование. При своей работе отладчик вызывает функцию WaitForDebugEvent в цикле и реагирует на отладочные события. Затем же, после обработки, вызывается ContinueDebugEvent, продолжая выполнение отлаживаемой программы дальше. Теперь, узнав о принципе действия, можно наконец-то приступить к распаковке, распаковываемой программой будет сам Armadillo 3.10

Конечно, без того, чтобы дамп на OEP снять - не обойтись. Но перед этим хотелось бы привести список, с чем придётся столкнуться. Это обнаружение отладчиков(для SoftIce спасает только IceExt 0.41), это обнаружение драйвера SuperBpm(?!), порча импорта, причем функции не заменяются переходниками, а копируются полностью(на StarForce похоже, только код пока не изменяется), также это динамическая распаковка, известная как CopyMem II, т.е. в памяти в момент времени присутствует лишь несколько страниц секции кода, нужных для работы программы, остальные помечены как отстуствующие в памяти, когда происходит обращение к такой странице, генерируется отладочное событие EXCEPTION_DEBUG_EVENT с отладочным кодом EXCEPTION_ACCESS_VIOLATION, которое обрабатывается процессом-отладчиком, т.е. подгружает нужную страницу в память. К тому же при распаковке проверяется первый байт API на наличие int3, поэтому придётся бряки на конец API ставить. И завершает всё это самое интересное - полная эмуляция переходов(jmp и jcc).

Что же, будем ловить создание импорта. Способ на этот раз такой - bpx 77E7ADA6 if *(esp+4)!=0 do "dd *(esp+4)" Адрес 77E7ADA6 - это всего лишь указатель на ret, т.е. на окончание GetModuleHandleA, и ставить такой bpx надо в контексте, где библиотека загружена, например перед этим ввести u GetModuleHandleA. Теперь можно подождать, когда же функция вызовется с какой-нибудь библиотекой, не имеющей к распаковке никакого отношения. Вот например сойдёт msvbvm60.dll, но в этом же ряду последняя библиотека - advapi32.dll, именно после получения её хэндла можно ловить GetprocAddress. Ловим, и видим, что адреса шифруются и записываются куда-то далеко от проги, т.е. на IAT это не тянет. Поэтому надо выйти из этого цикла(поставить бряк за командой jnz) и ловить GetProcAddress ещё раз. После выхода из функции надо внимательно следить, куда попадает значение eax(адрес импортируемой функции). Адрес этот был 43D148, значит IAT начинается где-то рядом, и правда, 43D000(это видно в айсе, если сделать dd 43D148 и прокрутить немного вверх). Теперь, если просмотреть в ImpRec'е, то можно заметить, что большинство функций представляют собой переходники с полностью скопированным кодом, который вряд ли каким-нибудь плугином можно идентифицировать. Поэтому надо немного изменить код так, чтобы вместо адресов переходников в IAT записывались нормальные адреса. Для этого придётся немного поисследовать процедуру создания импорта. После выхода из GetProcAddress eip=D85839, а при записи в IAT адреса функции - D9A828. Надо сдампить этот кусок памяти(Например через PE Tools, функция dump region, адрес D81000, длина 24000), и сунуть его в IDA(грузить как Binary, Loading offset=D81000). Главная ветка, как видно из листинга, лежит в адресах D9xxxx, а вызов GetProcAddress в D8xxxx, это просто процедура, которой передаются два параметра, причём точно такие же, как и GetProcAddress, и даже в том же порядке. Так зачем же было такую здоровую процедуру делать? Понятно, чем больше мусора, тем сложнее исследовать. Хотя тут особо много исследовать-то и не надо: первый же XREF всё и выдаёт - взгляните на адрес D9A737, там переход, если адрес функции, который заносится в переменную [ebp-187Ch], уже определён, и нет перехода, если он неопределён. Из этого следует, что если просто вписать туда два nop'а, то в IAT будут только адреса API, что нам и нужно. Чтобы попасть на это место, надо после выхода из вышеупомянутого цикла(где адреса шифруются) и поставить bpm D9A73E x. Теперь можно сделать своё "чёрное дело" - вписать 09090h по адресу D9A73E, и ... вылететь(!), т.к. оказывается армадилло проверяет CRC своего "распаковочного" кода. Но это не помешает восстановить импорт и пока временно сохранить дерево в какой-нибудь файл. Итак, импорт у нас уже есть.

Теперь будем искать функцию GetModuleHandleA для того, чтобы до OEP добраться. В смысле искать вхождение имени этой функции в файле ImpRec'а. Видим, что адрес функции(или переходника) будет находится по адерсу 43D0F4. Чтобы попасть в адресное пространство главной программы, надо поставить bpx showwindow и вызвать about, ну в смысле надо поймать её на функции, которую она должна использовать, вот для появления на экране about диалога нужна showwindow, поэтому её и надо ловить. Адрес GetModuleHandleA оказался D8905E, как бы винда не пыталась, загрузить kernel32.dll по этому адресу, у неё бы ничего не получилось, значит можно сделать вывод, что это переходник, который к тому же никуда и не переходит. Именно на этот адрес придётся ставить бряк для нахождения OEP, где-то после создания импорта. Найти OEP таким способом можно всего в 99% случаев, просто есть такие программеры, которые не любят эту функцию, а делают статическую линковку, и вместо GetModuleHandle используют константу. В таком случае приходится искать OEP по GetCommandLineA, GetVersion, GetStartupInfo, а уж если и это не помогает, значит прога 100% на асме и надо искать в памяти call на ExitProcess, и листать вверх, искать признаки OEP, но это уж вряд ли придётся делать. Тем более здесь брякается сразу на переходнике GetModuleHandleA. Кстати, совет - после создания импорта вызывается функция SetProcessWorkingSetSize. После срабатывания этой функции, которая вызывается для того, чтобы у процесса можно было прятать страницы памяти, можно ставить любые бряки, которые нужны в данной ситуации, т.к. импорт уже создан. Ну вот, в стеке на месте адреса возврата лежит 43576A, оттуда можно листать вверх. Ну допустим пролистали немного вверх на нашли OEP(если не нашли, значит не повезло, увы...), ну нашли значит, по первым байтам видно, на чём написано. Как известно, сейчас большинство программ пишут на Visual C++ с одной стороны, и на Delphi и Borland C++ Builder с другой. Борландовскую продукцию можно узнать по совсем небольшой процедуре(120-140 байт), из которой вызываются такие же небольшие процедуры, из которых ...(Ну можно раз 100 это написать. Вот говорят вообще, что самая распространённая инструкция - это mov, это ещё смотря какой компилятор, если Borland, то тогда самая распротранённая - это call. Не знаю, IDE у этих сред разработки очень даже неплохое, а вот про компиляторы даже говорить не хочеться). Ну вот, а Visual C++ можно узнать примерно по таким инструкциям:

_text:004356A2 push ebp
_text:004356A3 mov ebp, esp
_text:004356A5 push 0FFFFFFFFh
_text:004356A7 push offset unk_0_43E5A0
_text:004356AC push offset sub_0_435404
_text:004356B1 mov eax, large fs:0
_text:004356B7 push eax
_text:004356B8 mov large fs:0, esp
_text:004356BF sub esp, 58h
_text:004356C2 push ebx
_text:004356C3 push esi
_text:004356C4 push edi
_text:004356C5 mov [ebp+var_18], esp

Может чуть-чуть отличаться, но узнать всё равно можно. Именно это всё и находится в Armadillo на OEP. Теперь опять возникает проблема с дампом, по bpx, ни даже по bpm 4356A2 x айс вылезать не хочет, а уж если очень постараться, чтобы он вылез, то комп вообще почему-то перезагружается. Поэтому можно просто байты подменить в начале на jmp eip, или, как мне больше нравится, на
push FFFFFFFE
call 77E94D56; SuspendThread
Адреса API конечно свои в каждой винде, эти например для XP SP1. Мне это больше нравится, потому что процессор от этого не загружается, а с моим слабеньким процессором пока до диспетчера задач доползёшь, чтобы приоритет снизить, просто мышку двигать устанешь. Всё равно так и так в дампе байты править в HEX редакторе, только здесь править не 2, а 7. После снятия дампа даже невооружённым глазом можно заметить избыток инструкций add [eax], al. Их там даже больше, чем call'ов в проге, написанной на дельфи. Раз так, то придётся вспомнить про долгожданный CopyMem II. Чтобы получить полную секцию кода, придётся поисследовать, и даже побольше, чем при исследовании процедуры создания импорта. Для начала можно просто сдампить процесс-отладчик прямо во время работы, раз нам только код нужен, и также неплохо было бы найти место, где же распаковываются страницы кода. Поймать этот момент можно только по срабатыванию бряка на WriteProcessMemory. Теперь выходим из неё и давим F12, пока не перестанет айс вываливаться. Получилось два раза. Теперь мы находимся в главной ветке этой процедуры распаковки, потому что инструкции ret в конце нет, адрес 46919D. Видно, что мы вышли из процедуры, которая записывает код на место, или наоборот его удаляет. Принимает же она целых три параметры, первый - номер страницы кода, который высчитывается, как 401000h+PageNumber*1000h(Можно сразу расчитать, сколько страниц содержится у нас в секции кода - (43D000-401000)/1000 = 3C). Второй параметр - код для расшифровки страницы, а последний показывает, что делать со страницей, например 0 - записать а в неё код, а 1 - удалить из памяти. Теперь нам нужен такой цикл, чтобы код записать во все страницы. Но есть ещё несколько трудностей - это нужно сделать на OEP, т.к. прога может чего-нибудь инициализировать в секции кода. Конечно, это довольно редкое явление, но оно имеет место, поэтому надо учитывать. И ещё одно, дампить страницу надо сразу, т.к. при открытии какой-нибудь одной страницы другая закрывается. Вот, предлагаю рассмотреть следующий дампер для секции кода, написанный на FASM(больше всего подходит для таких целей. Скачать можно с wasm.ru, настраивать нужно только include, открывайте все файлы в директории include и меняйте %include% на путь к папке fasm\include. При желании можно переписать исходник на TASM или MASM, работать будет).

format PE GUI 4.0
entry start
include "d:\programs\fasm\include\win32a.inc" ;здесь вам лучше свой путь вставить
PROCESS_ID equ 0XXXh ;это ID процесса-отладчика, можно взять из SoftIce
PROCESS_ID0 equ 0XXXh ;ID главного процесса
EVENT_ALL_ACCESS equ 2031619
section ".all" code data readable writeable executable
start:
invoke OpenProcess, PROCESS_ALL_ACCESS, FALSE, PROCESS_ID ;открываем процесс-отладчик
mov ebx, eax
mov [debugger_process], eax
invoke VirtualAllocEx, ebx, NULL, _remote_size, MEM_COMMIT, PAGE_EXECUTE_READWRITE
mov [remote_mem], eax
mov edi, eax
;Пишем в контекст процесса отладчика удалённый код
invoke WriteProcessMemory, ebx, edi, _remote, _remote_size, tmp0
;Создаём удалённый тред в процессе отладчика, который будет открывать страницы
invoke CreateRemoteThread, ebx, NULL, 65536, edi, tmp0, NORMAL_PRIORITY_CLASS, tmp0
;Событие 1, его будет устанавливать удалённый тред, когда очередная страница будет открыта
invoke CreateEvent, NULL, FALSE, FALSE, _remote_event1
mov [event1_handle], eax
;Событие 2, будет устанавливать дампер, когда очередная страница записана на диск
invoke CreateEvent, NULL, FALSE, TRUE, _remote_event2
mov [event2_handle], eax
;открываем главный процесс
invoke OpenProcess, PROCESS_ALL_ACCESS, FALSE, PROCESS_ID0
mov ebx, eax
mov esi, 00401000h ;начало секции кода
;открываем файл для записи дампа секции кода
invoke DeleteFile, code_dmp
invoke CreateFile, code_dmp, GENERIC_WRITE, NULL, NULL, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL
mov ebp, eax
_loop:
;Ждём, когда удалённый тред открыет следующую страницу
invoke WaitForSingleObject, [event1_handle], NULL
;Читаем её из памяти
invoke ReadProcessMemory, ebx, esi, dump, 1000h, tmp0
;И шишем её в файл дампа
invoke WriteFile, ebp, dump, 1000h, tmp0, NULL
;Установка события, сигнализируем удалённому треду о том, что страница записана на диск
invoke SetEvent, [event2_handle]
add esi, 1000h
cmp esi, 0043D000h
jnz _loop ;Проверка на конец цикла
;Все чистим
invoke VirtualFreeEx, [debugger_process], [remote_mem], _remote_size, MEM_DECOMMIT
invoke CloseHandle, [debugger_process]
invoke CloseHandle, ebx
invoke CloseHandle, ebp
invoke ExitProcess, 0 ;выход из программы
;переменные
tmp0 dd 0
debugger_process dd 0
remote_mem dd 0
event1_handle dd 0
event2_handle dd 0
code_dmp db "code.dmp", 0 ;имя файла, в заторый запишется секция кода
;Код и данные удалённого треда
_remote:
call $+5
pop ebp
sub ebp, 5 ;получаем дельта-смещение удалённого кода
;Открываем созданные в дампере события по их названиям
lea eax, [ebp+_remote_event1-_remote]
push eax
push FALSE
push EVENT_ALL_ACCESS
mov ecx, 077E7564Dh
call ecx ;invoke OpenEvent, EVENT_ALL_ACCESS, FALSE, _remote_event1
mov [ebp+_remote_event1_handle-_remote], eax
lea eax, [ebp+_remote_event2-_remote]
push eax
push FALSE
push EVENT_ALL_ACCESS
mov ecx, 077E7564Dh
call ecx ;invoke OpenEvent, EVENT_ALL_ACCESS, FALSE, _remote_event2
mov [ebp+_remote_event2_handle-_remote], eax
_remote_loop:
;Ждём, пока дампер запишет страницу на диск
push NULL
push DWORD [ebp+_remote_event2_handle-_remote]
mov ecx, 077E7AC12h
call ecx ;invoke WaitForDebugEvent, [_remote_event2_handle], NULL
;Весь ниже лежащий код любезно предоставлен разработчиками Armadillo, можно взять из дампа процесса-отладчика по адресу 00469100. В нём вычисляется некоторый код, для того, чтобы раскриптовать страницу и затем записать её в главный процесс, т.е. поставить на место.
push 0
mov esi, [ebp+_remote_page-_remote]
shl esi, 4
mov eax, [ebp+_remote_page-_remote]
and eax, 80000007h
jns _remote_1
dec eax
or eax, 0FFFFFFF8h
inc eax
_remote_1:
xor ecx, ecx
mov cl, [eax+494650h]
mov edx, [ebp+_remote_page-_remote]
and edx, 80000007h
jns _remote_2
dec edx
or edx, 0FFFFFFF8h
inc edx
_remote_2:
xor eax, eax
mov al, [edx+494651h]
mov edi, [ecx*4+490260h]
xor edi, [eax*4+490260h]
mov ecx, [ebp+_remote_page-_remote]
and ecx, 80000007h
jns _remote_3
dec ecx
or ecx, 0FFFFFFF8h
inc ecx
_remote_3:
xor edx, edx
mov dl, [ecx+494652h]
xor edi, [edx*4+490260h]
mov eax, [ebp+_remote_page-_remote]
cdq
mov ecx, 1Ch
idiv ecx
mov ecx, edx
shr edi, cl
and edi, 0Fh
add esi, edi
mov edx, [496024h]
lea eax, [edx+esi*4]
push eax
mov ecx, [ebp+_remote_page-_remote]
push ecx
mov ecx, 46AEFFh
call ecx ;Это и есть вызов для открытия страницы
add esp, 0Ch
;Установка события, сигнализируем дамперу, что открыта следующая страница
push [ebp+_remote_event1_handle-_remote]
mov ecx, 077E75E37h
call ecx ;invoke SetEvent, [_remote_event1_handle]
inc [ebp+_remote_page-_remote]
cmp [ebp+_remote_page-_remote], 3Ch
jnz _remote_loop ;Проверка на конец цикла
ret ;Завершение треда
;Удалённые данные
_remote_event1 db "remote_event1", 0
_remote_event2 db "remote_event2", 0
_remote_event1_handle dd 0
_remote_event2_handle dd 0
_remote_page dd 0
_remote_size=$-_remote
dump rb 1000h ;буфер для считывания открытых страниц
section ".import" import data readable writeable
library kernel32, "kernel32.dll"
import kernel32,\
CloseHandle, "CloseHandle",\
CreateEvent, "CreateEventA",\
CreateFile, "CreateFileA", \
CreateRemoteThread, "CreateRemoteThread",\
DeleteFile, "DeleteFileA",\
ExitProcess, "ExitProcess",\
GetThreadContext, "GetThreadContext",\
OpenProcess, "OpenProcess",\
ReadProcessMemory, "ReadProcessMemory",\
SetEvent, "SetEvent",\
SetThreadContext, "SetThreadContext",\
VirtualAllocEx, "VirtualAllocEx",\
VirtualFreeEx, "VirtualFreeEx",\
WriteFile, "WriteFile",\
WaitForSingleObject, "WaitForSingleObject",\
WriteProcessMemory, "WriteProcessMemory"

Только перед использованием этого дампера надо зациклить на OEP главный процесс, и внести в начало кода(в смысле в начало исходника дампера) ID обоих процессов, и также не стоит забывать про то, что адреса API нужно свои вставить, всё-таки могут они отличаться. И ещё, под Win9X/ME дампер работать не будет! Просто ядра этих операционных систем не позволяют создавать удалённые потоки(вообще-то ядра этих систем ещё много чего не позволяют, поэтому быстрее стоит переходить на 2k, XP или Windows 2003). В папке с дампером должен появится файл code.dmp, который содержит нормальную(ну по сравнению с предыдущей секцией кода можно сказать, что и нормальную) секцию кода, которую можно вставить в наш дамп. Только не делайте Rebuild PE перед вставкой секции кода, её конец может потеряться(Ну там в конце нули, а любой PE Rebuilder их выкинет, и размер уменьшится).

Теперь надо бы секции протектора викинуть. Это придёться сделать даже тем, кто очень любит их оставлять, потому что ImpRec не захочет добавлять новую, типа их там и так много, в PE заголовке места нет. Удалять можно все, начиная с .text1(сначала из заголовка, а потом все разом из файла при помощи Rebuild PE), только секцию ресурсов сначала надо сохранить в какой-нибудь файл. Как бы вы не хотели её перестроить, ничего не получится. Resource rebuilder 1.0 by Dr. Golova выдал мне вместо перестроенной секции всего лишь Unhandled exception, sorry. Другие перестраиватели, которые я пробовал применять такого на перестраивали, что Restorator подавился...(Когда я проверял, почему Armadillo вылетает) Поэтому придётся оставить секцию ресурсов на месте(в смысле в памяти, а не на диске), а пока заняться восстановлением импорта. Восстановить его надо прежде, чем дописывать в конец файла эту секцию с ресурсами. Ну допустим сделали, секцию дописали, создали её в заголовке в соответствии с размерами, а приложение-то не Win32!(Обладатели Windows XP 64bit, не радуйтесь, оно и не Win64 тоже) Чтобы оно опять стало нормальным, надо сделать, чтобы VirtualSize секции .mackt + её же RVA равнялось RVA секции .rsrc(только RVA .mackt трогать лучше не надо, правьте только VirtualSize). теперь надо установить правильный SizeOfImage в Optional Header'е и сделать наконец-то Rebuild PE! Именно на этом моменте заканчивалась распаковка Armadillo предыдущих версий, но не 3.10. Если у вас получилось всё правильно, то прога должна вылетать где-то на 401Fxx, при выполнении инструкции int 3. Именно это и есть самая сложная часть распаковки, снятие эмуляции переходов, готовьтесь к самому тщательному исследованию этой самой эмуляции.

Первым делом нужно узнать адрес, откуда WaitForDebugEvent вызывается. Проще всего выполнить в IDA jump to name, а потом по XREF перейти на адрес 468AF0. Теперь нужно найти сравнение ExceptionCode отладочного события с EXCEPTION_BREAKPOINT. Искать намного легче в айсе, чем думать выполниться ли тот или иной переход. Поэтому ставим bpint 3 и ждём, когда брякнется в секции кода. Затем переходим в процесс-отладчик(Вот способ - набрать addr и найти там два armadillo. Текущий будет выделен, значит надо набрать addr <ID невыделенного armadillo>, вот и перейдёте), и ставим бряк после функции WaitForDebugEvent, а затем трассируем. Тут даже не нужно узнавать значение константы EXCEPTION_BREAKPOINT, айс её прямо так и показывает, но если кто хочет узнать, то ладно уж, подскажу - 80000003h. Сравнение же это происходит по адресу 4697C1, после него не должен выполниться переход jnz. Теперь, для того, чтобы знать, что делать, процессу-отладчику надо узнать, где же int 3 выполнился, для этого существует функция GetThreadContext. Вызывается она с адреса 469DA2, а структура CONTEXT заполняется по адресу ebp-144C. Значит адрес регистра eip в ней будет равен ebp-1394. Далее управление передаётся вот сюда:

_text1:00469DDA popa
_text1:00469DDB popf
_text1:00469DDC mov eax, [ebp-1394h] ; Чтение регистра EIP
_text1:00469DE2 mov [ebp-1450h], eax
_text1:00469DE8 mov dword ptr [ebp-1454h], 0
_text1:00469DF2 mov ecx, ds:dword_0_496054 ; 09EAh - общее число адресов с байтом 0CCh
_text1:00469DF8 mov [ebp-117Ch], ecx
_text1:00469DFE loc_0_469DFE: ; CODE XREF: _text1:00469E5E
_text1:00469DFE mov edx, [ebp-1454h]
_text1:00469E04 cmp edx, [ebp-117Ch]
_text1:00469E0A jge short loc_0_469E60
_text1:00469E0C mov eax, [ebp-117Ch]
_text1:00469E12 sub eax, [ebp-1454h]
_text1:00469E18 cdq
_text1:00469E19 sub eax, edx
_text1:00469E1B sar eax, 1
_text1:00469E1D mov ecx, [ebp-1454h]
_text1:00469E23 add ecx, eax
_text1:00469E25 mov [ebp-1458h], ecx
_text1:00469E2B mov edx, [ebp-1458h]
_text1:00469E31 mov eax, ds:dword_0_495FF8
_text1:00469E36 mov ecx, [ebp-1450h]
_text1:00469E3C cmp ecx, [eax+edx*4]
_text1:00469E3F jbe short loc_0_469E52
_text1:00469E41 mov edx, [ebp-1458h]
_text1:00469E47 add edx, 1
_text1:00469E4A mov [ebp-1454h], edx
_text1:00469E50 jmp short loc_0_469E5E
_text1:00469E52 ; ---------------------------------------------------------------------------
_text1:00469E52 loc_0_469E52: ; CODE XREF: _text1:00469E3F
_text1:00469E52 mov eax, [ebp-1458h]
_text1:00469E58 mov [ebp-117Ch], eax
_text1:00469E5E loc_0_469E5E: ; CODE XREF: _text1:00469E50
_text1:00469E5E jmp short loc_0_469DFE

Особых комментариев здесь не требуется, этот код представляет собой оптимизированный поиск в отсортированном массиве. Алгоритм такой, представьте, что у вас есть 20 чисел, каждое следующее больше предыдущего. Нам нужно найти, под каким номером идет какое-либо число(индекс в массиве). Мы устанавлинаем начальный индекс на последнее число и сравниваем его с данным. Пока число меньше числа из массива под индексом, мы делим этот индекс на два. Как тольно оно становится больше, начинаем поочерёдное сравнение, пока не будет совпадение, или число не окажется больше, чем из массива. Вот так и действует этот кусок кода. Если вы поняли алгоритм, то значит и должны понять, почему выход из цикла находится по адресу 469E0A. В айсе придётся опять смотреть, куда дальше передаётся управление после jge 469E60...

_text1:00469E85 popa
_text1:00469E86 mov ecx, [ebp-1454h] ;ecx=полученный в предыдущем цикле индекс
_text1:00469E8C mov edx, ds:dword_0_495FF8 ;edx - указатель на таблицу адресов с байтами 0CCh
_text1:00469E92 mov eax, [edx+ecx*4] ;Читаем адрес из массива с полученным в поиске индексом
_text1:00469E95 cmp eax, [ebp-1450h] ;сравниваем текущий EIP с прочитанным адресом; Просто исходя из алгоритма поиска в массиве может не быть адреса, равного EIP
_text1:00469E9B jnz loc_0_46A090 ;А это - переход на ContinueDebugEvent в случае, если в массиве действительно нет такого адреса(На месте разработчиков я бы сделал переход не на ContinueDebugEvent, а на TerminateProcess, т.к. если EIP нет в массиве, то значит в коде просто бряк поставили)
_text1:00469EA1 pusha
_text1:00469EA2 xor eax, eax ;опять запутанные переходы
_text1:00469EA4 jnz short loc_0_469EA8
_text1:00469EA6 jmp short loc_0_469EBD

Теперь рассмотрим следующий кусок кода между переходами.

_text1:00469EF9 popa
_text1:00469EFA popf
_text1:00469EFB lea ecx, [ebp-144Ch] ;это адрес структуры Context, заполенной ранее
_text1:00469F01 push ecx
_text1:00469F02 mov edx, ds:dword_0_496004 ;Адрес, пока не известно какой таблицы
_text1:00469F08 add edx, [ebp-1454h]
_text1:00469F0E mov al, [edx]
_text1:00469F10 push eax ;В eax - значение из таблицы
_text1:00469F11 call loc_0_46C721 ;Какая-то процедура, придётся исследовать
_text1:00469F16 add esp, 8 ;Выходит, что это cdecl процедура
_text1:00469F19 and eax, 0FFh
_text1:00469F1E test eax, eax
_text1:00469F20 jz loc_0_469FAA ;первая ветка с переходами
_text1:00469F26 pusha
_text1:00469F27 xor eax, eax
_text1:00469F29 jnz short loc_0_469F2D
_text1:00469F2B jmp short loc_0_469F42; а вот и вторая ветка

Вот теперь ещё и процедуру исследовать...

_text1:0046C721 push ebp
_text1:0046C722 mov ebp, esp
_text1:0046C724 sub esp, 14h
_text1:0046C727 push ebx
_text1:0046C728 push esi
_text1:0046C729 push edi
_text1:0046C72A pusha
_text1:0046C72B xor eax, eax ;Думаю, что понятно, что это такое
_text1:0046C72D jnz short loc_0_46C731
_text1:0046C72F jmp short loc_0_46C746 ;В конце концов попадём на адрес 46C74F
_text1:0046C74F popa
_text1:0046C750 mov dword ptr [ebp-8], 41h
_text1:0046C757 mov dword ptr [ebp-4], 0C0h
_text1:0046C75E mov eax, [ebp+8] ;Первый параметр, значение из таблицы
_text1:0046C761 and eax, 0FFh
_text1:0046C766 mov [ebp-0Ch], eax
_text1:0046C769 cmp dword ptr [ebp-0Ch], 11h
_text1:0046C76D ja loc_0_46C9AB ;переход, если это значение больше семнадцати, идёт на выход из функции и возврату нуля
_text1:0046C773 mov ecx, [ebp-0Ch]
_text1:0046C776 jmp ds:off_0_46C9DA[ecx*4] ;переход по таблице указателей

И действилельно, по адресу 46C9DA находится таблица с восемьнадцатью адресами, которые обрабатывают первый параметр этой функции, которым становится значение из таблицы, взятое под найденным индексом при сравнении EIP с таблицей адресов c, байтом 0ССh. Посмотрим, как же конкретно обрабатываеются эти самые значения(на примере нулевого значения):

_text1:0046C970 mov ecx, [ebp+0Ch] ;читается адрес структуры Context - второго параметра, переданного в функцию
_text1:0046C973 mov eax, [ecx+0C0h] ;Это читается регистр флагов EFLAGS
_text1:0046C979 and eax, 80h
_text1:0046C97E neg eax
_text1:0046C980 sbb eax, eax
_text1:0046C982 inc eax ;Всё это тонкости ассемблера... Короче так, оставляем восьмой бит регистра флагов(это флаг знака - SF). После neg eax флаг переноса устанавливается, если в eax не было нуля(флаг фыл установлен), и сбрасывается, если eax был равен нулю(флаг был сброшен). Затем после sbb eax, eax(это считай вычитание из нуля флага переноса) в eax окажется как бы [- состаяние флага SF], значит после inc eax всё встанет на свои места, в eax окажется число, противоположное состоянию флага SF. Более понятный аналог - shr eax, 7, and eax, 1 и xor eax, 1
_text1:0046C983 jmp short loc_0_46C9D3 ;Это просто выход из процедуры.

Т.е можно сделать вывод, что если в таблице под найденным индеском стоит значение 0, то возвращается значение флага SF. Теперь становится всё ясно, что это просто эмуляция переходов, т.к. армадилло в зависимости от состояния флагов корректирует регистр EIP, а та таблица, значение из которой передаётся в функцию - всего навсего лишь тип перехода. Для выяснения остальных типов придётся исследовать ещё 17 функций. Вот, что у меня получилось(соответсвие по типам):

0 = jns
1 = jl
2 = jnp
3 = jp
4 = jg
5 = jbe
6 = jmp
7 = ja
8 = js
9 = jnz
10 = jecxz
11 = jo
12 = jz
13 = jno
14 = jnc
15 = jle
16 = jc
17 = jge

Адрес 469F20 - переход(не один конечно, но всё равно туда) на 469FDC, где просто читается значение из другой таблицы по индексу, которой есть ни что иное, как размерность перехода - 1(переходы бывают 2 байта - все короткие, 5 байтов - длинный jmp и 6 байтов - длинные условные переходы, значит в этой таблице значения могут быть 1, 4 или 5), и прибавляется это к EIP в структуре Context, а затем просто переход на SetThreadContext(адрес 46A055), и далее - на ContinueDebugEvent. Всё это выполняется, если бы не выполнился условный переход в программе. Теперь, для восстановления кода надо просмотреть, что происходит, если бы переход выполнился(процедура проверки возвращает 1).
_text1:00469F4B popa
_text1:00469F4C mov eax, [ebp-1454h] ;это индекс читается
_text1:00469F52 xor edx, edx
_text1:00469F54 mov ecx, 10h
_text1:00469F59 div ecx ;делится индекс на 16, результат не важен. Это можно было сделать так - mov edx, [ebp-1454h] и and edx, 0Fh
_text1:00469F5B mov eax, [ebp-1454h] ;опять индекс читается
_text1:00469F61 mov ecx, ds:dword_0_495FF4 ;а это читается адрес с таблицей зашифрованных смещений переходов
_text1:00469F67 mov eax, [ecx+eax*4] ;читаем собственно зашифрованное смещение
_text1:00469F6A xor eax, [ebp+edx*4-1168h] ;а теперь расшифровываем его
_text1:00469F71 mov ecx, [ebp-1394h]
_text1:00469F77 add ecx, eax
_text1:00469F79 mov [ebp-1394h], ecx ;И прибавляем его к текущему eip, т.е. как бы переход в коде выполнился
_text1:00469F7F pusha
_text1:00469F80 xor eax, eax
_text1:00469F82 jnz short loc_0_469F86
_text1:00469F84 jmp short loc_0_469F9B ;Это уже переход на выполнение SetThreadContext, а затем на ContinueDebugEvent, можете сами проверить

Вот такая не очень сложная система эмуляции переходов. Казалось бы, что там - обработать все адреса из таблицы, поставить на их место соответствующие переходы и готово. А вот и неправильно, где-то я уже говорил об этом, что в этой таблице находятся все адреса, по которым расположены байты 0CCh, а не только те, где есть эмуляция перехода. К примеру, есть инструкция push 0CCh, адрес байта 0CCh в таблице будет, и попытка "восстановить" приведёт к окончательной порче кода. Выход один - использовать любой движок дизассемблера, и написать анализатор кода. К счастью здесь надобность в анализаторе отпадает, потому что весь код идёт подряд и нет всяких align 4. Единственное, что придётся долго и упорно жать на C(Команда make code) в IDA, чтобы найти данные в коде(через next data), чтобы прога для восстановления заодно и данные не обработала. Благодаря хорошей структуре кода мне удалось найти данные, ими оказались таблицы с адресами. Вот они 403D73 - 403DD7, 40B4C8 - 40B4D8 и 42CB8E - 42CBC6. Кстати, после адреса ~430000 все переходы на месте, поэтому восстанавливать будем с 401000 до 430000. Ниже я привожу пример кода восстановителя на C++, а именно Intel C++ 7.1 - очень неплохой компилятор, генерирует качественный код по сравнению с теми же VC++ и Borland C++. В VC++ это также можно скомпилировать в таком виде, для всего остального придётся переписывать. Комментарии достаточно подробны, даже если вы не знаете языка, то переписать на другой не составит труда. Итак, исходник:

//Исходник программы для восстановления секции кода после Armadillo 3.10
//Для работы нужно:
//Движок дизассемблера(можно использовать движок с сайта ollydbg, можно от IDA, но я здесь привожу пример с собственным движком, который сейчас доступен на странице www.cfiles.nm.ru, там же лежит и распаковынный этим способом Armadillo 3.10
//дампы: секции кода,
//Таблицы с адресами, содержащих байт 0CCh + 1(адрес инструкции следом за int3),
//таблицы типов переходов,
//таблицы с размерами переходов - 1(в смысле значения там - размер перехода - 1)
//таблицы со смещениями для переходов
//таблицы с 32-битными ключами для расшифровки смещений переходов
#pragma comment(linker,"/ENTRY:Main")
#pragma comment(linker,"/NODEFAULTLIB")
#pragma comment(linker,"/MERGE:.rdata=.text")
//Всё это максимально уменьшает размер файла, минимальный размер становится 1kb.
#include "windows.h"
//Включаемый файл движка дизассемблера
#include "disasm.h"
//файл с адресами после int 3
char addr_file[]="addrs.dmp"; //имя файла
DWORD addr_file_len; //размер файла
DWORD* addr; //указатель на файл в памяти, для остальных файлов всё аналогично
//секция кода
char code_file[]="code.dmp";
DWORD code_file_len;
BYTE* code;
//файл с типами переходов
char cjumps_file[]="cjumps.dmp";
DWORD cjumps_file_len;
BYTE* cjumps;
//файл с размерами инструкций переходов
char sizes_file[]="sizes.dmp";
DWORD sizes_file_len;
BYTE* sizes;
//файл с зашифрованными адресами, куда ссылаются переходы
char jmpoffsets_file[]="jmpoffsets.dmp";
DWORD jmpoffsets_file_len;
DWORD* jmpoffsets;
//файл с ключами для расшифровки адресов, на которые ссылаются переходы
char xortable_file[]="xortable.dmp";
DWORD xortable_file_len;
DWORD* xortable;
//число элементов во всех этих массивах
#define ELEMENTS_NUMBER 0x9EA
//массив имён загружаемых файлов
char* filenames[]={addr_file, code_file, cjumps_file, sizes_file, jmpoffsets_file, xortable_file};
//массив указателей на переменные, приминающие длины файлов
DWORD* filelengths[]={&addr_file_len, &code_file_len, &cjumps_file_len,
&sizes_file_len, &jmpoffsets_file_len, &xortable_file_len};
//массив указателей на переменные, принимающие указатели на считанные файлы
void** files[]={(void**)&addr, (void**)&code, (void**)&cjumps, (void**)&sizes, (void**)&jmpoffsets,
(void**)&xortable};
DWORD tmp0; //переменная типа "temporary"
//массив с опкодами коротких переходов
BYTE shortjmp[]={0x79, //jns
0x7C, //jl
0x7B, //jnp
0x7A, //jp
0x7F, //jg
0x76, //jbe
0xEB, //jmp
0x77, //ja
0x78, //js
0x75, //jnz
0xE3, //jecxz
0x70, //jo
0x74, //jz
0x71, //jno
0x73, //jnc
0x7E, //jle
0x7C, //jc
0x7D, //jge
};
//массив с опкодами длинных переходов
WORD longjmp[]={0x890F, //jnz
0x8C0F, //jl
0x8B0F, //jnp
0x8A0F, //jp
0x8F0F, //jg
0x860F, //jbe
0, //NO
0x870F, //ja
0x880F, //js
0x850F, //jnz
0, //NO
0x800F, //jo
0x840F, //jz
0x810F, //jno
0x830F, //jnc
0x8E0F, //jle
0x8C0F, //jc
0x8D0F, //jge
};
//структура для управления дизассемблированием
disasm_struct DSt;
//прототипы функций
//функция для чтения списка файлов
DWORD ReadAllFiles(char** filenames, DWORD** filelengths, void*** files, DWORD NumberOfFiles);
//функция для освобождения списка блоков памяти
void FreeMemory(void*** blockspointer, DWORD NumberOfMemoryBlocks);
//функция для генерации инструкции перехода
void GenerateInstruction(BYTE* buffer, BYTE jmptype, BYTE jmplen, DWORD jmpoffset);
void Main()
{
//Загрузка всех нужных файлов в память
tmp0=ReadAllFiles(filenames, filelengths, files, 6);
//Если какой-то файл не загрузился, то об этом выводится сообщение и программа закрывается
if (tmp0!=0xFFFFFFFF)
{MessageBox(NULL, "Cannot read file", filenames[tmp0], MB_ICONERROR);
FreeMemory(files, tmp0+1); //освобождение всех выделенных блоков памяти
ExitProcess(NULL);} //выход из программы
//заполняем структуру для управления дизассемблированием
DSt.CodeAddr=code; //стартовый адрес для дизассемблирования
DSt.VA=0x401000; //стартовый виртуальный адрес(дизасмится, будто бы код расположен там)
DSt.radix=DISASM_RADIX_HEXADECIMAL; //Все параметры для инструкций кроме адресов выводить в HEX
DSt.addr_auto_increment=1; //автоматическая установка адреса на следующую инструкцию
DSt.VA_auto_increment=1; //и виртуального адреса тоже
DSt.code_size=0; //это 32-битный код
while (DSt.VA<0x430000) //выполнять, пока текущий виртуальный адрес меньше 430000
{ //пропуск данных в секции кода
if (DSt.VA==0x403D73) {DSt.CodeAddr+=0x403DD7-0x403D73; DSt.VA=0x403DD7;}
if (DSt.VA==0x40B4C8) {DSt.CodeAddr+=0x40B4D8-0x40B4C8; DSt.VA=0x40B4D8;}
if (DSt.VA==0x42CB8E) {DSt.CodeAddr+=0x42CBC6-0x42CB8E; DSt.VA=0x42CBC6;}
if (disasm(&DSt)==1 && *(DSt.CodeAddr-1)==0xCC) //если встретили инструкцию int3
{DSt.VA--;
DSt.CodeAddr--; //устанавливаем эти указатели опять на int3, т.к на это место будет вставлен
соответствующий переход
tmp0=0;
//поиск совпадения адреса в таблице, результат(индекс) устанавливается в tmp0
while (tmp0 if (tmp0!=ELEMENTS_NUMBER) //если адрес в таблице присутствует
{ //вызываем функцию для вставки на место int3 перехода, который армадилло оттуда спёр
GenerateInstruction(DSt.CodeAddr, //адрес, куда вставлять переход
cjumps[tmp0], //тип перехода
sizes[tmp0]+1, //размер перехода
(jmpoffsets[tmp0]^xortable[tmp0&0xF])+1 //расшифрованное смещение
перехода от начала jmp, а не от адреса, указанного в
таблице, поэтому и минус 1
);}
else {DSt.VA++; DSt.CodeAddr++;}}} //это если int3 был в коде до упаковки
//запись восстановленной секции кода
DeleteFile("_code.dmp");
HANDLE hFile=CreateFile("_code.dmp", GENERIC_WRITE, FILE_SHARE_READ, NULL,
CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL);
WriteFile(hFile, code, code_file_len, &tmp0, NULL);
_exit:
ExitProcess(NULL);}
//функция для вставки нужного перехода
void GenerateInstruction(BYTE* buffer, BYTE jmptype, BYTE jmplen, DWORD jmpoffset)
{switch (jmplen)
{case 2 :
{buffer[0]=shortjmp[jmptype];
buffer[1]=BYTE(jmpoffset-2);
break;}
case 6 :
{*(WORD*)buffer=longjmp[jmptype];
*(DWORD*)(buffer+2)=jmpoffset-6;
break;}
case 5 :
{buffer[0]=0xE9;
*(DWORD*)(buffer+1)=jmpoffset-5;
break;}}
return;
}
//функция для чтения файлов по таблице
DWORD ReadAllFiles(char** filenames, DWORD** filelengths, void*** files, DWORD NumberOfFiles)
{ //LOCALS
HANDLE hFile;
for (DWORD i=0; i
{hFile=CreateFile(filenames[i], GENERIC_READ, FILE_SHARE_READ, NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile==INVALID_HANDLE_VALUE) return i;
*filelengths[i]=GetFileSize(hFile, NULL);
*files[i]=LocalAlloc(LPTR, *filelengths[i]);
ReadFile(hFile, *files[i], *filelengths[i], &tmp0, NULL);
CloseHandle(hFile);
if (*filelengths[i]!=tmp0) return i;}
return 0xFFFFFFFF;
}
//функция для освобождения нескольких блоков памяти по таблице
void FreeMemory(void*** blockspointer, DWORD NumberOfMemoryBlocks)
{for (DWORD i=0; i

Вот собственно и всё, что ещё можно сказать. Дампы всех таблиц надо снимать во время обработки int3, например, когда трассируете после адреса 469F4B. Размер можно узнать из кода, где происходит поиск совпадения с EIP адресом из таблицы, я ещё там алгоритм описал. Ну вот собственно и всё, заменяем секцию кода на нормальную(теперь уж точно нормальную!) и видим, что прога функционирует нормально, распаковка закончена!!!

Послесловие:
В общем можно сказать, что армадиллу снять труднее, чем остальные протекторы, и главная причина - объём проделываемой работы. Но тут есть такая вещь - распаковку армадиллы очень легко автоматизировать, по сравнению хотя бы с тем же аспром. Некоторые приёмы автоматизации уже здесь описывались, будь то снятие дампа секции кода(вспомните пример на FASM'е), и, конечно, восстановление условных переходов. Приведённый здесь восстановитель очень несовершенный, подойдёт только для такого же компактного кода, да и то возникают трудности с данными(здесь всего три массива данных, хотя и их нелегко найти, даже очень нелегко). Гораздо лучше будет восстанавливать анализатор, аналогичный IDA, т.е. начинать от точки входа, использовать базу с адресами и анализировать по веткам(call, jcc, jmp - начало новой ветки, ret, jmp - конец ветки с кодом). Главный алгоритм его - поиск кода, который может выполняться, ну и естесственно, пропуск данных и того кода, который ни при каких обстоятельствах не выполнится. Ну ладно, с восстановителем всё. Также можно заметить(по крайней мере не на очень быстрых компах, да в принципе на них тоже должно быть заметно), что возросла скорость работы программы, т.е. распаковав, мы провели некоторую оптимизацию. Из этого можно сделать вывод, что не следует паковать армадиллой программы, занимающиеся к примеру кодированием аудио и видео, также нельзя и игры паковать, т.е. все программы, которые очень активно работают с памятью, производительность очень сильно упадёт. Ну в общем, всё.

автор: dragon, который естесственно не несёт ответственности за противозаконное использование данных материалов.
мылить сюда: dtdcs@mail.ru

30 августа 2003 г.