Reverse Engeneering / обратная разработка в CTF

Реверс-инжиниринг — это исследование программы или устройства без исходников/доков, чтобы понять внутреннюю логику, структуру и работу.
Информацию собрал: Владимир Щеголев

Static Dynamic Tools Formats Architectures

Реверс в CTF и ИБ

В CTF:

  • дают исполняемый файл/бинарь;
  • нужно понять алгоритм работы;
  • найти скрытый флаг;
  • обойти проверки (напр., crackme);
  • исследовать шифрование или протоколы.

В сфере ИБ:

  • анализ вредоносного ПО;
  • поиск уязвимостей в бинарях;
  • понимание «чёрных ящиков» (драйверы и пр.).

Разделение реверса

  • Программный — анализ исполняемых файлов, библиотек, драйверов, прошивок.
  • Аппаратный — исследование схем, микроконтроллеров, микросхем памяти (например, извлечение прошивки).

Инструменты и особенности анализа

Статический анализ

Исследование без запуска программы: понять логику, структуру файла, алгоритмы.

Дизассемблеры

машинный код → ASM. Пример: IDA

Декомпиляторы

код → высокий уровень (C/Java). Пример: IDA

Анализаторы исполняемых файлов

структура: заголовки, секции, импорты/экспорты. Примеры: readelf/objdump, PEiD

Редакторы бинарей

правки данных вручную/скриптами. Пример: Hex Editor

Антиотладка/упаковщики

обход защит/идентификация. Пример: Detect It Easy

Динамический анализ

Исследование во время выполнения: поведение в реальном времени.

Отладчики

брейки, шаги, регистры, память. Пример: gdb

Мониторинг ОС

системные/библиотечные вызовы. Примеры: ProcMon, strace/ltrace

Песочницы/эмуляция

безопасный запуск подозрительных программ. Пример: QEMU

Сеть/трафик

перехват и анализ пакетов. Пример: Wireshark

Антиотладка: распространённые техники

Цель — затруднить динамический анализ. Почти всё обходится за минуты, если знать, что искать.

Windows

  • IsDebuggerPresent(), CheckRemoteDebuggerPresent(), OutputDebugString() (отслеживание поведения).
  • NtQueryInformationProcess c классами ProcessDebugPort / ProcessDebugFlags / ProcessBasicInformation.
  • Проверка PEB: флаг BeingDebugged, поля NtGlobalFlag.
  • Исключения/SEH: генерация INT 3, ловля странного поведения отладчика.
  • Тайминги: QueryPerformanceCounter / GetTickCount (замедление шагов).
  • Самоинжект/создание процессов с флагами DEBUG_ONLY_THIS_PROCESS, родительский процесс.

Linux/Unix

  • ptrace(PTRACE_TRACEME) и проверка ошибки при уже присоединённом трейсере.
  • Чтение /proc/self/status → поле TracerPid.
  • Сигналы/исключения: SIGTRAP, ловушки int3, проверка масок сигналов.
  • Тайминги: clock_gettime, rdtsc (для x86/x64).

Кроссплатформенные приёмы

  • Сканирование импортов/символов отладчика (OllyDbg/x64dbg/Frida).
  • TLS-коллбеки/конструкторы (выполняются до main()).
  • Анти-VM/анти-сандбокс: редкие инструкции/CPUID, имена устройств/драйверов, тайминги.

Самописные проверки антиотладки (и почему это ломается)

  • Проверка флага → ветка if (debug) exit;. Достаточно инвертировать условный переход (JNZ ↔ JZ) или зафиксировать возвращаемое значение.
  • Контроль целостности (CRC/хеш кода) → легко обойти, если считать контроль только на «критическом» участке или переопределять функцию хеша хуком.
  • Тайминги → стабилизируются хуками/патчем: подмена rdtsc/API, фиктивные значения.
  • Исключения → отладчик перенастраивается, а вызовы RaiseException/int3 патчатся в NOP/RET.
  • Inline-syscall → ловится по сигнатурам в дизассембле и заменяется заглушкой, либо перехватывается на входе в ядро с помощью гипера (в CTF обычно не нужно).
Итог: «антиотладка» чаще лишь задержка. В CTF она создана, чтобы ты нашёл и снял её правильно.

Обход антиотладки: быстрый план

  • 1) Найти проверку: по строкам/импортам (IsDebuggerPresent, ptrace, TracerPid), по INT 3, по таймингам.
  • 2) Отметить ветвление: в декомпиляторе посмотреть, что делает «плохая» ветка (выход/бред), поставить бряк на условный переход.
  • 3) Упростить: заменить условие на константу возврата (e.g., xor eax,eax; inc eax; rettrue), или инвертировать Jcc.
  • 4) Заглушки: Windows — подменить IsDebuggerPresent на функцию, возвращающую 0; Linux — LD_PRELOAD/Frida хук ptrace, возвращающий ошибку «как надо».
  • 5) Стабилизировать время: перехват GetTickCount/clock_gettime и возврат линейного счётчика.
  • 6) Проверить до main(): TLS-коллбеки/конструкторы — поставь бряки на них или вырежи в бинаре.

Патчинг: статический и динамический

Зачем нужен патчинг? Чтобы изменить поведение программы без исходников: отключить проверку лицензии/отладки, перепрыгнуть ненужные условия, подменить результат функции. В CTF это часто означает — вырезать лишние проверки и добраться до флага.

Статический

  • Хекспатчи: замена байтов в файле (например, 75 (JNZ)74 (JZ), 0F850F84).
  • NOP (No Operation): байт 0x90 — ничего не делает. Используется для «вырезания» проверки или условного перехода, сохраняя размер кода.
  • Нормализация: убрать упаковщик/обфускатор (UPX/DIE), восстановить импорты, пересобрать секции.
  • r2/IDA/Ghidra: редактировать инструкцию на месте, пересчитать контрольные суммы/размеры секций при необходимости.

Динамический

  • Патчи в памяти: x64dbg/WinDbg/gdb — изменить байты после загрузки, проверить поведение без записи на диск.
  • Хуки: LD_PRELOAD (ELF), IAT-hook (PE), Frida — перехват целевой функции и возврат контролируемого значения.
  • DBI: инструментирование (DynamoRIO/Pin) — для сложных кейсов, когда точечный патч неудобен.
Совет: сначала патч в памяти → проверка логики → только потом записывать в файл. (В IDA, после патчинга, нужно не забывать применять изменения и записывать в файл)

Стек и соглашения о вызовах (ABI)

Соглашения вызовов

  • x86 cdecl: аргументы справа-налево на стеке, чистит стек вызывающий.
  • x86 stdcall: чистит стек вызываемая, часто в WinAPI.
  • x86 fastcall: первые аргументы в регистрах (ECX, EDX), остальное — стек.
  • x64 System V (Linux/macOS): RDI, RSI, RDX, RCX, R8, R9, стек выравнен на 16, возврат в RAX.
  • x64 Microsoft (Windows): RCX, RDX, R8, R9 — регистры, «home space» на стеке под 4 аргумента.
  • Пролог/эпилог: push rbp; mov rbp,rsp; sub rsp, Nleave; ret. Полезно искать локалы/аргументы.

Регистры

  • EAX/RAX — аккумулятор, здесь чаще всего возвращаемое значение функции.
  • EBX/RBX — регистр данных, обычно сохраняется.
  • ECX/RCX, EDX/RDX — часто используются для аргументов или счетчиков циклов.
  • ESI/RSI, EDI/RDI — источники и приёмники данных при копировании.
  • ESP/RSP — указатель стека (Stack Pointer).
  • EBP/RBP — базовый указатель (Base Pointer), удобно искать локальные переменные и аргументы.
  • EIP/RIP — Instruction Pointer, указывает на следующую инструкцию.
  • Флаги (EFLAGS/RFLAGS) — биты состояния (Zero, Sign, Carry, Overflow), критично для условных переходов.

Регистры, стек и память

  • Стек хранит локальные переменные, аргументы функций и сохранённые значения регистров.
  • Регистры — быстрый «верхний слой» процессора, но часто значения из них складываются/берутся со стека при вызовах.
  • Области памяти: stack (динамически растёт вниз), heap (динамические аллокации), .data/.bss (глобальные), .text (код).
  • В стеке может оставаться «мусор» от предыдущих вызовов, если его не подчистили. Иногда этот мусор используют для восстановления строк или ключей при реверсе.
  • Регистры помогают «поймать» такие данные: если, например, RBP/RSP указывают на область, где раньше лежали аргументы или временные буферы, их можно просмотреть и найти незатёртые значения.
  • Таким образом, следя за регистрами и их смещениями относительно стека (rbp ± offset, rsp + N), можно обнаружить остатки старых данных, которые иначе были бы потеряны.
  • Слежение за стеком помогает: бряк на push/pop, просмотр локалов, восстановление аргументов и даже поиск «забытых» секретов.
Понимание ABI и регистров ускоряет поиск места проверки: смотри, где появляется флаг/булевский результат (обычно AL/AX/EAX/RAX).

Память процесса и защиты

  • Секции: .text (код), .rdata/.rodata (константы), .data, .bss, у ELF — .plt/.got.
  • ASLR/PIE: рандомизация баз; при отладке ищи «реальную» базу модуля и рассчитывай смещения от неё.
  • NX/DEP: запрет исполнения на стеке/хипе — влияет на эксплойтинг, но и на антиотладку с исключениями.
  • RELRO (ELF): защита GOT (частично/полностью); влияет на возможность перехвата через GOT.
  • Импорт/экспорт: таблицы IAT/Import Directory (PE) и GOT/PLT (ELF) — точки для хуков.
В основном, (не всегда!) в CTF часто достаточно подменить 1–2 перехода или хукнуть одну функцию
Пример-кейс:Дамп ELF прямо из IDA
Иногда во вложенном бинаре лежит ещё один ELF, картинка, либо что-то ещё. Это легко увидеть по сигнатурам. И этот файл можно срезать через savefile():

// путь к файлу, с которым работаем
auto fname = "/tmp/extracted.elf";
// начало: сигнатура 0x7F 45 4C 46 (\x7fELF)
auto address = 0x99999999;
// размер: считаем по Program Headers
auto size    = 0x99999999;
auto f = fopen(fname, "wb");
savefile(f, 0, address, size);
fclose(f);
  • Address — это адрес, где найден ELF-заголовок или другой, например, изображение. В IDA ищется по сигнатуре. В случае с ELF — 7F 45 4C 46.
  • Size — правильнее считать по заголовкам ELF: взять все Program Header и вычислить максимум p_offset + p_filesz. Это и будет реальный конец бинаря.
  • В простых тасках можно «грубо» взять до следующего блока памяти и потом проверить readelf -h, подрезав лишнее.

Шпаргалка: быстрые патчи в x86/x64

  • Инверсия ветки (short): JNZ (0x75)JZ (0x74)
  • Инверсия ветки (near): 0F 850F 84
  • NOP: 90 (одиночный), много NOP — 66 90 или последовательность 0F 1F ...
  • Быстрый выход из функции: C3 (RET) или 31 C0 C3 (xor eax,eax; ret)
  • Принудить истину: B0 01 C3 (mov al,1; ret)
Следи за размером патча: сохраняй длину или заполняй остаток NOP-ами.

Упаковщики: UPX / ASPack / VMProtect

Упаковщик сжимает/обфусцирует код и добавляет загрузчик. Цель — усложнить статический анализ и сигнатурный детект.

  • UPX — open source, типовой компрессор. Легко определяется и часто корректно распаковывается.
  • ASPack — старый коммерческий пакакер, часто с простыми антидамп/anti-debug приёмами.
  • VMProtect — защита через виртуализацию инструкций: часть кода исполняется в собственном VM-байткоде. Сложен для анализа/патча.

Как распознать

  • Сигнатуры в Detect It Easy/DIE, PEiD, r2 -I.
  • Аномалии секций: “UPX0/UPX1”, урезанные/странные заголовки, единственная исполняемая секция.
  • Импорт восстановлен загрузчиком (IAT «пустой», API резолвятся динамически).

Распаковка на практике

UPX (просто)

  • Стандартно: upx -d sample.exe — если не модифицирован.
  • Не менее стандартно: "upx -d" ищет сигнатуру "UPX!", иногда ее изменяют, например "BPX!", и в таком случае — перед тем, как избавляться от упаковщика, с помощью, например утилиты hexedit, ищем заголовок UPX! (прям в начале файла обычно), и меняем заголовок на верный, после чего работаем штатным путём
  • Если модифицирован: отладчик → бряк после распаковки в памяти → дамп процесса → восстановить IAT (Scylla/ImpRec/r2 iaitu).
  • Признак «после»: в памяти появляются нормальные секции/строки/импорты.

ASPack (средне)

  • Бряк на OEP загрузчика: ищем цикл распаковки и переход в оригинальный код (jmp oep).
  • Дамп процесса на OEP и восстановление импортов (IAT реконсрукция: Scylla/ImpRec, или скриптом в r2/IDA).
  • Частые трюки: анти-отладка/сброс контекстов — нейтрализуем патчами/хуками (см. раздел антиотладки).

VMProtect (сложно)

  • Не «распаковка», а снятие слоя шифрования/обход виртуализованных участков.
  • Ищем невиртуализированный периметр: обработчики ввода/парсеры/строки/API — там часто живёт логика проверки.
  • Подход CTF: вместо полной де-виртуализации — хук на функцию, возвращающую «валидный» результат; либо патч ветки после результата.
  • Инструменты: трассировка (x64dbg/WinDbg/gdb), DBI (DynamoRIO/Pin), Frida для быстрых рантайм-хуков.
Стратегия CTF: минимальная достаточность. Не «ломаем всё», а вырезаем путь к флагу.

Anti-VM трюки и как их обходить

Anti-VM пытается распознать виртуальную среду (VMware/VirtualBox/Hyper-V) и менять поведение.

Типовые проверки

  • CPUID/инструкции: чтение флагов гипервизора, редкие инструкции с иным таймингом.
  • Девайсы/драйверы: имена вроде VBOX, VMware, MAC-префиксы виртуальных карт.
  • Региcтр/файлы: ключи VMware Tools/Guest Additions, сервисы виртуализации.
  • BIOS/DMI/SMBIOS: строки производителя «VMware/VirtualBox/QEMU».
  • Тайминг: rdtsc, QueryPerformanceCounter, «jitter» в VM.
  • Кол-во ядер/ОЗУ: слишком «круглые» значения/малые лимиты по умолчанию.

Обход/смягчение

  • Маскировка VM: переименовать устройства, изменить DMI/SMBIOS строки, MAC-префиксы, увеличить ресурсы (ядра/ОЗУ/диск).
  • Отключить гостевые дополнения или перенастроить сервисы, чтобы не светили артефакты.
  • Хуки таймингов: перехватывать rdtsc/API времени (LD_PRELOAD/Frida) и возвращать линейные значения.
  • Патч проверок: найти ветку, зависящую от проверки VM, инвертировать/заглушить (см. «Шпаргалка патчей»).
  • Снимок «между»: сделать snapshot и бинарный дамп после прохождения check-point’а, работать дальше на «чистом» состоянии.
В CTF чаще всего достаточно: 1) вычленить проверку (строки/импорты/CPUID), 2) инвертировать один прыжок, 3) при необходимости — хук таймингов.

Дизассемблеры / отладчики / декомпиляторы

  • IDA — солянка: дизассемблер, декомпилятор, отладчик, плагины. IDA Pro — платная (как получать — каждый решает сам). Используют часто.
  • Ghidra — солянка как выше, но бесплатная. Где-то хуже, где-то лучше.
  • Radare2 — функционально богато, сложнее, open source.
  • x64dbg — дизассемблер/отладчик, развитие OllyDbg, open source.

Отладчики .NET

  • dnSpy — мощный инструмент для реверса и отладки .NET-программ.

Декомпиляторы Java / Android

  • APKTool — декомпиляция/сборка под Android.
  • Android Studio — IDE и сопутствующие инструменты.
  • JD-GUI — декомпилятор Java.

Декомпиляторы для Python

  • uncompyle6 — декомпилятор .pyc, open source (ограничение: не всегда корректен с новыми версиями Python).
  • uncompyle3 — более свежий декомпилятор байткода Python.
  • pycdc — декомпилятор байткода, open source.
  • nuitka extractor — помощник для бинарей, собранных Nuitka.
  • PyInstaller Extractor — разбор исполняемых, собранных PyInstaller.
  • REPL-инспекция — загрузить код и руками исследовать переменные/функции/классы (актуально для .pyc/обфускации).
Когда декомпиляторы/экстракторы не помогают — используем REPL-инспекцию
На новых версиях Python и/или с обфускацией классические инструменты могут падать или давать мусор. Типичные случаи, когда uncompyle/pycdc и экстракторы уступают REPL:
  • Новые байткоды Python 3.11–3.13+ — изменилась семантика/формат .pyc, старые декомпиляторы не поспевают.
  • PyArmor/obfuscators — обёртки вокруг функций, подмена импортов, «замороженные» модули.
  • PyInstaller с шифрованием/ключом — экстрактор достаёт .pyc, но они дополнительно модифицированы.
  • Nuitka — код транслирован в C/С++, декомпиляции .pyc нет (там уже не Python-байткод).
  • Cython — критичные части в .so/.pyd (C-расширение), декомпилятор Python тут бессилен.
  • Кастомный marshal/loader — нестандартная загрузка, подпорченные магические числа .pyc.

Идея REPL-инспекции: импортируем модуль в интерактивный Python и изучаем функции через их атрибут __code__ (объект типа code object) — видим константы, имена, локалы и сам байткод, не исполняя опасную логику.

Ключевые поля func.__code__ (самые полезные для реверса):
  • co_consts — кортеж литералов в функции (строки, числа, вложенные code-объекты и т.д.). Часто здесь прячут флаг/ключ.
  • co_names — имена, к которым обращается байткод (глобалы, импортированные функции/переменные).
  • co_varnames — локальные переменные и параметры.
  • co_code — байткод (bytes).
  • co_filename, co_firstlineno — «откуда родом» функция.
  • co_flags, co_stacksize, co_nlocals, co_freevars/co_cellvars — служебка VM (полезно при замыканиях).
Фишки, которые ускоряют CTF-реверс:
  • Рекурсивно обойти co_consts и собрать все строки/байты (во вложенных code object тоже бывают секреты).
  • Проверить __closure__ (замыкания): иногда флаг живёт в cell.cell_contents.
  • Подменить «барьер» прямо в REPL (например, m.check = lambda: True) и вызвать цель.
  • Если модуль обфусцирован и требует рантайм (PyArmor) — запускать REPL в папке с его runtime, чтобы import прошёл.

Т.е., при свежих версиях Python и обфускации REPL-инспекция часто даёт результат быстрее любой декомпиляции: co_consts + dis + (globals/closure) → флаг на ладони.

Мини-практикум:

>>> import builtins
>>> import ЗАДАЧА
Чекаем публичные имена внутри модуля:
>>> dir(task5) (тут будет большой вывод, один из которых допустим check_flag)
Можно их исследовать:
>>> f = m.ЦЕЛЕВАЯ_ФУНКЦИЯ (она же, допустим check_flag)
>>> f.__code__.co_consts
(None, 'SSU{demo_flag}', 123, (<code object inner at 0x...>, 'hint'))
Также, можно:
Посмотреть, к каким именам обращается байткод:
>>> f.__code__.co_names
('builtins', 'check', 'helper')
Какие локальные переменные и аргументы у функции:
>>>func.__code__.co_varnames
Дизассемблировать функцию и увидеть, где грузятся константы:
(import dis)
>>> dis.dis(f)
  0 LOAD_GLOBAL              1 (check)
  2 CALL_FUNCTION            0
  4 POP_TOP
  6 LOAD_CONST               1 ('SSU{demo_flag}')
  8 RETURN_VALUE

Прослушивание трафика / события / системные вызовы

  • ProcMon (Windows) — мониторинг активности процессов.
  • Wireshark — сниффер сетевого трафика.
  • strace / ltrace — системные вызовы (open/read/write/execve) и вызовы библиотек (printf/malloc/strcmp).

Полезные инструменты

  • Volatility — фреймворк анализа дампов памяти.
  • Detect It Easy — идентификация типа/формата/защиты файлов.
  • QEMU — виртуализация/эмуляция.
  • VirusTotal — проверка файлов на вредоносность.

Наиболее используемые форматы файлов

  • EXE (Windows)
  • ELF (Linux)
  • APK (Android)
  • IPA (iPhone)
  • Mach-O (macOS)

Наиболее используемые архитектуры

  • x86 / x64 — ПК/серверы
  • ARM — мобильные
  • AVR — встраиваемые
  • MIPS — встраиваемые