
Ctf Reverse
- 35 installs
- 1.6k repo stars
- Updated July 19, 2026
- wgpsec/aboutsecurity
Helps with ai & agent building tasks.
About
ctf-reverse is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- ctf-reverse
- AI & Agent Building
- AI-coding skill
Ctf Reverse by the numbers
- 35 all-time installs (skills.sh)
- +3 installs in the week ending Jul 27, 2026 (Skillselion tracking)
- Ranked #8,740 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/wgpsec/aboutsecurity --skill ctf-reverseAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 35 |
|---|---|
| repo stars | ★ 1.6k |
| Last updated | July 19, 2026 |
| Repository | wgpsec/aboutsecurity ↗ |
What it does
Helps with ai & agent building tasks.
Files
CTF 逆向工程
深入参考
以下参考资料按需加载,根据识别出的具体方向选择对应文件:
- 静态分析工具(GDB/Ghidra/radare2/IDA/WASM/APK/.NET) → references/tools.md
- 动态分析工具(Frida/angr/lldb/Qiling/Triton) → references/tools-dynamic.md
- 高级工具(VMProtect/BinDiff/反混淆/Rizin/补丁) → references/tools-advanced.md
- 反分析对抗(Linux/Windows反调试/反VM/反DBI/代码完整性) → references/anti-analysis.md
- 语言特征(Python字节码/Lua/WASM/.NET IL/Solidity) → references/languages.md
- 编译语言(Go/Rust/Swift/Kotlin/C++/D/Nim) → references/languages-compiled.md
- 平台特定(嵌入式固件/macOS Mach-O/Android/Flutter/HarmonyOS) → references/platforms.md
- 语言与平台综合 → references/languages-platforms.md
- 逆向模式(校验/编码/迷宫/虚拟机/游戏引擎) → references/patterns.md
- CTF 逆向模式 Part1(自定义加密/矩阵/Brainfuck JIT) → references/patterns-ctf.md
- CTF 逆向模式 Part2(约束求解/侧信道/混淆变换) → references/patterns-ctf-2.md
- CTF 逆向模式 Part3(Z3布尔电路/滑窗Popcount/多步加密) → references/patterns-ctf-3.md
- 运行时修补与Oracle(反分析Patch/多阶段Shellcode) → references/patterns-runtime.md
- CTF反分析实战(SIGILL/SIGFPE/ptrace时序/花指令) → references/anti-analysis-ctf.md
- 仿真与侧信道工具(Qiling/Triton/Unicorn/指令计数) → references/tools-emulation.md
- 硬件与特殊架构(HD44780 LCD/RISC-V/FPGA/自定义ISA) → references/platforms-hardware.md
- 逆向实战笔记(二进制类型速查/反调试绕过/常用patch) → references/field-notes.md
---
分类决策树
拿到逆向题?
├─ 识别文件类型: file binary
│ ├─ ELF → GDB + Ghidra
│ ├─ PE/DLL → x64dbg + IDA
│ ├─ Mach-O → lldb + Hopper
│ ├─ APK → apktool + jadx (Flutter → Blutter)
│ ├─ .NET → dnSpy / ILSpy
│ ├─ Python .pyc → uncompyle6 / decompyle3
│ ├─ WASM → wasm-decompile / wasm2wat
│ └─ 未知 → binwalk + strings + hexdump
├─ 分析策略
│ ├─ 静态优先 → Ghidra反编译 → 找 main/check 函数
│ ├─ 动态辅助 → GDB断点 / Frida hook
│ ├─ 符号执行 → angr(自动探路)
│ └─ 反混淆 → D-810 / GOOMBA / Miasm
├─ 有反调试? → [references/anti-analysis.md](references/anti-analysis.md)
│ ├─ ptrace → LD_PRELOAD hook
│ ├─ /proc/self/status → 修改返回值
│ └─ 时间检测 → 跳过或 patch
└─ 常见模式
├─ 逐字符校验 → 逐字节爆破/约束求解
├─ 矩阵变换 → numpy/Z3 逆运算
├─ 自定义VM → 提取opcode表 → 反汇编
└─ 迷宫 → BFS/DFS 自动求解快速启动命令
# 基础分析
file binary && checksec binary
strings -n 6 binary | grep -iE "flag|pass|correct"
objdump -d binary | head -100
# GDB 调试
gdb -q binary -ex 'b main' -ex 'r'
# Ghidra 无头分析
analyzeHeadless /tmp/proj proj -import binary -postScript ExportDecompiled.java
# angr 符号执行
python3 -c "
import angr
p = angr.Project('./binary')
s = p.factory.entry_state()
sm = p.factory.simgr(s)
sm.explore(find=0x TARGET_ADDR)
print(sm.found[0].posix.dumps(0))
"常见反调试绕过
| 技术 | 绕过方法 |
|---|---|
| ptrace(PTRACE_TRACEME) | LD_PRELOAD hook 返回0 |
| /proc/self/status | 修改 TracerPid |
| 时间检测 | patch 掉 rdtsc/clock |
| IsDebuggerPresent (Win) | PEB.BeingDebugged = 0 |
工具速查
| 工具 | 用途 |
|---|---|
| Ghidra | 免费反编译器(支持多架构) |
| GDB + pwndbg | Linux 动态调试 |
| Frida | 运行时 hook(跨平台) |
| angr | 符号执行引擎 |
| dogbolt.org | 在线多反编译器对比 |
{
"skill_name": "ctf-reverse",
"evals": [
{
"id": 1,
"name": "elf-static-analysis",
"prompt": "CTF 逆向题给了一个 stripped ELF 二进制文件。运行后要求输入密码,输入正确打印 flag。请描述逆向分析步骤。",
"expected_output": "Ghidra/IDA 反编译 → 定位 main → 找到字符串比较或校验函数 → 提取或逆向校验逻辑得到正确密码",
"expectations": [
"Ghidra|IDA|radare2|反编译|反汇编",
"main|entry|入口点|定位主函数",
"strcmp|memcmp|比较|校验函数",
"strings|字符串|交叉引用|xref",
"GDB|断点|动态调试|运行时"
],
"required_terms": [
"IDA",
"GDB",
"Ghidra"
]
},
{
"id": 2,
"name": "anti-debug-bypass",
"prompt": "CTF 逆向题的二进制文件在 GDB 下运行时行为异常(直接退出或输出错误结果)。怀疑有反调试保护。请描述如何绕过。",
"expected_output": "识别并绕过反调试:ptrace 检测用 LD_PRELOAD hook,时间检测 patch 掉 rdtsc,/proc/self/status 检查修改返回值",
"expectations": [
"ptrace|PTRACE_TRACEME|反调试|anti-debug",
"LD_PRELOAD|hook|劫持|返回0",
"patch|NOP|跳过|修改二进制",
"/proc/self/status|TracerPid|检测调试器",
"时间检测|rdtsc|clock|time check"
],
"required_terms": [
"PTRACE_TRACEME",
"LD_PRELOAD",
"/proc/self/status"
]
},
{
"id": 3,
"name": "angr-symbolic-execution",
"prompt": "CTF 逆向题有一个复杂的校验函数,输入 32 个字符,经过多轮变换后与硬编码值比较。手动逆向太复杂。请描述使用自动化工具求解的方法。",
"expected_output": "使用 angr 符号执行:设置入口状态,标记 find 地址(成功路径)和 avoid 地址(失败路径),自动求解输入",
"expectations": [
"angr|符号执行|symbolic execution|自动化",
"find|成功地址|目标地址|correct路径",
"avoid|失败地址|wrong路径|排除",
"stdin|posix.dumps(0)|提取输入|求解结果",
"SimulationManager|simgr|explore|状态管理"
],
"required_terms": [
"posix.dumps(0)",
"SimulationManager",
"angr"
]
},
{
"id": 4,
"name": "custom-vm-reverse",
"prompt": "CTF 逆向题是一个自定义虚拟机(VM):程序加载一段字节码,有 fetch-decode-execute 循环。请描述如何逆向这个 VM 并提取 flag。",
"expected_output": "分析 VM 架构:提取 opcode 映射表(switch-case),编写反汇编器将字节码转为可读指令,分析程序逻辑",
"expectations": [
"opcode|操作码|指令集|switch-case",
"反汇编器|disassembler|字节码|bytecode",
"寄存器|register|栈|stack|内存模型",
"fetch|decode|execute|执行循环",
"逻辑|校验|加密|flag提取|逆运算"
],
"required_terms": [
"opcode",
"switch-case",
"disassembler"
]
},
{
"id": 5,
"name": "dotnet-apk-reverse",
"prompt": "CTF 逆向题给了一个 .NET 可执行文件 challenge.exe。请描述如何反编译和分析它。",
"expected_output": "使用 dnSpy/ILSpy 反编译 .NET 程序,得到近乎源码级的 C# 代码,直接分析逻辑",
"expectations": [
"dnSpy|ILSpy|dotPeek|.NET反编译",
"C#|IL|MSIL|中间语言",
"反编译|decompile|源码级|可读代码",
"混淆|ConfuserEx|de4dot|反混淆",
"调试|断点|修改|运行时分析"
],
"required_terms": [
"dnSpy",
"ILSpy",
"dotPeek"
]
}
]
}
{
"skill_id": "ctf-reverse",
"recall_tests": [
{
"id": 1,
"type": "keyword_positive",
"description": "核心关键词",
"keywords": [
"reverse",
"逆向",
"ghidra",
"gdb"
]
},
{
"id": 2,
"type": "keyword_positive",
"description": "工具搜索",
"keywords": [
"ida",
"frida",
"angr",
"反调试"
]
},
{
"id": 3,
"type": "keyword_negative",
"description": "不应被取证召回",
"keywords": [
"volatility",
"pcap"
]
}
],
"llm_tests": [
{
"id": 1,
"name": "ctf-reverse-scenario",
"scenario": "CTF 逆向题给了一个 stripped 的 ELF 二进制文件,需要找到正确的序列号。请搜索逆向分析方法论。",
"max_rounds": 2,
"expect_tool_calls": [
{
"tool": "list_skills",
"keyword_contains": "reverse|逆向|binary|ida"
},
{
"tool": "read_skill",
"id": "ctf-reverse"
}
]
}
]
}
CTF Reverse - Anti-Analysis CTF Writeups
CTF-specific anti-analysis techniques: signal-handler tricks, instruction-trace inversion, call-less function chaining, parent-patched child binary dumping. For the core anti-analysis taxonomy (Linux/Windows anti-debug, anti-VM, anti-DBI, code integrity, anti-disassembly), see anti-analysis.md.
Table of Contents
- SIGILL Handler for Execution Mode Switching (Hack.lu 2015)
- SIGFPE Signal Handler Side-Channel via strace Counting (PlaidCTF 2017)
- Instruction Trace Inversion with Keystone and Unicorn (MeePwn CTF 2017)
- Call-less Function Chaining via Stack Frame Manipulation (THC CTF 2018)
- Parent-Patched Child Binary Dump via strace process_vm_writev (Google CTF Quals 2018)
---
SIGILL Handler for Execution Mode Switching (Hack.lu 2015)
Binaries may install SIGILL (illegal instruction) handlers to switch between x86 and x86-64 execution modes or implement custom opcode dispatch:
1. Signal registration: signal(SIGILL, handler) installs a callback for illegal instruction exceptions 2. Mode switching: The handler modifies the saved instruction pointer or segment registers to switch between 32-bit and 64-bit code 3. Custom opcodes: Invalid x86 instructions trigger the handler, which interprets operand bytes as custom VM opcodes
// Signal handler decodes "illegal" instructions as custom opcodes
void sigill_handler(int sig, siginfo_t *info, void *ucontext) {
ucontext_t *ctx = (ucontext_t *)ucontext;
unsigned char *pc = (unsigned char *)ctx->uc_mcontext.gregs[REG_RIP];
// Decode custom opcode from bytes at PC
// Advance PC past the custom instruction
ctx->uc_mcontext.gregs[REG_RIP] += opcode_length;
}Key insight: If a binary installs signal handlers for SIGILL/SIGSEGV/SIGTRAP early in execution, suspect custom instruction dispatch. Trace signal deliveries with strace -e signal or set GDB to not intercept: handle SIGILL nostop pass.
---
SIGFPE Signal Handler Side-Channel via strace Counting (PlaidCTF 2017)
Binary uses SIGFPE signal handlers for control flow, making static analysis unreliable. Brute-force by counting SIGFPE signals via strace — correct input characters produce more signals.
# Count SIGFPE signals per input character guess
for c in {a..z} {A..Z} {0..9}; do
count=$(echo -n "${c}AAAAAAA" | strace -e signal=SIGFPE ./binary 2>&1 | grep -c SIGFPE)
echo "$c: $count"
done
# Character producing the most SIGFPEs is correct
# Repeat for each position, extending the known prefixKey insight: Signal handlers (SIGFPE, SIGSEGV, SIGILL) create implicit control flow invisible to static analysis. The number of signals raised correlates with validation progress. Counting signals via strace -e signal=SIGFPE turns opaque signal-based validation into a measurable side-channel for character-by-character brute-force.
---
Instruction Trace Inversion with Keystone and Unicorn (MeePwn CTF 2017)
UPX-packed binary applies a sequence of arithmetic-only transforms (sub, add, xor, rol, ror) to the flag. No memory side-effects — purely register arithmetic. IDAPython traces non-jump instructions, the sequence is then inverted to recover the flag.
Inversion rules:
- Reverse the instruction sequence (last instruction first)
- Swap inverse pairs:
add ↔ sub,rol ↔ ror,xoris self-inverse
# IDAPython: collect non-jump instructions in the obfuscated routine
import idaapi, idc
def trace_transforms(start_ea, end_ea):
instructions = []
ea = start_ea
while ea < end_ea:
mnem = idc.print_insn_mnem(ea)
if mnem not in ('jmp', 'je', 'jne', 'call', 'ret'):
instructions.append((ea, mnem, idc.print_operands(ea)))
ea = idc.next_head(ea)
return instructions
transforms = trace_transforms(0x401000, 0x401200)
# Invert: reverse order, swap add/sub and rol/ror
inverse_map = {'add': 'sub', 'sub': 'add', 'rol': 'ror', 'ror': 'rol', 'xor': 'xor'}
inverted = [(mnem, op) for (_, mnem, op) in reversed(transforms)]
inverted = [(inverse_map.get(m, m), op) for m, op in inverted]# Assemble inverted instructions with Keystone, emulate with Unicorn
from keystone import *
from unicorn import *
from unicorn.x86_const import *
ks = Ks(KS_ARCH_X86, KS_MODE_64)
uc = Uc(UC_ARCH_X86, UC_MODE_64)
asm_src = '\n'.join(f'{mnem} {op}' for mnem, op in inverted)
encoding, _ = ks.asm(asm_src)
CODE_BASE = 0x400000
uc.mem_map(CODE_BASE, 0x10000)
uc.mem_write(CODE_BASE, bytes(encoding))
# Set initial register state to the observed output value
uc.reg_write(UC_X86_REG_RAX, known_output)
uc.emu_start(CODE_BASE, CODE_BASE + len(encoding))
flag_bytes = uc.reg_read(UC_X86_REG_RAX).to_bytes(8, 'little')PEB anti-debug note: If the binary reads PEB.BeingDebugged and uses it to select between two comparison target values, the traced instructions under IDAPython may use the debug-mode target. Patch BeingDebugged to 0 before tracing, or identify both branches and use the non-debug target value.
Key insight: Arithmetic-only obfuscation (no memory writes) is fully reversible by tracing, inverting the instruction sequence, and swapping inverse operations. PEB anti-debug can silently change comparison targets — always verify which branch is taken.
References: MeePwn CTF 2017
---
Call-less Function Chaining via Stack Frame Manipulation (THC CTF 2018)
Pattern: Binary hides function calls by building a linked list of function pointers on the stack, then modifying saved RBP and return addresses so leave; ret instructions chain through the list without any explicit CALL instructions. IDA fails to decompile because push/pop are unbalanced and function boundaries cannot be determined.
Each function in the chain: 1. Pushes operands and the next function's address onto the stack 2. Sets saved RBP to point to the next stack frame 3. Sets the return address to the next function 4. leave restores RSP from RBP (moving to next frame), ret jumps to the next function
# Reversed processing chain (each function applied via leave/ret):
def reverse_processing(byte):
res = byte | 0x80 # OR 0x80
res = res ^ 0xCA # XOR 0xCA
res = (res + 66) & 0xFF # ADD 66
res = res ^ 0xCA # XOR 0xCA (repeated)
res = (res + 66) & 0xFF
res = res ^ 0xCA
res = (res + 66) & 0xFF
res = res ^ 0xFE # XOR 0xFE (final)
return res
# Apply in reverse order, then reverse the character sequenceKey insight: By manipulating saved RBP to point to the next stack frame and saved RIP to the next function, leave; ret chains through functions without any call instructions. Disassemblers that track call/ret balance fail to identify function boundaries. Patch each function body individually for IDA to handle them.
Detection: Binary with many small code blocks ending in leave; ret but no corresponding call instructions. Stack contains interleaved function pointers and data. IDA shows "stack frame is too big" or fails to create functions.
References: THC CTF 2018
---
Parent-Patched Child Binary Dump via strace process_vm_writev (Google CTF Quals 2018)
Pattern (Keygenme): The binary forks. The child is stub code full of int3 (0xcc) traps. The parent uses ptrace + process_vm_writev to write the real instructions into the child right before each trap fires, then stepping continues. Static analysis of the child sees only junk; dynamic analysis in a single-process debugger misses the parent's writes.
Bypass — let strace do the work:
# Record every process_vm_writev the parent performs, including full iov contents.
strace -f -e trace=process_vm_writev -e write=all -o trace.log ./keygenme
# Each entry looks like:
# process_vm_writev(child_pid, [{iov_base="\x48\x89\xe5...", iov_len=12}], 1,
# [{iov_base=0x400c80, iov_len=12}], 1, 0) = 12Parse the log to extract (remote_addr, bytes) pairs and emit an IDA patch_bytes script:
import re, pathlib
patches = []
pattern = re.compile(
r'process_vm_writev\(\d+, \[{iov_base="([^"]+)", iov_len=(\d+)}\].*?\[{iov_base=(0x[0-9a-f]+)',
)
for m in pattern.finditer(pathlib.Path('trace.log').read_text()):
data = m.group(1).encode('latin1').decode('unicode_escape').encode('latin1')
addr = int(m.group(3), 16)
patches.append((addr, data))
with open('patch.py', 'w') as fh:
for addr, data in patches:
for i, b in enumerate(data):
fh.write(f'patch_byte({addr + i:#x}, {b:#x})\n')Load patch.py in IDA (File → Script file) to apply every parent-written instruction, turning the trap-riddled child into a fully readable binary. With the patched binary, the crypto routine is a plain loop — black-box the irreversible portion and replace the final strcmp with a leak of the expected value.
Key insight: Any anti-analysis scheme that uses a ptracer to rewrite the tracee's text is transparent to strace on the parent. process_vm_writev calls carry both the target address and the bytes, so a one-pass strace run is enough to dump the real code. The same trick works for self-modifying packers that use ptrace(PTRACE_POKEDATA) or write() into /proc/<pid>/mem.
References: Google CTF Quals 2018 — writeup 10330
---
CTF Reverse - Anti-Analysis Techniques & Bypasses
Comprehensive reference for anti-debugging, anti-VM, anti-DBI, and integrity-check techniques encountered in CTF challenges, with practical bypasses.
Table of Contents
- Linux Anti-Debug (Advanced)
- ptrace-Based
- /proc Filesystem Checks
- Timing-Based Detection
- Signal-Based Anti-Debug
- Syscall-Level Evasion
- Windows Anti-Debug (Advanced)
- PEB-Based Checks
- NtQueryInformationProcess
- Heap Flags
- TLS Callbacks
- Hardware Breakpoint Detection
- Software Breakpoint Detection (INT3 Scanning)
- Exception-Based Anti-Debug
- NtSetInformationThread (Thread Hiding)
- Anti-VM / Anti-Sandbox
- CPUID Hypervisor Bit
- MAC Address / Hardware Fingerprinting
- Timing-Based VM Detection
- File / Registry Artifacts
- Resource Checks (CPU Count, RAM, Disk)
- Anti-DBI (Dynamic Binary Instrumentation)
- Frida Detection
- Pin/DynamoRIO Detection
- Code Integrity / Self-Hashing
- Anti-Disassembly Techniques
- Opaque Predicates
- Junk Bytes / Overlapping Instructions
- Jump-in-the-Middle
- Function Chunking / Scattered Code
- Control Flow Flattening (Advanced)
- Mixed Boolean-Arithmetic (MBA) Identification & Simplification
- SIGILL Handler for Execution Mode Switching (Hack.lu 2015)
- Comprehensive Bypass Strategies
- Universal Bypass Checklist
- Layered Anti-Debug (Real-World Pattern)
- Quick Reference: Check to Bypass
---
Linux Anti-Debug (Advanced)
ptrace-Based
Self-ptrace (most common):
if (ptrace(PTRACE_TRACEME, 0, 0, 0) == -1) exit(1); // Already traced = debugger attachedBypasses:
# 1. LD_PRELOAD (see patterns.md for full hook)
LD_PRELOAD=./hook.so ./binary
# 2. Patch with pwntools
python3 -c "
from pwn import *
elf = ELF('./binary', checksec=False)
elf.asm(elf.symbols.ptrace, 'xor eax, eax; ret')
elf.save('patched')
"
# 3. GDB: catch the syscall
gdb ./binary
(gdb) catch syscall ptrace
(gdb) run
# When it stops at ptrace:
(gdb) set $rax = 0
(gdb) continue
# 4. Kernel config (requires root)
echo 0 > /proc/sys/kernel/yama/ptrace_scopeDouble-ptrace pattern:
// Fork child to ptrace parent — blocks all other debuggers
pid_t child = fork();
if (child == 0) {
ptrace(PTRACE_ATTACH, getppid(), 0, 0);
// Child sits in waitpid loop, keeping parent traced
} else {
// Parent continues with real logic
}Bypass: Kill the watchdog child process, then attach debugger.
/proc Filesystem Checks
// TracerPid check
FILE *f = fopen("/proc/self/status", "r");
// Looks for "TracerPid:\t0" — non-zero means debugger
// /proc/self/exe link check (some debuggers change this)
readlink("/proc/self/exe", buf, sizeof(buf));
// /proc/self/maps — check for debugger libraries
grep("frida", "/proc/self/maps");Bypasses:
# 1. LD_PRELOAD fopen/fread to fake /proc contents
# 2. Mount namespace isolation
unshare -m bash -c 'mount --bind /dev/null /proc/self/status && ./binary'
# 3. GDB: set breakpoint at fopen, change filename argument
(gdb) b fopen
(gdb) run
(gdb) set {char[20]} $rdi = "/dev/null"
(gdb) continueTiming-Based Detection
// rdtsc (CPU timestamp counter)
uint64_t start = __rdtsc();
// ... code ...
uint64_t delta = __rdtsc() - start;
if (delta > THRESHOLD) exit(1); // too slow = debugger
// clock_gettime
struct timespec ts1, ts2;
clock_gettime(CLOCK_MONOTONIC, &ts1);
// ... code ...
clock_gettime(CLOCK_MONOTONIC, &ts2);
// gettimeofday
struct timeval tv1, tv2;
gettimeofday(&tv1, NULL);Bypasses:
# 1. Frida hook (see tools-dynamic.md for clock_gettime hook)
# 2. GDB: skip rdtsc by patching with constant
(gdb) set {unsigned char[2]} 0x401234 = {0x90, 0x90} # NOP the rdtsc
# 3. Pin tool to fix TSC reads
# 4. faketime library
LD_PRELOAD=/usr/lib/faketime/libfaketime.so.1 FAKETIME="2024-01-01" ./binarySignal-Based Anti-Debug
// SIGTRAP handler — INT3 under debugger is caught by debugger, not handler
signal(SIGTRAP, handler);
__asm__("int3");
// If handler runs: no debugger. If debugger catches: debugged.
// SIGALRM timeout — kill self if analysis takes too long
signal(SIGALRM, kill_handler);
alarm(5);
// SIGSEGV handler that does real work (see patterns.md for MBA pattern)
signal(SIGSEGV, real_logic_handler);
*(int*)0 = 0; // deliberate crash → handler runs real codeBypasses:
# GDB: pass signals to program instead of handling them
(gdb) handle SIGTRAP nostop pass
(gdb) handle SIGALRM ignore
(gdb) handle SIGSEGV nostop pass
# For alarm-based: patch alarm() to return immediatelySyscall-Level Evasion
// Direct syscall instead of libc — bypasses LD_PRELOAD hooks
long ret;
asm volatile("syscall" : "=a"(ret) : "a"(101), "D"(0), "S"(0), "d"(0), "r"(0));
// Syscall 101 = ptrace on x86_64Bypass: Must patch the binary itself or use ptrace to intercept at syscall level.
# GDB: catch syscall
(gdb) catch syscall 101
(gdb) commands
> set $rax = 0
> continue
> end---
Windows Anti-Debug (Advanced)
PEB-Based Checks
// BeingDebugged flag (offset 0x2 in PEB)
bool debugged = NtCurrentPeb()->BeingDebugged;
// NtGlobalFlag (offset 0x68/0xBC in PEB)
// When debugger: FLG_HEAP_ENABLE_TAIL_CHECK | FLG_HEAP_ENABLE_FREE_CHECK | FLG_HEAP_VALIDATE_PARAMETERS = 0x70
DWORD flags = *(DWORD*)((BYTE*)NtCurrentPeb() + 0xBC); // 64-bit offset
if (flags & 0x70) exit(1);Bypass (x64dbg):
# ScyllaHide plugin auto-patches PEB fields
# Manual: dump PEB, zero BeingDebugged and NtGlobalFlagNtQueryInformationProcess
// ProcessDebugPort (0x7)
DWORD_PTR debugPort = 0;
NtQueryInformationProcess(GetCurrentProcess(), 7, &debugPort, sizeof(debugPort), NULL);
if (debugPort != 0) exit(1);
// ProcessDebugObjectHandle (0x1E)
HANDLE debugObj = NULL;
NTSTATUS status = NtQueryInformationProcess(GetCurrentProcess(), 0x1E, &debugObj, sizeof(debugObj), NULL);
if (status == 0) exit(1); // STATUS_SUCCESS means debugger present
// ProcessDebugFlags (0x1F) — returns inverse: 0 = debugger present
DWORD noDebug = 0;
NtQueryInformationProcess(GetCurrentProcess(), 0x1F, &noDebug, sizeof(noDebug), NULL);
if (noDebug == 0) exit(1);Bypass: Hook NtQueryInformationProcess to return fake values, or use ScyllaHide.
Heap Flags
// Process heap has debug flags when debugger attached
PHEAP heap = (PHEAP)GetProcessHeap();
// Flags at offset 0x70 (64-bit): should be HEAP_GROWABLE (0x2)
// ForceFlags at offset 0x74: should be 0
if (heap->Flags != 0x2 || heap->ForceFlags != 0) exit(1);TLS Callbacks
Key technique: TLS (Thread Local Storage) callbacks execute BEFORE main() / entry point.
// Registered in PE header's TLS directory
void NTAPI TlsCallback(PVOID DllHandle, DWORD Reason, PVOID Reserved) {
if (Reason == DLL_PROCESS_ATTACH) {
if (IsDebuggerPresent()) {
ExitProcess(1); // Kills process before main runs
}
}
}
#pragma comment(linker, "/INCLUDE:_tls_used")
#pragma data_seg(".CRT$XLB")
PIMAGE_TLS_CALLBACK callbacks[] = { TlsCallback, NULL };Detection in IDA/Ghidra: Check PE TLS Directory → AddressOfCallBacks. Functions listed there run before EP.
Bypass: Set breakpoint on TLS callback in x64dbg (Options → Events → TLS Callbacks), or patch the TLS directory entry.
Hardware Breakpoint Detection
// Read debug registers via GetThreadContext
CONTEXT ctx;
ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;
GetThreadContext(GetCurrentThread(), &ctx);
if (ctx.Dr0 || ctx.Dr1 || ctx.Dr2 || ctx.Dr3) exit(1);
// Also via exception handler: deliberate exception, check DR regs in handlerBypass:
# x64dbg: use software breakpoints instead, or hook GetThreadContext
# Frida: hook GetThreadContext to zero DR registersSoftware Breakpoint Detection (INT3 Scanning)
// CRC / hash check over code section
unsigned char *code = (unsigned char*)function_addr;
uint32_t checksum = 0;
for (int i = 0; i < code_size; i++) {
checksum += code[i];
if (code[i] == 0xCC) exit(1); // INT3 = software breakpoint
}
if (checksum != EXPECTED_CHECKSUM) exit(1);Bypass: Use hardware breakpoints (DR0-DR3) instead of software breakpoints. Or hook the scanning function.
Exception-Based Anti-Debug
// UnhandledExceptionFilter — under debugger, filter is NOT called
SetUnhandledExceptionFilter(handler);
RaiseException(EXCEPTION_ACCESS_VIOLATION, 0, 0, NULL);
// If handler runs: no debugger
// If debugger catches: debugger present
// INT 2D — debugger single-step anomaly
__asm { int 2dh } // Debugger silently consumes the exception
// If execution continues: debugger presentNtSetInformationThread (Thread Hiding)
// Hide thread from debugger — stops all debug events
typedef NTSTATUS(NTAPI *pNtSIT)(HANDLE, ULONG, PVOID, ULONG);
pNtSIT NtSIT = (pNtSIT)GetProcAddress(GetModuleHandle("ntdll"), "NtSetInformationThread");
NtSIT(GetCurrentThread(), 0x11 /*ThreadHideFromDebugger*/, NULL, 0);
// After this, debugger won't see breakpoints or exceptions from this threadBypass: Hook NtSetInformationThread to ignore class 0x11, or patch the call.
---
Anti-VM / Anti-Sandbox
CPUID Hypervisor Bit
int regs[4];
__cpuid(regs, 1);
if (regs[2] & (1 << 31)) { // ECX bit 31 = hypervisor present
exit(1);
}
// Hypervisor brand string
__cpuid(regs, 0x40000000);
char brand[13] = {0};
memcpy(brand, ®s[1], 12);
// "VMwareVMware", "Microsoft Hv", "KVMKVMKVM", "XenVMMXenVMM"Bypass: Patch cpuid results or use LD_PRELOAD to hook wrapper functions.
MAC Address / Hardware Fingerprinting
Known VM MAC prefixes:
VMware: 00:0C:29, 00:50:56
VirtualBox: 08:00:27
Hyper-V: 00:15:5D
Parallels: 00:1C:42
QEMU: 52:54:00Timing-Based VM Detection
// VM exits on privileged instructions are measurably slower
uint64_t start = __rdtsc();
__cpuid(regs, 0); // Forces VM exit
uint64_t delta = __rdtsc() - start;
if (delta > 500) { /* likely VM */ }File / Registry Artifacts
Files: C:\Windows\System32\drivers\vm*.sys, vbox*.dll, VBoxService.exe
Registry: HKLM\SOFTWARE\VMware, Inc.\VMware Tools
Services: VMTools, VBoxService
Processes: vmtoolsd.exe, VBoxTray.exe, qemu-ga.exe
Linux: /sys/class/dmi/id/product_name contains "VirtualBox"|"VMware"
dmesg | grep -i "hypervisor detected"Resource Checks (CPU Count, RAM, Disk)
// Sandboxes typically have minimal resources
SYSTEM_INFO si;
GetSystemInfo(&si);
if (si.dwNumberOfProcessors < 2) exit(1);
MEMORYSTATUSEX ms;
ms.dwLength = sizeof(ms);
GlobalMemoryStatusEx(&ms);
if (ms.ullTotalPhys < 2ULL * 1024 * 1024 * 1024) exit(1); // < 2GB RAM
// Disk size check (< 60GB = sandbox)
GetDiskFreeSpaceEx("C:\\", NULL, &total, NULL);Bypass: Use a VM configured with adequate resources (4+ CPUs, 8GB+ RAM, 100GB+ disk).
---
Anti-DBI (Dynamic Binary Instrumentation)
Frida Detection
// 1. Check /proc/self/maps for frida-agent
FILE *f = fopen("/proc/self/maps", "r");
while (fgets(line, sizeof(line), f)) {
if (strstr(line, "frida") || strstr(line, "gadget")) exit(1);
}
// 2. Check for Frida's default port (27042)
int sock = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr = {.sin_family=AF_INET, .sin_port=htons(27042), .sin_addr.s_addr=inet_addr("127.0.0.1")};
if (connect(sock, (struct sockaddr*)&addr, sizeof(addr)) == 0) exit(1);
// 3. Check for inline hooks (function prologue modification)
// Compare first bytes of libc functions against expected values
unsigned char *strcmp_bytes = (unsigned char *)strcmp;
if (strcmp_bytes[0] == 0xE9 || strcmp_bytes[0] == 0xFF) exit(1); // JMP = hooked
// 4. Thread name check
// Frida creates threads with names like "gmain", "gdbus", "frida-*"
DIR *dir = opendir("/proc/self/task");
while ((entry = readdir(dir))) {
char comm_path[256];
snprintf(comm_path, sizeof(comm_path), "/proc/self/task/%s/comm", entry->d_name);
// Read comm and check for "gmain", "gdbus"
}
// 5. Named pipe detection (Windows)
// Frida creates \\.\pipe\frida-* named pipesFrida bypass of Frida detection:
// Hook the detection functions themselves
Interceptor.attach(Module.findExportByName(null, "strstr"), {
onEnter(args) {
this.haystack = Memory.readUtf8String(args[0]);
this.needle = Memory.readUtf8String(args[1]);
},
onLeave(retval) {
if (this.needle && (this.needle.includes("frida") || this.needle.includes("gadget"))) {
retval.replace(ptr(0)); // Not found
}
}
});
// Early Frida load (before anti-DBI runs)
// Use frida-gadget as early-init shared libraryPin/DynamoRIO Detection
// Check for instrumentation libraries in /proc/self/maps
// Pin: "pin-", "pinbin", "pinatrace"
// DynamoRIO: "dynamorio", "drcov", "drrun"
// Instruction count timing — DBI adds overhead
// Execute known instruction sequence, compare execution time---
Code Integrity / Self-Hashing
// CRC32 over .text section
uint32_t crc = compute_crc32(text_start, text_size);
if (crc != EXPECTED_CRC) exit(1); // Code was modified (breakpoints, patches)
// MD5/SHA256 of function bodies
unsigned char hash[32];
SHA256(function_addr, function_size, hash);
if (memcmp(hash, expected_hash, 32) != 0) exit(1);Bypasses: 1. Hardware breakpoints (don't modify code, DR0-DR3) 2. Patch the comparison to always succeed 3. Hook the hash function to return expected value 4. Emulate instead of debug (Unicorn/Qiling — no code modification) 5. Snapshot + restore: dump memory before and after, diff to find checks
Self-checksumming in loops:
// Continuous integrity check in separate thread
void *watchdog(void *arg) {
while (1) {
if (compute_crc32(text_start, text_end - text_start) != saved_crc) {
memset(flag_buffer, 0, flag_len); // Destroy flag
exit(1);
}
usleep(100000);
}
}Bypass: Kill the watchdog thread or patch its sleep to infinite.
---
Anti-Disassembly Techniques
Opaque Predicates
; Condition that always evaluates the same way but looks data-dependent
mov eax, [some_memory]
imul eax, eax ; x^2
and eax, 1 ; x^2 mod 2 is always 0 for any x
jnz fake_branch ; Never taken, but disassembler doesn't know
; real code hereIdentification: Z3/SMT can prove branch is always/never taken.
Junk Bytes / Overlapping Instructions
jmp real_code
db 0xE8 ; Looks like start of CALL to linear disassembler
real_code:
mov eax, 1 ; Real code — disassembler may misalign hereFix: Switch to graph-mode disassembly (Ghidra/IDA handle this well). Manual: undefine and re-analyze from correct offset.
Jump-in-the-Middle
; Jumps into the middle of a multi-byte instruction
eb 01 ; jmp +1 (skip next byte)
e8 ; fake CALL opcode — disassembler tries to decode as call
90 ; real: NOP (landed here from jmp)Function Chunking / Scattered Code
Functions split into non-contiguous chunks connected by unconditional jumps. Defeats linear function boundary detection.
Tool: IDA's "Append function tail" or Ghidra's "Create function" at each chunk.
Control Flow Flattening (Advanced)
Beyond basic switch-case (see patterns.md): modern OLLVM variants use:
- Bogus control flow: Fake branches with opaque predicates
- Instruction substitution:
a + b→a - (-b),a ^ b→(a | b) & ~(a & b) - String encryption: Strings decrypted at runtime, cleared after use
Deobfuscation tools:
- D-810 (IDA plugin): Pattern-based deobfuscation, MBA simplification
- GOOMBA (Ghidra): Automated deobfuscation for OLLVM
- Miasm: Symbolic execution for deobfuscation
- Arybo / SiMBA: MBA expression simplification
# D-810: install in IDA plugins directory, Edit → Plugins → D-810
# Simplifies MBA expressions: (a | b) & ~(a & b) → a ^ b
# Removes opaque predicates via pattern matchingMixed Boolean-Arithmetic (MBA) Identification & Simplification
# Common MBA patterns and their simplified forms:
# (x & y) + (x | y) == x + y
# (x ^ y) + 2*(x & y) == x + y
# (x | y) - (x & ~y) == y
# ~(~x & ~y) == x | y (De Morgan's)
# (x | y) & ~(x & y) == x ^ y
# SiMBA tool for automated simplification:
# pip install simba-simplifier
from simba import simplify_mba
expr = "(a | b) + (a & b) - (~a & b)"
print(simplify_mba(expr)) # → a---
SIGILL Handler for Execution Mode Switching (Hack.lu 2015)
Binaries may install SIGILL (illegal instruction) handlers to switch between x86 and x86-64 execution modes or implement custom opcode dispatch:
1. Signal registration: signal(SIGILL, handler) installs a callback for illegal instruction exceptions 2. Mode switching: The handler modifies the saved instruction pointer or segment registers to switch between 32-bit and 64-bit code 3. Custom opcodes: Invalid x86 instructions trigger the handler, which interprets operand bytes as custom VM opcodes
// Signal handler decodes "illegal" instructions as custom opcodes
void sigill_handler(int sig, siginfo_t *info, void *ucontext) {
ucontext_t *ctx = (ucontext_t *)ucontext;
unsigned char *pc = (unsigned char *)ctx->uc_mcontext.gregs[REG_RIP];
// Decode custom opcode from bytes at PC
// Advance PC past the custom instruction
ctx->uc_mcontext.gregs[REG_RIP] += opcode_length;
}Key insight: If a binary installs signal handlers for SIGILL/SIGSEGV/SIGTRAP early in execution, suspect custom instruction dispatch. Trace signal deliveries with strace -e signal or set GDB to not intercept: handle SIGILL nostop pass.
---
Comprehensive Bypass Strategies
Universal Bypass Checklist
1. Identify all anti-analysis checks — search for: ptrace, IsDebuggerPresent, rdtsc, cpuid, NtQuery, GetTickCount, CheckRemoteDebuggerPresent, /proc/self, SIGTRAP, alarm 2. Static patching — NOP/patch checks with pwntools or Ghidra before running 3. LD_PRELOAD (Linux) — hook libc functions returning fake values 4. ScyllaHide (Windows x64dbg) — patches PEB, hooks NT functions automatically 5. Emulation (Unicorn/Qiling) — no debugger artifacts to detect 6. Kernel-level bypass — modify /proc/sys/kernel/yama/ptrace_scope, use prctl
Layered Anti-Debug (Real-World Pattern)
Many CTF challenges stack multiple checks:
1. TLS callback → IsDebuggerPresent (before main)
2. main() → ptrace(TRACEME)
3. Watchdog thread → timing check + /proc scan
4. Code section → self-CRC32 integrity
5. Signal handler → real logic in SIGSEGV handlerApproach: Identify ALL checks before patching. Patch or hook each one systematically. Run under emulator if too many to patch individually.
Quick Reference: Check to Bypass
| Anti-Debug Check | Platform | Bypass |
|---|---|---|
ptrace(TRACEME) | Linux | LD_PRELOAD, patch to ret 0, catch syscall |
IsDebuggerPresent | Windows | ScyllaHide, Frida hook, PEB patch |
NtQueryInformationProcess | Windows | ScyllaHide, hook ntdll |
rdtsc timing | Both | NOP rdtsc, Frida time hook, Pin |
/proc/self/status | Linux | Mount namespace, hook fopen |
alarm(N) | Linux | handle SIGALRM ignore in GDB |
SIGTRAP handler | Linux | handle SIGTRAP nostop pass |
| TLS callback | Windows | Break on TLS in x64dbg, patch |
| DR register scan | Windows | Use software BPs, hook GetThreadContext |
| INT3 scan / CRC | Both | Hardware BPs, patch CRC comparison |
| Frida detection | Both | Early-load gadget, hook strstr |
| CPUID hypervisor | Both | Patch CPUID result, bare metal |
| Thread hiding | Windows | Hook NtSetInformationThread |
Reverse Engineering Field Notes
Table of Contents
- Binary Types
- Python .pyc
- WASM
- Android APK
- Flutter APK (Dart AOT)
- .NET
- Packed (UPX)
- Tauri Packed Desktop Apps
- Anti-Debugging Bypass
- Specialized Patterns
- S-Box / Keystream Patterns
- Custom VM Analysis
- Python Bytecode Reversing
- Signal-Based Binary Exploration
- Malware Anti-Analysis Bypass via Patching
- Expected Values Tables
- x86-64 Gotchas
- Iterative Solver Pattern
- Unicorn Emulation (Complex State)
- Multi-Stage Shellcode Loaders
- Timing Side-Channel Attack
- Godot Game Asset Extraction
- Roblox Place File Analysis
- Unstripped Binary Information Leaks
- Custom Mangle Function Reversing
- Rust serde_json Schema Recovery
- Position-Based Transformation Reversing
- Hex-Encoded String Comparison
- CTF Case Notes
- Embedded ZIP + XOR License Decryption
- Stack String Deobfuscation (.rodata XOR Blob)
- Prefix Hash Brute-Force
- Mathematical Convergence Bitmap
- RISC-V Binary Analysis
- Sprague-Grundy Game Theory Binary
- Kernel Module Maze Solving
- Multi-Threaded VM with Channels
- CVP/LLL Lattice for Constrained Integer Validation
- Decision Tree Function Obfuscation
- Android JNI RegisterNatives Obfuscation
- Multi-Layer Self-Decrypting Binary
- GLSL Shader VM with Self-Modifying Code
- GF(2^8) Gaussian Elimination for Flag Recovery
- Z3 for Single-Line Python Boolean Circuit
- Sliding Window Popcount Differential Propagation
- Ruby/Perl Polyglot Constraint Satisfaction
- Verilog/Hardware RE
- Custom binfmt Kernel Module with RC4 Flat Binaries
- Hash-Resolved Imports / No-Import Ransomware
- ELF Section Header Corruption for Anti-Analysis
- Brainfuck Character-by-Character Static Analysis
- Brainfuck Side-Channel via Read Count Oracle
- Brainfuck Comparison Idiom Detection
- Backdoored Shared Library Detection
- Go Binary Reversing
- Go Binary UUID Patching for C2 Enumeration
- D Language Binary Reversing
- Rust Binary Reversing
- Frida Dynamic Instrumentation
- Frida Firebase Cloud Functions Bypass
- angr Symbolic Execution
- Qiling Emulation
- VMProtect / Themida Analysis
- Binary Diffing
- Advanced GDB (pwndbg, rr)
- macOS / iOS Reversing
- Embedded / IoT Firmware RE
- Kernel Driver Reversing
- Game Engine Reversing
- Swift / Kotlin Binary Reversing
- INT3 Patch + Coredump Brute-Force Oracle
- Signal Handler Chain + LD_PRELOAD Oracle
- Font Ligature Exploitation
- Instruction Counter as Cryptographic State
- Burrows-Wheeler Transform Inversion
- FRACTRAN Program Inversion
- Opcode-Only Trace Reconstruction
- Thread Race Signed Integer Overflow
- ESP32/Xtensa Firmware Reversing
- Custom VM Bytecode Lifting to LLVM IR
- SIGFPE Signal Handler Side-Channel
- Batch Crackme Automation via objdump
- Android DEX Runtime Bytecode Patching
- Fork + Pipe + Dead Branch Anti-Analysis
Binary Types
Python .pyc
Disassemble with marshal.load() + dis.dis(). Header: 8 bytes (2.x), 12 (3.0-3.6), 16 (3.7+). See languages.md.
WASM
wasm2c checker.wasm -o checker.c
gcc -O3 checker.c wasm-rt-impl.c -o checker
# WASM patching (game challenges):
wasm2wat main.wasm -o main.wat # Binary → text
# Edit WAT: flip comparisons, change constants
wat2wasm main.wat -o patched.wasm # Text → binaryWASM game patching (Tac Tic Toe, Pragyan 2026): If proof generation is independent of move quality, patch minimax (flip i64.lt_s → i64.gt_s, change bestScore sign) to make AI play badly while proofs remain valid. Invoke /ctf-misc for full game patching patterns (games-and-vms).
Android APK
apktool d app.apk -o decoded/ for resources; jadx app.apk for Java decompilation. Check decoded/res/values/strings.xml for flags. See tools.md.
Flutter APK (Dart AOT)
If lib/arm64-v8a/libapp.so + libflutter.so present, use Blutter: python3 blutter.py path/to/app/lib/arm64-v8a out_dir. Outputs reconstructed Dart symbols + Frida script. See tools.md.
.NET
- dnSpy - debugging + decompilation
- ILSpy - decompiler
Packed (UPX)
upx -d packed -o unpackedIf unpacking fails, inspect UPX metadata first: verify UPX section names, header fields, and version markers are intact. If metadata looks tampered or uncertain, review UPX source on GitHub to identify likely modification points.
Tauri Packed Desktop Apps
Tauri embeds Brotli-compressed frontend assets in the executable. Find index.html xrefs to locate asset index table, dump blobs, Brotli decompress. Reference: tauri-codegen/src/embedded_assets.rs.
Anti-Debugging Bypass
Common checks:
IsDebuggerPresent()/ PEB.BeingDebugged / NtQueryInformationProcess (Windows)ptrace(PTRACE_TRACEME)//proc/self/statusTracerPid (Linux)- TLS callbacks (run before main — check PE TLS Directory)
- Timing checks (
rdtsc,clock_gettime,GetTickCount) - Hardware breakpoint detection (DR0-DR3 via GetThreadContext)
- INT3 scanning / code self-hashing (CRC over .text section)
- Signal-based: SIGTRAP handler, SIGALRM timeout, SIGSEGV for real logic
- Frida/DBI detection:
/proc/self/mapsscan, port 27042, inline hook checks
Bypass: Set breakpoint at check, modify register to bypass conditional. pwntools patch: elf.asm(elf.symbols.ptrace, 'ret') to replace function with immediate return. See patterns.md.
For comprehensive anti-analysis techniques and bypasses (30+ methods with code), see anti-analysis.md.
Specialized Patterns
S-Box / Keystream Patterns
Xorshift32: Shifts 13, 17, 5 Xorshift64: Shifts 12, 25, 27 Magic constants: 0x2545f4914f6cdd1d, 0x9e3779b97f4a7c15
Custom VM Analysis
1. Identify structure: registers, memory, IP 2. Reverse executeIns for opcode meanings 3. Write disassembler mapping opcodes to mnemonics 4. Often easier to bruteforce than fully reverse 5. Look for the bytecode file loaded via command-line arg
See patterns.md for VM workflow, opcode tables, and state machine BFS.
Sequential key-chain brute-force: When a VM validates input in small blocks (e.g., 3 bytes = 2^24 candidates) with each block's output key feeding the next, brute-force each block sequentially with OpenMP parallelization. Compile solver with gcc -O3 -march=native -fopenmp. See patterns-ctf-3.md.
Python Bytecode Reversing
XOR flag checkers with interleaved even/odd tables are common. See languages.md for bytecode analysis tips and reversing patterns.
Signal-Based Binary Exploration
Binary uses UNIX signals as binary tree navigation; hook sigaction via LD_PRELOAD, DFS by sending signals. See patterns.md.
Malware Anti-Analysis Bypass via Patching
Flip JNZ/JZ (0x75/0x74), change sleep values, patch environment checks in Ghidra (Ctrl+Shift+G). See patterns-runtime.md.
Expected Values Tables
Locate with objdump -s -j .rodata binary | less — look near comparison instructions, size matches flag length.
x86-64 Gotchas
Sign extension and 32-bit truncation pitfalls. See patterns.md for details and code examples.
Iterative Solver Pattern
Try each byte (0-255) per position, match against expected output. Uniform transform shortcut: if one input byte only changes one output byte, build 0..255 mapping then invert. See patterns.md for full implementation.
Unicorn Emulation (Complex State)
from unicorn import * -- map segments, set up stack, hook to trace. Mixed-mode pitfall: 64-bit stub jumping to 32-bit via retf requires switching to UC_MODE_32 and copying GPRs + EFLAGS + XMM regs. See tools.md.
Multi-Stage Shellcode Loaders
Nested shellcode with XOR decode loops; break at call rax, bypass ptrace with set $rax=0, extract flag from mov instructions. See patterns-runtime.md.
Timing Side-Channel Attack
Validation time varies per correct character; measure elapsed time per candidate to recover flag byte-by-byte. See patterns-runtime.md.
Godot Game Asset Extraction
Use KeyDot to extract encryption key from executable, then gdsdecomp to extract .pck package. See languages-platforms.md.
Roblox Place File Analysis
Query Asset Delivery API for version history; parse .rbxlbin chunks (INST/PROP/PRNT) to diff script sources across versions. See languages-platforms.md.
Unstripped Binary Information Leaks
Pattern: Debug info and file paths leak author identity. Quick checks: strings binary | grep "/home/" (home dirs), file binary (stripped?), readelf -S binary | grep debug (debug sections).
Custom Mangle Function Reversing
Binary mangles input 2 bytes at a time with running state; extract target from .rodata, write inverse function. See patterns.md.
Rust serde_json Schema Recovery
Disassemble serde Visitor implementations to recover expected JSON schema; field names in order reveal flag. See languages-platforms.md.
Position-Based Transformation Reversing
Binary adds/subtracts position index; reverse by undoing per-index offset. See patterns.md.
Hex-Encoded String Comparison
Input converted to hex, compared against constant. Decode with xxd -r -p. See patterns.md.
CTF Case Notes
Embedded ZIP + XOR License Decryption
Binary with named symbols (EMBEDDED_ZIP, ENCRYPTED_MESSAGE) in .rodata → extract ZIP containing license, XOR encrypted message with license bytes to recover flag. No execution needed. See patterns-ctf-2.md.
Stack String Deobfuscation (.rodata XOR Blob)
Binary mmaps .rodata blob, XOR-deobfuscates, uses it to validate input. Reimplement verification loop with pyelftools to extract blob. Look for 0x9E3779B9, 0x85EBCA6B constants and rol32(). See patterns-ctf-2.md.
Prefix Hash Brute-Force
Binary hashes every prefix independently. Recover one character at a time by matching prefix hashes. See patterns-ctf-2.md.
Mathematical Convergence Bitmap
Pattern: Binary classifies coordinate pairs by Newton's method convergence (e.g., z^3-1=0). Grid of pass/fail results renders ASCII art flag. Key: the binary is a classifier, not a checker — reverse the math and visualize. See patterns-ctf.md.
RISC-V Binary Analysis
Statically linked, stripped RISC-V ELF. Use Capstone with CS_MODE_RISCVC | CS_MODE_RISCV64 for mixed compressed instructions. Emulate with qemu-riscv64. Watch for fake flags and XOR decryption with incremental keys. See tools.md.
Sprague-Grundy Game Theory Binary
Game binary plays bounded Nim with PRNG for losing-position moves. Identify game framework (Grundy values = pile % (k+1), XOR determines position), track PRNG state evolution through user input feedback. See patterns-ctf.md.
Kernel Module Maze Solving
Rust kernel module implements maze via device ioctls. Enumerate commands dynamically, build DFS solver with decoy avoidance, deploy as minimal static binary (raw syscalls, no libc). See patterns-ctf.md.
Multi-Threaded VM with Channels
Custom VM with 16+ threads communicating via futex channels. Trace data flow across thread boundaries, extract constants from GDB, watch for inverted validity logic, solve via BFS state space search. See patterns-ctf.md.
CVP/LLL Lattice for Constrained Integer Validation
Binary validates flag via matrix multiplication with 64-bit coefficients; solutions must be printable ASCII. Use LLL reduction + CVP in SageMath to find nearest lattice point in the constrained range. Two-phase pattern: Phase 1 recovers AES key, Phase 2 decrypts custom VM bytecode with another linear system (mod 2^32). See patterns-ctf-2.md.
Decision Tree Function Obfuscation
~200+ auto-generated functions routing input through polynomial comparisons. Script extraction via Ghidra headless rather than reversing each function manually. Constraint propagation from known output format cascades through arithmetic constraints. See patterns-ctf-2.md.
Android JNI RegisterNatives Obfuscation
RegisterNatives in JNI_OnLoad hides which C++ function handles each Java native method (no standard Java_com_pkg_Class_method symbol). Find the real handler by tracing JNI_OnLoad → RegisterNatives → fnPtr. Use x86_64 .so from APK for best Ghidra decompilation. See languages-platforms.md.
Multi-Layer Self-Decrypting Binary
N-layer binary where each layer decrypts the next using user-provided key bytes + SHA-NI. Use oracle (correct key → valid code with expected pattern). JIT execution with fork-per-candidate COW isolation for speed. See patterns-ctf-2.md.
GLSL Shader VM with Self-Modifying Code
Pattern: WebGL2 fragment shader implements Turing-complete VM on a 256x256 RGBA texture (program memory + VRAM). Self-modifying code (STORE opcode) patches drawing instructions. GPU parallelism causes write conflicts — emulate sequentially in Python to recover full output. See patterns-ctf-3.md.
GF(2^8) Gaussian Elimination for Flag Recovery
Pattern: Binary performs Gaussian elimination over GF(2^8) with the AES polynomial (0x11b). Matrix + augmentation vector in .rodata; solution vector is the flag. Look for constant 0x1b in disassembly. Addition is XOR, multiplication uses polynomial reduction. See patterns-ctf-2.md.
Z3 for Single-Line Python Boolean Circuit
Pattern: Single-line Python (2000+ semicolons) with walrus operator chains validates flag as big-endian integer via boolean circuit. Obfuscated XOR (a | b) & ~(a & b). Split on semicolons, translate to Z3 symbolically, solve in under a second. See patterns-ctf-3.md.
Sliding Window Popcount Differential Propagation
Pattern: Binary validates input via expected popcount for each position of a 16-bit sliding window. Popcount differences create a recurrence: bit[i+16] = bit[i] + (data[i+1] - data[i]). Brute-force ~4000-8000 valid initial 16-bit windows; each determines the entire bit sequence. See patterns-ctf-3.md.
Ruby/Perl Polyglot Constraint Satisfaction
Pattern: Single file valid in both Ruby and Perl, each imposing different constraints on a key. Exploits =begin/=end (Ruby block comment) vs =begin/=cut (Perl POD) to run different code per interpreter. Intersect constraints from both languages to recover the unique key. See languages-platforms.md.
Verilog/Hardware RE
Pattern: Verilog HDL source for state machines with hidden conditions gated on shift register history. Analyze always @(posedge clk) blocks and case statements to find correct input sequences. See languages-platforms.md.
Custom binfmt Kernel Module with RC4 Flat Binaries
Pattern: Kernel module registers binfmt handler for encrypted flat binaries. Reverse the .ko to find RC4 key (in movabs immediates), decrypt the flat binary, import at the fixed virtual address from the module's vm_mmap call. See patterns-ctf.md.
Hash-Resolved Imports / No-Import Ransomware
Pattern: Binary with zero visible imports resolves APIs via symbol name hashing at runtime. Skip the hash reversing — hook OpenSSL functions via LD_PRELOAD in Docker to capture AES keys directly. See patterns-ctf.md.
ELF Section Header Corruption for Anti-Analysis
Pattern: Corrupted section headers crash analysis tools but program headers are intact so binary runs normally. Patch e_shoff to zero or use readelf -l (program headers only). Flag hidden after corrupted sections with magic marker + XOR. See patterns-ctf.md.
Brainfuck Character-by-Character Static Analysis
Pattern: BF programs validating input have , (read char) followed by + operations whose count = expected ASCII value. Extract increment counts per input position to recover expected input without execution. See languages.md.
Brainfuck Side-Channel via Read Count Oracle
Pattern: BF input validators read more bytes when a character is correct. Count , operations per candidate — highest read count = correct byte. Character-by-character recovery. See languages.md.
Brainfuck Comparison Idiom Detection
Pattern: Compiled BF uses fixed idioms for equality checks (<[-<->] +<[>-<[-]]>[-<+>]). Instrument interpreter to detect patterns and extract comparison operands (expected flag bytes). See languages.md.
Backdoored Shared Library Detection
Binary works in GDB but fails when run normally (suid)? Check ldd for non-standard libc paths, then strings | diff the suspicious vs. system library to find injected code/passwords. See patterns-ctf.md.
Go Binary Reversing
Large static binary with go.buildid? Use GoReSym to recover function names (works even on stripped binaries). Go strings are {ptr, len} pairs — not null-terminated. Look for main.main, runtime.gopanic, channel ops (runtime.chansend1/chanrecv1). Use Ghidra golang-loader plugin for best results. See languages-compiled.md.
Go Binary UUID Patching for C2 Enumeration
Pattern: Go C2 client with UUID from -ldflags -X. Binary-patch UUID bytes (same length), register with C2, enumerate clients/files via API. See languages-compiled.md.
D Language Binary Reversing
D language binaries have unique symbol mangling (not C++ style). Template-heavy, many function variants. Look for _D prefix in symbols. See languages-compiled.md.
Rust Binary Reversing
Binary with core::panicking strings and _ZN mangled symbols? Use rustfilt for demangling. Panic messages contain source paths and line numbers — strings binary | grep "panicked" is the fastest approach. Option/Result enums use discriminant byte (0=None/Err, 1=Some/Ok). See languages-compiled.md.
Frida Dynamic Instrumentation
Hook runtime functions without modifying binary. frida -f ./binary -l hook.js to spawn with instrumentation. Hook strcmp/memcmp to capture expected values, bypass anti-debug by replacing ptrace return value, scan memory for flag patterns, replace validation functions. See tools-dynamic.md.
Frida Firebase Cloud Functions Bypass
Pattern: Android app validates via Firebase Cloud Functions. Post-login Frida hook constructs valid payload (UID + value + timestamp) and calls Cloud Function directly, bypassing QR/payment validation. See languages-platforms.md.
angr Symbolic Execution
Automatic path exploration to find inputs satisfying constraints. Load binary with angr.Project, set find/avoid addresses, call simgr.explore(). Constrain input to printable ASCII and known prefix for faster solving. Hook expensive functions (crypto, I/O) to prevent path explosion. See tools-dynamic.md.
Qiling Emulation
Cross-platform binary emulation with OS-level support (syscalls, filesystem). Emulate Linux/Windows/ARM/MIPS binaries on any host. No debugger artifacts — bypasses all anti-debug by default. Hook syscalls and addresses with Python API. See tools-dynamic.md.
VMProtect / Themida Analysis
VMProtect virtualizes code into custom bytecode. Identify VM entry (pushad-like), find handler table (large indirect jump), trace handlers dynamically. For CTF, focus on tracing operations on input rather than full devirtualization. Themida: dump at OEP with ScyllaHide + Scylla. See tools-advanced.md.
Binary Diffing
BinDiff and Diaphora compare two binaries to highlight changes. Essential when challenge provides patched/original versions. Export from IDA/Ghidra, diff to find vulnerability or hidden functionality. See tools-advanced.md.
Advanced GDB (pwndbg, rr)
pwndbg: context, vmmap, search -s "flag{", telescope $rsp. GEF alternative. Reverse debugging with rr record/rr replay — step backward through execution. Python scripting for brute-force and automated tracing. See tools-advanced.md.
macOS / iOS Reversing
Mach-O binaries: otool -l for load commands, class-dump for Objective-C headers. Swift: swift demangle for symbols. iOS apps: decrypt FairPlay DRM with frida-ios-dump, bypass jailbreak detection with Frida hooks. Re-sign patched binaries with codesign -f -s -. See platforms.md.
Embedded / IoT Firmware RE
binwalk -Me firmware.bin for recursive extraction. Hardware: UART/JTAG/SPI flash for firmware dumps. Filesystems: SquashFS (unsquashfs), JFFS2, UBI. Emulate with QEMU: qemu-arm -L /usr/arm-linux-gnueabihf/ ./binary. See platforms.md.
Kernel Driver Reversing
Linux .ko: find ioctl handler via file_operations struct, trace copy_from_user/copy_to_user. Debug with QEMU+GDB (-s -S). eBPF: bpftool prog dump xlated. Windows .sys: find DriverEntry → IoCreateDevice → IRP handlers. See platforms.md.
Game Engine Reversing
Unreal: extract .pak with UnrealPakTool, reverse Blueprint bytecode with FModel. Unity Mono: decompile Assembly-CSharp.dll with dnSpy. Anti-cheat (EAC, BattlEye, VAC): identify system, bypass specific check. Lua games: luadec/unluac for bytecode. See platforms.md.
Swift / Kotlin Binary Reversing
Swift: swift demangle symbols, protocol witness tables for dispatch, __swift5_* sections. Kotlin/JVM: coroutines compile to state machines in invokeSuspend, jadx with Kotlin mode for best decompilation. Kotlin/Native: LLVM backend, looks like C++ in disassembly. See languages-compiled.md.
INT3 Patch + Coredump Brute-Force Oracle
Patch 0xCC (INT3) after transform output, enable core dumps, brute-force each input character by extracting computed state from coredump via strings. Avoids full reverse of transformation. See patterns.md.
Signal Handler Chain + LD_PRELOAD Oracle
Binary uses signal handler chains for per-character password validation. Hook signal() via LD_PRELOAD -- the call to install the next handler confirms the current character is correct. See patterns.md.
Font Ligature Exploitation
Custom OpenType font maps multi-character ligature sequences to single glyphs; reverse the GSUB table to decode hidden messages. See patterns-ctf-3.md.
Instruction Counter as Cryptographic State
Pattern: Hand-written assembly uses a dedicated register (e.g., r12) as an instruction counter incremented after nearly every instruction. The counter feeds into XOR/ROL/multiply transformations on input bytes, making transformation path-dependent. Byte-by-byte brute force with Unicorn emulation recovers the flag. See patterns-ctf-3.md.
Burrows-Wheeler Transform Inversion
Invert BWT without terminator character by trying all possible row indices. Standard bwtool or manual column-sorting reconstruction. See patterns-ctf-3.md.
FRACTRAN Program Inversion
Esoteric language using iterated fraction multiplication. Invert by swapping numerator/denominator in fraction table, run output backward. I/O encoded as prime factorization exponents. See languages.md.
Opcode-Only Trace Reconstruction
Execution traces with only opcodes (no data) still leak info through branch decisions. Sorting algorithm comparisons reveal element ordering. Reconstruct by deduplicating trace, splitting into basic blocks. See tools-dynamic.md.
Thread Race Signed Integer Overflow
Game binary with thread-unsafe skill lock. Race between skill selection and damage calculation; cdqe sign-extends 0xFFFFFFFF to -1 (signed), causing HP overflow on subtraction. See patterns-ctf-3.md.
ESP32/Xtensa Firmware Reversing
No IDA support — use radare2 + ESP-IDF ROM linker script (esp32.rom.ld) for symbol resolution. Cross-reference with public ESP-IDF HTTP server examples to identify app logic. See patterns-ctf-3.md.
Custom VM Bytecode Lifting to LLVM IR
Transpile custom VM bytecode to LLVM IR, then use opt -O3 to simplify (inlining, constant folding, dead code elimination). Reduces 1300 lines to ~150 lines, revealing the underlying algorithm. See tools-advanced.md.
SIGFPE Signal Handler Side-Channel
SIGFPE signal handlers create implicit control flow invisible to static analysis. Count SIGFPE signals via strace -e signal=SIGFPE per candidate character -- correct characters produce more signals. See anti-analysis.md.
Batch Crackme Automation via objdump
Mass crackme challenges (100s of binaries) with identical structure: script objdump to extract CMP immediates and add/sub arithmetic sequences, then reverse-compute keys algebraically without execution. See patterns-ctf-3.md.
Android DEX Runtime Bytecode Patching
Native JNI library patches Dalvik bytecode in memory via /proc/self/maps + mprotect + XOR. Static APK analysis alone is insufficient -- extract XOR key and offsets from the native .so to reconstruct the runtime DEX. See languages-platforms.md.
Fork + Pipe + Dead Branch Anti-Analysis
Fork/pipe IPC where parent writes data and exits, child reads and continues. Real validation hidden in a dead branch (always-false comparison). strace reveals the fork/pipe pattern; patch the comparison constant to reach hidden code. See patterns-ctf-3.md.
CTF Reverse - Compiled Language Reversing (Go, Rust)
Table of Contents
- Go Binary Reversing
- Recognition
- Symbol Recovery
- Go Memory Layout
- Goroutine and Concurrency Analysis
- Common Go Patterns in Decompilation
- Go Binary Reversing Workflow
- Go Binary UUID Patching for C2 Client Enumeration (BSidesSF 2026)
- Rust Binary Reversing
- Recognition
- Symbol Demangling
- Common Rust Patterns in Decompilation
- Rust-Specific Analysis Tools
- Swift Binary Reversing
- Kotlin / JVM Binary Reversing
- JVM Bytecode (Android/Server)
- Kotlin/Native
- C++ Binary Reversing (Quick Reference)
- vtable Reconstruction
- RTTI (Run-Time Type Information)
- Standard Library Patterns
---
Go Binary Reversing
Go binaries are increasingly common in CTF challenges due to Go's popularity for CLI tools, network services, and malware.
Recognition
# Detect Go binary
file binary | grep -i "go"
strings binary | grep "go.buildid"
strings binary | grep "runtime.gopanic"
# Go version embedded in binary
strings binary | grep "^go1\."Key indicators:
- Very large static binary (even "hello world" is ~2MB)
- Embedded
go.buildidstring runtime.*symbols (even in stripped binaries, some remain)main.mainas entry point (notmain)- Strings like
GOROOT,GOPATH,/usr/local/go/src/
Symbol Recovery
Go embeds rich type and function information even in stripped binaries:
# GoReSym - recovers function names, types, interfaces from Go binaries
# https://github.com/mandiant/GoReSym
./GoReSym -d binary > symbols.json
# Parse output
python3 -c "
import json
with open('symbols.json') as f:
data = json.load(f)
for fn in data.get('UserFunctions', []):
print(f\"{fn['Start']:#x} {fn['FullName']}\")
"Ghidra with golang-loader:
# Install: Ghidra → Window → Script Manager → search "golang"
# Or use: https://github.com/getCUJO/ThreatFox/tree/main/ghidra-golang
# Recovers function names, string references, interface tablesredress (Go binary analysis):
# https://github.com/goretk/redress
redress -src binary # Reconstruct source tree
redress -pkg binary # List packages
redress -type binary # List types and methods
redress -interface binary # List interfacesGo Memory Layout
Understanding Go's data structures in decompilation:
# String: {pointer, length} (16 bytes on 64-bit)
# NOT null-terminated! Length field is critical.
struct GoString {
char *ptr; // pointer to UTF-8 data
int64 len; // byte length
};
# Slice: {pointer, length, capacity} (24 bytes on 64-bit)
struct GoSlice {
void *ptr; // pointer to backing array
int64 len; // current length
int64 cap; // allocated capacity
};
# Interface: {type_descriptor, data_pointer} (16 bytes)
struct GoInterface {
void *type; // points to type metadata (itab for non-empty interface)
void *data; // points to actual value
};
# Map: pointer to runtime.hmap struct
# Channel: pointer to runtime.hchan structIn Ghidra/IDA: When you see a function taking (ptr, int64) — it's likely a Go string. Three-field (ptr, int64, int64) is a slice.
Goroutine and Concurrency Analysis
# Identify goroutine spawns in disassembly
strings binary | grep "runtime.newproc"
# newproc1 is the internal goroutine creation function
# In GDB with Go support:
gdb ./binary
(gdb) source /usr/local/go/src/runtime/runtime-gdb.py
(gdb) info goroutines # List all goroutines
(gdb) goroutine 1 bt # Backtrace for goroutine 1Channel operations in disassembly:
runtime.chansend1→ch <- valueruntime.chanrecv1→value = <-chruntime.selectgo→select { case ... }runtime.closechan→close(ch)
Common Go Patterns in Decompilation
Defer mechanism:
runtime.deferproc→ registers deferred functionruntime.deferreturn→ executes deferred functions at function exit- Deferred calls execute in LIFO order — relevant for cleanup/crypto key wiping
Error handling (the `if err != nil` pattern):
# In disassembly, this appears as:
# call some_function → returns (result, error) as two values
# test rax, rax → check if error (second return value) is nil
# jne error_handlerString concatenation:
runtime.concatstrings→s1 + s2 + s3fmt.Sprintf→ formatted string building- Look for format strings in
.rodata:"%s%d","%x"
Common stdlib patterns in CTF:
// Crypto operations → look for these in strings/imports:
// "crypto/aes", "crypto/cipher", "crypto/sha256", "encoding/hex", "encoding/base64"
// Network operations:
// "net/http", "net.Dial", "bufio.NewReader"
// File operations:
// "os.Open", "io.ReadAll", "os.ReadFile"Go Binary Reversing Workflow
1. file binary # Confirm Go, get arch
2. GoReSym -d binary > syms.json # Recover symbols
3. strings binary | grep -i flag # Quick win check
4. Load in Ghidra with golang-loader # Apply recovered symbols
5. Find main.main # Entry point
6. Identify string comparisons # GoString {ptr, len} pairs
7. Trace crypto operations # crypto/* package usage
8. Check for embedded resources # embed.FS in Go 1.16+Go embed.FS (Go 1.16+): Binaries can embed files at compile time:
# Look for embedded file data
strings binary | grep "embed"
# Embedded files appear as raw data in the binary
# Search for known file signatures (PK for zip, PNG header, etc.)Key insight: Go's runtime embeds extensive metadata even in stripped binaries. Use GoReSym before any manual analysis — it often recovers 90%+ of function names, making decompilation dramatically easier. Go strings are {ptr, len} tuples, not null-terminated — Ghidra's default string analysis will miss them without the golang-loader plugin.
Detection: Large static binary (2MB+ for simple programs), go.buildid, runtime.gopanic, source paths like /home/user/go/src/.
Go Binary UUID Patching for C2 Client Enumeration (BSidesSF 2026)
Pattern (see-two): A Go-compiled C2 client has a UUID embedded via -ldflags -X. The C2 server uses mTLS for authentication. To enumerate other clients and their files, patch the UUID to register as a new client, then use the C2 API to list all clients and download their exfiltrated files.
Approach: 1. Extract embedded UUID from Go build metadata: go version -m client_binary 2. Binary-patch the UUID (simple byte replacement — Go strings have fixed-length backing arrays) 3. Register with the C2 server using the patched binary (mTLS certs are embedded or in distfiles) 4. Enumerate clients via API: GET /api/clients or iterate known endpoints 5. List and download files from each client's GCS bucket or file store 6. Grep downloaded files for the flag
# Extract Go build info
go version -m ./client_binary | grep ldflags
# Output shows: -X main.clientUUID=<uuid>
# Patch UUID in binary (replace old UUID bytes with new UUID)
python3 -c "
import sys
data = open('client_binary', 'rb').read()
old_uuid = b'original-uuid-value-here'
new_uuid = b'attacker-uuid-value-here'
patched = data.replace(old_uuid, new_uuid)
open('client_patched', 'wb').write(patched)
"
chmod +x client_patched
./client_patched --registerKey insight: Go binaries embed string values from -ldflags -X directly in the binary data section. Since Go strings are {ptr, len} pairs pointing to backing byte arrays, replacing the UUID bytes (same length) produces a valid patched binary. The mTLS certificates authenticate the client to the server but don't bind to a specific UUID.
References: BSidesSF 2026 "see-two"
---
Rust Binary Reversing
Rust binaries are common in modern CTFs, especially for crypto, systems, and security tooling challenges.
Recognition
# Detect Rust binary
strings binary | grep -c "rust"
strings binary | grep "rustc" # Compiler version
strings binary | grep "/rustc/" # Source paths
strings binary | grep "core::panicking" # Panic infrastructureKey indicators:
core::panicking::panicin strings- Mangled symbols starting with
_ZN(Itanium ABI) — e.g.,_ZN4main4main17h... .rustcsection in ELF- References to
/rustc/<commit_hash>/library/ - Large binary size (Rust statically links by default)
Symbol Demangling
# Rust uses Itanium ABI mangling (same as C++)
# rustfilt demangles Rust-specific symbols
cargo install rustfilt
nm binary | rustfilt | grep "main"
# Or use c++filt (works for most Rust symbols)
nm binary | c++filt | grep "main"
# In Ghidra: Window → Script Manager → search "Demangler"
# Enable "DemangleAllScript" for automatic demanglingCommon Rust Patterns in Decompilation
Option/Result enum:
# Option<T> in memory: {discriminant (0=None, 1=Some), value}
# Result<T, E>: {discriminant (0=Ok, 1=Err), union{ok_val, err_val}}
# In disassembly:
# cmp byte [rbp-0x10], 0 → check if None/Err
# je handle_none_caseVec<T> (same as Go slice):
struct RustVec {
void *ptr; // heap pointer
uint64 cap; // capacity
uint64 len; // length
};String / &str:
# String (owned): {ptr, capacity, length} — 24 bytes, heap-allocated
# &str (borrowed): {ptr, length} — 16 bytes, can point anywhere
# In decompilation, look for:
# alloc::string::String::from → String creation
# core::str::from_utf8 → byte slice to strIterator chains:
# .iter().map().filter().collect() compiles to loop fusion
# In disassembly: tight loop with inlined closures
# Look for: core::iter::adapters::map, filter, etc.Panic unwinding:
# Panic strings reveal source locations and error messages
strings binary | grep "panicked at"
strings binary | grep "called .unwrap().. on"
# These often contain file paths, line numbers, and variable namesRust-Specific Analysis Tools
# cargo-bloat: analyze binary size by function
cargo install cargo-bloat
cargo bloat --release -n 50
# Ghidra Rust helper scripts
# https://github.com/AmateursCTF/ghidra-rust (community scripts for Rust RE)Key insight: Rust panic messages are goldmines — they contain source file paths, line numbers, and descriptive error strings even in release builds. Always strings binary | grep "panicked" first. Rust's monomorphization means generic functions get duplicated per type — expect many similar-looking functions.
Detection: core::panicking, .rustc section, /rustc/ paths, _ZN mangled symbols with Rust-style module paths.
---
Swift Binary Reversing
See platforms.md for full Swift reversing guide including demangling, runtime structures, and Ghidra integration. Key quick reference:
# Detect Swift binary
strings binary | grep "swift"
otool -l binary | grep "swift"
# Demangle Swift symbols
swift demangle 's14MyApp0A8ClassC10checkInput6resultSbSS_tF'
# → MyApp.MyAppClass.checkInput(result: String) -> Bool
# Key runtime functions: swift_allocObject, swift_release, swift_once
# String: small (≤15 bytes inline) or large (heap pointer + length)
# Protocol witness tables = dynamic dispatch (like vtables)Detection: __swift5_* sections in Mach-O, swift_ runtime symbols, s prefix in mangled names.
---
Kotlin / JVM Binary Reversing
Kotlin compiles to JVM bytecode or native (via Kotlin/Native). Common in Android and server-side CTF.
JVM Bytecode (Android/Server)
# Detect Kotlin
strings classes.dex | grep "kotlin"
# Look for: kotlin.Metadata annotation, kotlin/jvm/internal/*
# Decompile
jadx classes.dex # Best for Kotlin bytecode
cfr classes.jar --kotlin # CFR with Kotlin mode
fernflower classes.jar output/ # IntelliJ's decompiler
# Kotlin-specific patterns in decompiled output:
# - Companion objects: ClassName$Companion
# - Data classes: copy(), component1(), component2(), toString()
# - Coroutines: ContinuationImpl, invokeSuspend, state machine
# - Null checks: Intrinsics.checkNotNull() everywhere
# - When expression: compiled as tableswitch/lookupswitch
# - Sealed classes: instanceof checks in chainKotlin coroutines in disassembly:
# Coroutines compile to state machines:
# invokeSuspend(result) {
# switch (this.label) {
# case 0: this.label = 1; return suspendFunction();
# case 1: processResult(result); return Unit;
# }
# }
# Each suspend point becomes a state in the switch.
# Follow the state machine to understand async flow.Kotlin/Native
# Kotlin/Native produces platform binaries (no JVM)
# Recognize by: konan, kotlin.native strings
strings binary | grep "konan"
# Much harder to reverse — no reflection metadata
# Uses LLVM backend, looks similar to C/C++ in disassembly
# Key functions: InitRuntime, DeinitRuntime, CreateStablePointer
# Memory management: automatic reference counting (not GC)Detection: kotlin.Metadata annotations (JVM), konan strings (Native), kotlin/ package paths.
---
C++ Binary Reversing (Quick Reference)
While C++ RE is well-covered by general tools, these patterns are CTF-specific:
vtable Reconstruction
# Virtual function tables (vtables):
# First 8 bytes of object → pointer to vtable
# vtable entries: [typeinfo_ptr, destructor, method1, method2, ...]
# In Ghidra: Data → Create Pointer at vtable address
# Identify polymorphic dispatch:
# mov rax, [rdi] # Load vtable from this pointer
# call [rax + 0x18] # Call 4th virtual method (0x18/8 = 3rd after typeinfo+dtor)RTTI (Run-Time Type Information)
# If not stripped, RTTI reveals class hierarchy
strings binary | grep -E "^[0-9]+[A-Z]" # Mangled type names
c++filt _ZTI7MyClass # → typeinfo for MyClass
# In Ghidra: search for vtable references, follow typeinfo pointer
# typeinfo struct: {vtable_for_typeinfo, name_string, base_class_ptr}Standard Library Patterns
std::string (libstdc++):
SSO (Small String Optimization): inline buffer for ≤15 chars
Layout: {char* ptr, size_t size, union{size_t cap, char buf[16]}}
std::vector<T>:
{T* begin, T* end, T* capacity_end}
std::map<K,V>:
Red-black tree: each node has {left, right, parent, color, key, value}
std::unordered_map<K,V>:
Hash table: {bucket_array, size, load_factor_max, ...}CTF Reverse - Platform & Framework-Specific Techniques
Table of Contents
- Roblox Place File Analysis
- Godot Game Asset Extraction
- Rust serde_json Schema Recovery
- Android JNI RegisterNatives Obfuscation (HTB WonderSMS)
- Frida Firebase Cloud Functions Bypass (BSidesSF 2026)
- Verilog/Hardware Reverse Engineering (srdnlenCTF 2026)
- Prefix-by-Prefix Hash Reversal (Nullcon 2026)
- Ruby/Perl Polyglot Constraint Satisfaction (BearCatCTF 2026)
- Electron App + Native Binary Reversing (RootAccess2026)
- Node.js npm Package Runtime Introspection (RootAccess2026)
For core language reversing (Python, BF/esolangs, DOS, Unity, OPAL), see languages.md. For Go and Rust binary reversing, see languages-compiled.md.
---
Roblox Place File Analysis
Pattern (MazeRunna, 0xFun 2026): Roblox game with flag hidden in older version; latest version contains decoy.
Version history via Asset Delivery API:
# Extract placeId and universeId from game page HTML
# Query each version (requires .ROBLOSECURITY cookie):
curl -H "Cookie: .ROBLOSECURITY=..." \
"https://assetdelivery.roblox.com/v2/assetId/{placeId}/version/1"
# Download location URL → place_v1.rbxlbinBinary format parsing: .rbxlbin files contain chunks:
- INST — class buckets and referent IDs
- PROP — per-instance fields (including
Script.Source) - PRNT — parent-child relationships (object tree)
Decode chunk payloads, walk PROP entries for Source field, dump Script.Source / LocalScript.Source per version, then diff.
Key lesson: Always check version history. Latest version may contain decoy flag while real flag is in an older version. Diff script sources across versions.
---
Godot Game Asset Extraction
Pattern (Steal the Xmas): Encrypted Godot .pck packages.
Tools:
Workflow: 1. Run KeyDot against game executable → extract encryption key 2. Input key into gdsdecomp 3. Extract and open project in Godot editor 4. Search scripts/resources for flag data
---
Rust serde_json Schema Recovery
Pattern (Curly Crab, PascalCTF 2026): Rust binary reads JSON from stdin, deserializes via serde_json, prints success/failure emoji.
Approach: 1. Disassemble serde-generated Visitor implementations 2. Each visitor's visit_map / visit_seq reveals expected keys and types 3. Look for string literals in deserializer code (field names like "pascal", "CTF") 4. Reconstruct nested JSON schema from visitor call hierarchy 5. Identify value types from visitor method names: visit_str = string, visit_u64 = number, visit_bool = boolean, visit_seq = array
{"pascal":"CTF","CTF":2026,"crab":{"I_":true,"cr4bs":1337,"crabby":{"l0v3_":["rust"],"r3vv1ng_":42}}}Key insight: Flag is the concatenation of JSON keys in schema order. Reading field names in order reveals the flag.
---
Android JNI RegisterNatives Obfuscation (HTB WonderSMS)
Pattern: Android app loads native library with System.loadLibrary(), but uses RegisterNatives in JNI_OnLoad instead of standard JNI naming convention (Java_com_pkg_Class_method). This hides which C++ function handles each Java native method.
Identification:
// In decompiled Java (jadx):
static { System.loadLibrary("audio"); }
private final native ProcessedMessage processMessage(SmsMessage msg);Standard JNI would have a symbol Java_com_rloura_wondersms_SmsReceiver_processMessage. If that symbol is missing from the .so, RegisterNatives is being used.
Finding the real handler in Ghidra: 1. Locate JNI_OnLoad (exported symbol, always present) 2. Trace to RegisterNatives(env, clazz, methods, count) call 3. The methods array contains {name, signature, fnPtr} structs 4. Follow fnPtr to find the actual native function
// JNI_OnLoad registers functions manually:
static JNINativeMethod methods[] = {
{"processMessage", "(Landroid/telephony/SmsMessage;)LProcessedMessage;", (void*)real_handler}
};
(*env)->RegisterNatives(env, clazz, methods, 1);Architecture selection for analysis:
# x86_64 gives best Ghidra decompilation (most similar to desktop code)
# Extract from APK:
unzip WonderSMS.apk -d extracted/
ls extracted/lib/x86_64/ # Prefer this over arm64-v8a for static analysisKey insight: RegisterNatives is a deliberate obfuscation technique — it decouples Java method names from native symbol names, making it impossible to find handlers by string search alone. Always check JNI_OnLoad first when reversing Android native libraries with stripped symbols.
Detection: Native method declared in Java + no matching JNI symbol in .so + JNI_OnLoad present. The library is typically stripped (no debug symbols).
---
Frida Firebase Cloud Functions Bypass (BSidesSF 2026)
Pattern (vinyl-drop, doremi): Android app validates actions (QR codes, purchases) via Firebase Cloud Functions. The expected payload format includes the Firebase UID, a value, and a timestamp. Use Frida to hook the app post-login, construct a valid payload, and call the Cloud Function directly.
// Frida hook to bypass QR validation
Java.perform(function() {
var FirebaseFunctions = Java.use('com.google.firebase.functions.FirebaseFunctions');
var FirebaseAuth = Java.use('com.google.firebase.auth.FirebaseAuth');
// Get current user UID after login
var auth = FirebaseAuth.getInstance();
var uid = auth.getCurrentUser().getUid();
// Construct valid payload: uid + amount + timestamp
var unixMs = Java.use('java.lang.System').currentTimeMillis();
var payload = uid + "+100+" + unixMs;
// Call the Cloud Function directly
var functions = FirebaseFunctions.getInstance();
var data = Java.use('java.util.HashMap').$new();
data.put("payload", payload);
functions.getHttpsCallable("validateScanPayload").call(data);
});Key insight: Firebase AppCheck and Cloud Functions rely on the client to construct valid payloads. Post-authentication, Frida can hook the app to call any Cloud Function with arbitrary parameters, bypassing client-side validation (QR scanning, payment processing, etc.).
When to recognize: Android app with google-services.json, Firebase dependencies in build.gradle, Cloud Function calls in decompiled code.
References: BSidesSF 2026 "vinyl-drop"
---
Verilog/Hardware Reverse Engineering (srdnlenCTF 2026)
Pattern (Rev Juice): Verilog HDL source for a vending machine with hidden product unlocked by specific coin insertion and selection sequence.
Approach: 1. Analyze Verilog modules to understand state machine and history tracking 2. Identify hidden conditions (e.g., product 8 enabled only when COINS_HISTORY array has specific values at specific taps) 3. Build timing model for each action type (how many clock cycles each operation takes) 4. Work backward from required history values to construct the correct input sequence
Timing model construction:
# Map each action to its cycle count (determined from Verilog state machines)
TIMING = {
"insert_coin": 3, # 3 cycles per coin insertion
"select_success": 7, # 7 cycles for successful product selection
"select_fail": 5, # 5 cycles for failed selection attempt
"cancel_with_coins": 4, # 4 cycles for cancel when coins > 0
"cancel_at_zero": 2, # 2 cycles for cancel when coins = 0
}
# COINS_HISTORY is a shift register updated each cycle
# History tap requirements (from Verilog conditions):
# H[0]=1, H[7]=4, H[28]=H[33]=H[38]=6
# H[63]=H[73]=2, H[80]=9
# (H[19]+H[21]+H[56]+H[69]) mod 32 = 0Key insight: Hardware challenges require understanding the exact timing model — each operation takes a specific number of clock cycles, and shift registers record history at fixed tap positions. Work backward from the required tap values to determine what action must have occurred at each cycle. The solution is often a specific sequence notation (e.g., I9C_SP6_CNL_I2C_SP2_I6C_SP6_SP6_SP5_CNL_I4C_SP1).
Detection: Look for .v or .sv (Verilog/SystemVerilog) files, always @(posedge clk) blocks, shift register patterns, and state machine case statements with hidden conditions gated on history values.
---
Prefix-by-Prefix Hash Reversal (Nullcon 2026)
See patterns-ctf-2.md for the full technique. This section covers language-specific considerations.
Language-specific notes:
- Hash algorithm may be uncommon (MD2, custom) — don't need to identify it, just match outputs by running the binary
- Use
subprocess.run()withtimeout=2to handle binaries that hang on bad input - For stripped binaries, check if
ltracereveals the hash function name (e.g.,MD2_Update)
---
Ruby/Perl Polyglot Constraint Satisfaction (BearCatCTF 2026)
Pattern (Polly's Key): A single file valid in both Ruby and Perl. Each language imposes different validation constraints on a 50-character key. Satisfy both simultaneously to decrypt the flag.
Polyglot structure exploits:
- Ruby:
=begin...=endis a block comment - Perl:
=begin...=cutis POD (Plain Old Documentation),=endis ignored - Different code runs in each language based on comment block boundaries
Typical constraints:
- Ruby: Character set must form a mathematical property (e.g., all 50 printable ASCII chars except
^used exactly once, each satisfyingXOR(val, (val-16) % 257)is a primitive root mod 257) - Perl: Ordering constraint via insertion sort inversion count (hardcoded inversion table determines exact permutation)
Solution approach: 1. Find the valid character set (mathematical constraint from one language) 2. Use the ordering constraint (from other language) to determine exact arrangement 3. Compute key hash (e.g., MD5) and decrypt
# Determine character ordering from inversion counts
def reconstruct_from_inversions(chars, inv_counts):
result = []
remaining = sorted(chars)
for i in range(len(chars) - 1, -1, -1):
# inv_counts[i] = number of elements to the left that are greater
idx = inv_counts[i]
result.insert(idx, remaining.pop(i))
return resultKey insight: Polyglot files exploit language-specific comment/block syntax to run different code in each interpreter. The constraints from both languages intersect to uniquely determine the key. Identify which code runs in which language by testing the file with both interpreters and comparing behavior.
Detection: File that runs under multiple interpreters (ruby file && perl file). Challenge mentions "polyglot" or provides a file ending in .rb that also looks like Perl.
---
Electron App + Native Binary Reversing (RootAccess2026)
Pattern (Rootium Browser): Electron desktop app bundles a native ELF/DLL binary for sensitive operations (vault, crypto, auth). The Electron layer is a wrapper; the real flag logic is in the native binary.
Extraction workflow: 1. Unpack Electron ASAR archive:
# Install ASAR tool
npm install -g @electron/asar
# Extract the app.asar archive
asar extract resources/app.asar app_extracted/
ls app_extracted/2. Locate native binary: Search for ELF/DLL files called from JavaScript:
# Find native binaries
find app_extracted/ -name "*.node" -o -name "*.so" -o -name "*vault*" -o -name "*auth*"
# Check JS for child_process.spawn or ffi-napi calls
grep -r "spawn\|execFile\|ffi\|require.*native" app_extracted/3. Reverse the native binary (XOR + rotation cipher example):
def decrypt_password(encrypted_bytes, key):
"""Common pattern: XOR with constant + bit rotation + key XOR."""
result = []
for i, byte in enumerate(encrypted_bytes):
decrypted = ((byte ^ 0x42) >> 3) ^ key[i % len(key)]
result.append(chr(decrypted))
return ''.join(result)
def decrypt_flag(encrypted_flag, password):
"""Flag uses password as key with position-dependent rotation."""
result = []
for i, byte in enumerate(encrypted_flag):
key_byte = ord(password[i % len(password)])
decrypted = ((byte ^ 0x7E) >> (i % 8)) ^ key_byte
result.append(chr(decrypted))
return ''.join(result)Key insight: Electron apps are JavaScript wrapping native code. Extract with asar, then focus on the native binary. The JS layer often contains the password verification flow in plaintext, revealing what the native binary expects. Look for encrypted data in the .data or .rodata sections of the ELF.
Detection: .asar files in resources/ directory, Electron framework files, package.json with electron dependency.
---
Node.js npm Package Runtime Introspection (RootAccess2026)
Pattern (RootAccess CLI): Obfuscated npm package with RC4 encoding, control flow flattening, and flag split across multiple fragments. Static analysis is impractical — use runtime introspection instead.
Dynamic analysis approach:
#!/usr/bin/env node
// 1. Load obfuscated modules
const cryptoMod = require('target-package/dist/lib/crypto.js');
const vaultMod = require('target-package/dist/lib/vault.js');
// 2. Enumerate all exported properties
for (const mod of [cryptoMod, vaultMod]) {
for (const key of Object.keys(mod)) {
const obj = mod[key];
console.log(`Export: ${key}`);
// List all methods including hidden ones
const props = Object.getOwnPropertyNames(obj);
const proto = Object.getOwnPropertyNames(obj.prototype || {});
console.log(' Own:', props);
console.log(' Proto:', proto);
}
}
// 3. Extract flag fragments
const Engine = cryptoMod.CryptoEngine;
const total = Engine.getTotalFragments();
let flag = '';
for (let i = 1; i <= total; i++) {
flag += Engine.getFragment(i);
}
console.log('Flag:', flag);
// 4. Check for hidden methods (common: __getFullFlag__, _debug, _raw)
const hidden = Object.getOwnPropertyNames(Engine)
.filter(p => p.startsWith('__') || p.startsWith('_'));
console.log('Hidden methods:', hidden);Key insight: Heavily obfuscated JavaScript (control flow flattening, RC4 string encoding, dead code) makes static analysis prohibitively slow. Runtime introspection via Object.getOwnPropertyNames() reveals all methods including hidden ones. The module's own decryption runs automatically when loaded — just call the decoded functions directly.
Detection: npm package with minified/obfuscated dist/ directory, challenge says "reverse engineer the CLI tool", package.json with custom commands.
CTF Reverse - Language-Specific Techniques
Table of Contents
- Python Bytecode Reversing (dis.dis output)
- Common Pattern: XOR Validation with Split Indices
- Bytecode Analysis Tips
- Python Opcode Remapping
- Identification
- Recovery
- Pyarmor 8/9 Static Unpack (1shot)
- DOS Stub Analysis
- Unity IL2CPP Games
- HarmonyOS HAP/ABC Reverse (abc-decompiler)
- Brainfuck/Esolangs
- Brainfuck Character-by-Character Static Analysis (BSidesSF 2026)
- Brainfuck Side-Channel via Read Count Oracle (BSidesSF 2026)
- Brainfuck Comparison Idiom Detection (BSidesSF 2026)
- UEFI Binary Analysis
- Transpilation to C
- Code Coverage Side-Channel Attack
- Functional Language Reversing (OPAL)
- Python Version-Specific Bytecode (VuwCTF 2025)
- Non-Bijective Substitution Cipher Reversing
For platform/framework-specific techniques (Android, Roblox, Godot, Electron, Node.js, Verilog, Ruby/Perl polyglot, etc.), see languages-platforms.md. For Go and Rust binary reversing, see languages-compiled.md.
---
Python Bytecode Reversing (dis.dis output)
Common Pattern: XOR Validation with Split Indices
Challenge gives raw CPython bytecode (dis.dis disassembly). Common pattern: 1. Check flag length 2. XOR chars at even indices with key1, compare to list p1 3. XOR chars at odd indices with key2, compare to list p2
Reversing:
# Given: p1, p2 (expected values), key1, key2 (XOR keys)
flag = [''] * flag_length
for i in range(len(p1)):
flag[2*i] = chr(p1[i] ^ key1) # Even indices
flag[2*i+1] = chr(p2[i] ^ key2) # Odd indices
print(''.join(flag))Bytecode Analysis Tips
LOAD_CONSTfollowed byCOMPARE_OPreveals expected valuesBINARY_XORidentifies the transformationBUILD_TUPLE/BUILD_LISTwith constants = expected output array- Loop structure:
FOR_ITER+BINARY_SUBSCR= iterating over flag chars CALL_FUNCTIONonord= character-to-int conversion
---
Python Opcode Remapping
Identification
Decompiler fails with opcode errors.
Recovery
1. Find modified opcode.pyc in PyInstaller bundle 2. Compare with original Python opcodes 3. Build mapping: {new_opcode: original_opcode} 4. Patch target .pyc 5. Decompile normally
Shortcut (Hack.lu CTF 2013): If the challenge bundles its own modified Python interpreter (e.g., a custom ./py binary), install uncompyle2/uncompyle6 into that interpreter's environment and decompile using the challenge's own runtime. The modified interpreter understands its own opcode mapping, so standard decompilation tools work without manual opcode recovery.
---
Pyarmor 8/9 Static Unpack (1shot)
- Tool:
Lil-House/Pyarmor-Static-Unpack-1shot - Use for Pyarmor 8.x/9.x armored scripts without executing sample code
- Quick signature check: payload typically starts with
PY+ six digits (Pyarmor 7 and earlierPYARMORformat is not supported)
Workflow: 1. Ensure target directory contains armored scripts and matching pyarmor_runtime library. 2. Run one-shot unpack to emit .1shot. outputs (disassembly + experimental decompile). 3. Treat disassembly as ground truth; verify decompiled source with bytecode when inconsistent.
python /path/to/oneshot/shot.py /path/to/scriptsOptional flags:
# Specify runtime explicitly
python /path/to/oneshot/shot.py /path/to/scripts -r /path/to/pyarmor_runtime.so
# Write outputs to another directory
python /path/to/oneshot/shot.py /path/to/scripts -o /path/to/outputNotes:
oneshot/pyarmor-1shotexecutable must exist before runningshot.py.- PyInstaller bundles or archives should be unpacked first, then processed with 1shot.
---
DOS Stub Analysis
PE files can hide code in DOS stub: 1. Check for large DOS stub in Ghidra/IDA 2. Run in DOSBox 3. Load in IDA as 16-bit DOS 4. Look for int 16h (keyboard input)
---
Unity IL2CPP Games
- Use Il2CppDumper to dump symbols
- If Il2CppDumper fails, consider that
global-metadata.datmay be encrypted; search strings/xrefs in the main binary and inspect the metadata loading path for custom decryption before dump. - Look for
Start()functions - Key derivation:
key = SHA256(companyName + "\n" + productName) - Decrypt server responses with derived key
Please note most of that the executable file for the PC platform is GameAssembly.dll or *Assembly.dll, for the Android is libil2cpp.so.
---
HarmonyOS HAP/ABC Reverse (abc-decompiler)
- Target files:
.happackage and embedded.abcbytecode - Tool:
https://github.com/ohos-decompiler/abc-decompiler - Download
jadx-dev-all.jarfrom releases
Critical startup note:
java -jarmay enter GUI mode- For CLI mode, always use:
java -cp "./jadx-dev-all.jar" jadx.cli.JadxCLI [options] <input>Most common commands:
# Basic decompile to directory
java -cp "./jadx-dev-all.jar" jadx.cli.JadxCLI -d "out" ".abc"
# Decompile .abc (recommended for this scenario)
java -cp "./jadx-dev-all.jar" jadx.cli.JadxCLI -m simple -d "out_hap" "modules.abc"Recommended parameters for this challenge:
-m simple: reduce high-level reconstruction to avoid SSA/PHI-heavy failures--log-level ERROR: keep only critical errors- Full recommended command:
java -cp "./jadx-dev-all.jar" jadx.cli.JadxCLI -m simple --log-level ERROR -d "out_abc_simple" "modules.abc"Parameter quick reference:
-doutput directory--helphelp
Notes:
.hapis a package: extract it first (zip), then locate and analyze.abc- Quote paths containing spaces or non-ASCII characters
- Use a new output directory name per run to avoid stale results
- Errors do not always mean full failure; prioritize
out_xxx/sources/ - If
autofails, switch to-m simplefirst
Standard workflow: 1. Run with -m simple --log-level ERROR 2. Inspect key business files in output (for example pages/Index.java) 3. If cleaner output is needed, retry with -m auto or -m restructure 4. If some methods still fail, keep the simple output and continue logic analysis via alternate paths
---
Brainfuck/Esolangs
- Check if compiled with known tools (BF-it)
- Understand tape/memory model
- Static analysis of cell operations
Brainfuck Character-by-Character Static Analysis (BSidesSF 2026)
Pattern (i-love-my-bf-part1): BF programs that validate input character-by-character follow a recognizable pattern: , (read char) followed by a sequence of + operations whose count equals the expected ASCII value of that character.
Extraction technique:
import re
bf_code = open('challenge.bf', 'r').read()
# Split on comma (input read) — each segment handles one character
segments = bf_code.split(',')
expected = []
for seg in segments[1:]: # Skip preamble before first comma
# Count consecutive '+' operations before any branch/output
plus_count = 0
for ch in seg:
if ch == '+':
plus_count += 1
elif ch in '-.[]><':
break # Stop at first non-increment operation
if plus_count > 0:
expected.append(chr(plus_count % 256))
flag = ''.join(expected)
print(f"Flag: {flag}")Variations:
-operations: character value =256 - minus_count- Mixed
+/-: net increment determines value - Cell reset (
[-]) between characters: each segment is independent - Loop-based multiplication:
[->>+++<<]multiplies by 3 — count the inner operations
Detection: Large BF file with repeating pattern of , followed by many + or - characters, then a comparison structure ([-] or [->+<] patterns).
Key insight: BF programs that check input are structurally simple — each input byte is compared against a constant built by incrementing a cell. Extract the increment counts to recover the expected input without running the program.
References: BSidesSF 2026 "i-love-my-bf-part1"
Brainfuck Side-Channel via Read Count Oracle (BSidesSF 2026)
Pattern (i-love-my-bf-part2): When a BF program validates input character-by-character, a correct character causes the program to consume MORE input bytes (advancing to check the next position). By counting how many , (read) operations execute for each candidate input, the character that triggers the most reads is correct.
import itertools
def bytes_read_running_bf(bf_code, input_iter, braces):
"""Run BF and count how many input bytes were consumed."""
tape = [0] * 30000
ptr = ip = reads = 0
input_list = list(input_iter)
input_idx = 0
while ip < len(bf_code):
c = bf_code[ip]
if c == ',':
if input_idx < len(input_list):
tape[ptr] = input_list[input_idx]
input_idx += 1
reads += 1
else:
return reads
elif c == '.': pass
elif c == '+': tape[ptr] = (tape[ptr] + 1) % 256
elif c == '-': tape[ptr] = (tape[ptr] - 1) % 256
elif c == '>': ptr += 1
elif c == '<': ptr -= 1
elif c == '[' and tape[ptr] == 0: ip = braces[ip]
elif c == ']' and tape[ptr] != 0: ip = braces[ip]
ip += 1
return reads
# Recover flag character by character
PRINTABLE = list(range(32, 127))
flag = []
for pos in range(50): # max flag length
best_byte = None
max_reads = 0
baseline = bytes_read_running_bf(bf, flag + [PRINTABLE[0]], braces)
for b in PRINTABLE[1:]:
reads = bytes_read_running_bf(bf, flag + [b], braces)
if reads > baseline:
best_byte = b
break
if best_byte is None:
break
flag.append(best_byte)
print(bytes(flag).decode())Key insight: BF input validation programs are sequential — they read one character, check it, and only read the next if it matches. The character causing more reads is correct because the program advances past the validation gate to check the next position.
References: BSidesSF 2026 "i-love-my-bf-part2"
Brainfuck Comparison Idiom Detection (BSidesSF 2026)
Pattern (i-love-my-bf-part3): BF programs compiled from higher-level languages use recognizable comparison idioms. The equality check <[-<->] +<[>-<[-]]>[-<+>] compares two adjacent cells. By instrumenting a BF interpreter to detect this pattern during execution, you can extract the comparison operands (expected flag bytes) directly from the tape.
EQ_PATTERN = "<[-<->] +<[>-<[-]]>[-<+>]"
def instrumented_bf_run(bf_code, dummy_input):
"""Run BF, detect equality comparisons, extract operands."""
tape = [0] * 30000
ptr = ip = 0
comparisons = []
while ip < len(bf_code):
# Check if current position starts the eq pattern
if bf_code[ip:ip+len(EQ_PATTERN)] == EQ_PATTERN:
# The two cells being compared are at ptr-2 and ptr-1
lhs = tape[ptr - 2] # User input byte
rhs = tape[ptr - 1] # Expected byte
comparisons.append((chr(lhs), chr(rhs)))
# ... normal BF execution ...
ip += 1
return comparisons
# Expected bytes from comparisons reveal the flagKey insight: Compiled BF programs reuse fixed idioms for operations like equality comparison, conditional branching, and loops. Pattern-matching these idioms in the BF source or during execution lets you extract constants without fully understanding the program logic.
Common BF idioms:
[-]— clear cell (set to 0)[->+<]— move cell right<[-<->] +<[>-<[-]]>[-<+>]— equality comparison of two cells
References: BSidesSF 2026 "i-love-my-bf-part3"
---
UEFI Binary Analysis
7z x firmware.bin -oextracted/
file extracted/* | grep "PE32+"- Bootkit replaces boot loader
- Custom VM protects decryption
- Lift VM bytecode to C
---
Transpilation to C
For heavily obfuscated code:
for opcode, args in instructions:
if opcode == 'XOR':
print(f"r{args[0]} ^= r{args[1]};")
elif opcode == 'ADD':
print(f"r{args[0]} += r{args[1]};")Compile with -O3 for constant folding.
---
Code Coverage Side-Channel Attack
Pattern (Coverup, Nullcon 2026): PHP challenge provides XDebug code coverage data alongside encrypted output.
How it works:
- PHP code uses
xdebug_start_code_coverage(XDEBUG_CC_UNUSED | XDEBUG_CC_DEAD_CODE | XDEBUG_CC_BRANCH_CHECK) - Encryption uses data-dependent branches:
if ($xored == chr(0)) ... if ($xored == chr(1)) ... - Coverage JSON reveals which branches were executed during encryption
- This leaks the set of XOR intermediate values that occurred
Exploitation:
import json
# Load coverage data
with open('coverage.json') as f:
cov = json.load(f)
# Extract executed XOR values from branch coverage
executed_xored = set()
for line_no, hit_count in cov['encrypt.php']['lines'].items():
if hit_count > 0:
# Map line numbers to the chr(N) value in the if-statement
executed_xored.add(extract_value_from_line(line_no))
# For each position, filter candidates
for pos in range(len(ciphertext)):
candidates = []
for key_byte in range(256):
xored = plaintext_byte ^ key_byte # or reverse S-box lookup
if xored in executed_xored:
candidates.append(key_byte)
# Combined with known plaintext prefix, this uniquely determines keyKey insight: Code coverage is a powerful oracle — it tells you which conditional paths were taken. Any encryption with data-dependent branching leaks information through coverage.
Mitigation detection: Look for branchless/constant-time crypto implementations that defeat this attack.
---
Functional Language Reversing (OPAL)
Pattern (Opalist, Nullcon 2026): Binary compiled from OPAL (Optimized Applicative Language), a purely functional language.
Recognition markers:
.impl(implementation) and.sign(signature) source filesIMPLEMENTATION/SIGNATUREkeywords- Nested
IF..THEN..ELSE..FIstructures - Functions named
f1,f2, ...fN(numeric naming) - Heavy use of
seq[nat],string,denotationtypes
Reversing approach: 1. Pure functions are mathematically invertible — reverse each step in the pipeline 2. Identify the transformation chain: f_final(f_n(...f_2(f_1(input))...)) 3. For each function, build the inverse
Aggregate brute-force for scramble functions: When a transformation accumulates state that depends on original (unknown) values:
# Example: f8 adds cumulative offset based on parity of original bytes
# offset contribution per element depends on whether pre-scramble value is even/odd
# Total offset S = sum of contributions, but S mod 256 has only 256 possibilities
decoded = base64_decode(target)
for total_offset_S in range(256):
candidate = [(b - total_offset_S) % 256 for b in decoded]
# Verify: recompute S from candidate values
recomputed_S = sum(contribution(i, candidate[i]) for i in range(len(candidate))) % 256
if recomputed_S == total_offset_S:
# Apply remaining inverse steps
result = apply_inverse_substitution(candidate)
if all(32 <= c < 127 for c in result):
print(bytes(result))Key lesson: When a scramble function has a chicken-and-egg dependency (result depends on original, which is unknown), brute-force the aggregate effect (often mod 256 = 256 possibilities) rather than all possible states (exponential).
---
Python Version-Specific Bytecode (VuwCTF 2025)
Pattern (A New Machine): Challenge targets specific Python version (e.g., 3.14.0 alpha).
Key requirement: Compile that exact Python version to disassemble bytecode — alpha/beta versions have different opcodes than stable releases.
# Build specific Python version
wget https://www.python.org/ftp/python/3.14.0/Python-3.14.0a4.tar.xz
tar xf Python-3.14.0a4.tar.xz
cd Python-3.14.0a4 && ./configure && make -j$(nproc)
./python -c "import dis, marshal; dis.dis(marshal.loads(open('challenge.pyc','rb').read()[16:]))"Common validation: Flag compared against tuple of squared ASCII values:
# Reverse: flag[i] = sqrt(expected_tuple[i])
import math
flag = ''.join(chr(int(math.isqrt(v))) for v in expected_values)---
Non-Bijective Substitution Cipher Reversing
Pattern (Coverup, Nullcon 2026): S-box/substitution table has collisions (multiple inputs map to same output).
Detection:
sbox = [...] # substitution table
if len(set(sbox)) < len(sbox):
print("Non-bijective! Collisions exist.")Building reverse lookup:
from collections import defaultdict
rev_sub = defaultdict(list)
for i, v in enumerate(sbox):
rev_sub[v].append(i)
# rev_sub[output] = [list of possible inputs]Disambiguation strategies: 1. Known plaintext format (e.g., ENO{, flag{) fixes key bytes at known positions 2. Side-channel data (code coverage, timing) eliminates impossible candidates 3. Printable ASCII constraint (32-126) reduces candidate space 4. Re-encrypt candidates and verify against known ciphertext