All skills
ljagiello avatar

/ctf-reverse

@61c2efe
by Lukasz Jagielloljagiello/ctf-skills3.4k stars
393

Provides reverse engineering techniques for CTF challenges. Use when the main job is to understand how a compiled, obfuscated, packed, or virtualized target works before exploiting or solving it, including binaries, APKs, WASM, firmware, custom VMs, bytecode, game clients, malware-like loaders, and anti-debug or anti-analysis logic. Do not use it when the vulnerability is already understood and the remaining task is exploitation; use pwn instead. Do not use it for pure web workflows, log or disk forensics, or standalone crypto problems unless reversing the implementation is the real blocker.

Use this Skill: https://skilld.dev/gh/ljagiello/ctf-skills/ctf-reverse

This session only. Nothing lands on disk.

tools.md

≈4k tokens on demand. Your agent reads this file only when SKILL.md points to it.

CTF Reverse - Tools Reference

Table of Contents

For dynamic instrumentation tools (Frida, angr, lldb, x64dbg), see tools-dynamic.md.


GDB

Basic Commands

gdb ./binary
run                      # Run program
start                    # Run to main
b *0x401234              # Breakpoint at address
b *main+0x100            # Relative breakpoint
c                        # Continue
si                       # Step instruction
ni                       # Next instruction (skip calls)
x/s $rsi                 # Examine string
x/20x $rsp               # Examine stack
info registers           # Show registers
set $eax=0               # Modify register

PIE Binary Debugging

gdb ./binary
start                    # Forces PIE base resolution
b *main+0xca            # Relative to main
b *main+0x198
run

One-liner Automation

gdb -ex 'start' -ex 'b *main+0x198' -ex 'run' ./binary

Memory Examination

x/s $rsi                 # String at RSI
x/38c $rsi               # 38 characters
x/20x $rsp               # 20 hex words from stack
x/10i $rip               # 10 instructions from RIP

Radare2

Basic Session

r2 -d ./binary           # Open in debug mode
aaa                      # Analyze all
afl                      # List functions
pdf @ main               # Disassemble main
db 0x401234              # Set breakpoint
dc                       # Continue
ood                      # Restart debugging
dr                       # Show registers
dr eax=0                 # Modify register

r2pipe Automation

import r2pipe
r2 = r2pipe.open('./binary', flags=['-d'])
r2.cmd('aaa')
r2.cmd('db 0x401234')

for char in range(256):
    r2.cmd('ood')        # Restart
    r2.cmd(f'dr eax={char}')
    output = r2.cmd('dc')
    if 'correct' in output:
        print(f"Found: {chr(char)}")

Ghidra

Headless Analysis

analyzeHeadless /path/to/project tmp -import binary -postScript script.py

Emulator for Decryption

EmulatorHelper emu = new EmulatorHelper(currentProgram);
emu.writeRegister("RSP", 0x2fff0000);
emu.writeRegister("RBP", 0x2fff0000);

// Write encrypted data
emu.writeMemory(dataAddress, encryptedBytes);

// Set function arguments
emu.writeRegister("RDI", arg1);

// Run until return
emu.setBreakpoint(returnAddress);
emu.run(functionEntryAddress);

// Read result
byte[] decrypted = emu.readMemory(outputAddress, length);

MCP Commands

  • Recon: list_functions, list_imports, list_strings
  • Analysis: decompile_function, get_xrefs_to
  • Annotation: rename_function, rename_variable

Unicorn Emulation

Basic Setup

from unicorn import *
from unicorn.x86_const import *

mu = Uc(UC_ARCH_X86, UC_MODE_64)

# Map code segment
mu.mem_map(0x400000, 0x10000)
mu.mem_write(0x400000, code_bytes)

# Map stack
mu.mem_map(0x7fff0000, 0x10000)
mu.reg_write(UC_X86_REG_RSP, 0x7fff0000 + 0xff00)

# Run
mu.emu_start(start_addr, end_addr)

Mixed-Mode (64 to 32) Switch

# When a 64-bit stub jumps into 32-bit code via retf/retfq:
# - retf pops 4-byte EIP + 2-byte CS (6 bytes)
# - retfq pops 8-byte RIP + 8-byte CS (16 bytes)

uc32 = Uc(UC_ARCH_X86, UC_MODE_32)
# Copy memory regions, then GPRs
reg_map = {
    UC_X86_REG_EAX: UC_X86_REG_RAX,
    UC_X86_REG_EBX: UC_X86_REG_RBX,
    UC_X86_REG_ECX: UC_X86_REG_RCX,
    UC_X86_REG_EDX: UC_X86_REG_RDX,
    UC_X86_REG_ESI: UC_X86_REG_RSI,
    UC_X86_REG_EDI: UC_X86_REG_RDI,
    UC_X86_REG_EBP: UC_X86_REG_RBP,
}
for e, r in reg_map.items():
    uc32.reg_write(e, mu.reg_read(r) & 0xffffffff)  # mu = 64-bit emulator from above
uc32.reg_write(UC_X86_REG_EFLAGS, mu.reg_read(UC_X86_REG_RFLAGS) & 0xffffffff)

# SSE-heavy blobs need XMM registers copied
for xr in [UC_X86_REG_XMM0, UC_X86_REG_XMM1, UC_X86_REG_XMM2, UC_X86_REG_XMM3,
           UC_X86_REG_XMM4, UC_X86_REG_XMM5, UC_X86_REG_XMM6, UC_X86_REG_XMM7]:
    uc32.reg_write(xr, mu.reg_read(xr))

# Run 32-bit, then copy regs/memory back to 64-bit

Tip: set UC_IGNORE_REG_BREAK=1 to silence warnings on unimplemented regs.

Register Tracing Hook

def hook_code(uc, address, size, user_data):
    if address == TARGET_ADDR:
        rsi = uc.reg_read(UC_X86_REG_RSI)
        print(f"0x{address:x}: rsi=0x{rsi:016x}")

mu.hook_add(UC_HOOK_CODE, hook_code)

Track Register Changes

prev_rsi = [None]
def hook_rsi_changes(uc, address, size, user_data):
    rsi = uc.reg_read(UC_X86_REG_RSI)
    if rsi != prev_rsi[0]:
        print(f"0x{address:x}: RSI changed to 0x{rsi:016x}")
        prev_rsi[0] = rsi

mu.hook_add(UC_HOOK_CODE, hook_rsi_changes)

Python Bytecode

Disassembly

import marshal, dis

with open('file.pyc', 'rb') as f:
    f.read(16)  # Skip header (varies by Python version)
    code = marshal.load(f)
    dis.dis(code)

Extract Constants

for ins in dis.get_instructions(code):
    if ins.opname == 'LOAD_CONST':
        print(ins.argval)

Pyarmor Static Unpack (1shot)

Repository: https://github.com/Lil-House/Pyarmor-Static-Unpack-1shot

# Basic usage (recursive processing)
python /path/to/oneshot/shot.py /path/to/scripts

# Specify pyarmor runtime library explicitly
python /path/to/oneshot/shot.py /path/to/scripts -r /path/to/pyarmor_runtime.so

# Save outputs to another directory
python /path/to/oneshot/shot.py /path/to/scripts -o /path/to/output

Notes:

  • oneshot/pyarmor-1shot must exist before running shot.py.
  • Supported focus: Pyarmor 8.x-9.x (PY + six digits header style).
  • Pyarmor 7 and earlier (PYARMOR header) are out of scope.
  • Disassembly output is generally reliable; decompiled source is experimental.

WASM Analysis

Decompile to C

wasm2c checker.wasm -o checker.c
gcc -O3 checker.c wasm-rt-impl.c -o checker

Common Patterns

  • w2c_memory - Linear memory array
  • wasm_rt_trap(N) - Runtime errors
  • Function exports: flagChecker, validate

Android APK

Extraction

apktool d app.apk -o decoded/   # Best - decodes XML
jadx app.apk                     # Decompile to Java
unzip app.apk -d extracted/      # Simple extraction

Key Locations

  • res/values/strings.xml - String resources
  • AndroidManifest.xml - App metadata
  • classes.dex - Dalvik bytecode
  • assets/, res/raw/ - Resources

Search

grep -r "flag\|CTF" decoded/
strings decoded/classes*.dex | grep -i flag

Flutter APK (Blutter)

# Run Blutter on arm64 build
python3 blutter.py path/to/app/lib/arm64-v8a out_dir

HarmonyOS HAP/ABC (abc-decompiler)

Repository: https://github.com/ohos-decompiler/abc-decompiler

# Extract .hap first to obtain .abc files
unzip app.hap -d hap_extracted/

Critical startup mode:

# Use CLI entrypoint (avoid java -jar GUI mode)
java -cp "./jadx-dev-all.jar" jadx.cli.JadxCLI [options] <input>
# Basic decompile
java -cp "./jadx-dev-all.jar" jadx.cli.JadxCLI -d "out" ".abc"

# Recommended for .abc
java -cp "./jadx-dev-all.jar" jadx.cli.JadxCLI -m simple --log-level ERROR -d "out_abc_simple" ".abc"

Notes:

  • Start with -m simple --log-level ERROR.
  • If auto fails, retry with -m simple first.
  • Errors do not always mean total failure; check out_xxx/sources/.
  • Use a fresh output directory per run.

.NET Analysis

Tools

  • dnSpy - Debugging + decompilation (best)
  • ILSpy - Decompiler
  • dotPeek - JetBrains decompiler

NativeAOT

  • Look for System.Private.CoreLib strings
  • Type metadata present but restructured
  • Search for length-prefixed UTF-16 patterns

Two-Stage XOR + AES-CBC Decode Pattern (Codegate 2013)

Pattern: .NET binary stores an encrypted byte array that undergoes XOR decoding followed by AES-256-CBC decryption. The same key value serves as both the AES key and IV.

Steps:

  1. Extract hardcoded byte array and key string from binary (dnSpy/ILSpy)
  2. XOR each byte (may be multi-pass, e.g., 0x25 then 0x58, equivalent to single 0x7D)
  3. Base64-decode the XOR result
  4. AES-256-CBC decrypt with RijndaelManaged using the extracted key as both Key and IV
from Crypto.Cipher import AES
from base64 import b64decode

# Step 1: XOR decode
data = bytearray(encrypted_bytes)
for i in range(len(data)):
    data[i] ^= 0x7D  # Combined XOR key (0x25 ^ 0x58)

# Step 2: Base64 decode
ct = b64decode(bytes(data))

# Step 3: AES-256-CBC decrypt (same value for key and IV)
key = b"9e2ea73295c7201c5ccd044477228527"  # Padded to 32 bytes
cipher = AES.new(key, AES.MODE_CBC, iv=key)
plaintext = cipher.decrypt(ct)

Key insight: When RijndaelManaged appears in .NET decompilation, check if Key and IV are set to the same value — this is a common CTF pattern. The XOR stage often serves as a simple obfuscation layer before the real crypto.


Packed Binaries

UPX

upx -d packed -o unpacked
strings binary | grep UPX     # Check for UPX signature

Custom Packers

  1. Set breakpoint after unpacking stub
  2. Dump memory
  3. Fix PE/ELF headers

PyInstaller

python pyinstxtractor.py binary.exe
# Look in: binary.exe_extracted/

LLVM IR

Convert to Assembly

llc task.ll --x86-asm-syntax=intel
gcc -c task.s -o file.o

RISC-V Binary Analysis (EHAX 2026)

Pattern (iguessbro): Statically linked, stripped RISC-V ELF binary. Can't run natively on x86.

Disassembly with Capstone:

from capstone import *

with open('binary', 'rb') as f:
    code = f.read()

# RISC-V 64-bit with compressed instruction support
md = Cs(CS_ARCH_RISCV, CS_MODE_RISCVC | CS_MODE_RISCV64)
md.detail = True

# Disassemble from entry point (check ELF header for e_entry)
TEXT_OFFSET = 0x10000  # typical for static RISC-V
for insn in md.disasm(code[TEXT_OFFSET:], TEXT_OFFSET):
    print(f"0x{insn.address:x}:\t{insn.mnemonic}\t{insn.op_str}")

Common RISC-V patterns:

  • li a0, N → load immediate (argument setup)
  • mv a0, s0 → register move
  • call offset → function call (auipc + jalr pair)
  • beq/bne a0, zero, label → conditional branch
  • sd/ld → 64-bit store/load
  • addiw → 32-bit add (W-suffix = word operations)

Key differences from x86:

  • No flags register — comparisons are inline with branch instructions
  • Arguments in a0-a7 (not rdi/rsi/rdx)
  • Return value in a0
  • Saved registers s0-s11 (callee-saved)
  • Compressed instructions (2 bytes) mixed with standard (4 bytes) — use CS_MODE_RISCVC

Anti-RE tricks in RISC-V:

  • Fake flags as string constants (check for "n0t_th3_r34l" patterns)
  • Timing anti-brute-force (rdtime instruction)
  • XOR decryption with incremental key: decrypted[i] = enc[i] ^ (key & 0xFF) ^ 0xA5; key += 7

Emulation: qemu-riscv64 -L /usr/riscv64-linux-gnu/ ./binary (needs cross-toolchain sysroot)


Binary Ninja

Interactive disassembler/decompiler with rapid community growth.

Decompilation outputs: High-Level Intermediate Language (HLIL), pseudo-C, pseudo-Rust, pseudo-Python.

# Open binary
binaryninja binary
# Headless analysis (Python API)
import binaryninja
bv = binaryninja.open_view("binary")
for func in bv.functions:
    print(func.name, hex(func.start))
    print(func.hlil)  # High-Level IL

Community plugins: Available via Plugin Manager (Ctrl+Shift+P → "Plugin Manager").

Free version: https://binary.ninja/free/ (cloud-based, limited features).

Advantages over Ghidra: Faster startup, cleaner IL representations, better Python API for scripting.


Decompiler Comparison with dogbolt.org

dogbolt.org runs multiple decompilers simultaneously on the same binary and shows results side-by-side.

Supported decompilers: Hex-Rays (IDA), Ghidra, Binary Ninja, angr, RetDec, Snowman, dewolf, Reko, Relyze.

When to use:

  • Decompiler output is confusing — compare with alternatives for clarity
  • One decompiler mishandles a construct — another may get it right
  • Quick triage without installing every tool locally
  • Validate decompiler correctness by cross-referencing outputs
# Upload via web interface: https://dogbolt.org/
# Or use the API:
curl -F "file=@binary" https://dogbolt.org/api/binaries/

Key insight: Different decompilers excel at different constructs. When one produces unreadable output, another often generates clearer pseudocode. Cross-referencing catches decompiler bugs.


Useful Commands

# File info
file binary
checksec --file=binary
rabin2 -I binary

# String extraction
strings binary | grep -iE "flag|secret"
rabin2 -z binary

# Sections
readelf -S binary
objdump -h binary

# Symbols
nm binary
readelf -s binary

# Disassembly
objdump -d binary
objdump -M intel -d binary

boolector SMT2 for Custom Hash Reversal (OTW Advent 2018)

Pattern: Custom hash functions built from bit operations fall to SMT solvers. boolector's QF_BV (bitvector) logic is noticeably faster than Z3 for such instances. Translate the hash into SMT2 directly, assert the output, and solve for the input.

(set-logic QF_BV)
(declare-fun input () (_ BitVec 64))
(assert (bvuge input #x0000000020202020))   ; printable lower bound
(assert (bvule input #x000000007e7e7e7e))   ; printable upper bound

; Emit the hash function as bvxor/bvrol/bvadd chains
(define-fun hash ((x (_ BitVec 64))) (_ BitVec 64) ...)
(assert (= (hash input) #xdeadbeefcafef00d))
(check-sat) (get-model)
boolector -m --output-format=smt2 hash.smt2

Key insight: Z3 is the default, but for bit-level hash puzzles boolector is often 10-100× faster. Emit SMT2 from IDA/r2 by lifting each basic block into bvxor/bvrol/bvadd and let the solver pick the preimage.

References: OverTheWire Advent 2018 — Jackinthebox, writeup 12789

Source: SKILL.md on GitHub

1 alert16d5 checks · Risk MEDIUM
  • Gen Agent Trust Hub16d

    This skill provides a comprehensive toolkit for reverse engineering. It contains instructions for the installation of numerous third-party security tools and libraries from various external sources, including the execution of remote shell scripts. Additionally, the skill is designed to process untrusted binary data and code from CTF challenges, which presents an attack surface for indirect prompt injection.

  • Socket16d

    3 alerts: gptSecurity

  • Snyk16d

    Risk: LOW · No issues

  • Runlayer6mo

    4/4 files flagged

  • ZeroLeaks5mo

    1 finding · Score: 86/100

Signed by skilld at 61c2efe. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 3 weeks ago.

Activeupdated 3 weeks ago
metadata
{
  "user-invocable": "false"
}
All 1 allowed tools
Bash Read Write Edit Glob Grep Task WebFetch WebSearch
Other metadata
compatibility
Requires filesystem-based agent (Claude Code or similar) with bash, Python 3, and internet access for tool installation.

README badge

README badge for ljagiello/ctf-skills/ctf-reverse