Распаковываем самое простое 4/5 PECompact версии 2.x

😁
Олдфаг
Статус
Оффлайн
Регистрация
27 Ноя 2016
Сообщения
2,091
Реакции[?]
2,025
Поинты[?]
6K
- Описание
Пожалуй, если кто-то и находится на 3-м месте, то это PECompact этой версии. Он менее распространен, чем UPX или AsPack и его автор не распространяет исходные тексты. Бесплатно можно получить триальную версию на 7 дней, а для неограниченного использования надо зарегистрироваться!
По сравнению с другими упаковщиками, у PECompact 2.x гораздо больше опций. Предусматривается оптимизация под скорость распаковки или минимальный размер. Можно выбрать - какие типы ресурсов оставить нетронутыми. И, самое главное, есть возможность:
1) внедрять перехватчики API-функций
2) выбрать другой кодек (это изменит алгоритм упаковки).
3) (и очень важно) - можно внедрить в программу водяные знаки.
Только мне непонятно - зачем они нужны.

- Описание
В файле находится 2 секции:
Код:
.text        rva 00001000, vsize 0006A000, offset 00000200, psize 00026000
.rsrc        rva 0006B000, vsize 00009000, offset 00026200, psize 00008600
Во второй секции находятся ресурсы и импорт:
Код:
resource directory        rva 0006B000, vsize 000077DE
import directory        rva 00072834, vsize 0000008F

После импорта расположены еще какие-то данные, о назначении которых мы еще не знаем.
Итак, импорт и ресурсы находятся во второй секции, стало быть, весь упакованный образ лежит в первой секции. Точка входа указывает на начало первой секции, что очень необычно.
Аттрибуты секций:
Секции имеют все аттрибуты, которые только можно. У первой секции есть и Execute, и Code и даже Initdata. У второй секции - только Execute и Code, что наводит на мысль, что в ней расположен код распаковщика.

- Сигнатура и Водяные знаки
Просмотрев файл, упакованный PECompact 2.x, мы увидим сигнатуру PEC2. Она находится в неиспользуемом поле 'relocation table offset' первой секции. Как было сказано ранее, упаковочная программа может внедрить в модуль "водяной знак".

- Анализ
Ну что ж, приступим к анализу кода.
Код:
mov eax, 0047335Ch
push eax
push fs[0]
mov fs:[0], esp
Видно, что пакер регистрирует свой SEH-обработчик, расположенный по адресу 0047335Ch, создав на стеке структуру след. вида:
0047335Ch
Old_SEH

Заметьте, что этот адрес находится в самом конце 2-ой секции, потому что ее физический конец находится по RVA-адресу 73600h (так как вирт. адрес конца секции = 74000, а разница между физич и вирт. размером секций составляет A00h).
Дальше пакер собирается нарушить доступ, записав ecx по нулевому адресу:
Код:
xor eax, eax
mov [eax], ecx

В результате управление должен получить SEH-обработчик по адресу 0047335Ch, ставим бряк на этот адрес, и снимаем его, когда мы на нем остановимся. После перехода в обработчки содержимое регистров полностью изменится. Что же делает обработчик?
Код:
0047335С:        mov eax, 0B2134h
00473361:        lea ecx, [eax+3C124Bh]
00473367:        mov [ecx+1], eax
Этот записывает по адресу (47337F+1) число 0B2134h. Как вы видите - адрес почти сопадает с текущим, значит происходит модификация кода. В результате модифицируется число в команде:
Код:
mov eax, 12345678h -> mov eax, 000B2134h

Далее код начинает разбираться в структуре, переданной через стек, в к-ой записана информация об исключении. На стеке лежит адрес возврата из ф-ии, которая нам сообщила об исключении, а ниже лежат 4 аргумента - это указатели, на информацию об исключении:
struct _EXCEPTION_RECORD *ExceptionRecord,
void * EstablisherFrame,
struct _CONTEXT *ContextRecord,
void * DispatcherContext

Вот значения этих указателей:
0012FCD4
0012FFBC
0012FCF0
0012FCA8

Код распаковщика записывает в edx значение первого указателя (mov edx, [esp+4]). Этот указатель ведет на самую главную структуру - ExceptionRecord, к-я содержит информацию о самом исключении:
typedef struct _EXCEPTION_RECORD {
DWORD ExceptionCode;
DWORD ExceptionFlags;
struct _EXCEPTION_RECORD *ExceptionRecord;
PVOID ExceptionAddress;
DWORD NumberParameters;
UINT_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
} EXCEPTION_RECORD;

Код обращается по смещению 0Ch от начала этой структуры, которому соответствует поле ExceptionAddress, и записывает его значение в edx (mov edx, [edx+C]). В этом регистре оказывается адрес, по которому возникло исключение:
Код:
edx = 00401016h
Затем записывает туда E9:
Код:
mov [edx], E9
, в результате чего команды, следующие за 'xor eax, eax':
Код:
89 08        mov [eax], ecx
50        push eax
45        inc ebp
43        inc ebx
превращаются:
Код:
E9 08 50 45 43        jmp 43856023
в элегантные шорты! Далее распаковщик модифицирует адрес этого перехода, записывая по адресу
Код:
00401017h число 00072364h:
add edx, 5
sub ecx, edx
mov [edx-4], ecx
В результате:
Код:
jmp 43856023 -> jmp 0047337F
Следующие 2 команды возвращают 0, сообщая операционной системе, что исключение обработано и выполнение процесса можно продолжить:
Код:
xor eax, eax
retn
Итак, мы вернулись назад, на тот адрес (00401016h), где возникло исключение. Теперь тут находится jmp 0047337Fh и он передает управление на код... расположенный за нашим SEH-обработчиком! Выходит, мы просто перешли к следующей команде:

Код:
xor eax, eax
retn				// были здесь
mov eax, 0B2134h	// а теперь мы здесь!
pop fs:[0]
add esp, 4
push ebp
push ebx
push ecx
push edi
push esi
push edx
lea ebx, [eax+3C1204h]
push'ы сохраняют регистры на стек (значит где-то они будут сняты pop'ами). Поскольку контекст регистов сейчас восстановился, то на стеке лежит адрес старого обработчика исключений (0x0012FFBC) и 'pop fs:[0]' его восстанавливает в звании. Это говорит о том, что извращения закончились и началась распаковка. Так как в алгоритме распаковки нам разбираться не надо, я не буду вас долго мучить и скажу лишь несколько слов:
Первый вызов (call eax) вызывает функцию VirtualAlloc, чтобы выделить память для распаковки.
'call ecx' вызывает функцию, лежащую ниже в памяти (неподалеку) - по адресу 0x0047328Ah.
'call edi' передает управление коду, скопированному в выделенный блок памяти.
В конце секции, перед нулями находится код:
Код:
pop edx
pop esi
pop edi
pop ecx
pop ebx
pop ebp
jmp eax
00 00
00 00
...
Команды 'pop' восстанавливают значения регистров, что говорит о завершении распаковки. 'jmp eax' осуществляет переход на оригинальную точку входа.

- Точка входа
Итак, пакер мы изучили. Как же будет выглядеть его точка входа?
Код:
mov eax, 0047335Ch
push eax
push fs[0]
mov fs:[0], esp
xor eax, eax
mov [eax], ecx
Первые 2 команды можно заменить на 'push 0047335Ch', а лишний байт занять nop'ом и поменять местами. Поэтому на них не стоит пологаться.
4-я и 5-ая команды могут быть заменены на другие. Причина в том, что пакер модифицирует код, вставляя команду 'jmp 0047337Fh' вместо 'mov [eax], ecx'. Поскольку jmp состоит из 5-ти байтов, а mov - из 2-х, то после команды 'mov [eax], ecx' у нас есть еще 3 свободных байта. Этого достаточно чтобы:
Код:
xor eax, eax     -> mov eax, 00000000h
mov [eax], ecx    -> div eax, eax
Пакер не различает - какое исключение произошло, поэтому Access Violantion можно заменить на Divide by zero. Правда, адрес исключения увеличится на 2 байта, но это не скажется на работоспособности программы. Возможно, что 2-ю и 3-ю команды тоже можно заменить на что-нибудь другое без потери работоспособности.
Так что... по точке входа ориентироваться нельзя! PEiD так и делает - ей наплевать на entrypoint, определяя PECompact 2.x как-то по-другому.

- Ручная распаковка
Ничего сложного. В точке входа на стек заносится адрес обработчика исключений:
Код:
mov eax, 0047335Ch
push eax
Переходим по этому адресу и ищем внизу retn и ставим бряк на следующей команде:
Код:
xor eax, eax
retn
mov eax, 0B2134h    <- сюда ставим бряк
pop fs:[0]
Теперь запускаем программу и передав исключение программе, останавливаемся в точке входа останова. Все, теперь как попасть в OEP - каждый выбирает сам. Можно найти точку выхода из пакера - тогда ищите внизу кучу pop'ов и jmp, расположенные перед нулями:
Код:
pop edx
pop esi
pop edi
pop ecx
pop ebx
pop ebp
jmp eax
00 00
00 00
...
Ставим бряк на jmp и достигнув его, делаем шаг, оказываясь в OEP. Снимаем дамп.
Можно после остановки на команде 'mov eax, 0B2134h' включить трассировку, установив условием трассировки попадание в границы первой секции.

- Признаки
Текстовые строки:
1) 1-я и 2-ая секция имеет имя '.text' и '.rsrc'.
2) У первой секции в поле relocation table offset записана сигнатура 50h, 45h, 43h, 32h, которая в текстовом виде выглядит как 'PEC2'.
Точка входа ненадежна, но можно сказать, что в ней присутствует регистрация SEH-обработчика и последующая генерация исключения.
О структуре PE-файла можно сказать следующее:
1) Ресурсы и таблица импорта находятся в последней секции
2) Точка входа расположена близко к концу последней секции
3) Первая секция обладает аттрибутами Execute, Code, Initdata, а последняя - Execute и Code.

- Качество упаковки
По качеству упаковки можно сказать, что PECompact упаковывает:
* лучше, чем AsPack, так как в нем всего 2-3 секции, благорадя чему теряется мало места на выравнивание
* хуже, чем UPX, так как помимо распаковки PECompact занимается самомодификацие, запутыванием алгоритма и копированием данных в памяти - из-за этого код распаковщика занимает больше места и портит статистику упаковки
 
Сверху Снизу