Причина смерти - вскрытие, или распаковываем программу вручную

March 25, 2025 — visilii

Разработчики вредоносов, как известно, любят упаковывать свои творения при помощи т.н. пакеров, дабы затруднить обнаружение и статический анализ их творения.

В этой статье я покажу, как распаковать такую программу вручную, а также проблемы, с которыми я столкнулся, пытаясь заставить распакованную программу снова работать.


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

Схема упакованной программы

Если нам неизвестен использованный упаковщик или для него просто не существует программы-анпакера, то остаётся лишь ручная распаковка - запустить запакованную программу под дебаггером, дождаться, пока она себя распакует, и сдампить распакованную программу. Звучит просто, но на деле возникают сложности, в частности, с таблицей импортов - регионом в памяти экзешника, который содержит все динамически импортируемые функции, например, из библиотек WinAPI - упаковщики зачастую стирают или искажают эту таблицу.

К счастью, есть специальные утилиты, которые делают всё за нас - нам остаётся лишь найти изначальную точку входа программы, с которой начинается выполнение после того, как программа распаковалась.

Рассмотрим на конкретном примере. У нас есть программа, которая выводит на экран пустое окно:

Подопытная программа

wog.h - это просто библиотека, которую я разрабатываю для облегчения создания нативных интерфейсов под Windows. На результат наших опытов она не влияет, просто импортирует обычные для Windows GUI библиотеки вроде user32 и gdi32.

Скомпилируем эту программу:

clang .\wog.h .\example.c -luser32 -lgdi32

И упакуем её с помощью популярного пакера UPX:

upx a.exe -o packed.exe
UPX в действии

DIE распознаёт использованный упаковщик:

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

Забудем про тот факт, что утилита UPX также умеет распаковывать запакованные ею программы, и попробуем проделать это вручную.

Откроем нашу программу в x64dbg. Напомню, нам необходимо найти изначальную точку входа программы и дойти до неё, после чего сдампить распаковавшуюся программу на диск.

Есть несколько способов найти оригинальную точку входа (OEP):


1. Условный трейсинг

Самый долгий, но и самый простой способ. Дело в том, что большинство пакеров создают отдельные секции для программы-распаковщика и для распакованной исходной программы - в нашем случае это UPX1 и UPX0, соответственно. То есть если текущая секция сменилась, значит, распаковщик завершил работу и передал управление исходной программе.

Открыв нашу программу в x64dbg, нажмём F9 один раз, чтобы перейти к точке входа распаковщика. Чтобы остановить выполнение, как только произойдёт смена секции, найдём адрес начала текущей секции. В x64dbg откроем калькулятор и введём mem.base(cip):

Калькулятор

Скопируем шестнадцатиричный адрес и, выбрав в меню “Трассировка > Трассировка с обходом…”, введём условие остановки: mem.base(cip) != <Адрес начала секции>. К максимальному числу трассировок советую добавить пару нулей. Нажимаем ОК и ждём. На моём компьютере весь процесс занимает несколько минут, после чего мы оказываемся в точке входа внутри секции UPX0 с распакованным кодом:

OEP


2. Точка останова на памяти

Куда более быстрый способ, требующий, однако, пары лишних телодвижений.

В самом начале пользовательского кода программы видим последовательность push-инструкций, сохраняющих значения регистров на стек. (Вместо них также может присутствовать единственная инструкция pusha/pushad). Программа сохраняет регистры перед началом распаковки, а затем восстанавливает их с помощью последовательности инструкций pop (или popa/popad).

Последовательность push

Мы можем прошагать эти push-инструкции (F8), а затем нажать ПКМ на значении регистра RSP (вершина стека) в правой части экрана -> “Перейти к дампу”. Нажав ПКМ на первый адрес в дампе, выберем “Точка останова” > “Аппаратная, доступ” > DWORD.

RSP
Точка останова

Таким образом, выполнение остановится, как только записанные на стек значения регистров будут вновь прочитаны программой.

Теперь выполняем (F9) и останавливаемся на последовательности pop-инструкций, восстанавливающих регистры, а чуть дальше видим безусловный прыжок к секции UPX0.


Итак, мы оказались в самом начале распакованной программы. Что теперь? В x64dbg есть замечательная встроенная утилита - Scylla. Откроем её:

Scylla

Нажимаем IAT Autosearch, следом Get Imports, и в меню появляются все импорты, которые смогла найти Scylla. Утилита услужливо восстановила таблицу импортов, стёртую UPX. В моём случае, впрочем, появляются также четыре неверных импорта, о происхождении которых у меня есть лишь смутные догадки:

IAT

Эти невалидные импорты можно стереть (Cut thunk), после чего нажать сначала Dump для создания дампа, а затем Fix Dump (выбрать только что созданный дамп).

Распакованный дамп готов! Можем открыть его в любом просмотрщике PE-файлов и увидеть, что все импорты и строки вернулись.

Всё работает

…правда, почему-то бинарь вырос в размерах по сравнению с несжатым оригиналом, а при попытке запустить его он тихо вылетает. Я сумел (как мне кажется) выяснить причину вылетов - указатели на строки, содержащие имена библиотек, а также на исполняемый код ссылаются на невалидную память, т.е. где-то в процессе дампа теряется часть данных (по ссылке выше предполагают, что дело в таблице релокаций, .reloc). Мне пока не удалось понять, как её восстановить, однако для статического анализа полученный файл более чем достаточен - импорты и строки на месте, и Ghidra показывает код, соответствующий оригиналу 1 к 1.

Думаю, я ещё вернусь к попыткам оживить распакованный бинарь, но пока я доволен результатом.

UPD 29.06: Хотя я пока более не экспериментировал с распаковкой UPX-подобных пакеров, я наткнулся на любопытную статью на тему. Оставлю её здесь:

https://hshrzd.wordpress.com/2025/03/22/unpacking-executables-with-tinytracer-pe-sieve/

badge: proud member of the 250KB Club badge: proud member of the darktheme.club
previous no ai webring siteno ai webringrandom no ai webring sitenext no ai webring site
Кнопки
Don't feed the AI! uBlock Origin Now! Edited with Vim I miss XP!
Piracy Now! Best viewed with Open Eyes built listening to Winamp CSS is difficult

Домен предоставлен FreeDNS