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

😁
Олдфаг
Статус
Оффлайн
Регистрация
27 Ноя 2016
Сообщения
2,091
Реакции[?]
2,025
Поинты[?]
6K
За время существования сменилась 1 старшая версия. Последняя версия - 2.98 и не загорами 3-я.
Между упаковщиками версий 1.x и 2.x есть заметная разница. Поэтому будем рассматривать их отдельно.

- Описание
Итак, продолжается наша эпопея с упаковщиками без антиотладочных приемов. Чем же он тогда отличается? А тем, что в нем есть:
1) одна ловушка (на которую часто попадаются новички)
2) копирование и раскодирование кода, из-за которого нахождение точки выхода в самом начале работы невозможно (как мы это делали с UPX и AsPack).
В связи с этим, перед описанием ручной распаковки мы проведем анализ упаковщика.

- Как выглядит?
Посмотрим файл, обработанный версией 1.33. Содержит 3 секции (на примере файла start.exe):
Код:
pec1    rva 00001000, vsize 0003A000, offset 00000400, psize 00015E00
pec2    rva 0003B000, vsize 00023000, offset 00016200, psize 00003800
.rsrc    rva 0005E000, vsize 00001000, offset 00019A00, psize 00000400
Очень похоже на UPX. Однако, нет - первая секция не пуста. И судя по разнице физического и виртуального размера - они обе упакованы. В первой секции находятся упакованные секции, а во второй - ресурсы и распаковщик (по точке входа).
Третья секция (.rsrc) - очень интересная. Она занимает всего 1000h и вместо ресурсов в ней - таблица импорта. Неужели автор пакера решил нас обмануть? На самом деле ресурсы лежат во второй секции (pec2) в упакованном состоянии:
Код:
import directory            rva 00196000, vsize 00000447
resource directory        rva 00192000, vsize 00000F08

Итак, перед нами - пока первый пример, когда секция ресурсов теряет свое родное имя. Причем, вопреки распространенной ситуации, ресурсы занимают не всю секцию - остальная часть занята данными упаковщика.
С аттрибутами секций PECompact ведет себя правильно, в отличие от AsPack. В первой секции после распаковки будет секция кода, поэтому у нее установлены необходимые аттрибуты Execute и Code. Во второй секции - ресурсы и данные, поэтому - Initdata. Из третьей секции распаковкщик только читает, поэтому она имеет только аттрибуты Read и Shared.
Один файл PECompact'а более старшей версии (1.68-1.84) выглядит немного по-другому (на примере файла AdobeUpdateManager.exe версии 2.0):
Код:
pec1    rva 00001000, vsize 0007A000, offset 00000400, psize 00033400
.rsrc    rva 0007B000, vsize 00039000, offset 00033800, psize 00015E00
pec    rva 000B4000, vsize 00004000, offset 00049600, psize 00000600
.rsrc    rva 000B8000, vsize 00001000, offset 00049C00, psize 00000600
Откуда здесь еще одна секция ресурсов? При рассмотрении оказывается что ничего особенного тут нет - просто секция 'pec2' разбита на 2 части. В секцию '.rsrc' попали ресурсы, а в секцию '.pec' - данные упаковщика. Плюс у 'pec' установлен аттрибут Execute. Такую хирургическую операцию легко сделать в PE-редакторе, так что, скорее всего, это не исключение из правил, я чье-то личное вмешательство.

- Импорт
PECompact дополнительно использует VirtualAlloc и VirtualFree для выделения памяти.

- Сигнатура
Во всех файлах, упакованных PECompact'ом есть знак. В PE-заголовке появляется сигнатура PECO. Она находится в неиспользуемом поле "pointer to symbol table".

- Анализ
Начинаем изучать код распаковщика и ищем точку выхода.
Сначала надо пройти через pushfd без трассировки, чтобы бит трассировки не попал в флаги:
Код:
jmps  .00406608    ; Прыгаем на (1)
push 000012С0   
retn           
pushfd            ; (1)
pushad
call 00406611    ; Вызываем (2)
Для этого можно поставить бряк на следующую команду и продолжить выполнение. call пропускает одну команду, поэтому положенный на стек адрес, - скорее всего не для возврата, а определения адреса загрузки. Изучив код, можно сказать, что все хитрости релизованы в только в точке входа, а дальше код открыт как зонтик.
Н-р, вот таким завуалированным способом выделяется 4 байта на стеке:
Код:
mov eax, esp
add eax, 4
xchg eax, ebx
mov esp, ebx
А вот адрес, занесенный на стек call'ом:
Код:
mov ebx, [ebx-4]
Он используется для относительной адресации. Но пакер выполняет с ним не одну операцию, а две - сначала вычитает, а потом прибавляет, получается абсолютный адрес. Получив адрес точки входа, пакер завуалировано модифицирует код в точке входа следующим образом:
[00406603] += 4066A6h
[00406600] = 9090h

Выполняемые действия непросто понять через диззассемблер, но элементарно определить в отладчике.
В результате в entrypoint возникают след. изменения:
Код:
jmp 00406608h  -> nop, nop
push 000012C0h -> push 004012C0h
retn           == retn
Адрес 004012C0h очень близко к базе загрузки, ничего другого, кроме OEP здесь быть не может! Ура! Мы узнали настоящую точку входа! Но ставить бряки на этот код бессмыслено. Чтобы мы не ставили - бряки на выполнение, чтение, запись, условная трассировка - ничего не срабатывает. Почему, блин!? Потому что это ловушка, на которую пакер никогда не передаст управления! Он даже постарался привлечь наше внимание изменением кода.

Скажу заранее, что на этом все хитрости закончены - никаких модификаций и жанглирования регистрами больше не будет. Дальше есть цикл пересылки данных:
Код:
pop edi
lea esi, [ebp+408070]        //esi = 00406670h
mov ecx, 39Dh
Прямо за циклом есть код по адресу 00406670h размером 39Dh. Сначала пакер модифицирует число в первой команде этого кода:
Код:
mov [ebp+408071h], eax
В результате 'mov ebp, 0h -> mov ebp, -18Eh'. А потом цикл копирует этот код вперед на адрес 00407EE2h и передает туда управление retn'ом. Скопированный код выполняет системный вызов:
Enter 0,0
eax = FFFFFE72
ecx = 000000000
edx = 7C90E4F4
ebx = 00407EE2
ebp = FFFFFE72
esi = 00407F24
edi = 00408D54

Который делает неизвестно чего [не знаю я :( ].
Потом пакер опять в цикле копирует небольшой кусок кода:
Код:
pop esi
pop edi
mov eax, ecx
sar ecx, 2
rep movsd
add ecx, 3
rep movsb
jmp 00407F24
И копирует опять неподолеку. Останавливаемся на jmp'е и делаем шаг, попадая в только что скопированный код. Непродалеку вызывается функция VirtualAlloc:
Код:
push 4                //PAGE_READWRITE
push 1000            //MEM_COMMIT
push [ebp+40854Ch]    //5000h
push 0
call [ebp+408540h]    // вызов VirtualAlloc
mov edi, eax         // запоминаем адрес выделенного блока
Так пакер просит выделить 5000h байт памяти с правами чтения и записи. И сразу бросается туда копировать (lodsd). Можно сделать вывод, что выделенный блок используется для хранения временных данных при распаковке.

Дальше идет цикл с насильным выходом:
Код:
push edi
or eax, eax        // eax равен нулю?
je 00407FA4        // равен, выходим из цикла
...
pop esi
pop edi
jmp 00407F6D    // продолжаем цикл
// конец цикла
pop eax
lea esi, ss:[ebp+4085e2]
Ставим бряк за циклом и ждем остановки.
после цикла нас ждет еще больший цикл, замешанный из команд:
Код:
....
lodsd,
....
rep movsb
....
sar ecx, eax
....
rep stosd
....
, к-е явно занимаются распаковкой. В конце мы находим - jmp на начало, а в самом начале - все теже

Код:
or eax, eax
je addr2
, направленные на конец цикла. Все это наталкивает нас на мысль о таком же бесконечном цикле с выходом в начале. Поэтому переходим по адресу, указанному в je addr2. Вообщем, весь дальнейший код занимается распаковкой и нам надо только найти точку выхода из пакера.
В скопированном коде есть передача управления на OEP. Логично предположить, что если в начале были:
Код:
pushfd
pushad
...
add esp, 4
, то стало быть, в конце будут popad и popfd.
Ищем в памяти ниже текущей позиции следующий фрагмент:
Код:
popad
popfd
push eax
push 004012С0
retn 4
Команда retn 4 передает управление на OEP, стало быть, 004012С0 - это ее адрес. Кстати, он совпадает с адресом перехода в ловушке. Так что кроме ловушки автор подложил нам хорошую подсказку, которую можно использовать при ручной распаковке.

- Точка входа
Посмотрим...
Код:
jmps  .0004B4008        ; Прыгаем на (1)
push 000022C77
retn
pushfd                ; (1)
pushad
call 0004B4011        ; Вызываем (2)
xor eax, eax
mov eax, esp        ; (2)
add eax, 4
xchg ebx, eax
mov ebx, [ebx][-4]
sub ebx, 0040903F
...
push ebx
push ebx
push ebx
push ebx

Встречается команда xcng для обмена 2-х регистров местами. Поскольку в начале находится jmp, то можно его проигнорировать и установить точку входа сразу на адрес перехода (pushfd). И антивирусы обманываются, и PEiD считает, что это 'PEBundle 02-3.x'. Значит, точка входа может может начинаться начинаться с jmp или pushfd. Команды pushfd и pushad можно поменять местами - для этого придется поменять и pop'ы в точке выхода. Или совсем убрав, заменив на nop'ы или незначащие команды, повторив операцию в точке выхода. Команда 'xor eax, eax' используется для выравнивания и может быть любой. В дальнейших командах можно использовать другие регистры или просто сделать 'add esp, 4'. В итоге получается, привязываться - не к чему.
Сигнатура будет такой:
*, 9C/60, 60/9C, E8 02 00 00 00 ?? ?? * 93 * 87
93 и 87 - это xchg. Но как уже сказано, сигнатура у PECompact надежной быть не может. В конце, на расстоянии 45-55h байт, есть 4 push'а, регистры в которых можно изменить.

- Ручная распаковка
Рассмотрим 2 способа:

*** На основе адреса возврата в ловушке ***
Во время анализа мы определили, что адрес в ловушке совпадает с адресом настоящей точки входа. Это значит, что адрес OEP мы будем знать в самом начале:
Код:
jmps  .00406608
push 000012С0    ; RVA-OEP   
retn       
pushfd
pushad
call 00406611
Можно прогнать код до retn, чтобы упаковщик добавил к нему адрес загрузки:
Код:
nop, nop
push 004012C0    ; VA OEP
retn
Ну а дальше дело техники - нам надо на нем остановиться:
1) Либо ставим аппаратный бряк на исполнение
2) Либо трассируем с остановкой при записи в этот адрес. Когда остановимся по записи, ставим обычный бряк и при условии что пакер больше сюда писать не будет, мы остановимся в OEP.
Этот способ - "неправильный". Мы ориентировались на адрес в ловушке, а на нее никогда не передается управление. Можно изменить этот адрес, и тогда этот способ не будет работать.

*** Прохождение всего кода ***
Здесь будем использовать информацию, полученную во время анализа.
После создания ловушки в точки входа - первый цикл копирования кода:
Код:
mov exc, 39D
rep movsd
pop edi
retn

Проходим его и останавливаемся на команде retn. Эта команда передаст управление на только что скопированный код, который находился ниже.
Далее нас ждет еще 2 небольших цикла:
Код:
mov eax, ecx
sar ecx, 2
rep movsd
add ecx, 3
rep movsb
jmp addr1
Останавливаемся на последней команде и делаем шаг. Как уже известно, дальше пойдет распаковка - это значит, что весь код уже скопирован и раскодирован, и точка выхода из пакера - тоже.
Ищем выход:
Код:
pushfd
pushad
push eax
push OEP
retn 4
Это и будет точкой выхода. Останавливаемся на retn 4, делаем шаг - и мы в настоящей точке входа! Делаем дамп.

- Признаки
Текстовые строки и имена секций:
1) В поле 'pointer to symbol' у PE-заголовка находится сигнатура 'PECO'.
2) первые 2 секции имеют имена 'pec1' и 'pec2'.
Точка входа очень не надежна и позволяет сильные изменения. Самые постоянные признаки, это пожалуй:
1) на значительном расстоянии (до 30h байт) должны быть 2 команды xcng
2) на расстоянии 45-55h байт есть четыре команды push с одинаковым регистром
Структура PE-файла:
1) как минимум 3 секции
2) в начале второй секции находятся ресурсы
3) точка входа - во второй секции, считая с конца
4) последнюю секцию занимает только таблица импорта
5) у первой секции есть аттрибуты Execute и Code, у второй - Initdata, а у последней - Initdata и Shared.
Последний признак ненадежен, так как все эти аттрибуты можно убрать, не потеряв функциональность.
 
Сверху Снизу