Семь 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 проходит через одну и ту же последовательность. Перескакивание этапов - главная причина, почему люди тратят два часа на задачу, которая решается за двадцать минут.- Triage - определение типа файла (
file, Detect-It-Easy), архитектуры, наличия упаковщика, энтропия секций - Распаковка (если упаковщик обнаружен) - автоматическая или ручная, с поиском OEP и дампом памяти
- Статический анализ - дизассемблирование и декомпиляция в IDA Free или Ghidra, поиск строк и перекрёстных ссылок
- Динамический анализ - отладка в x64dbg или GDB, трассировка ключевых функций, анализ поведения при разных вводах
- Реверс алгоритма - восстановление логики валидации: XOR-каскады, конечные автоматы, кастомные шифры
- Решение - извлечение флага, написание keygen или патч бинаря
Инструменты для статического и динамического анализа
Требования к окружению
- ОС: 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 decompiler | Cloud 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.x | CLI-интерфейс, скриптуемость, кроссплатформенность, встроенный отладчик | Порог входа - сотни команд, которые надо помнить или гуглить | Быстрый 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не выдают ничего осмысленного
.rsrc объяснялась PNG-ресурсом внутри файла - это нормально. Отличить высокую энтропию ресурсов от высокой энтропии кода - навык, который приходит с анализом десятков бинарей. Ресурсы сжаты "по природе" (изображения, иконки), а упакованный код сжат умышленно. Разница видна по контексту: если энтропия 7.8 в .text - почти наверняка пакер, если в .rsrc - скорее всего картинка.Распаковка исполняемых файлов и поиск OEP
Автоматическая распаковка 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:
- Загрузить упакованный PE. Отладчик останавливается на Entry Point упаковщика - это НЕ OEP оригинальной программы
- Выполнить Step Over (F8) один раз. Первая инструкция большинства пакеров -
pushad, сохраняющая все регистры в стек. Значение ESP послеpushadуменьшается на 0x20 (8 регистров по 4 байта) - Правый клик на значении ESP в панели регистров, "Follow in Dump". Первые 4 байта в дампе - адрес, на который указывает ESP после push
- Выделить первые 4 байта в Memory Dump, ПКМ, Breakpoint, Hardware Access DWORD. Это hardware breakpoint на чтение стекового фрейма
- Нажать F9 (Run). Отладчик остановится, когда пакер выполнит
popad- конец распаковочного стаба - Несколько Step Over (F8). Следующие инструкции обычно содержат
jmpна адрес OEP или непосредственно начало оригинального кода - Открыть плагин Scylla в x64dbg, в поле OEP ввести найденный адрес. IAT Autosearch, Get Imports, Dump, Fix Dump. Scylla создаст распакованный PE с восстановленной таблицей импортов (IAT)
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: реверс алгоритма валидации
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}") # четырёхзначное число
Decision tree: патч, serial fishing или keygen
| Условие задачи | Метод решения |
|---|---|
| Серийник генерируется в памяти, сравнивается через strcmp | Serial 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 без подсказок.
Последнее редактирование модератором: