visilii's blog

Блог о программировании, инфосеке и чём-то ещё
Гостевая книга | Ссылки

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

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.

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

Теги: инфобез, malware, reverse-engineering

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
Free Website Counter
Free Website Counter

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