# CTF Reverse - Dynamic Analysis Tools ## Table of Contents - [Frida (Dynamic Instrumentation)](#frida-dynamic-instrumentation) - [Installation](#installation) - [Basic Function Hooking](#basic-function-hooking) - [Anti-Debug Bypass](#anti-debug-bypass) - [Memory Scanning and Patching](#memory-scanning-and-patching) - [Function Replacement](#function-replacement) - [Tracing and Stalker](#tracing-and-stalker) - [r2frida (Radare2 + Frida Integration)](#r2frida-radare2--frida-integration) - [Frida for Android/iOS](#frida-for-androidios) - [Frida Memoization for Recursive Function Speedup (hxp CTF 2017)](#frida-memoization-for-recursive-function-speedup-hxp-ctf-2017) - [angr (Symbolic Execution)](#angr-symbolic-execution) - [angr Installation](#angr-installation) - [Basic Path Exploration](#basic-path-exploration) - [Symbolic Input with Constraints](#symbolic-input-with-constraints) - [Hook Functions to Simplify Analysis](#hook-functions-to-simplify-analysis) - [Exploring from Specific Address](#exploring-from-specific-address) - [Common Patterns and Tips](#common-patterns-and-tips) - [Dealing with Path Explosion](#dealing-with-path-explosion) - [angr CFG Recovery](#angr-cfg-recovery) - [lldb (LLVM Debugger)](#lldb-llvm-debugger) - [Basic Commands](#basic-commands) - [Scripting (Python)](#scripting-python) - [x64dbg (Windows Debugger)](#x64dbg-windows-debugger) - [Key Features](#key-features) - [Scripting](#scripting) - [Common CTF Workflow](#common-ctf-workflow) - [Qiling Framework (Cross-Platform Emulation)](#qiling-framework-cross-platform-emulation) - [Qiling Installation](#qiling-installation) - [Basic Usage](#basic-usage) - [Anti-Debug Bypass via Emulation](#anti-debug-bypass-via-emulation) - [Input Fuzzing with Qiling](#input-fuzzing-with-qiling) - [Triton (Dynamic Symbolic Execution)](#triton-dynamic-symbolic-execution) - [Intel Pin Instruction-Counting Side Channel (Hackover CTF 2015)](#intel-pin-instruction-counting-side-channel-hackover-ctf-2015) - [Intel Pin Instruction Counting with Genetic Algorithm (hxp CTF 2017)](#intel-pin-instruction-counting-with-genetic-algorithm-hxp-ctf-2017) - [Opcode-Only Trace Reconstruction (0CTF 2016)](#opcode-only-trace-reconstruction-0ctf-2016) - [LD_PRELOAD time() Freeze for Deterministic Analysis (EKOPARTY 2017)](#ld_preload-time-freeze-for-deterministic-analysis-ekoparty-2017) - [LD_PRELOAD memcmp Side-Channel for Byte-by-Byte Bruteforce (Blaze CTF 2018)](#ld_preload-memcmp-side-channel-for-byte-by-byte-bruteforce-blaze-ctf-2018) --- ## Frida (Dynamic Instrumentation) Frida injects JavaScript into running processes for real-time hooking, tracing, and modification. Essential for anti-debug bypass, runtime inspection, and mobile RE. ### Installation ```bash pip install frida-tools frida # Verify frida --version ``` ### Basic Function Hooking ```javascript // hook.js — intercept a function and log arguments/return value Interceptor.attach(Module.findExportByName(null, "strcmp"), { onEnter: function(args) { this.arg0 = Memory.readUtf8String(args[0]); this.arg1 = Memory.readUtf8String(args[1]); console.log(`strcmp("${this.arg0}", "${this.arg1}")`); }, onLeave: function(retval) { console.log(` → ${retval}`); } }); ``` ```bash # Attach to running process frida -p $(pidof binary) -l hook.js # Spawn and instrument from start frida -f ./binary -l hook.js --no-pause # One-liner: hook strcmp and dump comparisons frida -f ./binary --no-pause -e ' Interceptor.attach(Module.findExportByName(null, "strcmp"), { onEnter(args) { console.log("strcmp:", Memory.readUtf8String(args[0]), Memory.readUtf8String(args[1])); } }); ' ``` ### Anti-Debug Bypass ```javascript // Bypass ptrace(PTRACE_TRACEME) — returns 0 (success) without calling Interceptor.attach(Module.findExportByName(null, "ptrace"), { onEnter: function(args) { this.request = args[0].toInt32(); }, onLeave: function(retval) { if (this.request === 0) { // PTRACE_TRACEME retval.replace(ptr(0)); console.log("[*] ptrace(TRACEME) bypassed"); } } }); // Bypass IsDebuggerPresent (Windows) var isDbg = Module.findExportByName("kernel32.dll", "IsDebuggerPresent"); Interceptor.attach(isDbg, { onLeave: function(retval) { retval.replace(ptr(0)); } }); // Bypass timing checks — hook clock_gettime to return constant Interceptor.attach(Module.findExportByName(null, "clock_gettime"), { onLeave: function(retval) { // Force constant timestamp to defeat timing checks var ts = this.context.rsi || this.context.x1; // x86 or ARM Memory.writeU64(ts, 0); // tv_sec Memory.writeU64(ts.add(8), 0); // tv_nsec } }); ``` ### Memory Scanning and Patching ```javascript // Scan for flag pattern in memory Process.enumerateRanges('r--').forEach(function(range) { Memory.scan(range.base, range.size, "66 6c 61 67 7b", { // "flag{" onMatch: function(address, size) { console.log("[FLAG] Found at:", address, Memory.readUtf8String(address, 64)); }, onComplete: function() {} }); }); // Patch instruction (NOP out a check) var addr = Module.findBaseAddress("binary").add(0x1234); Memory.patchCode(addr, 2, function(code) { var writer = new X86Writer(code, { pc: addr }); writer.putNop(); writer.putNop(); writer.flush(); }); ``` ### Function Replacement ```javascript // Replace a validation function to always return true var checkFlag = Module.findExportByName(null, "check_flag"); Interceptor.replace(checkFlag, new NativeCallback(function(input) { console.log("[*] check_flag called with:", Memory.readUtf8String(input)); return 1; // always valid }, 'int', ['pointer'])); ``` ### Tracing and Stalker ```javascript // Trace all calls in a function (Stalker — instruction-level tracing) var targetAddr = Module.findExportByName(null, "main"); Stalker.follow(Process.getCurrentThreadId(), { transform: function(iterator) { var instruction; while ((instruction = iterator.next()) !== null) { if (instruction.mnemonic === "call") { iterator.putCallout(function(context) { console.log("CALL at", context.pc, "→", ptr(context.pc).readPointer()); }); } iterator.keep(); } } }); ``` ### r2frida (Radare2 + Frida Integration) ```bash # Attach radare2 to process via Frida r2 frida://spawn/./binary # r2frida commands \ii # List imports \il # List loaded modules \dt strcmp # Trace strcmp calls \dc # Continue execution \dm # List memory maps ``` ### Frida for Android/iOS ```bash # Android (requires rooted device or Frida server) adb push frida-server /data/local/tmp/ adb shell "chmod 755 /data/local/tmp/frida-server && /data/local/tmp/frida-server &" # Hook Android Java methods frida -U -f com.example.app -l hook_android.js --no-pause ``` ```javascript // hook_android.js — hook Java method Java.perform(function() { var MainActivity = Java.use("com.example.app.MainActivity"); MainActivity.checkPassword.implementation = function(input) { console.log("[*] checkPassword called with:", input); var result = this.checkPassword(input); console.log("[*] Result:", result); return result; }; }); ``` **Key insight:** Frida excels where static analysis fails — obfuscated code, packed binaries, and runtime-generated data. Hook comparison functions (`strcmp`, `memcmp`, custom validators) to extract expected values without reversing the algorithm. Use `Interceptor.attach` for observation, `Interceptor.replace` for modification. **When to use:** Anti-debugging bypass, extracting runtime-computed keys, hooking crypto functions to dump plaintext, mobile app analysis, packed binary inspection. ### Frida Memoization for Recursive Function Speedup (hxp CTF 2017) Hook a recursive function with Frida, memoize results, and replay cached values to skip redundant computation. Fibonacci-like recursive challenges with exponential complexity become instant with memoization. ```javascript // memo_hook.js — memoize a recursive function to skip redundant calls var memo = {}; var funcAddr = ptr("0x400abc"); // Address of the recursive function var retAddr = ptr("0x400def"); // Address of the function's ret instruction Interceptor.attach(funcAddr, { onEnter: function(args) { this.key = args[0].toInt32(); if (memo[this.key] !== undefined) { // Skip computation entirely: set return value and jump to ret this.context.rax = memo[this.key]; this.context.rip = retAddr; } }, onLeave: function(retval) { // Cache the result for future calls with the same argument memo[this.key] = retval.toInt32(); } }); ``` ```bash # Usage frida -f ./binary -l memo_hook.js --no-pause ``` For multi-argument functions, build a composite key: ```javascript Interceptor.attach(funcAddr, { onEnter: function(args) { this.key = args[0].toInt32() + "," + args[1].toInt32(); if (memo[this.key] !== undefined) { this.context.rax = memo[this.key]; this.context.rip = retAddr; } }, onLeave: function(retval) { memo[this.key] = retval.toInt32(); } }); ``` **Key insight:** Frida's `Interceptor` can both read and modify register state, allowing you to skip function execution entirely by setting `rax` (return value) and `rip` (to the `ret` instruction). This works on any recursive function where the same arguments produce the same result. Exponential-time recursive computations (Fibonacci, Ackermann, tree traversals) become linear with memoization. **References:** hxp CTF 2017 --- ## angr (Symbolic Execution) angr automatically explores program paths to find inputs satisfying constraints. Solves many flag-checking binaries in minutes that take hours manually. ### angr Installation ```bash pip install angr ``` ### Basic Path Exploration ```python import angr import claripy # Load binary proj = angr.Project('./binary', auto_load_libs=False) # Find address of "Correct!" print, avoid "Wrong!" print # Get these from disassembly (objdump -d or Ghidra) FIND_ADDR = 0x401234 # Address of success path AVOID_ADDR = 0x401256 # Address of failure path # Create simulation manager and explore simgr = proj.factory.simgr() simgr.explore(find=FIND_ADDR, avoid=AVOID_ADDR) if simgr.found: found = simgr.found[0] # Get stdin that reaches the target print("Flag:", found.posix.dumps(0)) # fd 0 = stdin ``` ### Symbolic Input with Constraints ```python import angr import claripy proj = angr.Project('./binary', auto_load_libs=False) # Create symbolic input (e.g., 32-byte flag) flag_len = 32 flag_chars = [claripy.BVS(f'flag_{i}', 8) for i in range(flag_len)] flag = claripy.Concat(*flag_chars + [claripy.BVV(b'\n')]) # Constrain to printable ASCII state = proj.factory.entry_state(stdin=flag) for c in flag_chars: state.solver.add(c >= 0x20) state.solver.add(c <= 0x7e) # Constrain known prefix: "flag{" state.solver.add(flag_chars[0] == ord('f')) state.solver.add(flag_chars[1] == ord('l')) state.solver.add(flag_chars[2] == ord('a')) state.solver.add(flag_chars[3] == ord('g')) state.solver.add(flag_chars[4] == ord('{')) state.solver.add(flag_chars[flag_len-1] == ord('}')) simgr = proj.factory.simgr(state) simgr.explore(find=0x401234, avoid=0x401256) if simgr.found: found = simgr.found[0] result = found.solver.eval(flag, cast_to=bytes) print("Flag:", result.decode()) ``` ### Hook Functions to Simplify Analysis ```python import angr proj = angr.Project('./binary', auto_load_libs=False) # Hook printf to avoid path explosion in I/O @proj.hook(0x401100, length=5) # Address of call to printf def skip_printf(state): pass # Do nothing, just skip # Hook sleep/anti-debug functions @proj.hook(0x401050, length=5) # Address of call to sleep def skip_sleep(state): pass # Replace a function with a summary class AlwaysSucceed(angr.SimProcedure): def run(self): return 1 proj.hook_symbol('check_license', AlwaysSucceed()) ``` ### Exploring from Specific Address ```python # Start from middle of function (skip initialization) state = proj.factory.blank_state(addr=0x401200) # Set up registers/memory manually state.regs.rdi = 0x600000 # Pointer to input buffer state.memory.store(0x600000, b"AAAA" + b"\x00" * 28) simgr = proj.factory.simgr(state) simgr.explore(find=0x401300, avoid=0x401350) ``` ### Common Patterns and Tips ```python # Pattern 1: argv-based input state = proj.factory.entry_state(args=['./binary', flag_sym]) # Pattern 2: Multiple find/avoid addresses simgr.explore( find=[0x401234, 0x401300], # Any success path avoid=[0x401256, 0x401400] # All failure paths ) # Pattern 3: Find by output string (no address needed) def is_successful(state): stdout = state.posix.dumps(1) # fd 1 = stdout return b"Correct" in stdout def should_avoid(state): stdout = state.posix.dumps(1) return b"Wrong" in stdout simgr.explore(find=is_successful, avoid=should_avoid) # Pattern 4: Timeout protection simgr.explore(find=0x401234, avoid=0x401256, num_find=1) # Or use exploration techniques: simgr.use_technique(angr.exploration_techniques.DFS()) # Depth-first simgr.use_technique(angr.exploration_techniques.LengthLimiter(max_length=500)) ``` ### Dealing with Path Explosion ```python # Use DFS instead of BFS (default) for flag checkers simgr.use_technique(angr.exploration_techniques.DFS()) # Limit symbolic memory operations state.options.add(angr.options.ZERO_FILL_UNCONSTRAINED_MEMORY) state.options.add(angr.options.ZERO_FILL_UNCONSTRAINED_REGISTERS) # Hook expensive functions (crypto, hashing) to avoid explosion import hashlib class SHA256Hook(angr.SimProcedure): def run(self, data, length, output): # Concretize input and compute hash concrete_data = self.state.solver.eval( self.state.memory.load(data, self.state.solver.eval(length)), cast_to=bytes ) h = hashlib.sha256(concrete_data).digest() self.state.memory.store(output, h) proj.hook_symbol('SHA256', SHA256Hook()) ``` ### angr CFG Recovery ```python # Control flow graph for understanding structure cfg = proj.analyses.CFGFast() print(f"Functions found: {len(cfg.functions)}") # Find main for addr, func in cfg.functions.items(): if func.name == 'main': print(f"main at {addr:#x}") break # Cross-references node = cfg.model.get_any_node(0x401234) print("Predecessors:", [hex(p.addr) for p in cfg.model.get_predecessors(node)]) ``` **Key insight:** angr works best on flag-checker binaries with clear success/failure paths. For complex binaries, hook expensive functions (crypto, I/O) and use DFS exploration. Start with the simplest approach (just find/avoid addresses) before adding constraints. If angr is slow, constrain input to printable ASCII and add known prefix. **When to use:** Flag validators with branching logic, maze/path-finding binaries, constraint-heavy checks, automated binary analysis. Less effective for: heavy crypto, floating-point math, complex heap operations. --- ## lldb (LLVM Debugger) Primary debugger for macOS/iOS. Also works on Linux. Preferred for Swift/Objective-C and Apple platform binaries. ### Basic Commands ```bash lldb ./binary (lldb) run # Run program (lldb) b main # Breakpoint on main (lldb) b 0x401234 # Breakpoint at address (lldb) breakpoint set -r "check.*" # Regex breakpoint (lldb) c # Continue (lldb) si # Step instruction (lldb) ni # Next instruction (lldb) register read # Show all registers (lldb) register write rax 0 # Modify register (lldb) memory read 0x401000 -c 32 # Read 32 bytes (lldb) x/s $rsi # Examine string (GDB-style) (lldb) dis -n main # Disassemble function (lldb) image list # Loaded modules + base addresses ``` ### Scripting (Python) ```python # lldb Python scripting import lldb def hook_strcmp(debugger, command, result, internal_dict): target = debugger.GetSelectedTarget() process = target.GetProcess() thread = process.GetSelectedThread() frame = thread.GetSelectedFrame() arg0 = frame.FindRegister("rdi").GetValueAsUnsigned() arg1 = frame.FindRegister("rsi").GetValueAsUnsigned() s0 = process.ReadCStringFromMemory(arg0, 256, lldb.SBError()) s1 = process.ReadCStringFromMemory(arg1, 256, lldb.SBError()) print(f'strcmp("{s0}", "{s1}")') # Register in lldb: command script add -f script.hook_strcmp hook_strcmp ``` **Key insight:** Use lldb for macOS binaries (Mach-O), iOS apps, and when GDB isn't available. `image list` gives ASLR slide for PIE binaries. Scripting API is more structured than GDB's. --- ## x64dbg (Windows Debugger) Open-source Windows debugger with modern UI. Alternative to OllyDbg/WinDbg for Windows RE challenges. ### Key Features ```bash # Launch x64dbg.exe binary.exe # 64-bit x32dbg.exe binary.exe # 32-bit # Essential shortcuts F2 → Toggle breakpoint F7 → Step into F8 → Step over F9 → Run Ctrl+G → Go to address Ctrl+F → Find pattern in memory ``` ### Scripting ```bash # x64dbg command line bp 0x401234 # Breakpoint SetBPX 0x401234, 0, "log {s:utf8@[esp+4]}" # Log string arg on hit run # Continue StepOver # Step over ``` ### Common CTF Workflow 1. Set breakpoint on `GetWindowTextA`/`MessageBoxA` for GUI crackers 2. Trace back from success/failure message 3. Use **Scylla** plugin for IAT reconstruction on packed binaries 4. **Snowman** decompiler plugin for quick pseudo-C **Key insight:** x64dbg has built-in pattern scanning, hardware breakpoints, and conditional logging. For Windows CTF binaries, it's often faster than IDA/Ghidra for dynamic analysis. Use the **xAnalyzer** plugin for automatic function argument annotation. --- ## Qiling Framework (Cross-Platform Emulation) Qiling emulates binaries with OS-level support (syscalls, filesystem, registry). Built on Unicorn but adds the OS layer that Unicorn lacks. ### Qiling Installation ```bash pip install qiling # Download rootfs for target OS: git clone https://github.com/qilingframework/rootfs ``` ### Basic Usage ```python from qiling import Qiling from qiling.const import QL_VERBOSE # Linux ELF emulation ql = Qiling(["./binary", "arg1"], "rootfs/x8664_linux", verbose=QL_VERBOSE.DEFAULT) ql.run() # Windows PE emulation (no Windows needed!) ql = Qiling(["rootfs/x86_windows/bin/binary.exe"], "rootfs/x86_windows") ql.run() # ARM/MIPS emulation (IoT firmware) ql = Qiling(["rootfs/arm_linux/bin/binary"], "rootfs/arm_linux") ql.run() ``` ### Anti-Debug Bypass via Emulation ```python from qiling import Qiling ql = Qiling(["./binary"], "rootfs/x8664_linux") # Hook ptrace syscall — return 0 (success) def hook_ptrace(ql, ptrace_request, pid, addr, data): ql.log.info("ptrace bypassed") return 0 ql.os.set_syscall("ptrace", hook_ptrace) # Hook specific address (e.g., anti-VM check) def skip_check(ql): ql.arch.regs.rax = 0 # Force success ql.log.info(f"Skipped check at {ql.arch.regs.rip:#x}") ql.hook_address(skip_check, 0x401234) ql.run() ``` ### Input Fuzzing with Qiling ```python # Emulate binary with different inputs to find flag import string from qiling import Qiling def test_input(candidate): ql = Qiling(["./binary"], "rootfs/x8664_linux", verbose=QL_VERBOSE.DISABLED, stdin=candidate.encode()) ql.run() return ql.os.stdout.read() for ch in string.printable: output = test_input("flag{" + ch) if b"Correct" in output: print(f"Found: {ch}") ``` **Advantages over GDB/Frida:** - No debugger artifacts (bypasses all anti-debug by default) - Cross-platform without hardware (ARM, MIPS, RISC-V on x86 host) - Scriptable with Python (faster iteration than GDB) - Snapshot/restore for brute-forcing **Key insight:** Qiling emulates the entire OS layer (syscalls, filesystem, registry), not just the CPU. This means anti-debug checks like `ptrace(TRACEME)` naturally return success without patching, and you can analyze ARM/MIPS binaries on an x86 host without QEMU or real hardware. **When to use:** Foreign architecture binaries, IoT firmware, heavy anti-debug, automated testing of many inputs. --- ## Triton (Dynamic Symbolic Execution) See [tools-advanced.md](tools-advanced.md#triton-dynamic-symbolic-execution) for full Triton reference. Quick usage: ```python from triton import * ctx = TritonContext(ARCH.X86_64) # Symbolize input buffer for i in range(32): ctx.symbolizeMemory(MemoryAccess(0x600000 + i, CPUSIZE.BYTE), f"flag_{i}") # Process instructions and collect constraints # At comparison point, solve for flag model = ctx.getModel(ctx.getPathConstraintsAst()) flag = ''.join(chr(v.getValue()) for _, v in sorted(model.items())) ``` **Key insight:** Triton excels at single-path DSE (Dynamic Symbolic Execution) where angr's path explosion is a problem. Feed it a concrete execution trace, symbolize specific inputs, and solve for constraints at comparison points. Faster than angr for linear code paths with known execution flow. **Best for:** Single-path symbolic execution, deobfuscation, taint analysis. Faster than angr for linear code paths. --- ## Intel Pin Instruction-Counting Side Channel (Hackover CTF 2015) **Pattern:** Brute-force input character-by-character against a binary using Intel Pin's `inscount0` tool. Each correct character causes deeper execution (more instructions) in the comparison logic. ```python import string from subprocess import Popen, PIPE pin = './pin' tool = './source/tools/ManualExamples/obj-ia32/inscount0.so' binary = './target' key = '' while True: best_count, best_char = 0, '' for c in string.printable: cmd = [pin, '-injection', 'child', '-t', tool, '--', binary] p = Popen(cmd, stdout=PIPE, stdin=PIPE, stderr=PIPE) p.communicate((key + c + '\n').encode()) with open('inscount.out') as f: count = int(f.read().split()[-1]) if count > best_count: best_count, best_char = count, c key += best_char print(f"Found: {key}") ``` **Key insight:** Movfuscated binaries (compiled with `movfuscator`) expand every instruction into sequences of `mov` operations, making static analysis impractical. However, character-by-character comparison still creates measurable instruction count differences. Pin's `inscount0.so` counts total executed instructions — the correct character at each position causes ~1000+ more instructions (proceeding further in the comparison). Also works for obfuscated binaries with sequential input checks. --- ### Intel Pin Instruction Counting with Genetic Algorithm (hxp CTF 2017) For self-modifying code that decrypts the next chunk only after each character check passes, standard character-by-character Pin counting fails because the search space is too large and characters may interact. Use a genetic algorithm instead to explore the input space more efficiently. ```python import subprocess import random import string PIN_PATH = '/tmp/pin-3.5/pin' TOOL_PATH = 'source/tools/ManualExamples/obj-intel64/inscount0.so' def fitness(candidate): """Run binary under Pin and return instruction count as fitness.""" proc = subprocess.Popen( [PIN_PATH, '-t', TOOL_PATH, '--', './binary'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, stderr = proc.communicate(candidate.encode()) # inscount0 writes count to stderr or inscount.out try: with open('inscount.out') as f: return int(f.read().split()[-1]) except: return 0 def mutate(individual, rate=0.1): """Randomly mutate characters in the individual.""" result = list(individual) for i in range(len(result)): if random.random() < rate: result[i] = random.choice(string.printable[:62]) return result # Genetic algorithm parameters FLAG_LEN = 40 POP_SIZE = 100 SURVIVORS = 20 # Initialize random population population = [random.choices(string.printable[:62], k=FLAG_LEN) for _ in range(POP_SIZE)] for generation in range(10000): # Score each individual by instruction count scored = [(fitness(''.join(p)), p) for p in population] scored.sort(reverse=True) best_score, best_individual = scored[0] print(f"Gen {generation}: {best_score} {''.join(best_individual)}") # Keep top survivors, mutate to refill population survivors = [s[1] for s in scored[:SURVIVORS]] population = survivors + [mutate(random.choice(survivors)) for _ in range(POP_SIZE - SURVIVORS)] ``` **Modified Pin for Go binaries (table-lookup flag checking):** When standard `inscount` fails because counter increments don't correlate with correctness (e.g., table-lookup comparison), modify Pin's icount tool to only count executions at the success-branch address. Brute-force character-by-character with this targeted counter: ```cpp // Modified inscount0.cpp — count only executions of a specific address static ADDRINT target_addr = 0x401234; // success-branch address static UINT64 target_count = 0; VOID CountAtTarget(ADDRINT ip) { if (ip == target_addr) target_count++; } VOID Instruction(INS ins, VOID *v) { INS_InsertCall(ins, IPOINT_BEFORE, (AFUNPTR)CountAtTarget, IARG_INST_PTR, IARG_END); } ``` **Key insight:** When each correct character unlocks a new code section (self-modifying or multi-stage decryption), instruction count increases monotonically with correctness. A genetic algorithm explores the input space more efficiently than character-by-character brute-force because it can discover multiple correct characters simultaneously. Converges in approximately 30 minutes for 40-character flags. For table-lookup comparisons where total instruction count doesn't correlate, target a specific branch address instead. **References:** hxp CTF 2017 --- ## Opcode-Only Trace Reconstruction (0CTF 2016) Given an execution trace with only opcodes (no register/memory values), reconstruct the program: sort/dedup trace by address, split into basic blocks, annotate functions. Sorting algorithms are particularly vulnerable -- branch decisions leak element ordering. **Approach:** 1. Sort trace entries by address, deduplicate to recover code layout 2. Identify basic block boundaries (jumps, calls, returns) 3. Map branch taken/not-taken decisions from trace order 4. For sorting algorithms, partition comparisons reveal relative ordering of all input elements **Key insight:** Execution traces without data values still leak information through branch decisions. Quicksort partition comparisons reveal which element is greater/lesser at each step, enabling full recovery of the sorted input from branch direction alone. --- ## LD_PRELOAD time() Freeze for Deterministic Analysis (EKOPARTY 2017) Override `time()` via LD_PRELOAD to return a constant value, freezing any timestamp-seeded PRNG. Once the binary's cipher becomes deterministic, brute-force each output byte without understanding the VM or cipher internals. ```c // freeze_time.c — compile: gcc -shared -fPIC -o freeze.so freeze_time.c #include time_t time(time_t *t) { if (t) *t = 1234567890; return 1234567890; } ``` ```bash # Build and use: gcc -shared -fPIC -o freeze.so freeze_time.c LD_PRELOAD=./freeze.so ./binary # Byte-at-a-time oracle: run with frozen time, try each candidate byte, # observe output — correct byte produces expected output character. for byte in $(seq 0 255); do output=$(echo -n "$(printf '\x%02x' $byte)" | LD_PRELOAD=./freeze.so ./binary) # Check output against known/expected done ``` If `srand()` or `rand()` is also involved, override `rand()` too: ```c int rand(void) { return 42; } ``` **Key insight:** LD_PRELOAD function interception freezes non-determinism sources (time, rand). Once deterministic, even complex VMs become tractable byte-at-a-time oracles. **References:** EKOPARTY CTF 2017 --- ### LD_PRELOAD memcmp Side-Channel for Byte-by-Byte Bruteforce (Blaze CTF 2018) **Pattern:** Replace `memcmp` with an LD_PRELOAD library that returns the number of matching bytes instead of the standard -1/0/1 result. This converts any memcmp-based validation into a byte-by-byte oracle. Automate with GDB Python scripting to bruteforce each character position. ```c // memcmp_hook.c - compile: gcc -shared -fPIC -o hook.so memcmp_hook.c int memcmp(const char *s1, const char *s2, int n) { int cnt = 0; for (int i = 0; i < n; ++i) { if (s1[i] == s2[i]) cnt++; else break; } return cnt; } ``` ```bash # Use with GDB: LD_PRELOAD=./hook.so gdb ./binary # Set breakpoint after memcmp, read return value to count matching bytes # Iterate characters at each position to find the one that increases count ``` **Key insight:** Replacing memcmp via LD_PRELOAD to return match count converts any comparison-based validation into a byte-by-byte oracle. Combined with GDB scripting, this automates bruteforce of password/flag checks without reversing the validation algorithm. **Detection:** Binary uses `memcmp` or `strcmp` for flag validation (visible in `ltrace` output or import table). The comparison function is called with user input and a computed/stored expected value. **References:** Blaze CTF 2018