name: ctf-reverse
description: Reverse engineering: ELF/PE/Mach-O, WASM, .NET, APK (Flutter/Dart), Python bytecode, Go/Rust/Swift/Kotlin, custom VMs, anti-debug/anti-VM, VMProtect/Themida, eBPF, Ghidra/IDA/radare2/Frida/angr/Qiling. Dispatch on file magic + loader signature.
license: MIT
compatibility: Requires filesystem-based agent (Claude Code or similar) with bash, Python 3, and internet access for tool installation.
allowed-tools: Bash Read Write Edit Glob Grep Task WebFetch WebSearch
metadata:
user-invocable: "false"
CTF Reverse Engineering
Quick reference for RE challenges. For detailed techniques, see supporting files.
Additional Resources
- tools.md — GDB, Ghidra, radare2, IDA, Binary Ninja, Unicorn, WASM, pyc, packed
- tools-dynamic.md — Frida, angr, lldb, x64dbg, Qiling, Triton, Pin instruction-counting
- tools-advanced.md — VMProtect/Themida, BinDiff, D-810/GOOMBA, TTF GSUB, AVX2 Z3 lift
- tools-advanced-2.md — 2025-26: GB-scale PE Unicorn+angr hybrid (VirtualProtect-gated unpackers)
- anti-analysis.md — Linux/Windows anti-debug, anti-VM, anti-DBI, MBA, self-hashing
- patterns.md — custom VMs, nanomites, LLVM obfuscation, S-box, SECCOMP/BPF, multi-thread
- patterns-ctf.md — comp patterns part 1: hidden opcodes, LD_PRELOAD, GBA MITM, maze kmod
- patterns-ctf-2.md — part 2: multi-layer brute, CVP integer, decision-tree, perf oracle, VM misident
- patterns-ctf-3.md — 2025-26: genetic algorithm / hill-climb over opaque additive scoring
- languages.md — Python bytecode, pyarmor, UEFI, esolangs, HarmonyOS, Godot, Electron
- languages-compiled.md — Go (GoReSym), Rust, Swift, Kotlin/JVM, C++ vtables, .pyc forgery
- platforms.md — Mach-O, iOS jailbreak, embedded firmware, kernel drivers, game engines, CAN
Pattern Recognition Index
Dispatch on observable binary features, not challenge titles.
Signal (from file, readelf, strings, nm) | Technique → file |
|---|
ELF with __libc_start_main, small main, direct syscalls | Basic RE patterns → patterns.md |
| ELF with large unrecognised opcode-dispatch loop (switch on byte → handler) | Custom VM reversing → patterns.md |
readelf -l shows RWX segment + self-writes to .text | Self-modifying / multi-layer decryption → patterns-ctf-2.md |
| Binary that modifies its round constants and re-encrypts output | Binary-as-keystream-oracle (patch I/O boundary) → patterns-ctf-2.md |
ptrace(PTRACE_TRACEME) / /proc/self/status TracerPid / rdtsc timing | Anti-debug detection → anti-analysis.md |
__Py_* or PyMarshal strings | Python bytecode / pyc reversing → languages.md |
runtime. prefix in strings, go.buildinfo | Go reversing (GoReSym) → languages-compiled.md |
Rust demangling (_ZN/_RN), core::panicking | Rust reversing → languages-compiled.md |
Mach-O header FEEDFACE/FEEDFACF | macOS/iOS RE → platforms.md |
.wasm magic (00 61 73 6D) | WASM → languages.md, ctf-misc/games-and-vms.md |
.apk/classes.dex, libflutter.so, kernel.dill | APK / Flutter reversing → languages.md |
| Unicorn/QEMU used as a sandbox with host-side memory read helpers | Host/guest hook divergence → patterns-ctf-2.md (and ctf-pwn/advanced-exploits-2.md) |
.rodata blob + XOR loop with known constants / stored expected bytes | Stack-string deobfuscation → patterns-ctf-2.md |
SHA-NI instructions, per-layer key read from stdin | Multi-layer brute-force JIT → patterns-ctf-2.md |
| Per-char early-exit compare loop + local execution allowed | perf_event_open instruction-count oracle → patterns-ctf-2.md |
| Custom VM whose handlers are pop/push but docs claim "register-based" + banned bytes | Arch misidentification + banned-byte synthesis → patterns-ctf-2.md |
.pyc with loader that checks only first 16 bytes | PEP-552 magic-header forgery → languages-compiled.md |
Go binary with runtime.itab symbols intact but stripped strings | GoReSym/typelinks restore → languages-compiled.md |
bpftool prog list shows non-standard eBPF prog | eBPF FSM syscall-sequence decomp → languages-compiled.md (+ ctf-pwn/sandbox-escape.md) |
TTF/OTF with abnormally dense GSUB; glyphs named hex_*/one/zero | GSUB ligature stego DAG reverse → tools-advanced.md |
AVX2 vpaddb/vpshufb in tight loop over input | Lane-wise Z3 lifting → tools-advanced.md |
PE ≥ 500 MB, multiple VirtualProtect(...,RWX) + inline decrypt + call/jmp rax after each | Unicorn layer-graph + per-layer angr solve → tools-advanced-2.md |
Flat chain of hundreds of if (input[i] op const) score += kN; win if score >= THR | Separability probe → hill-climb → GA → patterns-ctf-3.md |
Recognize the artefact or opcode pattern. The title is noise.
For inline code/cheatsheet quick references (grep patterns, one-liners, common payloads), see quickref.md. The Pattern Recognition Index above is the dispatch table — always consult it first; load quickref.md only if you need a concrete snippet after dispatch.
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
Self-ptrace (most common):
if (ptrace(PTRACE_TRACEME, 0, 0, 0) == -1) exit(1);
Bypasses:
LD_PRELOAD=./hook.so ./binary
python3 -c "
from pwn import *
elf = ELF('./binary', checksec=False)
elf.asm(elf.symbols.ptrace, 'xor eax, eax; ret')
elf.save('patched')
"
gdb ./binary
(gdb) catch syscall ptrace
(gdb) run
(gdb) set $rax = 0
(gdb) continue
echo 0 > /proc/sys/kernel/yama/ptrace_scope
Double-ptrace pattern:
pid_t child = fork();
if (child == 0) {
ptrace(PTRACE_ATTACH, getppid(), 0, 0);
} else {
}
Bypass: Kill the watchdog child process, then attach debugger.
/proc Filesystem Checks
FILE *f = fopen("/proc/self/status", "r");
readlink("/proc/self/exe", buf, sizeof(buf));
grep("frida", "/proc/self/maps");
Bypasses:
unshare -m bash -c 'mount --bind /dev/null /proc/self/status && ./binary'
(gdb) b fopen
(gdb) run
(gdb) set {char[20]} $rdi = "/dev/null"
(gdb) continue
Timing-Based Detection
uint64_t start = __rdtsc();
uint64_t delta = __rdtsc() - start;
if (delta > THRESHOLD) exit(1);
struct timespec ts1, ts2;
clock_gettime(CLOCK_MONOTONIC, &ts1);
clock_gettime(CLOCK_MONOTONIC, &ts2);
struct timeval tv1, tv2;
gettimeofday(&tv1, NULL);
Bypasses:
(gdb) set {unsigned char[2]} 0x401234 = {0x90, 0x90}
LD_PRELOAD=/usr/lib/faketime/libfaketime.so.1 FAKETIME="2024-01-01" ./binary
Signal-Based Anti-Debug
signal(SIGTRAP, handler);
__asm__("int3");
signal(SIGALRM, kill_handler);
alarm(5);
signal(SIGSEGV, real_logic_handler);
*(int*)0 = 0;
Bypasses:
(gdb) handle SIGTRAP nostop pass
(gdb) handle SIGALRM ignore
(gdb) handle SIGSEGV nostop pass
Syscall-Level Evasion
long ret;
asm volatile("syscall" : "=a"(ret) : "a"(101), "D"(0), "S"(0), "d"(0), "r"(0));
Bypass: Must patch the binary itself or use ptrace to intercept at syscall level.
(gdb) catch syscall 101
(gdb) commands
> set $rax = 0
> continue
> end
Windows Anti-Debug (Advanced)
PEB-Based Checks
bool debugged = NtCurrentPeb()->BeingDebugged;
DWORD flags = *(DWORD*)((BYTE*)NtCurrentPeb() + 0xBC);
if (flags & 0x70) exit(1);
Bypass (x64dbg):
# ScyllaHide plugin auto-patches PEB fields
# Manual: dump PEB, zero BeingDebugged and NtGlobalFlag
NtQueryInformationProcess
DWORD_PTR debugPort = 0;
NtQueryInformationProcess(GetCurrentProcess(), 7, &debugPort, sizeof(debugPort), NULL);
if (debugPort != 0) exit(1);
HANDLE debugObj = NULL;
NTSTATUS status = NtQueryInformationProcess(GetCurrentProcess(), 0x1E, &debugObj, sizeof(debugObj), NULL);
if (status == 0) exit(1);
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
PHEAP heap = (PHEAP)GetProcessHeap();
if (heap->Flags != 0x2 || heap->ForceFlags != 0) exit(1);
TLS Callbacks
Key technique: TLS (Thread Local Storage) callbacks execute BEFORE main() / entry point.
void NTAPI TlsCallback(PVOID DllHandle, DWORD Reason, PVOID Reserved) {
if (Reason == DLL_PROCESS_ATTACH) {
if (IsDebuggerPresent()) {
ExitProcess(1);
}
}
}
#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
CONTEXT ctx;
ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;
GetThreadContext(GetCurrentThread(), &ctx);
if (ctx.Dr0 || ctx.Dr1 || ctx.Dr2 || ctx.Dr3) exit(1);
Bypass:
Software Breakpoint Detection (INT3 Scanning)
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);
}
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
SetUnhandledExceptionFilter(handler);
RaiseException(EXCEPTION_ACCESS_VIOLATION, 0, 0, NULL);
__asm { int 2dh }
NtSetInformationThread (Thread Hiding)
typedef NTSTATUS(NTAPI *pNtSIT)(HANDLE, ULONG, PVOID, ULONG);
pNtSIT NtSIT = (pNtSIT)GetProcAddress(GetModuleHandle("ntdll"), "NtSetInformationThread");
NtSIT(GetCurrentThread(), 0x11 , NULL, 0);
Bypass: 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)) {
exit(1);
}
__cpuid(regs, 0x40000000);
char brand[13] = {0};
memcpy(brand, ®s[1], 12);
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:00
Timing-Based VM Detection
uint64_t start = __rdtsc();
__cpuid(regs, 0);
uint64_t delta = __rdtsc() - start;
if (delta > 500) { }
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)
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);
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
FILE *f = fopen("/proc/self/maps", "r");
while (fgets(line, sizeof(line), f)) {
if (strstr(line, "frida") || strstr(line, "gadget")) exit(1);
}
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);
unsigned char *strcmp_bytes = (unsigned char *)strcmp;
if (strcmp_bytes[0] == 0xE9 || strcmp_bytes[0] == 0xFF) exit(1);
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);
}
Frida bypass of Frida detection:
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));
}
}
});
Pin/DynamoRIO Detection
Code Integrity / Self-Hashing
uint32_t crc = compute_crc32(text_start, text_size);
if (crc != EXPECTED_CRC) exit(1);
unsigned char hash[32];
SHA256(function_addr, function_size, hash);
if (memcmp(hash, expected_hash, 32) != 0) exit(1);
Bypasses:
- Hardware breakpoints (don't modify code, DR0-DR3)
- Patch the comparison to always succeed
- Hook the hash function to return expected value
- Emulate instead of debug (Unicorn/Qiling — no code modification)
- Snapshot + restore: dump memory before and after, diff to find checks
Self-checksumming in loops:
void *watchdog(void *arg) {
while (1) {
if (compute_crc32(text_start, text_end - text_start) != saved_crc) {
memset(flag_buffer, 0, flag_len);
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 here
Identification: 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 here
Fix: 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
Mixed Boolean-Arithmetic (MBA) Identification & Simplification
from simba import simplify_mba
expr = "(a | b) + (a & b) - (~a & b)"
print(simplify_mba(expr))
Comprehensive Bypass Strategies
Universal Bypass Checklist
- Identify all anti-analysis checks — search for:
ptrace, IsDebuggerPresent, rdtsc, cpuid, NtQuery, GetTickCount, CheckRemoteDebuggerPresent, /proc/self, SIGTRAP, alarm
- Static patching — NOP/patch checks with pwntools or Ghidra before running
- LD_PRELOAD (Linux) — hook libc functions returning fake values
- ScyllaHide (Windows x64dbg) — patches PEB, hooks NT functions automatically
- Emulation (Unicorn/Qiling) — no debugger artifacts to detect
- 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 handler
Approach: 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 |
CTF Reverse - Compiled Language Reversing (Go, Rust)
Table of Contents
Go Binary Reversing
Go binaries are increasingly common in CTF challenges due to Go's popularity for CLI tools, network services, and malware.
Recognition
file binary | grep -i "go"
strings binary | grep "go.buildid"
strings binary | grep "runtime.gopanic"
strings binary | grep "^go1\."
Key indicators:
- Very large static binary (even "hello world" is ~2MB)
- Embedded
go.buildid string
runtime.* symbols (even in stripped binaries, some remain)
main.main as entry point (not main)
- Strings like
GOROOT, GOPATH, /usr/local/go/src/
Symbol Recovery
Go embeds rich type and function information even in stripped binaries:
./GoReSym -d binary > symbols.json
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:
redress (Go binary analysis):
redress -src binary
redress -pkg binary
redress -type binary
redress -interface binary
Go 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;
int64 len;
};
# Slice: {pointer, length, capacity} (24 bytes on 64-bit)
struct GoSlice {
void *ptr;
int64 len;
int64 cap;
};
# Interface: {type_descriptor, data_pointer} (16 bytes)
struct GoInterface {
void *type;
void *data;
};
# Map: pointer to runtime.hmap struct
# Channel: pointer to runtime.hchan struct
In 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
strings binary | grep "runtime.newproc"
gdb ./binary
(gdb) source /usr/local/go/src/runtime/runtime-gdb.py
(gdb) info goroutines
(gdb) goroutine 1 bt
Channel operations in disassembly:
runtime.chansend1 → ch <- value
runtime.chanrecv1 → value = <-ch
runtime.selectgo → select { case ... }
runtime.closechan → close(ch)
Common Go Patterns in Decompilation
Defer mechanism:
runtime.deferproc → registers deferred function
runtime.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_handler
String concatenation:
runtime.concatstrings → s1 + s2 + s3
fmt.Sprintf → formatted string building
- Look for format strings in
.rodata: "%s%d", "%x"
Common stdlib patterns in CTF:
Go Binary Reversing Workflow
1. file binary
2. GoReSym -d binary > syms.json
3. strings binary | grep -i flag
4. Load in Ghidra with golang-loader
5. Find main.main
6. Identify string comparisons
7. Trace crypto operations
8. Check for embedded resources
Go embed.FS (Go 1.16+): Binaries can embed files at compile time:
strings binary | grep "embed"
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/.
Rust Binary Reversing
Rust binaries are common in modern CTFs, especially for crypto, systems, and security tooling challenges.
Recognition
strings binary | grep -c "rust"
strings binary | grep "rustc"
strings binary | grep "/rustc/"
strings binary | grep "core::panicking"
Key indicators:
core::panicking::panic in strings
- Mangled symbols starting with
_ZN (Itanium ABI) — e.g., _ZN4main4main17h...
.rustc section in ELF
- References to
/rustc/<commit_hash>/library/
- Large binary size (Rust statically links by default)
Symbol Demangling
cargo install rustfilt
nm binary | rustfilt | grep "main"
nm binary | c++filt | grep "main"
Common 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_case
Vec (same as Go slice):
struct RustVec {
void *ptr;
uint64 cap;
uint64 len;
};
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 str
Iterator 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:
strings binary | grep "panicked at"
strings binary | grep "called .unwrap().. on"
Rust-Specific Analysis Tools
cargo install cargo-bloat
cargo bloat --release -n 50
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:
strings binary | grep "swift"
otool -l binary | grep "swift"
swift demangle 's14MyApp0A8ClassC10checkInput6resultSbSS_tF'
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)
strings classes.dex | grep "kotlin"
jadx classes.dex
cfr classes.jar --kotlin
fernflower classes.jar output/
Kotlin 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
strings binary | grep "konan"
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)
strings binary | grep -E "^[0-9]+[A-Z]"
c++filt _ZTI7MyClass
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, ...}
.pyc PEP-552 Magic-Header Forgery (source: pwn.college AoP 2025 day 9)
Trigger: loader validates the first 16 bytes of a .pyc (magic + timestamp + source-hash tag) but doesn't verify the bytecode body.
Signals: custom importlib.abc.Loader that checks data[:16]; no hashlib call on data[16:].
Mechanic: Python 3.7+ PEP-552 pyc layout: 4B magic | 4B flags | 8B (timestamp+size OR hash). Prepend the expected header verbatim, append attacker bytecode, loader accepts. Trivial template:
import importlib.util, marshal
magic = importlib.util.MAGIC_NUMBER
with open('out.pyc','wb') as f:
f.write(magic + b'\x00\x00\x00\x00' + b'\x00'*8 + marshal.dumps(my_code))
Go Interface/itab Restore via GoReSym (source: HTB University 2025 Starshard Reassembly)
Trigger: Go binary with interface-typed method calls (itab tables); standard strings/objdump yield little; Go runtime typelinks intact.
Signals: runtime.itab symbol present; go.funcinfo.* section; dispatch through [itab+OFF].
Mechanic: run GoReSym or go-symbol-restore — recovers the typelinks table and resolves each itab to its concrete type + method set. Feed into Ghidra with type propagation to see virtual dispatch as plain calls. Pattern applies to any stripped Go 1.18+ binary.
eBPF kprobe FSM Gated by Syscall Sequence (source: pwn.college AoP 2025 day 4)
Trigger: eBPF attached to a kprobe that mutates a BPF_MAP_TYPE_HASH based on syscall arg hashes; flag only releases when map reaches a specific state.
Signals: bpftool prog list non-standard entry; bpftool prog dump xlated id N shows state-machine transitions.
Mechanic: see ctf-pwn/sandbox-escape.md cross-ref for the same technique from the pwn angle. For RE: lift bytecode via angr's bpf-ir loader, symbolic-execute to find a sequence of (syscall, arg) tuples reaching accept state.
CTF Reverse - Language & Platform-Specific Techniques
Table of Contents
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:
- Check flag length
- XOR chars at even indices with key1, compare to list p1
- XOR chars at odd indices with key2, compare to list p2
Reversing:
flag = [''] * flag_length
for i in range(len(p1)):
flag[2*i] = chr(p1[i] ^ key1)
flag[2*i+1] = chr(p2[i] ^ key2)
print(''.join(flag))
Bytecode Analysis Tips
LOAD_CONST followed by COMPARE_OP reveals expected values
BINARY_XOR identifies the transformation
BUILD_TUPLE/BUILD_LIST with constants = expected output array
- Loop structure:
FOR_ITER + BINARY_SUBSCR = iterating over flag chars
CALL_FUNCTION on ord = character-to-int conversion
Python Opcode Remapping
Identification
Decompiler fails with opcode errors.
Recovery
- Find modified
opcode.pyc in PyInstaller bundle
- Compare with original Python opcodes
- Build mapping:
{new_opcode: original_opcode}
- Patch target .pyc
- 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 earlier PYARMOR format is not supported)
Workflow:
- Ensure target directory contains armored scripts and matching
pyarmor_runtime library.
- Run one-shot unpack to emit
.1shot. outputs (disassembly + experimental decompile).
- Treat disassembly as ground truth; verify decompiled source with bytecode when inconsistent.
python /path/to/oneshot/shot.py /path/to/scripts
Optional flags:
python /path/to/oneshot/shot.py /path/to/scripts -r /path/to/pyarmor_runtime.so
python /path/to/oneshot/shot.py /path/to/scripts -o /path/to/output
Notes:
oneshot/pyarmor-1shot executable must exist before running shot.py.
- PyInstaller bundles or archives should be unpacked first, then processed with 1shot.
DOS Stub Analysis
PE files can hide code in DOS stub:
- Check for large DOS stub in Ghidra/IDA
- Run in DOSBox
- Load in IDA as 16-bit DOS
- Look for
int 16h (keyboard input)
Unity IL2CPP Games
- Use Il2CppDumper to dump symbols
- If Il2CppDumper fails, consider that
global-metadata.dat may 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:
.hap package and embedded .abc bytecode
- Tool:
https://github.com/ohos-decompiler/abc-decompiler
- Download
jadx-dev-all.jar from releases
Critical startup note:
java -jar may enter GUI mode
- For CLI mode, always use:
java -cp "./jadx-dev-all.jar" jadx.cli.JadxCLI [options] <input>
Most common commands:
java -cp "./jadx-dev-all.jar" jadx.cli.JadxCLI -d "out" ".abc"
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:
-d output directory
--help help
Notes:
.hap is 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
auto fails, switch to -m simple first
Standard workflow:
- Run with
-m simple --log-level ERROR
- Inspect key business files in output (for example
pages/Index.java)
- If cleaner output is needed, retry with
-m auto or -m restructure
- 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
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
with open('coverage.json') as f:
cov = json.load(f)
executed_xored = set()
for line_no, hit_count in cov['encrypt.php']['lines'].items():
if hit_count > 0:
executed_xored.add(extract_value_from_line(line_no))
for pos in range(len(ciphertext)):
candidates = []
for key_byte in range(256):
xored = plaintext_byte ^ key_byte
if xored in executed_xored:
candidates.append(key_byte)
Key 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 files
IMPLEMENTATION / SIGNATURE keywords
- Nested
IF..THEN..ELSE..FI structures
- Functions named
f1, f2, ... fN (numeric naming)
- Heavy use of
seq[nat], string, denotation types
Reversing approach:
- Pure functions are mathematically invertible — reverse each step in the pipeline
- Identify the transformation chain:
f_final(f_n(...f_2(f_1(input))...))
- For each function, build the inverse
Aggregate brute-force for scramble functions:
When a transformation accumulates state that depends on original (unknown) values:
decoded = base64_decode(target)
for total_offset_S in range(256):
candidate = [(b - total_offset_S) % 256 for b in decoded]
recomputed_S = sum(contribution(i, candidate[i]) for i in range(len(candidate))) % 256
if recomputed_S == total_offset_S:
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.
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:
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 = [...]
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)
Disambiguation strategies:
- Known plaintext format (e.g.,
ENO{, flag{) fixes key bytes at known positions
- Side-channel data (code coverage, timing) eliminates impossible candidates
- Printable ASCII constraint (32-126) reduces candidate space
- Re-encrypt candidates and verify against known ciphertext
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:
curl -H "Cookie: .ROBLOSECURITY=..." \
"https://assetdelivery.roblox.com/v2/assetId/{placeId}/version/1"
Binary 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:
- gdsdecomp - Extract Godot packages
- KeyDot - Extract encryption key from Godot executables
Workflow:
- Run KeyDot against game executable → extract encryption key
- Input key into gdsdecomp
- Extract and open project in Godot editor
- 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:
- Disassemble serde-generated
Visitor implementations
- Each visitor's
visit_map / visit_seq reveals expected keys and types
- Look for string literals in deserializer code (field names like
"pascal", "CTF")
- Reconstruct nested JSON schema from visitor call hierarchy
- 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.
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:
- Analyze Verilog modules to understand state machine and history tracking
- Identify hidden conditions (e.g., product 8 enabled only when
COINS_HISTORY array has specific values at specific taps)
- Build timing model for each action type (how many clock cycles each operation takes)
- Work backward from required history values to construct the correct input sequence
Timing model construction:
TIMING = {
"insert_coin": 3,
"select_success": 7,
"select_fail": 5,
"cancel_with_coins": 4,
"cancel_at_zero": 2,
}
Key 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() with timeout=2 to handle binaries that hang on bad input
- For stripped binaries, check if
ltrace reveals the hash function name (e.g., MD2_Update)
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:
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:
- Locate
JNI_OnLoad (exported symbol, always present)
- Trace to
RegisterNatives(env, clazz, methods, count) call
- The
methods array contains {name, signature, fnPtr} structs
- Follow
fnPtr to find the actual native function
static JNINativeMethod methods[] = {
{"processMessage", "(Landroid/telephony/SmsMessage;)LProcessedMessage;", (void*)real_handler}
};
(*env)->RegisterNatives(env, clazz, methods, 1);
Architecture selection for analysis:
unzip WonderSMS.apk -d extracted/
ls extracted/lib/x86_64/
Key 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).
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...=end is a block comment
- Perl:
=begin...=cut is POD (Plain Old Documentation), =end is 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 satisfying XOR(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:
- Find the valid character set (mathematical constraint from one language)
- Use the ordering constraint (from other language) to determine exact arrangement
- Compute key hash (e.g., MD5) and decrypt
def reconstruct_from_inversions(chars, inv_counts):
result = []
remaining = sorted(chars)
for i in range(len(chars) - 1, -1, -1):
idx = inv_counts[i]
result.insert(idx, remaining.pop(i))
return result
Key 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:
- Unpack Electron ASAR archive:
npm install -g @electron/asar
asar extract resources/app.asar app_extracted/
ls app_extracted/
- Locate native binary: Search for ELF/DLL files called from JavaScript:
find app_extracted/ -name "*.node" -o -name "*.so" -o -name "*vault*" -o -name "*auth*"
grep -r "spawn\|execFile\|ffi\|require.*native" app_extracted/
- 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
const cryptoMod = require('target-package/dist/lib/crypto.js');
const vaultMod = require('target-package/dist/lib/vault.js');
for (const mod of [cryptoMod, vaultMod]) {
for (const key of Object.keys(mod)) {
const obj = mod[key];
console.log(`Export: ${key}`);
const props = Object.getOwnPropertyNames(obj);
const proto = Object.getOwnPropertyNames(obj.prototype || {});
console.log(' Own:', props);
console.log(' Proto:', proto);
}
}
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);
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 - Competition-Specific Patterns (Part 2)
Table of Contents
Multi-Layer Self-Decrypting Binary (DiceCTF 2026)
Pattern (another-onion): Binary with N layers (e.g., 256), each reading 2 key bytes, deriving keystream via SHA-256 NI instructions, XOR-decrypting the next layer, then jumping to it. Must solve within a time limit (e.g., 30 minutes).
Oracle for correct key: Wrong key bytes produce garbage code. Correct key bytes produce code with exactly 2 call read@plt instructions (next layer's reads). Brute-force all 65536 candidates per layer using this oracle.
JIT execution approach (fastest):
void *text = mmap((void*)0x400000, text_size, PROT_RWX, MAP_FIXED|MAP_PRIVATE, fd, 0);
void *bss = mmap((void*)bss_addr, bss_size, PROT_RW, MAP_FIXED|MAP_SHARED, shm_fd, 0);
for (int candidate = 0; candidate < 65536; candidate++) {
pid_t pid = fork();
if (pid == 0) {
mmap(bss_addr, bss_size, PROT_RW, MAP_FIXED|MAP_PRIVATE, shm_fd, 0);
inject_key(candidate >> 8, candidate & 0xff);
((void(*)())layer_addr)();
if (count_read_calls(next_layer_addr) == 2) signal_found(candidate);
_exit(0);
}
}
Performance tiers:
| Approach | Speed | 256-layer estimate |
|---|
| Python subprocess | ~2/s | days |
| Ptrace fork injection | ~119/s | 6+ hours |
| JIT + fork-per-candidate | ~1000/s | 140 min |
| JIT + shared BSS + 32 workers | ~3500/s | ~17 min |
Shared BSS optimization: BSS (16MB+) stored in /dev/shm as MAP_SHARED in parent. Children remap as MAP_PRIVATE for COW. Reduces fork overhead from 16MB page-table setup to ~4KB.
Key insight: Multi-layer decryption challenges are fundamentally about building fast brute-force engines. JIT execution (mapping binary memory into solver, running code directly as function calls) is orders of magnitude faster than ptrace. Fork-based COW provides free memory isolation per candidate.
Gotchas:
- Real binary may use
call (0xe8) instead of jmp (0xe9) for layer transitions — adjust tail patching
- BSS may extend beyond ELF MemSiz via kernel brk mapping — map extra space
- SHA-NI instructions work even when not advertised in
/proc/cpuinfo
Embedded ZIP + XOR License Decryption (MetaCTF 2026)
Pattern (License To Rev): Binary requires a license file as argument. Contains an embedded ZIP archive with the expected license, and an XOR-encrypted flag.
Recognition:
strings reveals EMBEDDED_ZIP and ENCRYPTED_MESSAGE symbols
- Binary is not stripped —
nm or readelf -s shows data symbols in .rodata
file shows PIE executable, source file named licensed.c
Analysis workflow:
- Find data symbols:
readelf -s binary | grep -E "EMBEDDED|ENCRYPTED|LICENSE"
- Extract embedded ZIP:
import struct
with open('binary', 'rb') as f:
data = f.read()
zip_start = data.find(b'PK\x03\x04')
open('embedded.zip', 'wb').write(data[zip_start:zip_start+384])
- Extract license from ZIP:
unzip embedded.zip
- XOR decrypt the flag:
license = open('license.txt', 'rb').read()
enc_msg = open('encrypted_msg.bin', 'rb').read()
flag = bytes(a ^ b for a, b in zip(enc_msg, license))
print(flag.decode())
Key insight: No need to run the binary or bypass the expiry date check. The embedded ZIP and encrypted message are both in .rodata — extract and XOR offline.
Disassembly confirms:
memcmp(user_license, decompressed_embedded_zip, size) — license validation
- Date parsing with
sscanf("%d-%d-%d") on EXPIRY_DATE= field
- XOR loop:
ENCRYPTED_MESSAGE[i] ^ license[i] → putc() per byte
Lesson: When a binary has named symbols (EMBEDDED_*, ENCRYPTED_*), extract data directly from the binary without execution. XOR with known plaintext (the license) is trivially reversible.
Stack String Deobfuscation from .rodata XOR Blob (Nullcon 2026)
Pattern (stack_strings_1/2): Binary mmaps a blob from .rodata, XOR-deobfuscates it, then uses the blob to validate input. Flag is recovered by reimplementing the verification loop.
Recognition:
mmap() call followed by XOR loop over .rodata data
- Verification loop with running state (
eax, ebx, r9) updated with constants like 0x9E3779B9, 0x85EBCA6B, 0xA97288ED
rol32() operations with position-dependent shifts
- Expected bytes stored in deobfuscated buffer
Approach:
- Extract
.rodata blob with pyelftools:
from elftools.elf.elffile import ELFFile
with open(binary, "rb") as f:
elf = ELFFile(f)
ro = elf.get_section_by_name(".rodata")
blob = ro.data()[offset:offset+size]
- Recover embedded constants (length, magic values) by XOR with known keys from disassembly
- Reimplement the byte-by-byte verification loop:
- Each iteration: compute two hash-like values from running state
- XOR them together and with expected byte to recover input byte
- Update running state with constant additions
Variant (stack_strings_2): Adds position permutation + state dependency on previous character:
- Position permutation: byte
i may go to position pos[i] in the output
- State dependency:
need = (expected - rol8(prev_char, 1)) & 0xFF
- Must track
state variable that updates to current character each iteration
Key constants to look for:
0x9E3779B9 (golden ratio fractional, common in hash functions)
0x85EBCA6B (MurmurHash3 finalizer constant)
0xA97288ED (related hash constant)
rol32() with shift i & 7
Prefix Hash Brute-Force (Nullcon 2026)
Pattern (Hashinator): Binary hashes every prefix of the input independently and outputs one digest per prefix. Given N output digests, the flag has N-1 characters.
Attack: Recover input one character at a time:
for pos in range(1, len(target_hashes)):
for ch in charset:
candidate = known_prefix + ch + padding
hashes = run_binary(candidate)
if hashes[pos] == target_hashes[pos]:
known_prefix += ch
break
Key insight: If each prefix hash is independent (no chaining/HMAC), the problem decomposes into N x |charset| binary executions. This is the hash equivalent of byte-at-a-time block cipher attacks.
Detection: Binary outputs multiple hash lines. Changing last character only changes last hash. Different input lengths produce different numbers of output lines.
CVP/LLL Lattice for Constrained Integer Validation (HTB ShadowLabyrinth)
Pattern: Binary validates flag via matrix multiplication where grouped input characters are multiplied by coefficient matrices and checked against expected 64-bit results. Standard algebra fails because solutions must be printable ASCII (32-126). Lattice-based CVP (Closest Vector Problem) with LLL reduction solves this efficiently.
Identification:
- Binary groups input characters (e.g., 4 at a time)
- Each group is multiplied by a coefficient matrix
- Results compared against hardcoded 64-bit values
- Need integer solutions in a constrained range (printable ASCII)
SageMath CVP solver:
from sage.all import *
def solve_constrained_matrix(coefficients, targets, char_range=(32, 126)):
"""
coefficients: list of coefficient rows (e.g., 4 values per group)
targets: expected output values
char_range: valid character range (printable ASCII)
"""
n = len(coefficients[0])
mid = (char_range[0] + char_range[1]) // 2
M = matrix(ZZ, n + len(targets), n + len(targets))
scale = 1000
for i, row in enumerate(coefficients):
for j, c in enumerate(row):
M[j, i] = c
M[n + i, i] = 1
for j in range(n):
M[j, len(targets) + j] = scale
target_vec = vector(ZZ, [t - sum(c * mid for c in row)
for row, t in zip(coefficients, targets)]
+ [0] * n)
L = M.LLL()
closest = L * L.solve_left(target_vec)
solution = [closest[len(targets) + j] // scale + mid for j in range(n)]
return bytes(solution)
Two-phase validation pattern:
- Phase 1 (matrix math): Solve via CVP/LLL → recovers first N characters
- First N characters become AES key → decrypt
file.bin (XOR last 16 bytes + AES-256-CBC + zlib decompress)
- Phase 2 (custom VM): Decrypted bytecode runs in custom VM, validates remaining characters via another linear system (mod 2^32)
Modular linear system solving (Phase 2 — VM validation):
import numpy as np
from sympy import Matrix
M_mod = Matrix(coefficients) % (2**32)
v_mod = Matrix(targets) % (2**32)
solution = M_mod.solve(v_mod)
Key insight: When a binary validates input through linear combinations with large coefficients and the solution must be in a small range (printable ASCII), this is a lattice problem in disguise. LLL reduction + CVP finds the nearest lattice point, recovering the constrained solution. Cross-reference: invoke /ctf-crypto for LLL/CVP fundamentals (advanced-math.md in ctf-crypto).
Detection: Binary performs matrix-like operations on grouped input, compares against 64-bit constants, and a brute-force search space is too large (e.g., 256^4 per group × 12 groups).
Decision Tree Function Obfuscation (HTB WonderSMS)
Pattern: Binary routes input through ~200+ auto-generated functions, each computing a polynomial expression from input positions, comparing against a constant, and branching left/right. The tree makes static analysis impractical without scripted extraction.
Identification:
- Large number of similar functions with random-looking names (e.g.,
f315732804)
- Each function computes arithmetic on specific input positions
- Functions call other tree functions or a final validation function
- Decompiled code shows
if (expr cmp constant) call_left() else call_right()
Ghidra headless scripting for mass extraction:
from ghidra.program.model.listing import *
from ghidra.program.model.symbol import *
fm = currentProgram.getFunctionManager()
results = []
for func in fm.getFunctions(True):
name = func.getName()
if name.startswith('f') and name[1:].isdigit():
inst_iter = currentProgram.getListing().getInstructions(func.getBody(), True)
for inst in inst_iter:
if inst.getMnemonicString() == 'CMP':
operand = inst.getOpObjects(1)
if operand:
results.append((name, int(operand[0].getValue())))
Constraint propagation from known output format:
- Start from known output bytes (e.g.,
http://HTB{...}) → fix several input positions
- Fixed positions cascade through arithmetic constraints → determine dependent positions
- Tree root equation pins down remaining free variables
- Recognize English words in partial flag to disambiguate multiple solutions
Key insight: Auto-generated decision trees look overwhelming but are repetitive by construction. Script the extraction (Ghidra, Binary Ninja, radare2) rather than reversing each function manually. The tree is just a dispatcher — the real logic is in the leaf function and its constraints.
Detection: Binary with hundreds of similarly-structured functions, 3-5 input position references per function, branching to two other functions or a common leaf.