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

Реверс инжиниринг упакованного бинаря в Ghidra: распаковка UPX и извлечение паролей

Реверс инжиниринг упакованного бинаря в Ghidra: распаковка UPX и извлечение паролей
Время чтения: 14 мин.

В исследовании Arch Cloud Labs по криптомайнеру Skidmap — после снятия UPX-упаковки в секции данных открытым текстом лежали логины и пароли для PAM-модулей CentOS. Не зашифрованные, не обфусцированные — просто строки. И это не единичный случай. UPX остаётся самым популярным пакером среди малвари на Linux и Windows: стилеры, дропперы, RAT-агенты — заметная часть семплов на столе аналитика завёрнута в UPX-обёртку с кредами от C2-серверов внутри. Задача — снять упаковку и достать строки. Дальше — полный цикл: от определения, что бинарь упакован, до момента, когда Ghidra показывает в декомпиляторе строку с паролем от управляющего сервера.

Зачем атакующие пакуют бинари и прячут в них пароли

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

Вторая задача — скрыть конфигурацию. Многие стилеры и RAT хранят адреса C2-серверов, API-токены и учётные данные прямо в теле бинаря. Пока файл упакован, команда strings не покажет эти значения — всё выглядит как случайный набор байтов. После распаковки строки становятся доступны в секциях .data или .rdata. В терминах MITRE ATT&CK хранение паролей внутри файла — Credentials In Files (T1552.001, тактика Credential Access).

Зачем это аналитику: захардкоженный пароль от SMTP-релея позволяет заблокировать канал эксфильтрации. API-токен Telegram-бота — отключить получение команд. IP-адрес C2 — сформировать IoC (Indicator of Compromise — наблюдаемый артефакт, указывающий на компрометацию) и передать команде реагирования. Один распакованный бинарь может закрыть инцидент за час вместо суток.

Место в цепочке атаки. Упакованный бинарь с кредами внутри чаще всего появляется на этапе initial access (доставка через фишинг или эксплойт) или execution (запуск загрузчиком первого этапа). После запуска бинарь распаковывает себя в память — Deobfuscate/Decode Files or Information (T1140) — извлекает захардкоженные креды и использует их для подключения к C2 или lateral movement. Наша задача — повторить распаковку на рабочем столе аналитика и забрать всё, что атакующий спрятал.

Требования к окружению

Весь анализ проводится в изолированной виртуальной машине. Никогда не открывайте семплы малвари на хостовой ОС.

  • ОС: Windows 10/11 внутри VM (VirtualBox или VMware Workstation Player). Сеть внутри VM отключена.
  • RAM: минимум 8 ГБ для VM. Ghidra на JVM потребляет 2–4 ГБ при автоанализе крупных бинарей; x64dbg, DIE и CFF Explorer работают параллельно.
  • Ghidra 11.x — с ghidra-sre.org. Требует JDK 17+ (скачивается с adoptium.net). Установка: распаковать архив, запустить ghidraRun.bat. Активно поддерживается (последнее обновление: 2024).
  • x64dbg — бесплатный отладчик для Windows (x64dbg.com). Включает плагин Scylla для дампа памяти и восстановления IAT (Import Address Table — таблица адресов импортированных функций; без неё дамп не запустится и Ghidra не увидит имена WinAPI-вызовов). Активно поддерживается (последнее обновление: 2024).
  • DIE (Detect It Easy) — утилита для определения пакера и анализа энтропии. GitHub: horsicq/DIE-engine. Активный проект.
  • CFF Explorer — инспектор PE-заголовков и секций (ntcore.com).
  • UPX — для пробной распаковки через upx -d (upx.github.io). Если автоматическая распаковка работает — отлично; если нет — ручная обязательна. Но пройти ручной процесс стоит в любом случае.

Инструменты для реверс инжиниринга Windows PE: что для чего

Для реверса упакованного бинаря нужны инструменты двух классов: динамический анализ (отладчик для распаковки) и статический анализ (дизассемблер с декомпилятором для разбора распакованного кода).

Инструмент Преимущества Ограничения Когда использовать Когда не использовать
Ghidra 11.x Бесплатный декомпилятор, поддержка x86/x64/ARM/MIPS, расширяемость скриптами JVM-overhead, автоанализ упакованного PE даёт мусор Статический анализ распакованного бинаря, восстановление логики Динамическая распаковка, отладка
x64dbg Лёгкий, быстрый, встроенный дампер Scylla, plugin-экосистема Только Windows, нет декомпилятора Ручная распаковка, поиск OEP, снятие дампа памяти Статический анализ больших бинарей
DIE Мгновенное определение пакера, график энтропии по секциям Только идентификация — код не анализирует Первичная разведка: упакован ли файл и чем Глубокий анализ
IDA Free 9.x Быстрый дизассемблер, удобный Graph View с цветовой навигацией Декомпилятор Hex-Rays только в платной версии Экспресс-проверка гипотез на x86-бинарях Полный анализ без декомпилятора
radare2 6.x Консольный, скриптуемый, полностью open-source Кривая обучения крутая, интерфейс — на любителя Автоматизация рутины, batch-анализ Визуальный разбор для первого знакомства

Для задачи «распаковать UPX и найти креды» оптимальная связка: DIE (разведка) → x64dbg (распаковка) → Ghidra (статический анализ). Каждый инструмент закрывает свой этап; попытка делать всё в одном — компромисс.

Как распознать упакованный PE-файл

Прежде чем распаковывать, нужно подтвердить, что бинарь действительно упакован. PE-файл (Portable Executable — стандартный формат исполняемых файлов Windows: .exe, .dll, .sys) начинается с заголовка DOS HEADER с магическим значением MZ (0x5A4D) — по нему PE-файл отличается от других форматов. За ним следуют NT HEADER, таблица секций и сами секции. Три быстрых признака упаковки:

Признак 1: strings возвращает мусор. Выполните strings sample.exe | findstr /i upx в командной строке Windows (или strings sample.exe | grep -i upx в Linux). Если бинарь упакован UPX, в выводе почти всегда будут маркеры:

UPX0
UPX1
UPX!
This file is packed with the UPX executable packer

Если вместо осмысленных строк программы видите только эти маркеры и набор случайных символов — бинарь упакован. Маркеры UPX отсутствуют, но строк подозрительно мало — возможно, другой пакер.

Признак 2: DIE определяет пакер. Загрузите файл в Detect It Easy. Программа проанализирует сигнатуры заголовков и секций и покажет тип пакера (UPX, ASPack, Themida и т.д.). Для стандартного UPX определение мгновенное. DIE показывает «Nothing found» при подозрительной картине из пункта 1 — заголовки UPX могут быть намеренно повреждены автором малвари.

Признак 3: нестандартные имена секций. Обычный скомпилированный PE содержит секции .text (исполняемый код), .data (инициализированные данные), .rdata (данные только для чтения), .rsrc (ресурсы). UPX заменяет стандартные секции на UPX0 и UPX1. Откройте файл в CFF Explorer, перейдите в Section Headers. Видите UPX0 / UPX1 — бинарь упакован UPX.

Entropy анализ упаковщика

Entropy (энтропия) — численная оценка «хаотичности» данных по шкале от 0 (полностью предсказуемые данные, например нули) до 8 (максимально случайные). Обычный скомпилированный код — энтропия 5–6.5. Сжатые или зашифрованные данные — 7–8. В DIE есть встроенный график энтропии по секциям. Характерная картина UPX: секция UPX0 с энтропией около 0 (она пуста на диске — код распакуется туда в рантайме), секция UPX1 с энтропией выше 7 (сжатые данные).

Зачем это знать: если все секции с энтропией ниже 7 и стандартными именами — бинарь, скорее всего, не упакован, и его можно сразу загружать в Ghidra. Экономия времени на ненужной распаковке.

Распаковка UPX вручную через x64dbg

Стандартный способ — команда upx -d sample.exe. Работает для немодифицированных UPX-бинарей и занимает секунду. Но авторы малвари часто правят заголовки UPX: меняют сигнатуру UPX!, переименовывают секции UPX0/UPX1 в случайные имена. Автоматическая распаковка после таких правок ломается с ошибкой. Тогда единственный путь — ручная распаковка через отладчик.

Даже если upx -d работает, пройти ручной процесс хотя бы раз стоит: он формирует навык, который пригодится при встрече с любым пакером. Принцип везде одинаковый.

Предпосылки: Windows-VM с установленным x64dbg, семпл на рабочем столе. Сеть в VM отключена.

Поиск OEP через трассировку в отладчике

OEP (Original Entry Point) — адрес, с которого начинает работу оригинальная программа после того, как распаковщик отработал. Задача: найти OEP и снять дамп памяти в момент, когда код уже распакован, но ещё не начал выполняться.

Шаг 1. Загрузите семпл в x64dbg: File → Open, выберите файл. Отладчик остановится на системной точке входа (ntdll). Нажмите F9 — x64dbg дойдёт до Entry Point бинаря (адрес из PE-заголовка). Ожидаемый результат: в строке статуса показан адрес EP, в окне листинга виден код распаковщика UPX.

Шаг 2. На x86-бинарях распаковщик UPX начинается с инструкции PUSHAD — она сохраняет все восемь регистров общего назначения в стек одной командой. Обычно идёт первой или второй после EP. На x64 вместо PUSHAD будет серия отдельных PUSH для каждого регистра (инструкция PUSHAD в 64-битном режиме не существует). Ожидаемый результат: вы видите PUSHAD или серию PUSH RAX, PUSH RCX… в начале кода.

Шаг 3. Выполните PUSHAD, нажав F8 (шаг через одну инструкцию). Теперь ESP (указатель стека) указывает на сохранённые регистры. Запомните значение ESP из панели регистров. Установите аппаратный брейкпоинт на чтение по этому адресу: в панели регистров правый клик на ESP → Follow in Dump → в окне дампа правый клик на первом байте → Breakpoint → Hardware, Access, DWORD. Логика: распаковщик закончит работу и выполнит POPAD (восстановит регистры), что обратится к этому адресу в стеке. Отладчик остановится.

Шаг 4. Нажмите F9 (Run). Отладчик сработает, когда распаковщик обратится к сохранённым регистрам. Ожидаемый результат: вы видите POPAD или серию POP, а через несколько инструкций — JMP на «далёкий» адрес. Этот JMP — переход на OEP.

Шаг 5. Выполняйте инструкции по F8 до JMP. Адрес назначения этого перехода — OEP. Выполните JMP (F8). Ожидаемый результат: вы оказались в коде, который выглядит как начало обычной функции — PUSH EBP / MOV EBP, ESP (x86) или SUB RSP, 0x28 (x64). Запишите адрес OEP — он понадобится при работе со Scylla.

Если вместо «нормального» кода видите мусор — скорее всего, промахнулись с брейкпоинтом. Перезагрузите семпл (File → Restart) и повторите с шага 2. Это нормально, с первого раза не у всех получается.

Memory dump распакованного процесса

Шаг 6. Код в памяти полностью распакован. Откройте плагин Scylla: Plugins → Scylla. В поле OEP впишите адрес из шага 5. Нажмите IAT Autosearch — Scylla найдёт таблицу импортов. Нажмите Get Imports — в списке появятся DLL и функции. Нажмите Dump — файл сохранится как sample_dump.exe. Затем нажмите Fix Dump и укажите сохранённый дамп — Scylla создаст sample_dump_SCY.exe с восстановленной IAT. Ожидаемый результат: файл sample_dump_SCY.exe на диске, размер в 2–5 раз больше исходного упакованного файла.

Scylla не находит IAT автоматически — попробуйте вручную указать адрес и размер IAT через CFF Explorer. Для стандартного UPX автоматика срабатывает почти всегда.

Статический анализ в Ghidra: от дампа до захардкоженных паролей

Распакованный дамп готов. Переходим к Ghidra — здесь мы разбираем файл без его запуска, чисто по коду.

Предпосылки: Ghidra установлена, JDK 17+ настроен, файл sample_dump_SCY.exe лежит в папке проекта.

Шаг 7. Запустите Ghidra, создайте проект: File → New Project → Non-Shared Project. Перетащите дамп в окно проекта. Ghidra определит формат (PE, x86 или x64) и предложит параметры импорта — настройки по умолчанию подходят. Двойной клик по файлу откроет Code Browser. На вопрос «Analyze?» ответьте Yes. Для 32-битных PE включите опцию WindowsPE x86 Propagate External Parameters — она подставляет имена параметров WinAPI-функций в декомпилированный код, что сильно упрощает чтение. Ожидаемый результат: после завершения автоанализа (прогресс-бар внизу справа) в Symbol Tree слева появятся Imports, Exports и обнаруженные функции.

Почему нельзя загружать упакованный бинарь в Ghidra напрямую? Ghidra разбирает содержимое файла как есть, без выполнения кода. Упакованный бинарь содержит сжатые данные вместо реального кода. Автоанализ создаст ложные функции, мусорные xref (перекрёстные ссылки) и бессмысленный псевдокод в декомпиляторе. Я видел, как новички тратят часы на анализ «функций», которых в программе нет. Сначала распаковка — потом Ghidra.

Восстановление строк после распаковки

Шаг 8. Откройте окно Defined Strings: Window → Defined Strings. Ghidra покажет все строки, обнаруженные при автоанализе. Если до распаковки strings возвращал мусор, теперь список заполнен читаемым текстом: URL-адреса, сообщения об ошибках, имена файлов, форматные строки (%s, %d), имена реестровых ключей.

Для целенаправленного поиска — Search → Memory: введите паттерн (http://, password, smtp, .onion, admin). Ghidra покажет все адреса, где он встречается. Двойной клик переносит в листинг; связка листинга и декомпилятора (правая панель) показывает одновременно ассемблер и C-подобный псевдокод — подсветка строки в одном окне подсвечивает соответствующий фрагмент в другом. Это одна из самых удобных фич Ghidra.

Строки, собираемые на стеке побайтово (серия MOV BYTE [ESP+N], 0xXX), не попадут в Defined Strings. Их можно обнаружить при просмотре декомпилированного кода функций: Ghidra покажет серию присваиваний элементам массива. Переименуйте переменную (правый клик → Rename Variable) и задайте тип char[N] — строка станет читаемой.

Поиск захардкоженных учётных данных в секции .data

Шаг 9. Учётные данные в распакованном бинаре хранятся несколькими способами:

Открытый текст в .data / .rdata. Самый частый случай у массовой малвари. Строки вида admin:P@ssw0rd, smtp_password=..., URL с кредами в query-параметрах — видны прямо в Defined Strings. Нажмите на строку, затем правый клик → References → Show References to Address. Ghidra покажет все места, где эта строка используется. Перейдите в функцию-потребитель — декомпилятор покажет контекст: строка передаётся в connect(), send() или другую сетевую функцию.

Пример того, как это выглядит в декомпиляторе Ghidra:

void FUN_004012a0(void) {
    char *host = "185.141.XX.XX";
    int port = 4443;
    char *user = "bot_agent";
    char *pass = "kR9#mP2x!vL4";
    socket_connect(host, port);
    auth_send(user, pass);
}

В реальности имена функций будут обезличены (FUN_004012a0), переменные — безымянны (local_28, iVar1). Ваша задача — переименовать переменные и функции по смыслу: если функция получает IP и порт, а затем вызывает connect из ws2_32.dll — это функция установки соединения. Переименуйте, и код станет читаемым.

Строки, формируемые в рантайме. XOR-расшифровка или побайтовая сборка. В декомпиляторе выглядит как цикл с операцией XOR над массивом и константным ключом. Найдите ключ, расшифруйте вручную или напишите Ghidra-скрипт через API find() и findBytes().

Шаг 10. Зафиксируйте все артефакты: IP-адреса, домены, порты, логины, пароли, API-токены, пути к файлам. Это IoC для команды IR (Incident Response — реагирование на инциденты): IP уходит в blocklist, пароль от Telegram-бота позволяет перехватить управление, SMTP-креды — заблокировать канал эксфильтрации.

Минилаб для отработки: тренировочный семпл за десять минут

Для практики не нужен реальный образец малвари. Соберите тестовый бинарь самостоятельно.

Предпосылки: Windows-VM, установленный MinGW (GCC для Windows) и UPX.

Напишите простую C-программу с «учётными данными» в виде строковых литералов — логин, пароль, IP-адрес, порт. Скомпилируйте через gcc test.c -o test.exe. Убедитесь, что strings test.exe показывает ваши «креды» в открытом виде. Упакуйте: upx test.exe -o test_packed.exe. Проверьте: strings test_packed.exe теперь не показывает строки программы — только маркеры UPX. Загрузите test_packed.exe в DIE, убедитесь, что пакер определяется. Дальше — шаги 1–10 из этой статьи. Ожидаемый финальный результат: в Ghidra в Defined Strings появляются ваши тестовые «креды», а декомпилятор показывает функцию, где они используются.

Первый раз цикл займёт 30–40 минут. После нескольких повторений — 10–15. Это нормальная скорость.

Ограничения техники и когда она не работает

Ручная распаковка через PUSHAD/POPAD работает для стандартного UPX на x86. Ситуации, где метод деградирует или неприменим:

Модифицированный UPX с anti-debug. Авторы малвари добавляют проверки на присутствие отладчика — Debugger Evasion (T1622): вызовы IsDebuggerPresent, проверки PEB-флага BeingDebugged, timing-проверки через rdtsc. В x64dbg это обходится через плагин ScyllaHide (прячет отладчик от процесса) или ручной патч условных переходов. Но распознавание anti-debug — отдельная тема, и к ней стоит переходить после уверенного владения базой.

x64-бинари. Инструкция PUSHAD существует только в 32-битном режиме. На x64 UPX использует серию отдельных PUSH. Принцип тот же (брейкпоинт на RSP), но автоматизм, наработанный на x86, потребует адаптации.

Кастомные пакеры. Бинарь упакован Themida, VMProtect или самописным пакером — трюк с PUSHAD не сработает. Универсального рецепта нет: каждый пакер требует индивидуального анализа. Как пишут на Reverse Engineering StackExchange — «debug it until you understand it, no matter how much time it takes».

Stripped payloads (T1027.008). Из бинаря удалены отладочные символы и метаданные — Ghidra не сможет автоматически восстановить имена функций. Все функции будут FUN_XXXXXXXX, переменные — local_N. Анализ возможен, но занимает кратно больше времени и требует опыта в чтении ассемблера.

Зашифрованные креды. Учётные данные расшифровываются в рантайме (AES, RC4 с нетривиальным ключом) — одного статического анализа в Ghidra недостаточно. Потребуется комбинация: отладка в x64dbg до момента расшифровки, затем дамп расшифрованных строк из памяти процесса.

Большинство UPX-семплов в массовых кампаниях (стилеры, криптомайнеры, простые RAT) распаковываются штатно. Сложности начинаются с целевых атак, где пакер — первый уровень из нескольких. Но без базового навыка ручной распаковки разобраться с многослойной защитой невозможно.

Девять из десяти материалов по распаковке UPX заканчиваются на фразе «теперь у вас есть распакованный файл». На практике это середина работы. Самая ценная часть — умение быстро привязать найденные артефакты к действиям атакующего: пароль от SMTP означает эксфильтрацию через почту, IP с портом 4443 — C2, Telegram-токен — бот получает команды через мессенджер. Без этой привязки реверс остаётся гимнастикой для ума без практического выхода.

И ещё одно наблюдение. Автоматическая распаковка через upx -d создаёт ощущение владения навыком, которого нет. Инструмент работает — результат есть — задача закрыта. Но рано или поздно прилетает семпл с запатченным заголовком, где upx -d молча ломается. Аналитик, который ни разу не проходил ручной цикл через отладчик, в этот момент оказывается в тупике. Ручная распаковка — страховка для нештатных ситуаций, а не упражнение для красоты. На CTF таких ситуаций каждая третья, в реальном IR — реже, но цена ошибки выше. Если только начинаешь и хочешь пройти путь от базы до первых задач системно — IB Basics закрывает именно эту задачу, без пропусков и академического тона.

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