Статья Реверс-инжиниринг для CTF: анализ упаковщиков, поиск OEP и написание KeyGen

Ночная лаборатория CTF: монитор с отладчиком x64dbg, подсвеченный адрес OEP и граф энтропии UPX. Янтарная лампа, бирюзовая LED-подсветка стола, размытые огни серверной стойки.


Семь RE-задач PicoCTF 2024 - UPX-packed бинарь на 100 очков, три уровня антиотладки через IsDebuggerPresent с эскалацией до 400 очков и Classic Crackme с кастомным шифровальным алгоритмом - покрывают весь спектр навыков категории reverse. Русскоязычные writeup'ы по реверс-инжинирингу CTF обычно заканчиваются на этапе "нашёл строку в OllyDbg, вот флаг": в разборе на OTUS весь анализ - один брейкпоинт на функцию сравнения, а writeup crackme "Крестики" на Habr покрывает IDA-анализ без упаковки. Ручная распаковка исполняемых файлов с поиском OEP, обход anti-debug через патчинг регистров и написание keygen по реверснутой математике - то, что отделяет задачи начального уровня от задач на 300+ очков. Ниже - разбор каждого звена этой цепочки с конкретными командами в x64dbg и листингами из Ghidra. Без воды, с hex-адресами и реальными приёмами.

Цепочка анализа RE-задачи: от получения бинаря до флага​

Каждое задание в категории reverse на CTF проходит через одну и ту же последовательность. Перескакивание этапов - главная причина, почему люди тратят два часа на задачу, которая решается за двадцать минут.
  1. Triage - определение типа файла (file, Detect-It-Easy), архитектуры, наличия упаковщика, энтропия секций
  2. Распаковка (если упаковщик обнаружен) - автоматическая или ручная, с поиском OEP и дампом памяти
  3. Статический анализ - дизассемблирование и декомпиляция в IDA Free или Ghidra, поиск строк и перекрёстных ссылок
  4. Динамический анализ - отладка в x64dbg или GDB, трассировка ключевых функций, анализ поведения при разных вводах
  5. Реверс алгоритма - восстановление логики валидации: XOR-каскады, конечные автоматы, кастомные шифры
  6. Решение - извлечение флага, написание keygen или патч бинаря
В терминах MITRE ATT&CK распаковка - обращение техники Software Packing (T1027.002, тактика Defense Evasion). Обход антиотладки - работа с техникой Debugger Evasion (T1622, тактики Defense Evasion и Discovery). Деобфускация кода - Deobfuscate/Decode Files or Information (T1140). Задачи CTF моделируют техники реальных вредоносных программ в контролируемой среде, и навыки их решения напрямую переносятся в malware analysis и incident response. Это не теория - тот же ESP-трик, который спасает на CTF, работает один в один при анализе APT-сэмплов.

Инструменты для статического и динамического анализа​

1784565916839.webp

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

  • ОС: Windows 10/11 x64 для PE-анализа (x64dbg, IDA) + GNU/Linux VM для ELF-бинарей и radare2
  • RAM: минимум 8 ГБ (Ghidra съедает 2–4 ГБ на средний бинарь); 16 ГБ при одновременной работе Ghidra + x64dbg + VM
  • Дисковое пространство: IDA Free ~500 МБ, Ghidra ~1 ГБ с учётом JDK, x64dbg ~50 МБ
  • Зависимости: Java 17+ для Ghidra; .NET Framework 4.x для некоторых плагинов x64dbg
  • Сеть: IDA Freeware 8.x использует cloud decompiler - нужен интернет; Ghidra работает полностью offline

Trade-off таблица: выбор инструмента​

ИнструментПреимуществаОграниченияКогда использоватьКогда НЕ использовать
Detect-It-Easy (DIE)Быстрая идентификация пакера, энтропия по секциям, анализ PE-заголовковНе дизассемблирует, не декомпилируетПервый шаг любого анализа: triage бинаряНикогда не пропускать
IDA Free 8.xЛучший graph view для control flow, cloud decompilerCloud decompiler иногда лежит; бесплатная версия без встроенного отладчикаСтатический анализ x86/x64 PE и ELFКогда нужна офлайн-декомпиляция
Ghidra 11.xПолноценный декомпилятор, офлайн-работа, скрипты Java/PythonИнтерфейс - к нему надо привыкнуть; хуже обрабатывает C++ vtable и шаблоныОсновной инструмент для декомпиляции, особенно без интернетаНе заменяет отладчик для PE
x64dbg + ScyllaHideОтличный отладчик, плагины для обхода anti-debug, встроенный Scylla для дампаТолько Windows; требует навыка работы с breakpointsДинамический анализ PE, ручная распаковка, обход антиотладкиELF-бинари (нужен GDB)
radare2 6.xCLI-интерфейс, скриптуемость, кроссплатформенность, встроенный отладчикПорог входа - сотни команд, которые надо помнить или гуглитьБыстрый triage в терминале, скриптованный анализЕсли нужен GUI без опыта с CLI

Статус поддержки: DIE - активная разработка, последний релиз 2024; IDA Free 8.4 - актуальная версия; Ghidra 11.x - активный репозиторий NSA на GitHub, обновления 2024; x64dbg - ежемесячные обновления; radare2 - текущая ветка master 6.1.9.

Идентификация упаковщика и анализ энтропии PE-секций​

Первый шаг с любым бинарём - кинуть его в Detect-It-Easy. DIE опознаёт распространённые упаковщики (UPX, ASPack, PECompact, NSPack) по сигнатурам в PE-заголовке и точке входа. Если DIE показывает чистый результат - расслабляться рано: кастомные пакеры и модифицированный UPX с затёртыми сигнатурами пройдут мимо детектора.

Второй индикатор - энтропия секций. DIE показывает энтропию каждой секции PE-файла отдельно. Нормальная энтропия секции .text - 5.5–6.5 бит на байт. Значение выше 7.0 с высокой вероятностью указывает на сжатие или шифрование. UPX-packed бинарь имеет характерный профиль: секция UPX0 с почти нулевым raw-размером (область для распаковки в память) и секция UPX1 с энтропией 7.5+, где лежит сжатый код.

На что смотреть в упакованном PE-файле:
  • Несоответствие Virtual Size и Raw Size секций - виртуальный размер .text в 5-10 раз больше raw. Классика UPX: оригинальный код разворачивается в виртуальное адресное пространство при запуске
  • Минимальная таблица импортов - упакованный бинарь импортирует только LoadLibraryA и GetProcAddress из kernel32.dll, остальные API восстанавливаются в рантайме через Native API (T1106, MITRE ATT&CK)
  • Точка входа в нестандартной секции - EP не в .text, а в последней секции файла или секции с подозрительным именем
  • Пустые строки - strings или rabin2 -z не выдают ничего осмысленного
В разборе crackme "Крестики" с HackerLab (по данным writeup'а на Habr) DIE показал отсутствие пакера, а высокая энтропия секции .rsrc объяснялась PNG-ресурсом внутри файла - это нормально. Отличить высокую энтропию ресурсов от высокой энтропии кода - навык, который приходит с анализом десятков бинарей. Ресурсы сжаты "по природе" (изображения, иконки), а упакованный код сжат умышленно. Разница видна по контексту: если энтропия 7.8 в .text - почти наверняка пакер, если в .rsrc - скорее всего картинка.

Распаковка исполняемых файлов и поиск OEP​

1784565942309.webp

Автоматическая распаковка UPX и её пределы​

Для стандартного UPX хватает одной команды: upx -d packed.exe -o unpacked.exe. В задаче "Packer" на PicoCTF 2024 (100 очков) решение выглядело именно так: strings out | tail показывал UPX-маркеры в конце файла, после upx -d out -o original и rabin2 -z original | grep -i flag флаг извлекался из строковых данных распакованного бинаря. Десять секунд работы - 100 очков в карман.

Автоматическая распаковка UPX ломается в нескольких случаях:
  • Модифицированы UPX-маркеры - секции переименованы с UPX0/UPX1 на произвольные имена, upx -d выдаёт ошибку "not packed by UPX"
  • Использован форк UPX с изменённым алгоритмом сжатия - структура валидна, но декомпрессия даёт мусор
  • После UPX применён дополнительный протектор - overlay data повреждён, PE checksum невалиден
  • Самописный упаковщик мимикрирует под UPX - имена секций совпадают, но логика распаковки другая
Тут уже без ручной распаковки через отладчик не обойтись.

Ручная распаковка: ESP-трик и поиск точки входа OEP​

ESP-трик - основной метод поиска OEP (Original Entry Point). Суть в том, что большинство упаковщиков сохраняют контекст регистров при входе в распаковочный стаб и восстанавливают его перед передачей управления на OEP. Мы этим и пользуемся.

Пошаговый алгоритм в x64dbg для x86 PE:
  1. Загрузить упакованный PE. Отладчик останавливается на Entry Point упаковщика - это НЕ OEP оригинальной программы
  2. Выполнить Step Over (F8) один раз. Первая инструкция большинства пакеров - pushad, сохраняющая все регистры в стек. Значение ESP после pushad уменьшается на 0x20 (8 регистров по 4 байта)
  3. Правый клик на значении ESP в панели регистров, "Follow in Dump". Первые 4 байта в дампе - адрес, на который указывает ESP после push
  4. Выделить первые 4 байта в Memory Dump, ПКМ, Breakpoint, Hardware Access DWORD. Это hardware breakpoint на чтение стекового фрейма
  5. Нажать F9 (Run). Отладчик остановится, когда пакер выполнит popad - конец распаковочного стаба
  6. Несколько Step Over (F8). Следующие инструкции обычно содержат jmp на адрес OEP или непосредственно начало оригинального кода
  7. Открыть плагин Scylla в x64dbg, в поле OEP ввести найденный адрес. IAT Autosearch, Get Imports, Dump, Fix Dump. Scylla создаст распакованный PE с восстановленной таблицей импортов (IAT)
Для x64 бинарей есть нюанс: инструкций pushad/popad в x86-64 ISA нет. Пакеры используют последовательность push rax; push rcx; ... push r15. Принцип тот же - hardware breakpoint на RSP после сохранения регистров. Число сохраняемых регистров варьируется: некоторые пакеры сохраняют только volatile registers (RAX, RCX, RDX, R8–R11), так что смотрите внимательно, сколько push'ей пролетело.

ScyllaHide нужно активировать ДО начала анализа, если бинарь содержит anti-debug проверки - иначе отладчик будет обнаружен до того, как ESP-трик сработает. Я на этом терял минут двадцать, пока не выработал привычку включать ScyllaHide первым делом.

Альтернативные методы поиска OEP​

Анализ секций PE-файла. Если пакер создаёт пустую секцию для распакованного кода (Virtual Size значительно больше Raw Size), можно поставить memory breakpoint на запись в эту секцию. Когда пакер закончит запись распакованного кода, ставим breakpoint на выполнение - первый hit будет OEP или близко к нему. Работает для UPX и похожих пакеров, где целевая секция заранее известна.

Трассировка переходов. В x64dbg можно использовать Trace Over с условием остановки при выходе EIP/RIP за пределы секции упаковщика. Если EP в секции по адресам 0x00410000–0x00415000, условие трассировки - остановка при переходе за этот диапазон. Первый адрес за границей секции пакера - вероятный OEP. Метод медленнее ESP-трика, но надёжнее для нестандартных пакеров.

SEH-based распаковка. Некоторые пакеры используют Structured Exception Handling для передачи управления на OEP через контролируемое исключение. Breakpoint на ntdll.KiUserExceptionDispatcher позволяет отследить, куда перенаправляется выполнение после обработки исключения.

Когда стандартные методы не работают: если пакер использует многоступенчатую распаковку (стаб A распаковывает стаб B, который распаковывает основной код), ESP-трик даст промежуточную точку, а не финальный OEP. Решение простое - повторять ESP-трик с каждой промежуточной точки, как матрёшку разбирать, пока не доберётесь до нативного кода с осмысленными строками и импортами.

Обход антиотладки в crackme и CTF-заданиях​

Антиотладка в CTF - моделирование техники Debugger Evasion (T1622). Три уровня WinAntiDbg на PicoCTF 2024 (0x100 на 200 очков, 0x200 на 300 и 0x300 на 400) последовательно повышали сложность проверок.

IsDebuggerPresent - самая частая проверка. WinAPI-функция возвращает 1, если процесс запущен под отладчиком (читает поле BeingDebugged из PEB). В WinAntiDbg0x100 обход сводился к установке breakpoint на инструкцию test eax, eax после вызова IsDebuggerPresent, запуску программы и ручной замене EAX с 1 на 0 в панели регистров. Дальше jz перенаправляет выполнение на ветку с флагом вместо ветки "debugger detected". Три клика - и готово.

ScyllaHide для автоматического обхода. Плагин для x64dbg скрывает отладчик от стандартных проверок: IsDebuggerPresent, NtQueryInformationProcess (ProcessDebugPort, ProcessDebugObjectHandle), CheckRemoteDebuggerPresent, PEB.NtGlobalFlag, heap flags. Профиль "VMProtect" в настройках закрывает большинство user-mode проверок.

Прямой патч PEB. В x64dbg через CommandLine можно перейти к PEB, найти смещение +0x02 (BeingDebugged) и заменить 0x01 на 0x00. Обходит IsDebuggerPresent без патчинга самого вызова функции. Грубо, но работает.

Timing-based anti-debug (T1497.001, System Checks). Crackme повышенной сложности используют RDTSC или GetTickCount для измерения интервала между инструкциями. Если время слишком велико - значит, отладчик замедляет выполнение, и программа меняет поведение. ScyllaHide подменяет результаты timing-функций автоматически.

Тип проверкиScyllaHide обходитТребует ручного обхода
IsDebuggerPresentДаНет
NtQueryInformationProcessДаНет
RDTSC / GetTickCount timingДаНет
Kernel-mode anti-debug (int 2Dh)ЧастичноДа, нужен kernel debugger
Hypervisor-based проверкиНетДа, запуск на bare metal
Self-modifying code + integrity checkНетДа, нужен patch + пересчёт хеша

В writeup crackme "Крестики" с HackerLab автор отмечал наличие IsDebuggerPresent в таблице импортов, но функция не использовалась в основном коде для изменения поведения. Момент важный: наличие anti-debug API в импортах ещё не означает, что она реально применяется. Прежде чем тратить время на обход - проверьте cross-references на функцию в IDA (клавиша X). Если xref'ов ноль - это мёртвый импорт, не ведитесь.

Написание keygen: реверс алгоритма валидации​

1784565967504.webp

Serial fishing vs алгоритмический реверс​

Serial fishing - поиск правильного серийника в памяти процесса через breakpoint на функции сравнения строк (strcmp, wcscmp, lstrcmpA). Подход работает, если программа генерирует эталонный серийник и сравнивает его с пользовательским вводом. Преимущество: решение за 5 минут. Ограничение: если проверка использует поэлементное вычисление или хеширование, эталонной строки в памяти не будет.

Алгоритмический реверс - декомпиляция логики валидации и инвертирование математических операций. Единственный путь, когда:
  • Проверка использует XOR-каскады, кастомные хеши или модульную арифметику
  • Серийник зависит от имени пользователя (name-serial scheme)
  • Проверка выполняется по частям без единой точки сравнения

Реверс XOR-каскада из реального crackme​

В crackme "Крестики" (по данным writeup'а на Habr) алгоритм валидации серийника работал так: тело флага формата CODEBY{XXXX-XXXX-XXXX-XXXX}, где каждый блок XXXX - четырёхзначное число, проходящее через цепочку XOR и арифметических операций. Декомпилятор IDA показывал проверку первого блока: (((v4 ^ 0xDFAF7) + 22098798) ^ 0x23B97B) == 24947582. Дополнительное условие - имя пользователя должно равняться строке MASTER_OF_CODEBY.

Чтобы написать keygen, инвертируем каждую операцию в обратном порядке:
Python:
# Keygen: инвертируем XOR-каскад для первого блока серийника
# XOR обращается повторным XOR, сложение - вычитанием
# Порядок - обратный оригинальному коду
target = 24947582
step1 = target ^ 0x23B97B       # инвертируем последний XOR
step2 = step1 - 22098798        # инвертируем сложение
result = step2 ^ 0xDFAF7        # инвертируем первый XOR
print(f"Блок 1: {result:04d}")  # четырёхзначное число
Тот же подход - для каждого из четырёх блоков серийника. XOR инвертируется повторным XOR с тем же ключом, сложение - вычитанием. Порядок операций при инверсии - строго обратный порядку в оригинальном коде. Если перепутаете последовательность - получите мусор, и отлаживать придётся уже собственный keygen.

Decision tree: патч, serial fishing или keygen​

Условие задачиМетод решения
Серийник генерируется в памяти, сравнивается через strcmpSerial fishing: breakpoint на strcmp, читаем эталон
Поэлементная проверка, XOR-каскад, арифметикаАлгоритмический реверс, написание keygen
Флаг не зависит от ввода, нужно обойти проверкуПатч: замена jz на jnz или NOP условного перехода
Алгоритм использует SHA/MD5 хешированиеBrute-force кандидатов + проверка хеша
Проверка реализована через VM или конечный автоматСимвольное исполнение (angr, Triton) или ручная трассировка

Ограничения техник и когда переходить к другим методам​

Виртуализация кода (Themida, VMProtect). ESP-трик неприменим, если протектор виртуализирует код: вместо распаковки в нативный x86/x64 он транслирует инструкции в байткод собственной виртуальной машины. Для деобфускации нужны специализированные devirtualizer'ы или символьное исполнение. В CTF задачи с полной виртуализацией встречаются на уровне 500+ очков - и там уже совсем другой разговор.

Managed-код (.NET, Java). Инструменты для native RE бесполезны для .NET PE (IL-код) или Java JAR (байткод JVM). Нужны dnSpy/ILSpy для .NET и JD-GUI/Procyon для Java. Энтропия секций в managed PE может быть высокой из-за IL-метаданных, но это не упаковка в классическом смысле. Не путайте - сэкономите время.

Junk Code Insertion (T1027.016). Вставка бессмысленных инструкций - dead code, opaque predicates - не шифрует код, но делает декомпиляцию нечитаемой. Ghidra справляется с простыми вариантами через оптимизацию декомпилятора, но opaque predicates и code interleaving требуют ручного анализа в graph view. Тут терпение важнее инструмента.

Multi-stage unpacking. Если пакер использует многоступенчатую распаковку (стаб A распаковывает стаб B, который распаковывает основной код), ESP-трик даст адрес промежуточного стаба, а не финальный OEP. Решение: повторять ESP-трик с каждой промежуточной точки, пока не дойдёте до нативного кода с осмысленными строками и импортами. Матрёшка - она и в RE матрёшка.

Kernel-mode anti-debug. Проверки через int 2Dh или driver-level хуки не обходятся ScyllaHide. Нужен kernel debugger (WinDbg) или виртуализированный отладчик (HyperDbg). На CTF такие проверки встречаются только в задачах категории pwn/kernel уровня hard.

Минилаб для отработки распаковки и написания keygen​

📚 Часть контента скрыта. Этот материал доступен участникам сообщества с рангом One Level или выше
Получить доступ просто — достаточно зарегистрироваться и проявить активность на форуме

Упаковщики в CTF - это не про знание одной команды upx -d. Это про понимание структуры PE-файла на уровне секций, про работу с hardware breakpoints и про системный подход к бинарю, который автор задачи сознательно сделал неудобным для анализа. ESP-трик покрывает порядка 80% упакованных задач на CTF уровня medium. Остальные 20% - кастомные пакеры и виртуализация, где нужен опыт и терпение.

Но основная проблема решающих не в технике, а в подходе: большинство CTF-игроков тратят часы на перебор плагинов для автоматической распаковки вместо того, чтобы за 10 минут пройти ESP-трик руками. Автоматизация - зло, когда она подменяет понимание. Навык ручной распаковки окупается не только на соревнованиях. В incident response те же техники работают один в один - APT-группы используют модифицированный UPX именно потому, что автоматические распаковщики его не берут.

Keygen-подход - реверс математики вместо патча ветвления - учит думать как автор защиты, и это ценнее десятка решённых задач через замену jz на jnz. На HackerLab есть RE-задачи, где весь этот pipeline нужно пройти end-to-end без подсказок.
 
Последнее редактирование модератором:
Мы в соцсетях:

Взломай свой первый сервер и прокачай скилл — Начни игру на HackerLab

Похожие темы

🚀 Первый раз на Codeby?
Гайд для новичков: что делать в первые 15 минут, ключевые разделы, правила
Начать здесь →
🧭 Навигатор · ИБ 2026
Не знаешь, какой трек твой?
5 направлений ИБ, реальные зарплаты и точка входа для каждого — в одном треде.
JuniorSenior+
100K → 600K+ ₽ /мес
Открыть навигатор →
🔴 Свежие CVE, 0-day и инциденты
То, о чём ChatGPT ещё не знает — обсуждаем в реальном времени
Threat Intel →
💼 Вакансии и заказы в ИБ
Pentest, SOC, DevSecOps, bug bounty — работа и проекты от проверенных компаний
Карьера в ИБ →

HackerLab