Перейти к содержимому

Распаковка UPX вручную: статика, динамика и где я теряю время зря

Распаковка UPX вручную: статика, динамика и где я теряю время зря
Время чтения: 11 мин.

На учебном сэмпле из песочницы — 98 КБ, три импорта в таблице, энтропия секции UPX1 под 7.8 — я прошёл полный цикл ручной распаковки двумя способами. Статический путь без запуска файла: 40 секунд. Динамический через отладчик x64dbg: больше часа с учётом снятия дампа и починки импортов. Разница в 90 раз. Но это не значит, что динамика бесполезна — есть конкретные ситуации, когда без неё никак. Дальше разбираю оба подхода по шагам с инструментами и замерами, чтобы стало понятно, когда какой путь выбирать и на чём конкретно горит время.

Как распознать UPX-упаковку: статический анализ упакованных файлов

Прежде чем что-то распаковывать, убедитесь, что файл действительно упакован. Упаковка (packing) — сжатие или обфускация исполняемого файла, при котором оригинальный код прячется внутри обёртки-загрузчика. При запуске загрузчик (unpacking stub — небольшой фрагмент кода, добавленный упаковщиком) распаковывает оригинальные данные в память и передаёт им управление. В классификации MITRE ATT&CK — это открытая база тактик и техник атакующих, где каждой технике присвоен идентификатор вида T1027 — упаковка описана как Software Packing (T1027.002) в тактике Defense Evasion. Зачем это злоумышленникам: упакованный бинарник не ловится сигнатурным анализом антивируса, а статический разбор кода превращается в ковыряние сжатой каши.

UPX (Ultimate Packer for eXecutables) — самый распространённый упаковщик с открытым исходным кодом. По данным JPCERT/CC, UPX — основной упаковщик для Linux-малвари (включая ботнет Mirai и его варианты). На Windows его тоже любят как стартовый инструмент обфускации — порог входа нулевой.

Вот признаки, по которым определяется анализ UPX packer в сэмпле:

Нестандартные имена секций. Обычный PE-файл (Portable Executable — стандартный формат исполняемых файлов Windows) содержит секции .text, .data, .rsrc. У UPX-упакованного файла секции называются UPX0, UPX1, UPX2. Видно в любом PE-просмотрщике: PEStudio, CFF Explorer, PE-bear.

Минимальное количество импортов. Нормальный исполняемый файл импортирует десятки функций из системных библиотек (kernel32.dll, user32.dll). У упакованного файла в IAT (Import Address Table — таблица адресов импортированных функций, по которой программа «знает», где в памяти лежат нужные ей системные функции) обычно только 2-3 записи: LoadLibraryA и GetProcAddress. Этого хватает загрузчику, чтобы после распаковки подгрузить остальные функции динамически.

Высокая энтропия. Энтропия — мера «случайности» данных, шкала от 0 до 8. Обычный машинный код даёт 5-6. Сжатые данные — выше 7. Если секция UPX1 показывает 7.5-7.99, перед вами почти наверняка сжатый код.

Разрыв между Raw Size и Virtual Size. Секция UPX0 на диске часто имеет Raw Size = 0 (пуста), но Virtual Size в несколько сотен килобайт. Это «пещера» — область памяти, куда загрузчик распакует оригинальный код при запуске.

Для проверки я использую Detect It Easy (DIE) — бесплатная утилита, определяющая упаковщик по сигнатурам. UPX тоже умеет проверять свои файлы:

# Определение упаковщика через Detect It Easy
diec suspect.exe

# Проверка через сам UPX (без распаковки, только тест)
upx -t suspect.exe

Если DIE показывает «UPX» или upx -t отвечает «[OK]» — переходите к распаковке. Ответ «not packed by UPX» означает: файл либо не упакован UPX, либо заголовки намеренно модифицированы.

Для более глубокой проверки можно использовать Python-библиотеку pefile (нужен Python 3.8+ и pip install pefile). Скрипт ниже показывает энтропию каждой секции и количество импортов — два ключевых индикатора упаковки:

import pefile
pe = pefile.PE("suspect.exe")
for s in pe.sections:
    name = s.Name.decode().rstrip('\x00')
    print(f"  {name:8s}  Entropy: {s.get_entropy():.2f}  "
          f"Raw: {s.SizeOfRawData}  Virtual: {s.Misc_VirtualSize}")
if hasattr(pe, 'DIRECTORY_ENTRY_IMPORT'):
    total = sum(len(e.imports) for e in pe.DIRECTORY_ENTRY_IMPORT)
    print(f"Imports: {total}")
    if total < 10:
        print("[!] Very few imports - likely packed")

Ожидаемый вывод для UPX-сэмпла: секция UPX0 с Raw Size = 0, секция UPX1 с энтропией выше 7, импортов — единицы. Если видите такую картину, сомнений нет.

Статическая распаковка упакованного сэмпла

Статическая распаковка означает, что файл НЕ запускается — вся работа происходит с файлом на диске. UPX хранит в упакованном файле собственные служебные заголовки: l_info (контрольная сумма, magic-байты «UPX!»), p_info (размеры оригинального файла) и b_info (параметры каждого сжатого блока). Благодаря этим данным сам UPX может развернуть файл обратно одной командой.

Порядок действий:

  1. Запускаете upx -d suspect.exe -o unpacked.exe. Ключ -d — decompress, -o задаёт имя выходного файла, чтобы не перезаписать оригинал. UPX прочитает свои заголовки, определит алгоритм сжатия и восстановит оригинальный PE-файл.
  2. Проверяете результат: diec unpacked.exe не должен показывать упаковщик. Команда file unpacked.exe подтвердит корректный PE-формат.
  3. Открываете распакованный файл в Ghidra или IDA Free (бесплатные дизассемблеры для статического анализа кода). Если видны осмысленные имена функций и строки — распаковка прошла.

Весь процесс от команды до проверки — меньше минуты. Отсюда мой замер: 40 секунд.

Но если автор малвари модифицировал UPX-заголовки после упаковки, upx -d выдаст ошибку «CantUnpackException» или «not packed by UPX». Тогда два варианта: починить заголовки (подробнее в последнем разделе) или перейти к динамическому подходу.

Динамический анализ malware: OEP поиск при распаковке и дамп процесса из памяти

Динамический анализ — запуск файла в изолированной среде и наблюдение через отладчик. При распаковке UPX вручную цель конкретна: дождаться момента, когда загрузчик закончит распаковку, и снять дамп — копию содержимого памяти процесса. Ключевой момент здесь — OEP (Original Entry Point, оригинальная точка входа). Это первая инструкция настоящей программы после того, как загрузчик отработал.

Что нужно перед началом: изолированная виртуальная машина без сети (Windows), установленный x64dbg, плагин Scylla (обычно встроен в x64dbg).

ESP-трик и tail jump: два способа найти OEP

Шаг 1. Загрузка. Открываете файл в x64dbg. Отладчик остановится на системном брейкпоинте. Нажимаете F9 (Run) — выполнение дойдёт до Entry Point упакованного файла, начала кода загрузчика.

Шаг 2. Распознаёте паттерн. UPX-загрузчик начинается с инструкции pushad (сохраняет все регистры в стек) и заканчивается popad (восстанавливает их) перед прыжком на OEP. Финальный прыжок — tail jump — обычно jmp на адрес далеко за пределами текущей секции, в область UPX0, куда уже распакован оригинальный код.

Шаг 3. ESP-трик (быстрый способ). Сразу после pushad значение ESP (указатель стека — регистр процессора, который показывает текущую вершину стека) изменится, потому что на стек положены все регистры. Ставите Hardware Breakpoint on Access (аппаратная точка останова на обращение к памяти) на адрес, куда указывает ESP. Логика: когда загрузчик выполнит popad, он обратится к этому адресу, и отладчик остановится. Следующая инструкция после остановки — тот самый tail jump на OEP.

Альтернативный способ — ручной поиск. Пролистываете код загрузчика вниз (он компактный, обычно несколько сотен инструкций) и ищете пару popad → jmp <далёкий_адрес>. Адрес jmp и есть OEP.

Шаг 4. Переход. Нажимаете F9, отладчик останавливается около popad. Один Step Over (F8) через jmp — вы на OEP. Записываете адрес.

Шаг 5. Дамп через Scylla. В x64dbg открываете Scylla (Plugins → Scylla). В поле OEP вводите найденный адрес. Нажимаете «Dump» — Scylla сохраняет содержимое памяти в PE-файл.

Восстановление таблицы импортов — главная потеря времени

Дамп из памяти — ещё не готовый файл. IAT в нём указывает на адреса функций в памяти текущего сеанса, а не на записи в PE-формате. Без восстановления импортов файл не запустится и не откроется нормально в дизассемблере.

В Scylla нажимаете «IAT Autosearch» → «Get Imports» → проверяете список (не должно быть записей «unresolved») → «Fix Dump». Scylla создаст файл с суффиксом _SCY — финальный распакованный бинарник.

Именно здесь горит время. Шаги 1-4 занимают 5-10 минут. А восстановление импортов (import table reconstruction) иногда затягивается на 30-40 минут: IAT Autosearch не всегда корректно определяет границы таблицы, появляются unresolved-записи, приходится перебирать адреса вручную. На одном сэмпле я потратил 40 минут именно на этом шаге — загрузчик перезаписал часть оригинальной IAT нулями.

Сравнение статического и динамического анализа при реверс-инжиниринге UPX

Конкретные замеры по трём учебным UPX-сэмплам:

Этап Статика (upx -d) Динамика (x64dbg + Scylla)
Определение упаковщика 10 сек (DIE / upx -t) 10 сек (те же инструменты)
Распаковка кода 5 сек (одна команда) 5-10 мин (поиск OEP, трассировка)
Восстановление импортов Не нужно (UPX восстанавливает сам) 10-40 мин (Scylla + ручная правка)
Верификация результата 30 сек (diec + Ghidra) 30 сек (diec + Ghidra)
Итого ~1 мин 15-50 мин

Зачем тогда динамический подход? Три случая, когда без него не обойтись:

Модифицированные заголовки. Автор малвари изменил magic-байты или имена секций — upx -d не сработает. Можно починить заголовки вручную, но иногда проще пройти через отладчик. Особенно если модификация затрагивает несколько структур одновременно.

Кастомизированный UPX. Некоторые авторы пересобирают UPX с изменёнными алгоритмами сжатия или stub-кодом. Статическая распаковка стандартным upx -d здесь невозможна в принципе — нужен другой бинарник UPX или динамика.

Универсальность навыка. Техника поиска OEP и снятия дампа работает с любым упаковщиком — Themida, ASPack, PECompact. UPX — самый простой для отработки, но подход один и тот же. Просто сложность возрастает на порядок. Обучение реверс-инжинирингу malware на UPX — способ набить руку перед серьёзными протекторами.

Снятие упаковщика вручную: anti-UPX трюки и восстановление PE-заголовков

Авторы малвари давно знают, что аналитики первым делом пробуют upx -d. Поэтому модифицируют упакованный файл после упаковки, чтобы сломать статическую распаковку — это техника Anti-UPX Unpacking. По данным JPCERT/CC, она широко используется в ботнете Mirai и его вариантах, в майнерах, в малвари SBIDIOT, а также в инструментах APT-группировки Lazarus (например, ELF-VSingle заменяет magic на «MEMS»).

Три типовые модификации, которые ломают upx -d:

Замена magic-байтов. UPX хранит сигнатуру «UPX!» (hex: 55 50 58 21) в заголовке l_info. Авторы заменяют её на произвольное значение или нули. Этого достаточно, чтобы upx -d отказался работать.

Переименование секций. Стандартные имена UPX0/UPX1 заменяются на .text/.data или случайные строки. UPX проверяет имена при распаковке — без них отказывает.

Обнуление размеров. В заголовке p_info хранятся p_filesize и p_blocksize оригинального файла. Если обнулить их, UPX не выделит буфер для распаковки.

Починка обычно тривиальна. Самый прямой способ: открыть файл в hex-редакторе (HxD, 010 Editor), найти место, где должна быть сигнатура «UPX!», и восстановить её вручную. В большинстве случаев magic-байты — единственное, что модифицировано. После исправления upx -d отрабатывает штатно.

JPCERT/CC выпустил модифицированную версию UPX — upx-mod (доступна на GitHub: JPCERTCC/upx-mod), которая игнорирует проверку magic-байтов. Одна команда: upx-mod -d suspect.exe -o unpacked.exe — и модифицированный сэмпл распаковывается без ручной правки hex. Если вы занимаетесь анализом малвари регулярно, этот инструмент экономит минуты на каждом сэмпле с anti-UPX модификацией.

Для автоматического обнаружения anti-UPX трюков JPCERT/CC предлагает YARA-правила (YARA — язык описания паттернов для поиска в файлах по байтовым сигнатурам). Правила детектируют ELF-бинарники, в которых magic-байты UPX заменены на нестандартные значения, и опубликованы вместе с upx-mod. Аналогичную логику можно построить для PE-формата: искать характерные UPX-структуры (секции с Raw Size = 0, высокая энтропия) при отсутствии стандартной сигнатуры «UPX!».

Если модификация глубже — затронуты одновременно magic, секции и размеры — восстановление заголовков вручную становится трудоёмким. В таких случаях быстрее перейти к динамической распаковке через x64dbg: загрузчику самому не нужны корректные заголовки UPX, он работает с уже сжатыми данными напрямую.

Главное, что я вынес из практики: порядок действий при распаковке должен быть жёстко выстроен по нарастающей сложности. Сначала upx -d. Не сработало — upx-mod -d. Опять ошибка — hex-правка заголовков и повторная попытка. И только если всё это не помогло — запускаете x64dbg и идёте через OEP. Этот порядок экономит часы в неделю.

Одна вещь, которую я понял не сразу. Я потратил десятки часов на ручной реверс-инжиниринг UPX через отладчик, прежде чем осознал масштаб потери времени. Начинающий реверсер смотрит красивые видео с трассировкой в x64dbg, с пошаговым прохождением через pushad/popad, со снятием дампа — и думает, что это и есть «настоящий реверс». А потом приходит на первую работу в команду малварь-анализа, получает 30 сэмплов за смену и обнаруживает, что 25 из них распаковываются за секунду через upx -d. Динамическая распаковка — навык, необходимый для понимания механики и для нестандартных случаев. Но если он превращается в единственный инструмент — проблема в привычке. Я видел аналитиков, которые автоматически запускали x64dbg для каждого UPX-сэмпла, не попробовав сначала upx -d. Это как открывать Ghidra для любого .exe, не запустив strings. Автоматика, полуавтоматика, ручная работа — именно в таком порядке. Через полгода практики в голове формируется чёткая карта: увидел UPX — upx -d; не сработало — upx-mod; опять ошибка — hex-правка; совсем глухо — отладчик. Каждый следующий шаг включается только когда предыдущий не дал результата. Если вы только выстраиваете эту базу и хотите системно разобраться в инструментах до того, как перейти к разбору сэмплов — на IB Basics берут с любого старта, без требования «вы должны знать ассемблер на уровне X».

Эту тему и смежные навыки разбирают на практике в курсе «Профессия Реверс-инженер» Codeby Academy.