Reverse Engeneering / обратная разработка в CTF
Реверс-инжиниринг — это исследование программы или устройства без исходников/доков,
чтобы понять внутреннюю логику, структуру и работу.
Информацию собрал: Владимир Щеголев
Реверс в 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 обычно не нужно).
Обход антиотладки: быстрый план
- 1) Найти проверку: по строкам/импортам (IsDebuggerPresent, ptrace, TracerPid), по INT 3, по таймингам.
- 2) Отметить ветвление: в декомпиляторе посмотреть, что делает «плохая» ветка (выход/бред), поставить бряк на условный переход.
- 3) Упростить: заменить условие на константу возврата (e.g.,
xor eax,eax; inc eax; ret
→ true), или инвертировать Jcc. - 4) Заглушки:
Windows — подменить
IsDebuggerPresent
на функцию, возвращающую 0; Linux — LD_PRELOAD/Frida хукptrace
, возвращающий ошибку «как надо». - 5) Стабилизировать время: перехват
GetTickCount
/clock_gettime
и возврат линейного счётчика. - 6) Проверить до
main()
: TLS-коллбеки/конструкторы — поставь бряки на них или вырежи в бинаре.
Патчинг: статический и динамический
Зачем нужен патчинг? Чтобы изменить поведение программы без исходников: отключить проверку лицензии/отладки, перепрыгнуть ненужные условия, подменить результат функции. В CTF это часто означает — вырезать лишние проверки и добраться до флага.
Статический
- Хекспатчи: замена байтов в файле (например,
75
(JNZ) →74
(JZ),0F85
→0F84
). - NOP (No Operation): байт
0x90
— ничего не делает. Используется для «вырезания» проверки или условного перехода, сохраняя размер кода. - Нормализация: убрать упаковщик/обфускатор (UPX/DIE), восстановить импорты, пересобрать секции.
- r2/IDA/Ghidra: редактировать инструкцию на месте, пересчитать контрольные суммы/размеры секций при необходимости.
Динамический
- Патчи в памяти: x64dbg/WinDbg/gdb — изменить байты после загрузки, проверить поведение без записи на диск.
- Хуки: LD_PRELOAD (ELF), IAT-hook (PE), Frida — перехват целевой функции и возврат контролируемого значения.
- DBI: инструментирование (DynamoRIO/Pin) — для сложных кейсов, когда точечный патч неудобен.
Стек и соглашения о вызовах (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, N
…leave; 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
, просмотр локалов, восстановление аргументов и даже поиск «забытых» секретов.
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) — точки для хуков.
Иногда во вложенном бинаре лежит ещё один 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 85
↔0F 84
- NOP:
90
(одиночный), много NOP —66 90
или последовательность0F 1F ...
- Быстрый выход из функции:
C3
(RET) или31 C0 C3
(xor eax,eax; ret) - Принудить истину:
B0 01 C3
(mov al,1; ret)
Упаковщики: 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 для быстрых рантайм-хуков.
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’а, работать дальше на «чистом» состоянии.
Дизассемблеры / отладчики / декомпиляторы
- 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/обфускации).
На новых версиях 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 (полезно при замыканиях).
- Рекурсивно обойти
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 — встраиваемые