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

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

wog.h
- это просто библиотека, которую я разрабатываю для облегчения создания нативных интерфейсов под Windows. На результат наших опытов она не влияет, просто импортирует обычные для Windows GUI библиотеки вродеuser32
иgdi32
.
Скомпилируем эту программу:
clang .\wog.h .\example.c -luser32 -lgdi32
И упакуем её с помощью популярного пакера UPX:
upx a.exe -o packed.exe

DIE распознаёт использованный упаковщик:
Если попытаться в таком виде открыть программу в каком-нибудь
декомпиляторе, мы увидим странный код, не имеющий ничего общего с
изначальной программой, но что самое главное - теперь мы не можем
просмотреть строки программы при помощи strings
, а в
импортах функций будет куда меньше, чем в оригинальном экзешнике.
Забудем про тот факт, что утилита UPX также умеет распаковывать запакованные ею программы, и попробуем проделать это вручную.
Откроем нашу программу в x64dbg. Напомню, нам необходимо найти изначальную точку входа программы и дойти до неё, после чего сдампить распаковавшуюся программу на диск.
Есть несколько способов найти оригинальную точку входа (OEP):
1. Условный трейсинг
Самый долгий, но и самый простой способ. Дело в том, что большинство пакеров создают отдельные секции для программы-распаковщика и для распакованной исходной программы - в нашем случае это UPX1 и UPX0, соответственно. То есть если текущая секция сменилась, значит, распаковщик завершил работу и передал управление исходной программе.
Открыв нашу программу в x64dbg, нажмём F9 один раз, чтобы перейти к
точке входа распаковщика. Чтобы остановить выполнение, как только
произойдёт смена секции, найдём адрес начала текущей секции. В x64dbg
откроем калькулятор и введём mem.base(cip)
:

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

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

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


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

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

Эти невалидные импорты можно стереть (Cut thunk), после чего нажать сначала Dump для создания дампа, а затем Fix Dump (выбрать только что созданный дамп).
Распакованный дамп готов! Можем открыть его в любом просмотрщике PE-файлов и увидеть, что все импорты и строки вернулись.

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