Реверс
Анализ строк и констант
Анализ строк и констант — это когда ты ищешь в бинарном файле все текстовые сообщения, подсказки, пароли, ключи, магические числа и другие готовые значения, которые автор программы оставил прямо внутри кода. В CTF эта тема относится в первую очередь к категориям Reverse и Crypto. Чаще всего такие строки и константы встречаются в задачах, где дают исполняемый файл, и флаг/ключ/алгоритм частично или полностью лежит в открытом виде внутри бинарника. Новичку это одна из самых первых и самых полезных тем, потому что в 40–50% reverse-задач начального и среднего уровня решение сводится именно к поиску и пониманию строк. Когда научишься быстро находить все строки и видеть, где они используются — ты начнёшь решать многие задачи за 5–15 минут вместо нескольких часов.
Словарь терминов
строка
Последовательность символов (текст), которая лежит в программе
.rodata
Секция, где обычно хранятся все строки и константы только для чтения
магическое число
Специальное число (константа), которое автор вставил в код для какой-то цели
хардкодный ключ
Ключ или пароль, который прямо записан в программе
обфускация строк
Способ спрятать строку, чтобы её нельзя было сразу прочитать
XOR-обфускация
Каждый байт строки изменяется через XOR с одним и тем же значением
cross-reference
Ссылка в дизассемблере, показывающая, где именно используется эта строка
strings
Все читаемые текстовые строки, которые можно вытащить из файла
rabin2
Утилита, которая показывает информацию о бинарнике, включая строки
Binwalk
Инструмент для поиска встроенных файлов и строк внутри бинарника
floss
Программа, которая автоматически расшифровывает обфусцированные строки
Поиск строк (strings, rabin2, Binwalk, floss)
Самый простой и первый шаг — вытащить все читаемые строки из файла. Обычно в бинарнике лежат сообщения типа "Wrong password", "Enter key:", "Correct! Flag is:", "Too slow!" и т.д. Часто среди них прячется сам флаг, пароль или подсказка.
Типичные строки, которые сразу дают решение:
"flag{"
"ctf{"
"password is"
"secret key"
"congratulations"
В CTF это первый шаг почти всегда: вытащить строки и посмотреть, нет ли среди них чего-то подозрительно похожего на флаг или ключ. Если строки обфусцированы — переходят к другим методам.
Константы, магические числа, хардкодные ключи
Многие программы используют фиксированные числа, которые автор вставил прямо в код.
Примеры:
XOR-ключ: 0x55, 0xAA, 42
длина ключа: 32, 16
магические константы шифрования: 0x9E3779B9 (tea), 0x61C88647 (xxtea)
хардкодные ключи: 32-байтные массивы байт или строки типа "thisisaverysecretkey123"
В CTF такие константы часто лежат прямо в .rodata или в функциях в виде mov rax, 0xdeadbeef. Их ищут по подозрительным числам или массивам байт рядом со строками.
Обфускация строк (XOR, RC4, custom encoding)
Авторы часто прячут важные строки, чтобы их нельзя было сразу увидеть.
Самые частые способы:
XOR с одним байтом: каждая буква XOR-ится с фиксированным значением (часто 0xAA или 0x5F)
XOR с повторяющимся ключом: строка XOR-ится с ключом, который повторяется
RC4 — потоковый шифр, иногда используется для простых задач
custom encoding: сдвиг по алфавиту, base64 внутри XOR, перестановка байт
В CTF обычно дают либо саму функцию расшифровки, либо ключ лежит рядом. Достаточно найти цикл XOR и применить его обратно к зашифрованной строке.
String references и cross-references в дизассемблере
Самая ценная информация — не просто найти строку, а понять, где и как она используется. В дизассемблере есть cross-references (xrefs): список всех мест, где эта строка упоминается.
Типичные сценарии:
строка "Wrong!" имеет xref только в одной функции → там проверка пароля
строка "flag{" используется в printf → это и есть флаг
строка "Enter key:" стоит перед scanf → дальше идёт сравнение
В CTF всегда после поиска строк смотрят xrefs: куда они передаются (в strcmp, memcmp, printf). Это сразу показывает логику проверки или вывода.
Итог
Главное, что нужно запомнить: в большинстве reverse-задач флаг, пароль или ключ либо лежит в строках в открытом виде, либо прячется очень близко к ним (в константах, в XOR-обфускации, в функциях рядом).
Ключевые слова и термины для запоминания: строки, .rodata, магическое число, хардкодный ключ, обфускация строк, XOR-обфускация, cross-reference, string references.
Эти знания помогают:
сразу вытащить все строки и найти среди них флаг или пароль
понять, где программа сравнивает твой ввод с правильной строкой
расшифровать обфусцированные строки, увидев XOR-цикл
перейти от просто найденной строки к функции, где она используется
за 2–5 минут понять основную логику проверки в 50% reverse-задач
Начни каждую reverse-задачу именно с поиска строк и их cross-references — это самый быстрый способ понять, что вообще происходит в программе и где лежит решение.
Антиотладочные и антиреверс техники
Антиотладочные и антиреверс техники — это специальные хитрости, которые автор программы вставляет, чтобы сделать анализ и отладку максимально сложными или вообще невозможными. В CTF эта тема относится в первую очередь к категориям Reverse и Pwn. Чаще всего такие защиты встречаются в задачах среднего и высокого уровня, где дают бинарник, который сразу крашится в отладчике, меняет свой код на лету или сильно запутывает логику. Новичку важно понимать, что это не значит «задача нерешаема», а просто нужно знать несколько типичных приёмов и как их обойти. Когда ты научишься быстро распознавать и нейтрализовать эти защиты — ты перестанешь бояться «антидебаг» задач и начнёшь решать их за 10–30 минут вместо нескольких часов.
Словарь терминов
антиотладка
Техники, которые мешают отладчику нормально работать
антиреверс
Техники, которые усложняют понимание кода программы
IsDebuggerPresent
Функция Windows, которая говорит «меня отлаживают»
ptrace
Механизм в Linux, которым отладчик прикрепляется к программе
RDTSC
Инструкция, которая считывает счётчик тактов процессора
timing check
Проверка, сколько времени прошло между двумя точками
self-modifying code
Код, который меняет сам себя во время выполнения
integrity check
Проверка, не изменили ли код программы
CRC32 / CRC64
Простая контрольная сумма для проверки целостности
packing
Сжатие и шифрование бинарника, чтобы его было сложно открыть
obfuscation
Намеренное запутывание кода (перемешивание инструкций, мусор)
virtualization
Код выполняется внутри виртуальной машины, которую создал автор
Проверка наличия отладчика (IsDebuggerPresent, ptrace, NtQueryInformationProcess)
Программа может специально спрашивать: «а меня сейчас отлаживают?»
IsDebuggerPresent — простая функция в Windows. Если отладчик прикреплён — возвращает 1 → программа может крашнуться или изменить поведение.
ptrace — в Linux. Программа пытается прикрепиться к самой себе через ptrace(PTRACE_TRACEME). Если уже есть отладчик — ptrace вернёт ошибку → программа это видит.
NtQueryInformationProcess — более скрытный способ в Windows. Спрашивает у системы информацию о процессе → есть поле BeingDebugged.
В CTF чаще всего:
программа выходит, если видит отладчик
меняет логику (например возвращает фейковый флаг)
Обход простой: патчить вызов на xor eax,eax (всегда 0) или nop.
Timing checks, RDTSC, GetTickCount, QueryPerformanceCounter
Программа меряет время между двумя точками. Если ты идёшь в отладчике шаг за шагом — времени проходит очень много → программа понимает, что её отлаживают.
Популярные способы замера:
RDTSC — считывает количество тактов процессора (очень точный)
GetTickCount — миллисекунды с запуска системы
QueryPerformanceCounter — высокоточный счётчик
Типичный код: rdtsc mov [saved_time], rax ...много инструкций... rdtsc sub rax, [saved_time] cmp rax, 1000000 ja being_debugged
В CTF это часто встречается перед важными проверками (strcmp, дешифровка флага).
Обход:
патчить сравнение (jmp вместо ja)
ускорять выполнение (skip timing check)
использовать аппаратные точки останова (они не сильно увеличивают время)
Self-modifying code, integrity checks, CRC32/CRC64
Self-modifying code — программа меняет свои же инструкции во время работы. Например, сначала xor eax,eax → потом меняет на mov eax,1. В отладчике это видно плохо, дизассемблер путается.
Integrity checks — программа считает контрольную сумму своего кода (CRC32, CRC64, MD5 и т.д.). Если ты поставил breakpoint или патч — сумма не совпадёт → краш или фейковый результат.
В CTF это часто:
код расшифровывает сам себя только в runtime
проверяет .text на изменения перед выводом флага
Обход:
найти и пропатчить проверку контрольной суммы
запускать без точек останова (только run-to)
ставить hardware breakpoints (они не меняют код)
Packing, obfuscation, virtualization (VMProtect, Themida, UPX)
Packing — программа сжимается/шифруется (UPX — самый простой). После запуска распаковывается в память → статический анализ почти бесполезен.
Obfuscation — код запутывают:
вставляют тонну мусорных инструкций
перемешивают блоки
используют много jmp в разные стороны
Virtualization — код превращается в байт-код для виртуальной машины (VMProtect, Themida). Программа интерпретирует этот байт-код → обычный дизассемблер ничего не показывает.
В CTF:
UPX распаковывается одной командой
сложные обфускаторы и виртуализация — задача обычно решается через динамическую отладку и патчинг
Обход:
запускать и смотреть, что происходит в runtime
патчить проверки на целостность
искать точку, где флаг уже расшифрован
Итог
Главное, что нужно запомнить: антиотладка и антиреверс — это не «непробиваемая защита», а набор типовых проверок, которые почти всегда можно найти и выключить.
Ключевые слова и термины для запоминания: антиотладка, IsDebuggerPresent, ptrace, timing check, RDTSC, self-modifying code, integrity check, CRC32, packing, obfuscation, virtualization.
Эти знания помогают:
сразу понять, почему программа крашится в gdb при запуске
быстро найти и пропатчить проверку на отладчик
обойти timing check, поставив hardware breakpoint или патч
не пугаться, когда код меняет сам себя или сильно запутан
перейти от статического анализа к динамическому, когда видишь VM или packer
Начни с того, чтобы в каждой задаче проверять: крашится ли сразу в отладчике → есть ли странные jmp и crc → меняется ли код в runtime. Как только научишься это видеть и патчить — антиреверс-задачи превратятся в обычные reverse-задачи, только чуть дольше.
Дизассемблинг и анализ ассемблера
Дизассемблинг — это когда ты смотришь на машинный код (байты программы) и превращаешь его в понятные человеку инструкции ассемблера, чтобы понять, что именно делает программа. В CTF эта тема относится в первую очередь к категориям Reverse и Pwn. Чаще всего встречается в задачах, где дают бинарный файл и нужно найти пароль, алгоритм проверки, секретный ключ, точку перехода на флаг или понять логику защиты. Новичку это нужно освоить в первую очередь, потому что без умения читать ассемблер почти невозможно решать reverse-задачи и многие pwn-задачи. Когда научишься отличать mov от cmp, понимать, куда прыгает jmp и как передаются аргументы в функции — ты начнёшь видеть логику программы как на ладони и решать задачи гораздо быстрее.
Словарь терминов
дизассемблер
Программа, которая превращает байты машинного кода в читаемый ассемблер
ассемблер
Язык низкого уровня, где каждая строка — одна инструкция процессора
x86/x64
Самая распространённая архитектура процессоров в CTF (32 и 64 бита)
ARM
Архитектура мобильных устройств и многих embedded-систем
Thumb/Thumb-2
Укороченный режим инструкций в ARM (экономит место)
MIPS
Архитектура, часто встречается в роутерах и старых устройствах
RISC-V
Открытая современная архитектура, иногда дают в CTF
регистр
Очень быстрая память внутри процессора (rax, rdi, rsp и т.д.)
calling convention
Правила: как передавать аргументы в функцию и кто чистит стек
System V AMD64 ABI
Стандартные правила вызова функций в 64-бит Linux
mov
Инструкция копирования данных из одного места в другое
push / pop
Кладёт / берёт значение со стека
call / jmp
Вызов функции / безусловный прыжок
cmp / test
Сравнение двух значений (для условных переходов)
Архитектуры x86/x64, ARM (Thumb/Thumb-2), MIPS, AVR, RISC-V
В CTF чаще всего встречается x86_64 (64-битный Intel/AMD) — это стандарт для Linux/Windows задач. Все инструкции длинные, много регистров (rax, rbx, rcx, rdx, rsi, rdi, rbp, rsp, r8–r15).
ARM (особенно Thumb-2) — в задачах с мобильными приложениями, роутерами, IoT-устройствами. Инструкции короче, регистры называются r0–r12, sp, lr, pc.
MIPS — часто в задачах про сетевые устройства и старые консоли. Регистры $t0–$t9, $s0–$s7, $a0–$a3 для аргументов, $v0 для возврата.
RISC-V — новая открытая архитектура, иногда дают в современных задачах. Похожа на MIPS, но проще и чище.
AVR — микроконтроллеры (Arduino), редко, но бывает в embedded-задачах.
В 80% случаев в CTF будет x86_64 — начни с него. Если видишь ARM/MIPS — просто запомни, что регистры и вызовы функций другие, но логика та же.
Основные инструкции (mov, push/pop, call/jmp, cmp/test, add/sub)
Это самые частые инструкции, которые ты будешь видеть постоянно.
mov — копирует значение: mov rax, 1337 или mov [rbx], rax
push / pop — кладёт/берёт со стека: push rbp — сохраняет старый rbp
call — вызывает функцию: call puts@plt
jmp — прыжок: jmp loc_1234 (безусловный)
cmp / test — сравнение: cmp rax, rbx или test rax, rax (проверка на 0)
add / sub — сложение/вычитание: add rax, 8 или sub rsp, 0x20
je / jne / jz / jnz — условные прыжки после cmp/test
В CTF почти вся логика проверки пароля — это цепочка cmp + jne. Ищи их — там обычно и прячется флаг или ключ.
Calling conventions (cdecl, stdcall, fastcall, System V AMD64 ABI)
Calling convention — правила, как передавать аргументы в функцию и кто убирает их из стека после вызова.
cdecl (старый 32-бит): аргументы справа налево через стек, вызывающий чистит стек.
stdcall — аргументы через стек, вызываемая функция чистит стек (часто Windows API).
fastcall — первые аргументы в регистрах (ecx, edx), остальное стек.
System V AMD64 ABI (64-бит Linux):
первые 6 аргументов: rdi, rsi, rdx, rcx, r8, r9
дальше — стек
возвращаемое значение — rax
вызывающий чистит стек
В CTF почти всегда используется System V AMD64 ABI (Linux x64). Если видишь mov rdi, строка — это первый аргумент функции (puts, strcmp и т.д.).
Работа с регистрами (rax, rdi, rsi, rdx, rbp, rsp)
Регистры — это «карманы» процессора, где хранятся данные во время работы.
rax — чаще всего возвращаемое значение функции
rdi, rsi, rdx, rcx, r8, r9 — первые аргументы функций (по System V ABI)
rbp — указатель на текущий стековый фрейм (часто сохраняется)
rsp — указатель на вершину стека (push уменьшает, pop увеличивает)
rbx, r12–r15 — сохраняемые регистры (функция должна их восстановить)
В CTF чаще всего:
смотришь mov rdi, что-то — это аргумент 1
call strcmp@plt — сравнивает rdi и rsi
mov rax, 0 — функция вернула успех/неудачу
Запомни первые 6 регистров-аргументов — это ключ к пониманию вызовов.
Итог
Главное, что нужно запомнить: программа на ассемблере — это просто перемещение данных между регистрами и памятью, сравнения и прыжки по условиям. Всё остальное (пароли, ключи, флаги) прячется именно в этих mov, cmp и call.
Ключевые слова и термины для запоминания: дизассемблер, x86_64, ARM, регистр, mov, push/pop, call/jmp, cmp/test, System V AMD64 ABI, rdi, rsi, rdx, rax, rbp, rsp.
Эти знания помогают:
найти строку с флагом или «correct password» в дизассемблере
понять, где программа сравнивает твой ввод с правильным значением
увидеть, как передаются аргументы в strcmp, puts, system
посчитать смещение в стеке для переполнения буфера
быстро отличить обычную функцию от проверки ключа
Начни с x86_64 и основных инструкций mov/cmp/call — и большинство reverse-задач начального и среднего уровня перестанут быть страшными. Просто читай код сверху вниз, как книгу, и ищи, где программа принимает решение.
Эксплуатация уязвимостей в бинарниках
Эксплуатация уязвимостей в бинарниках — это когда ты находишь ошибку в программе и специально её используешь, чтобы заставить программу сделать то, что ты хочешь: прочитать флаг, запустить shell или обойти проверку. В CTF эта тема относится в первую очередь к категории Pwn (бинарная эксплуатация). Чаще всего такие уязвимости встречаются в задачах, где дают исполняемый файл и удалённый сервер, а нужно получить флаг или shell на этом сервере. Новичку понимать эти базовые уязвимости критически важно, потому что 70–80% всех pwn-задач строятся именно на них. Когда ты научишься распознавать buffer overflow, use-after-free и ROP — ты перестанешь бояться большинства задач и начнёшь их решать системно.
Словарь терминов
buffer overflow
Переполнение буфера — запись за пределы выделенной памяти
format string
Уязвимость в printf, когда пользовательский ввод идёт как форматная строка
integer overflow
Переполнение целого числа — значение выходит за пределы типа
use-after-free
Использование указателя после того, как память уже освобождена
double-free
Дважды освободить один и тот же кусок памяти
heap overflow
Переполнение буфера в динамической памяти (куче)
ROP
Return-Oriented Programming — цепочка коротких кусочков кода, заканчивающихся ret
one-gadget
Готовый адрес в libc, который сразу даёт shell без аргументов
ret2libc
Прыжок на функцию из libc (обычно system)
ret2dlresolve
Техника подделки структур линковки для вызова функции без знания адреса
Buffer overflow, format string, integer overflow
Buffer overflow Программа выделяет маленький массив (буфер), но позволяет записать в него больше данных. Лишние байты затирают то, что лежит дальше по памяти — чаще всего return address на стеке.
Format string Когда printf получает пользовательский ввод как форматную строку (printf(user_input)). Ты можешь читать со стека (%x, %s) или писать в память (%n).
Integer overflow Когда число выходит за пределы типа (например uint8_t = 255 + 1 → 0). Может привести к неправильному выделению памяти или неверной проверке длины.
В CTF это самые частые уязвимости начального и среднего уровня. Buffer overflow — для перезаписи return address. Format string — для утечки адресов или записи в GOT. Integer overflow — для выделения слишком маленького/большого буфера.
Use-after-free, double-free, heap overflow
Use-after-free (UAF) Программа освобождает память (free/delete), но продолжает использовать указатель на неё. Если память уже занята другим объектом — можно читать/писать в чужие данные.
Double-free Дважды вызвать free на один и тот же указатель. В tcache это кладёт chunk дважды → следующий malloc вернёт его снова → можно подменить указатели.
Heap overflow Переполнение буфера в выделенном куске кучи. Можно затереть метаданные следующего чанка или указатели внутри.
В CTF эти уязвимости дают мощные примитивы:
arbitrary alloc (malloc на любой адрес)
arbitrary write (перезапись GOT, __free_hook)
leak адресов libc
ROP, JOP, COP, blind ROP
ROP (Return-Oriented Programming) Когда NX запрещает исполнение стека — используем уже существующие кусочки кода (gadgets), заканчивающиеся ret. Собираем цепочку: pop rdi → "/bin/sh" → system.
JOP (Jump-Oriented Programming) Похоже на ROP, но вместо ret используем jmp или call.
COP (Call-Oriented Programming) Цепочка на основе call вместо ret.
Blind ROP Когда нет вывода (no leak) — угадываем адреса гаджетов через side-channel (время ответа, краши).
В CTF ROP — основная техника, когда переполнение есть, но NX включён. Blind ROP — для задач без вывода или с сильным ASLR.
One-gadget, ret2libc, ret2dlresolve
One-gadget Готовый адрес в libc, который одним прыжком вызывает execve("/bin/sh", NULL, NULL). Не нужно готовить аргументы — просто прыгаешь туда.
ret2libc Прыжок на функцию из libc (чаще всего system) + передача строки "/bin/sh".
ret2dlresolve Подделка структур динамической линковки → заставляем программу вызвать любую функцию без знания её адреса.
В CTF это самые надёжные способы получить shell:
one-gadget — если знаешь базу libc
ret2libc — если знаешь "/bin/sh" и system
ret2dlresolve — когда ASLR полный и нет утечек
Итог
Главное, что нужно запомнить: почти все pwn-задачи строятся на нескольких базовых уязвимостях (переполнение, UAF, format string), а защита (NX, ASLR, RELRO) обходится через ROP, ret2libc или one-gadget.
Ключевые слова и термины для запоминания: buffer overflow, format string, use-after-free, double-free, heap overflow, ROP, one-gadget, ret2libc, ret2dlresolve, blind ROP.
Эти знания помогают:
понять, что делать, когда видишь gets/scanf без проверки длины (buffer overflow → ROP)
использовать format string для утечки адресов и записи в GOT
комбинировать UAF + double-free для tcache poisoning и arbitrary alloc
прыгать на one-gadget или system после утечки libc
обходить NX и ASLR через ret2libc или ret2dlresolve
Начни с buffer overflow и ret2libc — это решает большинство задач начального и среднего уровня. Потом добавляй UAF и ROP — и сможешь браться за medium-hard задачи уверенно.
Инструменты для reverse engineering
Инструменты для reverse engineering — это специальные программы, которые помогают разбирать бинарные файлы, смотреть их код, отлаживать выполнение, искать строки, патчить байты и понимать, что делает программа без исходного кода. В CTF эта тема относится в первую очередь к категориям Reverse и Pwn. Такие инструменты используются почти в каждой reverse-задаче и во многих pwn-задачах, где нужно понять логику бинарника, найти флаг, обойти проверку или подготовить эксплойт. Новичку важно знать хотя бы основные из них, потому что без правильного инструмента ты можешь потратить часы на то, что решается за минуты. Когда разберёшься в базовом наборе (Ghidra + Cutter + Frida + pwntools) — ты начнёшь решать задачи в 3–5 раз быстрее и перестанешь бояться «непонятных» бинарников.
Словарь терминов
reverse engineering
Разбор готовой программы, чтобы понять, как она работает
дизассемблер
Программа, которая показывает машинный код в виде ассемблера
отладчик
Инструмент для запуска программы шаг за шагом и просмотра памяти
статический анализ
Разбор файла без его запуска
динамический анализ
Анализ программы во время её выполнения
патчинг
Изменение байтов в файле или в памяти программы
ROP
Техника сборки цепочки из коротких кусочков кода программы
gadget
Короткий кусок кода, заканчивающийся ret
Frida
Инструмент для динамического перехвата и изменения функций
checksec
Показывает, какие защиты включены в бинарнике (NX, Canary и т.д.)
Ghidra, IDA Pro, Binary Ninja, Radare2, Cutter
Это основные программы для статического анализа — смотрят на бинарник без запуска.
Ghidra — бесплатный инструмент от NSA, очень мощный и популярный в CTF. Показывает дизассемблер, декомпилятор (псевдо-C код), граф функций, строки, cross-references.
IDA Pro — коммерческий «золотой стандарт», но дорогой. Очень удобный интерфейс, отличный декомпилятор, плагины.
Binary Ninja — современный, красивый, с хорошим декомпилятором, средняя цена.
Radare2 — полностью бесплатный, консольный, очень гибкий, но сложный для новичков.
Cutter — графическая оболочка над Radare2, бесплатная и очень дружелюбная.
В CTF чаще всего используют Ghidra или Cutter — они бесплатные и покрывают 95% нужд. Открываешь бинарник → смотришь main → ищешь строки → следуешь по вызовам.
Frida, Objection, r2frida для динамического анализа
Frida — самый мощный инструмент для динамического анализа и патчинга. Позволяет вставить свой код в запущенную программу, перехватывать вызовы функций, менять аргументы, читать/писать память.
Objection — удобная обёртка над Frida, специально для мобильных приложений и iOS/Android.
r2frida — связка Radare2 + Frida: можно дизассемблировать и патчить прямо в запущенном процессе.
В CTF Frida используют, когда:
статический анализ сложный (VM, обфускация)
нужно перехватить strcmp/printf и увидеть аргументы
нужно патчить функцию на лету (заменить проверку на всегда true)
Pwntools, ROPgadget, Ropper, one_gadget
Pwntools — библиотека Python для написания эксплойтов. Умеет подключаться к серверу, паковать адреса, генерировать циклические паттерны, собирать ROP.
ROPgadget / Ropper — ищут гаджеты (pop rdi; ret, mov rax, rbx; ret) в бинарнике и libc.
one_gadget — показывает адреса в разных libc, где один гаджет сразу даёт shell.
В CTF это основной набор для pwn-задач: pwntools — для отправки payload ROPgadget — для поиска гаджетов one_gadget — когда нужна быстрая победа после утечки libc
Checksec, patchelf, seccomp-tools, ltrace/strace
checksec — показывает защиты бинарника: NX, Canary, PIE, RELRO, seccomp.
patchelf — меняет заголовки ELF: отключает NX, меняет entry point, добавляет библиотеки.
seccomp-tools — анализирует seccomp-фильтры (какие syscall разрешены).
ltrace / strace — показывают вызовы библиотечных функций и системные вызовы.
В CTF это первый шаг анализа:
checksec → понимаешь, можно ли ROP, есть ли canary
seccomp-tools → видишь, запрещён ли execve
ltrace/strace → смотришь, какие функции вызываются при вводе
Итог
Главное, что нужно запомнить: reverse и pwn-задачи решаются не только знаниями теории, но и умением быстро применять правильные инструменты для анализа и модификации бинарника.
Ключевые слова и термины для запоминания: Ghidra, Cutter, Frida, pwntools, ROPgadget, one_gadget, checksec, patchelf, seccomp-tools, dynamic analysis, static analysis.
Эти знания помогают:
за 30 секунд понять, какие защиты включены (checksec)
открыть бинарник и сразу увидеть main и строки (Ghidra/Cutter)
перехватить вызов strcmp и увидеть пароль (Frida)
собрать ROP-цепочку за минуту (pwntools + ROPgadget)
отключить NX или изменить точку входа для отладки (patchelf)
Начни с Ghidra/Cutter + checksec — это база для reverse. Потом добавь pwntools и Frida — и сможешь решать почти все pwn-задачи комфортно. Эти инструменты — твои руки в мире бинарников, без них всё будет в десятки раз дольше и сложнее.
Основы обратной разработки бинарных файлов
Обратная разработка (reverse engineering) бинарных файлов — это когда ты берёшь уже скомпилированную программу (без исходного кода) и пытаешься понять, что она делает, где лежат строки, функции, проверки и ключи. В CTF эта тема относится в первую очередь к категории Reverse. Чаще всего встречается в задачах, где дают исполняемый файл (Linux ELF, Windows PE, иногда macOS Mach-O) и просят найти флаг, ключ, пароль или обойти защиту. Новичку важно освоить основы, потому что почти все reverse-задачи начинаются именно с понимания структуры файла: где код, где строки, как программа запускается. Когда разберёшься в ELF/PE, секциях и endianness — ты перестанешь паниковать при виде непонятного бинарника и начнёшь быстро находить нужные куски.
Словарь терминов
ELF
Формат исполняемых файлов в Linux
PE
Формат исполняемых файлов в Windows (.exe, .dll)
Mach-O
Формат исполняемых файлов в macOS
.text
Секция, где лежит машинный код программы (инструкции)
.data
Секция с глобальными переменными, которым сразу дали значение
.bss
Секция для глобальных переменных без начального значения (нули)
.rodata
Секция только для чтения — строки, константы, сообщения
GOT
Таблица, куда записываются адреса функций из библиотек
PLT
Таблица-посредник для вызова функций из библиотек
точка входа
Адрес, с которого начинается выполнение программы
endianness
Порядок байтов в многобайтовых числах (little или big)
выравнивание
Данные лежат по адресам, кратным 4, 8 или 16 байтам
padding
Дополнительные нулевые байты для соблюдения выравнивания
динамические зависимости
Библиотеки (libc, libcrypto и т.д.), которые нужны программе
Формат ELF (Linux), PE (Windows), Mach-O (macOS) и их структура
ELF — самый частый формат в CTF (Linux). Начинается с magic bytes 7f 45 4c 46. Основные части:
ELF Header — битность, endianness, точка входа
Program Headers — какие куски грузить в память и куда
Section Headers — описание всех секций (.text, .data и т.д.)
PE — формат Windows. Начинается с MZ (4D 5A), потом PE-сигнатура. Главные части: DOS Header, PE Header, Optional Header, Section Table.
Mach-O — macOS/iOS. Встречается реже в CTF, но иногда дают. Имеет fat header (для нескольких архитектур) и сегменты (__TEXT, __DATA).
В CTF почти всегда дают 64-битный ELF на Linux — это стандарт. Знать структуру нужно, чтобы понять, где начинается код и где искать строки.
Секции .text, .data, .bss, .rodata, GOT, PLT, динамические зависимости
.text — исполняемый код (ассемблерные инструкции). Только чтение и выполнение.
.data — инициализированные глобальные переменные (int key = 1337;).
.bss — неинициализированные глобальные переменные (int password[100];). Заполняется нулями при запуске.
.rodata — только чтение: строки ("Correct password!"), константы, форматные строки.
GOT — Global Offset Table. Сюда динамический линкователь записывает адреса функций из библиотек.
PLT — Procedure Linkage Table. Посредник: программа вызывает PLT → PLT смотрит в GOT → GOT ведёт к настоящей функции.
Динамические зависимости — список библиотек (libc.so.6, libcrypto.so), которые нужны программе. Видно в readelf -d.
В CTF чаще всего:
в .rodata ищут строки с флагом, паролем, "wrong", "correct"
в .text ищут проверки и win-функции
GOT/PLT используют для понимания, какие функции вызываются
Заголовки, точки входа, векторы прерываний и startup-код
Заголовки — начало файла: ELF Header, PE Header и т.д. Содержат: архитектуру, точку входа, количество секций/сегментов.
Точка входа — адрес, с которого процессор начинает выполнять код (обычно _start в ELF).
Startup-код — первые инструкции (_start):
настраивает стек
вызывает __libc_start_main
передаёт argc, argv, envp
Вектор прерываний (в kernel-задачах) — таблица, куда записаны адреса обработчиков прерываний.
В CTF точка входа нужна, чтобы начать анализ с самого начала программы. Startup-код часто пропускают и сразу идут к main, но иногда флаг прячут именно в _start.
Endianness, выравнивание и padding в бинарниках
Endianness Little-endian (x86/x64): младший байт по младшему адресу. Число 0x41424344 лежит как 44 43 42 41. Big-endian: старший байт первым (редко в CTF).
Выравнивание Данные выравниваются по 4/8/16 байтам для скорости процессора. Например, 64-битный указатель должен лежать по адресу, кратному 8.
Padding Дополнительные байты (обычно 00) между полями структуры или секциями, чтобы соблюсти выравнивание.
В CTF это важно при:
упаковке адресов в payload (всегда little-endian на x64)
понимании, почему после строки идёт мусор
расчёте точного смещения в структурах
Итог
Главное, что нужно запомнить: любой бинарник — это просто структурированный набор байтов: заголовки говорят, где что лежит, секции делят код и данные, endianness и выравнивание влияют на чтение чисел.
Ключевые слова и термины для запоминания: ELF, PE, .text, .data, .bss, .rodata, GOT, PLT, точка входа, startup-код, little-endian, выравнивание, padding, динамические зависимости.
Эти знания помогают:
сразу понять, какой формат файла перед тобой (ELF/PE)
найти строки и сообщения об успехе/ошибке в .rodata
определить, где начинается main и какие функции вызываются через PLT
правильно упаковывать адреса в эксплойт (little-endian)
знать, откуда программа стартует и почему первые инструкции странные
Освой чтение ELF/PE заголовков и поиск секций — и большинство reverse-задач перестанут быть «чёрным ящиком», а станут понятной картой с нужными адресами и строками.
Отладка бинарных файлов
Отладка бинарных файлов — это когда ты запускаешь программу под специальным контролем и в любой момент можешь остановить её, посмотреть, что лежит в памяти, какие значения в регистрах и куда она сейчас собирается прыгнуть. В CTF эта тема относится в первую очередь к категориям Reverse и Pwn. Чаще всего отладка нужна в задачах, где дают непонятный бинарник и ты должен понять, как он проверяет пароль, где лежит флаг, как работает шифрование или как сработает переполнение. Новичку это один из самых важных навыков, потому что без отладки ты будешь просто гадать, а с хорошей отладкой — точно увидишь, что происходит внутри программы. Когда научишься ставить точки останова и смотреть стек/регистры — количество решаемых reverse и pwn-задач вырастет в разы.
Словарь терминов
отладчик
Программа, которая позволяет управлять выполнением другой программы
GDB
Самый популярный отладчик для Linux
GEF / Pwndbg
Удобные дополнения к GDB, специально для задач CTF
x64dbg
Мощный отладчик с красивым интерфейсом для Windows
breakpoint
Точка останова — программа остановится, когда дойдёт до этого места
hardware breakpoint
Точка останова, которая работает через специальный механизм процессора
watchpoint
Остановка, когда изменяется определённая ячейка памяти
стек
Область памяти, где хранятся локальные переменные и адреса возврата
регистры
Быстрые ячейки внутри процессора (rax, rdi, rsp и т.д.)
дамп памяти
Снимок того, что сейчас лежит в определённой области памяти
patching
Изменение кода программы прямо во время её выполнения
dynamic debugging
Отладка запущенной программы в реальном времени
GDB + GEF/Pwndbg, x64dbg, OllyDbg, WinDbg
GDB — главный отладчик для Linux. Сам по себе не очень удобный, но с плагинами GEF или Pwndbg становится просто идеальным для CTF.
GEF/Pwndbg показывают:
красивый стек, регистры, кучу
команды heap, vmmap, checksec, telescope
автоматический подсчёт offset’а до return address
x64dbg — лучший выбор для Windows. Очень красивый интерфейс, удобно ставить точки останова, смотреть память, патчить на лету.
OllyDbg — старый, но до сих пор встречается в задачах под Windows XP/7. WinDbg — мощный, но тяжёлый отладчик от Microsoft, чаще нужен для kernel-задач.
В CTF 80–90% задач — это Linux x64 → начинай с GDB + GEF/Pwndbg. Windows-задачи — сразу x64dbg.
Постановка точек останова (breakpoints), watchpoints, hardware breakpoints
Breakpoint (break / b) Обычная точка останова: программа остановится, как только дойдёт до этой инструкции. Самый частый способ: b main, b *0x4012ab (по адресу)
Hardware breakpoint Ставится на процессорном уровне (максимум 4 штуки). Используется, когда обычный break не срабатывает (например self-modifying code).
Watchpoint (watch / wa) Остановка, когда изменяется определённая переменная/ячейка памяти. Очень полезно, когда хочешь поймать момент, когда что-то записывается в canary, пароль или флаг.
В CTF чаще всего:
ставят break на main или на strcmp/puts
watch на переменную, в которую пишут ввод пользователя
hardware break, если программа сама себя меняет
Анализ стека, регистров, памяти и дампов
Когда программа остановилась на breakpoint, ты можешь посмотреть:
регистры — rax, rdi, rsi, rsp, rbp (info registers / context)
стек — что лежит сверху (x/20gx $rsp, telescope $rsp)
память — x/50xb 0x7fffffffde00 — посмотреть 50 байт в шестнадцатеричном виде
дамп — dump memory, чтобы сохранить кусок памяти в файл
Самые полезные команды:
x/s $rdi — посмотреть строку, на которую указывает rdi
x/20i $rip — показать 20 следующих инструкций
vmmap — где какие области памяти (стек, куча, libc, программа)
В CTF это нужно, чтобы:
увидеть, что лежит на стеке перед return address
понять, какой аргумент попал в strcmp
найти строку флага в памяти
Dynamic debugging и patching в runtime
Dynamic debugging — это когда ты отлаживаешь программу вживую, а не просто смотришь статический дизассемблер.
Patching в runtime — изменение кода или данных прямо во время выполнения:
меняешь jne на jmp (обход проверки)
заменяешь mov rax, 0 на mov rax, 1 (делаешь «успех»)
пишешь nop вместо проверки canary
В CTF это часто используют:
чтобы быстро проверить гипотезу (а что будет, если обойти эту проверку?)
чтобы сделать отладку проще (убрать надоедливый анти-отладочный код)
чтобы получить флаг, если программа его выводит только при определённом условии
Итог
Главное, что нужно запомнить: отладка — это не просто «посмотреть, что происходит», а самый быстрый способ понять логику программы, найти нужные адреса, строки и переменные.
Ключевые слова и термины для запоминания: отладчик, GDB, GEF, Pwndbg, x64dbg, breakpoint, hardware breakpoint, watchpoint, стек, регистры, дамп памяти, patching, dynamic debugging.
Эти знания позволяют:
мгновенно увидеть, куда прыгает программа после твоего ввода
посчитать точное смещение до return address через стек
найти адрес строки флага или правильного пароля
быстро проверить, сработает ли обход проверки (патч jne → jmp)
поймать момент изменения canary или важной переменной через watchpoint
Начни с GDB + GEF/Pwndbg, поставь break на main и просто ходи по программе шаг за шагом (nexti / stepi) — уже через 3–5 задач ты начнёшь чувствовать себя уверенно внутри любого бинарника.
Патчинг и модификация бинарных файлов
Патчинг и модификация бинарных файлов — это когда ты меняешь код или данные уже готовой программы (бинарника), чтобы заставить её вести себя по-другому: обойти проверку, пропустить условие, вернуть успех вместо неудачи или вывести флаг. В CTF эта тема относится в первую очередь к категориям Reverse и Pwn. Чаще всего встречается в задачах, где дают бинарник с защитой (проверка пароля, таймер, анти-отладка, ограничение системных вызовов), и проще всего решить её, изменив несколько байтов в файле или прямо во время выполнения. Новичку это один из самых быстрых и эффективных способов решать reverse-задачи, потому что часто достаточно поменять одну инструкцию — и задача решена за 5 минут. Когда научишься видеть, где программа проверяет условие и как это можно пропатчить — ты начнёшь решать многие задачи, которые другие обходят сложными эксплойтами или долгими реверсами.
Словарь терминов
патчинг
Изменение байтов в бинарном файле или в памяти во время работы
nop
Инструкция «ничего не делай» — просто пропуск места
jmp
Безусловный прыжок — заставляет программу всегда идти в нужную ветку
ret
Завершить функцию и вернуться назад
entry point
Адрес, с которого начинается выполнение программы
секция
Кусок файла, где лежат код, данные или строки
HxD
Простой hex-редактор для изменения байтов файла
runtime patching
Изменение кода или данных прямо во время выполнения программы
Frida
Инструмент для динамического патчинга и перехвата функций
ptrace
Механизм Linux, которым можно менять память запущенной программы
gdb
Отладчик, который позволяет патчить инструкции на лету
Патчинг инструкций (nop, jmp, ret, mov eax,1)
Самый простой и частый вид патчинга — замена одной или нескольких инструкций.
Популярные замены:
jne → jmp — вместо «если не равно — прыгай» делаем «всегда прыгай» (обход проверки пароля)
je → jmp — всегда считаем, что условие выполнено
любую проверку → nop — просто убираем инструкцию (например проверку длины ввода)
mov eax, 0 → mov eax, 1 — функция всегда возвращает успех
call exit → ret — программа не выходит, а продолжает работу
В CTF чаще всего патчат:
сравнение строк (strcmp → всегда возвращает 0)
проверку ключа или флага
таймер или счётчик попыток
Практика: находишь cmp + jne → меняешь jne на jmp → программа всегда проходит проверку.
Добавление/удаление секций, изменение entry point
Иногда нужно не просто заменить инструкцию, а серьёзно поменять структуру файла.
Изменение entry point — меняешь адрес, с которого программа стартует. Можно заставить её начинать сразу с твоего кода или с функции, которая выводит флаг.
Добавление секции — вставляешь новый кусок с кодом или строкой флага. Можно добавить секцию .text с shellcode и прыгнуть туда.
Удаление секции — убираешь ненужные части, чтобы файл стал меньше или обошёл проверку размера.
В CTF это используют:
когда программа проверяет хеш всего файла — меняешь entry point на функцию вывода флага
когда нужно вставить свой код, но места мало — добавляешь новую секцию
Инструменты: HxD, 010 Editor, Cutter, Binary Ninja
HxD и 010 Editor — hex-редакторы. Позволяют открыть бинарник как набор байтов и поменять любые значения вручную.
Cutter и Binary Ninja — современные реверс-инструменты с графическим интерфейсом. В них можно:
видеть дизассемблер и граф потока
патчить инструкции прямо в интерфейсе (nop, change to jmp)
менять entry point и добавлять секции
В CTF hex-редактор нужен для быстрого патча 1–2 байт. Cutter/Binary Ninja — когда нужно точно найти место для патча и сразу увидеть, как изменился граф.
Patching в runtime через Frida, ptrace, gdb
Runtime patching — изменение программы уже после запуска.
GDB — самый простой способ:
break на нужной инструкции
set (unsigned char) $rip = 0x90 (nop)
set $rax = 1
continue
ptrace — низкоуровневый способ в Linux. Можно прикрепиться к процессу и писать в его память.
Frida — самый удобный современный инструмент. Позволяет:
хукать функции
менять аргументы и возвращаемые значения
патчить память и код на лету
В CTF runtime patching используют:
когда статический патч ломает контрольные суммы
когда программа меняет себя сама
когда нужно обойти анти-отладку динамически
Итог
Главное, что нужно запомнить: почти любую защиту в reverse-задачах можно обойти, заменив одну-две инструкции или подменив поведение функции в runtime.
Ключевые слова и термины для запоминания: патчинг, nop, jmp, ret, entry point, runtime patching, Frida, ptrace, gdb, hex-редактор.
Эти знания помогают:
обойти проверку пароля, заменив jne на jmp
заставить программу всегда возвращать успех, пропатчив mov eax,0 на mov eax,1
быстро проверить гипотезу, пропатчив условие в gdb
добавить свой код или изменить точку входа, когда статический анализ не помогает
обойти анти-отладку и тайминги, патча динамически
Начни с простого: открывай бинарник в hex-редакторе, ищи cmp + j* → меняй на nop или jmp. Это решает 30–50% reverse-задач начального и среднего уровня быстрее всего. Потом добавляй gdb и Frida — и сможешь патчить почти всё, что угодно.
Работа с динамическими библиотеками
Работа с динамическими библиотеками — это понимание, как программа подключает и вызывает функции из внешних файлов (.so в Linux), где хранятся их адреса и как это можно использовать для обмана программы. В CTF эта тема относится в первую очередь к категории Pwn (бинарная эксплуатация). Чаще всего встречается в задачах, где нужно подменить поведение стандартных функций (puts, printf, system, free), обойти проверки или получить shell без прямого переполнения. Новичку важно её освоить, потому что почти все современные pwn-задачи используют GOT/PLT и дают возможность повлиять на разрешение символов. Когда разберёшься, как работает lazy binding и что можно подменить через LD_PRELOAD — сможешь решать задачи, которые выглядят «невозможными» без утечек адресов.
Словарь терминов
динамическая библиотека
Файл (.so), где лежат функции, которые программа берёт при запуске
GOT
Таблица, куда во время работы записываются настоящие адреса функций
PLT
Таблица-посредник: программа вызывает PLT, а PLT смотрит в GOT
lazy binding
Адрес функции ищется только в момент первого вызова
immediate binding
Все адреса функций ищутся сразу при запуске программы
LD_PRELOAD
Переменная окружения, которая заставляет программу загрузить нашу библиотеку первой
LD_AUDIT
Механизм, позволяющий следить за всеми вызовами функций
LD_LIBRARY_PATH
Путь, где программа ищет библиотеки в первую очередь
full RELRO
Защита: GOT становится только для чтения сразу после запуска
symbol resolution
Процесс поиска настоящего адреса функции по её имени
interposition
Подмена стандартной функции своей версией
__free_hook
Указатель в libc, который вызывается вместо free
GOT/PLT, lazy binding, immediate binding
Когда программа вызывает puts — она не знает заранее, где лежит настоящий puts в libc. Поэтому используется PLT (Procedure Linkage Table) — маленький кусок кода для каждой функции.
Первый вызов:
программа → PLT → GOT (пустой) → _dl_runtime_resolve
линковщик находит настоящий адрес puts → записывает его в GOT
дальше все вызовы идут быстро: программа → PLT → GOT → настоящий puts
Это называется lazy binding — ленивое связывание (адрес ищется только при первом вызове).
Immediate binding — все адреса ищутся сразу при запуске (LD_BIND_NOW=1). Тогда GOT заполняется до запуска main.
В CTF lazy binding — это шанс перезаписать GOT до первого вызова функции, чтобы puts прыгал на system.
LD_PRELOAD, LD_AUDIT, LD_LIBRARY_PATH для хукинга
LD_PRELOAD — самая мощная переменная. Если указать свою библиотеку — она загрузится раньше libc, и её функции с теми же именами будут использоваться вместо оригинальных.
Пример: пишем свою puts, которая вызывает system("/bin/sh") → программа думает, что вызывает puts, а на самом деле получает shell.
LD_LIBRARY_PATH — заставляет искать библиотеки в указанных папках в первую очередь. Можно подсунуть свою libc с изменёнными функциями.
LD_AUDIT — позволяет загружать библиотеку-аудитор, которая получает уведомления о каждом вызове функции. Можно перехватывать и менять аргументы/результаты.
В CTF это часто используют:
для подмены free на system
для перехвата strcmp и возврата 0 (пароль всегда верный)
для вызова запрещённых функций через хук
LD_BIND_NOW и full RELRO
LD_BIND_NOW — заставляет программу выполнить immediate binding (все адреса в GOT сразу). Очень часто включают в задачах, чтобы запретить GOT overwrite после запуска.
Full RELRO — GOT становится read-only сразу после заполнения. Перезаписать GOT уже нельзя — классический GOT overwrite не работает.
В CTF full RELRO заставляет переходить к другим техникам:
ret2dlresolve (подделка структур линковки)
перезапись __free_hook / __malloc_hook
one_gadget
использование LD_PRELOAD для хукинга до запуска main
Symbol resolution и interposition
Symbol resolution — процесс, когда программа ищет функцию по имени (puts, system, free) и записывает её адрес в GOT.
Interposition — когда мы заставляем программу использовать нашу функцию вместо оригинальной. Самый простой способ — LD_PRELOAD с библиотекой, где есть функция с тем же именем.
В CTF это часто комбинируют:
пишем библиотеку с __libc_start_main, которая сразу вызывает system
или с free, которая вызывает system("/bin/sh")
запускаем программу с LD_PRELOAD=./evil.so
Итог
Главное, что нужно запомнить: программа не знает заранее адреса функций из libc — она ищет их через GOT/PLT, и это можно использовать для подмены поведения.
Ключевые слова и термины для запоминания: GOT, PLT, lazy binding, immediate binding, LD_PRELOAD, LD_AUDIT, full RELRO, symbol resolution, interposition, __free_hook.
Эти знания помогают:
понять, почему puts вызывает system после подмены GOT
использовать LD_PRELOAD для перехвата free/puts/strcmp
обойти full RELRO через хукинг до запуска программы
быстро увидеть в checksec, можно ли перезаписать GOT
комбинировать GOT overwrite с lazy binding для простых задач
Начни с того, чтобы в каждой задаче смотреть checksec и понимать: lazy или immediate binding. Как только увидишь Partial RELRO и lazy — сразу думай про GOT overwrite или LD_PRELOAD. Это откроет тебе огромный пласт pwn-задач, которые выглядят сложными только на первый взгляд.
Работа с виртуальными машинами и кастомными интерпретаторами
Работа с виртуальными машинами и кастомными интерпретаторами — это когда автор задачи вместо обычного кода делает специальную «мини-программу внутри программы»: свой собственный язык команд (bytecode), который потом выполняет маленький интерпретатор. В CTF эта тема относится в первую очередь к категориям Reverse и Pwn. Чаще всего такие VM встречаются в задачах высокого уровня сложности, где обычный дизассемблер показывает непонятный набор jmp и mov, а флаг спрятан внутри логики виртуальной машины. Новичку важно хотя бы знать, что это существует и как выглядит, потому что без понимания «это не баг, это специально VM» ты просто закроешь задачу и пойдёшь дальше. Когда научишься отличать обычный код от VM и хотя бы грубо понимать, что делает каждая команда — ты начнёшь решать задачи, которые 90% участников считают «нечеловеческими».
Словарь терминов
виртуальная машина
Маленькая программа, которая выполняет не обычный код, а свой собственный набор команд
bytecode
Последовательность чисел/байт, которую понимает только эта виртуальная машина
интерпретатор
Часть программы, которая читает bytecode и выполняет нужные действия
opcode
Один байт или число, которое говорит VM, какую команду делать (push, add, jmp)
VM dispatch
Главный цикл интерпретатора: берёт следующий opcode и прыгает на его обработчик
custom VM
Виртуальная машина, которую автор написал специально для этой задачи
devirtualization
Процесс превращения bytecode обратно в понятный обычный код
symbolic execution
Способ анализа, когда вместо реальных чисел подставляют символы и следят за условиями
VMProtect
Популярный протектор, который превращает код в сложную виртуальную машину
obfuscation
Запутывание кода, чтобы его было трудно понять
Custom VM (PicoCTF VM, custom bytecode)
Custom VM — это когда автор сам придумывает набор команд (push 5, add, xor, jmp_if_zero и т.д.) и пишет маленький интерпретатор, который их выполняет. Обычно bytecode лежит в массиве байт или строке, а интерпретатор — это большой switch по opcode.
Признаки custom VM в CTF:
огромный switch/case на 100–300 случаев
массив байт с непонятными значениями (часто в .rodata)
много jmp внутри одной функции
странные операции с регистрами или стеком VM
В CTF custom VM часто используется для:
шифрования/дешифрования флага
проверки ключа
реализации крипто-алгоритма
Практика: находишь массив bytecode → смотришь обработчики каждого opcode → пишешь эмулятор на Python, который выполняет этот bytecode и выводит флаг.
Reverse engineering интерпретаторов (Python bytecode, Lua, Java bytecode)
Иногда VM не придумана с нуля, а взята готовая: Python bytecode (.pyc), Lua bytecode, Java .class.
Python bytecode Файлы .pyc содержат команды вроде LOAD_CONST, BINARY_ADD, CALL_FUNCTION. Интерпретатор — это eval_frame в CPython.
Lua bytecode Lua VM очень простая: 38 опкодов, стековая машина. Часто встречается в задачах с играми или скриптами.
Java bytecode .class-файлы с opcodes вроде aload_0, invokevirtual, iconst_1. JVM — стековая машина.
В CTF reverse таких интерпретаторов сводится к:
извлечению bytecode
поиску таблицы опкодов
написанию дизассемблера или эмулятора
VMProtect, CodeVirtualizer, Themida VM
Это коммерческие протекторы, которые превращают обычный код в сложную виртуальную машину.
Признаки:
код выглядит как огромный цикл с switch по огромному числу случаев
много push/pop внутри VM
странные вычисления с одним-двумя регистрами
анти-отладка внутри VM
В CTF такие VM встречаются редко в чистом виде, но часто как часть задачи (автор берёт идею VMProtect и делает свою упрощённую версию).
Практика: не пытаешься понять каждую инструкцию VM, а ищешь, где она расшифровывает/выводит флаг или проверяет ключ → патчишь или эмулируешь только эту часть.
Devirtualization и symbolic execution
Devirtualization — попытка превратить VM-код обратно в обычный читаемый код. Самый простой способ — написать эмулятор VM на Python и посмотреть, какие операции она делает над входными данными.
Symbolic execution — когда вместо реальных чисел подставляешь символы (переменные). Программа выполняется «символически» → можно увидеть все возможные пути и условия.
В CTF symbolic execution используют:
чтобы понять, какие значения приводят к флагу
чтобы автоматически найти ключ, который проходит все проверки
Практика: если VM небольшая — эмулируешь её вручную и смотришь, что она делает с вводом. Если большая — ищешь точку, где флаг уже в памяти, и патчишь.
Итог
Главное, что нужно запомнить: если код выглядит как огромный switch по странным байтам — это почти всегда виртуальная машина, а не баг или ошибка.
Ключевые слова и термины для запоминания: виртуальная машина, bytecode, интерпретатор, opcode, VM dispatch, custom VM, devirtualization, symbolic execution, VMProtect, obfuscation.
Эти знания помогают:
не пугаться, когда вместо нормального кода видишь 500 jmp и switch
сразу искать массив bytecode и главный цикл интерпретатора
понять, что флаг/ключ обрабатывается внутри VM, а не в обычном коде
написать простой эмулятор на Python для маленькой VM и получить флаг
быстро отличить настоящую VM от простой обфускации
Начни с того, чтобы в каждой непонятной reverse-задаче спросить себя: «а это не VM?» — и искать switch по байтам или массиву. Как только найдёшь — пиши эмулятор, и задача часто решается за 20–40 минут вместо недельного реверса.