│ Category: Reverse Engineering
│ Difficulty: Medium
│ Points: 200
│ Author: Imattas aka Zemi
────────────────────────────────────────────────────────────────────────────────
--[ Challenge Description ]--
│ You think you can reverse engineer? Let's test out your speed.
────────────────────────────────────────────────────────────────────────────────
--[ Recon / Initial Analysis ]--
This challenge tests your ability to quickly reverse engineer binaries. The
"speed" element strongly suggests that:
1. The server provides a binary (or series of binaries) that must be analyzed
and solved within a time limit.
2. Manual reversing would be too slow -- you need automated analysis.
3. Common techniques include:
- angr (symbolic execution) to automatically find inputs that reach a
"success" path
- z3 (SMT solver) to solve constraint systems extracted from the binary
- Binary instrumentation with tools like Frida or PIN
- Automated disassembly with Ghidra scripting or radare2/rizin
The typical pattern for "speed reversing" challenges:
- Connect to a server via netcat
- Receive a binary (base64 encoded or downloadable via URL)
- Analyze the binary to find the correct input/password/key
- Send it back before the timeout expires
- Repeat for multiple rounds
────────────────────────────────────────────────────────────────────────────────
--[ Vulnerability / Observation ]--
The binary likely contains a check_flag() or verify() function that compares
user input against some computed value. Using angr, we can symbolically execute
the binary to find the input that leads to a "Correct" output. Because rounds
are time-limited, the solve has to be fully automated: identify the
success/failure output strings, explore for the path that prints success while
avoiding the failure path, and recover the satisfying stdin/argv.
────────────────────────────────────────────────────────────────────────────────
--[ Exploitation / Solution ]--
:: Step 1: Connect to the challenge server
-- bash --
nc <challenge_host> <challenge_port>
The server sends a binary (possibly base64-encoded) and expects the correct
input within a time limit.
:: Step 2: Download and analyze the binary
-- bash --
# If a URL is provided:
wget <binary_url> -O challenge_binary
chmod +x challenge_binary
# Quick analysis
file challenge_binary
strings challenge_binary | grep -i "correct\|wrong\|flag\|success\|fail"
:: Step 3: Use angr for automated solving
angr uses symbolic execution to explore all possible paths through a binary. We
tell it to find the path that reaches the "success" output and avoid the
"failure" output.
:: Step 4: Alternatively, use Ghidra headless for decompilation
-- bash --
analyzeHeadless /tmp ghidra_project -import challenge_binary -postScript DecompileScript.py
:: Step 5: Submit the answer before timeout
────────────────────────────────────────────────────────────────────────────────
--[ Full Exploit Script ]--
-- python --
#!/usr/bin/env python3
"""
Autorev 1 - picoCTF 2026
Category: Reverse Engineering | Points: 200
Automated reverse engineering solver that uses angr for symbolic execution
to quickly solve binaries within a time constraint.
The challenge likely sends binaries that must be solved rapidly. This script:
1. Connects to the challenge server
2. Downloads/receives the binary
3. Uses angr to find the correct input via symbolic execution
4. Sends the answer back before the timeout
Usage:
python3 solve.py <host> <port>
python3 solve.py [path_to_binary]
"""
import angr
import claripy
import sys
import os
import re
import base64
import tempfile
import struct
import subprocess
# Optional: for network interaction
try:
from pwn import *
HAS_PWNTOOLS = True
except ImportError:
import socket
HAS_PWNTOOLS = False
FLAG_PATTERN = re.compile(rb"picoCTF\{[^}]+\}")
# ──────────────────────────────────────────────────────────────────
# angr-based binary solver
# ──────────────────────────────────────────────────────────────────
def solve_with_angr(binary_path, find_strs=None, avoid_strs=None):
"""
Use angr symbolic execution to find input that reaches a 'success' state.
Args:
binary_path: Path to the ELF binary
find_strs: List of byte strings indicating success (e.g., [b"Correct"])
avoid_strs: List of byte strings indicating failure (e.g., [b"Wrong"])
Returns:
The solution input as bytes, or None if no solution found.
"""
if find_strs is None:
find_strs = [b"Correct", b"correct", b"Success", b"success",
b"Good", b"Right", b"right", b"Yes", b"FLAG",
b"picoCTF", b"Congrat", b"Well done", b"Access granted"]
if avoid_strs is None:
avoid_strs = [b"Wrong", b"wrong", b"Incorrect", b"incorrect",
b"Fail", b"fail", b"No", b"denied", b"Invalid",
b"Try again", b"Bad"]
print(f"[*] Loading binary: {binary_path}")
proj = angr.Project(binary_path, auto_load_libs=False)
# ── Strategy 1: Use stdin as symbolic input ──
print("[*] Strategy 1: Symbolic stdin exploration")
state = proj.factory.entry_state(
stdin=angr.SimFile("/dev/stdin", content=claripy.BVS("stdin", 200 * 8)),
add_options={
angr.options.ZERO_FILL_UNCONSTRAINED_MEMORY,
angr.options.ZERO_FILL_UNCONSTRAINED_REGISTERS,
}
)
simgr = proj.factory.simulation_manager(state)
# Define find/avoid conditions based on output strings
def is_find(s):
output = s.posix.dumps(1) # stdout
return any(fs in output for fs in find_strs)
def is_avoid(s):
output = s.posix.dumps(1) # stdout
return any(av in output for av in avoid_strs)
print("[*] Exploring paths...")
simgr.explore(find=is_find, avoid=is_avoid)
if simgr.found:
found_state = simgr.found[0]
solution = found_state.posix.dumps(0) # stdin
# Clean the solution: strip null bytes and trailing garbage
solution = solution.split(b"\x00")[0].strip()
print(f"[+] Solution found: {solution}")
# Check if the flag is in stdout
stdout_output = found_state.posix.dumps(1)
flag_match = FLAG_PATTERN.search(stdout_output)
if flag_match:
print(f"[+] Flag in output: {flag_match.group().decode()}")
return solution
print("[-] Strategy 1 failed, trying Strategy 2...")
# ── Strategy 2: Use argv as symbolic input ──
print("[*] Strategy 2: Symbolic argv exploration")
argv_sym = claripy.BVS("argv1", 100 * 8)
state2 = proj.factory.entry_state(
args=[binary_path, argv_sym],
add_options={
angr.options.ZERO_FILL_UNCONSTRAINED_MEMORY,
angr.options.ZERO_FILL_UNCONSTRAINED_REGISTERS,
}
)
simgr2 = proj.factory.simulation_manager(state2)
simgr2.explore(find=is_find, avoid=is_avoid)
if simgr2.found:
found_state = simgr2.found[0]
solution = found_state.solver.eval(argv_sym, cast_to=bytes)
solution = solution.split(b"\x00")[0].strip()
print(f"[+] Solution found (argv): {solution}")
stdout_output = found_state.posix.dumps(1)
flag_match = FLAG_PATTERN.search(stdout_output)
if flag_match:
print(f"[+] Flag in output: {flag_match.group().decode()}")
return solution
print("[-] Strategy 2 failed, trying Strategy 3...")
# ── Strategy 3: Find target address from strings ──
print("[*] Strategy 3: Address-based exploration")
cfg = proj.analyses.CFGFast()
find_addrs = []
avoid_addrs = []
for func_addr in cfg.functions:
func = cfg.functions[func_addr]
for block in func.blocks:
try:
block_bytes = proj.loader.memory.load(block.addr, block.size)
for fs in find_strs:
if fs in block_bytes:
find_addrs.append(block.addr)
for av in avoid_strs:
if av in block_bytes:
avoid_addrs.append(block.addr)
except:
pass
if find_addrs:
state3 = proj.factory.entry_state(
stdin=angr.SimFile("/dev/stdin", content=claripy.BVS("stdin2", 200 * 8)),
add_options={
angr.options.ZERO_FILL_UNCONSTRAINED_MEMORY,
angr.options.ZERO_FILL_UNCONSTRAINED_REGISTERS,
}
)
simgr3 = proj.factory.simulation_manager(state3)
simgr3.explore(find=find_addrs, avoid=avoid_addrs)
if simgr3.found:
found_state = simgr3.found[0]
solution = found_state.posix.dumps(0)
solution = solution.split(b"\x00")[0].strip()
print(f"[+] Solution found (addr-based): {solution}")
return solution
print("[-] All angr strategies exhausted.")
return None
def solve_with_strings(binary_path):
"""Quick heuristic: look for hardcoded flags or passwords in strings."""
print("[*] Checking strings for embedded flag/password...")
result = subprocess.run(
["strings", binary_path], capture_output=True, text=True
)
# Check for flag directly in strings
flag_match = re.search(r"picoCTF\{[^}]+\}", result.stdout)
if flag_match:
print(f"[+] Flag found in strings: {flag_match.group()}")
return flag_match.group().encode()
return None
# ──────────────────────────────────────────────────────────────────
# Network interaction (for server-based challenges)
# ──────────────────────────────────────────────────────────────────
def solve_remote(host, port):
"""
Connect to the challenge server, receive binaries, solve them,
and send answers back within the time limit.
"""
if HAS_PWNTOOLS:
conn = remote(host, int(port))
else:
conn = socket.create_connection((host, int(port)))
round_num = 0
while True:
round_num += 1
print(f"\n{'=' * 60}")
print(f"Round {round_num}")
print(f"{'=' * 60}")
if HAS_PWNTOOLS:
data = conn.recvuntil(b"\n", timeout=10)
data_str = data.decode(errors="replace")
else:
data = conn.recv(65536)
data_str = data.decode(errors="replace")
print(f"[*] Received: {data_str[:200]}...")
# Check if we got a flag already
flag_match = FLAG_PATTERN.search(data)
if flag_match:
print(f"[+] FLAG: {flag_match.group().decode()}")
break
# Try to extract a binary (base64 encoded)
b64_match = re.search(r"[A-Za-z0-9+/]{100,}={0,2}", data_str)
if b64_match:
binary_data = base64.b64decode(b64_match.group())
elif b"ELF" in data:
# Raw binary data
elf_start = data.index(b"\x7fELF")
binary_data = data[elf_start:]
else:
# Maybe there's a download URL
url_match = re.search(r"(https?://\S+)", data_str)
if url_match:
import urllib.request
binary_data = urllib.request.urlopen(url_match.group()).read()
else:
print("[-] Could not extract binary from server data")
if HAS_PWNTOOLS:
# Try receiving more
more_data = conn.recv(timeout=5)
data += more_data
data_str = data.decode(errors="replace")
flag_match = FLAG_PATTERN.search(data)
if flag_match:
print(f"[+] FLAG: {flag_match.group().decode()}")
break
continue
# Write binary to temp file
with tempfile.NamedTemporaryFile(suffix=".bin", delete=False) as f:
f.write(binary_data)
bin_path = f.name
os.chmod(bin_path, 0o755)
print(f"[*] Binary saved to {bin_path} ({len(binary_data)} bytes)")
# Solve with angr
solution = solve_with_strings(bin_path) or solve_with_angr(bin_path)
# Cleanup temp binary
os.unlink(bin_path)
if solution:
print(f"[*] Sending solution: {solution}")
if HAS_PWNTOOLS:
conn.sendline(solution)
else:
conn.sendall(solution + b"\n")
else:
print("[-] Could not solve this round")
break
# Final receive to catch any flag output
try:
if HAS_PWNTOOLS:
final = conn.recvall(timeout=5)
else:
final = conn.recv(65536)
flag_match = FLAG_PATTERN.search(final)
if flag_match:
print(f"[+] FLAG: {flag_match.group().decode()}")
except:
pass
if HAS_PWNTOOLS:
conn.close()
def solve_local(binary_path):
"""Solve a single local binary."""
binary_path = os.path.abspath(binary_path)
if not os.path.exists(binary_path):
print(f"[-] File not found: {binary_path}")
return
print(f"[*] Solving local binary: {binary_path}")
# Try strings first (fastest)
result = solve_with_strings(binary_path)
if result:
print(f"[+] Solution: {result.decode(errors='replace')}")
# Run binary with found input
proc = subprocess.run(
[binary_path], input=result, capture_output=True, timeout=10
)
print(f"[*] Output: {proc.stdout.decode(errors='replace')}")
return
# Try angr (comprehensive)
result = solve_with_angr(binary_path)
if result:
print(f"[+] Solution: {result.decode(errors='replace')}")
# Run binary with found input
proc = subprocess.run(
[binary_path], input=result, capture_output=True, timeout=10
)
output = proc.stdout.decode(errors="replace")
print(f"[*] Output: {output}")
flag_match = FLAG_PATTERN.search(output.encode())
if flag_match:
print(f"[+] FLAG: {flag_match.group().decode()}")
else:
print("[-] Could not solve binary automatically")
print(" Manual analysis steps:")
print(" 1. ghidra or IDA: decompile and find check function")
print(" 2. gdb: set breakpoint at comparison, examine registers")
print(" 3. ltrace/strace: trace library/system calls")
print(" 4. r2 -A binary; afl; pdf @main")
def main():
if len(sys.argv) < 2:
print("Usage:")
print(" python3 solve.py <host> <port> # Remote challenge")
print(" python3 solve.py <binary_path> # Local binary")
print("")
print("Examples:")
print(" python3 solve.py titan.picoctf.net 52847")
print(" python3 solve.py ./challenge_binary")
sys.exit(1)
if len(sys.argv) == 3 and sys.argv[2].isdigit():
# Remote mode
solve_remote(sys.argv[1], sys.argv[2])
else:
# Local mode
solve_local(sys.argv[1])
if __name__ == "__main__":
main()
────────────────────────────────────────────────────────────────────────────────
--[ Key Takeaways ]--
- "Speed reversing" challenges are won by automation, not manual disassembly --
symbolic execution with angr finds the satisfying input across rounds before the
timeout.
- Drive angr with success/failure output strings (find=/avoid=), falling back
from symbolic stdin to symbolic argv to address-based exploration found via
CFGFast().
- Always try the cheap heuristic first: strings | grep picoCTF can short-circuit
the whole thing if the flag is hardcoded.
- Wrap the solver in a network loop (pwntools remote) that extracts each binary
(base64 / raw ELF / download URL), solves it, and replies with sendline.