│ Category: Reverse Engineering
│ Difficulty: Easy
│ Points: 100
│ Author: Imattas aka Zemi
────────────────────────────────────────────────────────────────────────────────
--[ Challenge Description ]--
│ Your task is to analyze and exploit a password-protected binary called
bypassme.bin and binary performs a multi-step verification process before
granting access.
────────────────────────────────────────────────────────────────────────────────
--[ Recon / Initial Analysis ]--
This is a classic reverse engineering challenge where a binary performs multiple
password verification steps. The goal is to either:
1. Extract the password by reverse engineering the verification logic, or
2. Bypass the checks entirely by patching the binary (changing conditional
jumps)
The "multi-step verification" suggests the binary has several sequential checks,
such as:
- String length verification
- Character-by-character comparison
- Hash/checksum validation
- Anti-debugging checks
- Obfuscated comparisons
Initial reconnaissance:
-- bash --
file bypassme.bin
checksec --file=bypassme.bin
strings bypassme.bin | grep -i "pass\|flag\|correct\|wrong\|step\|verify"
────────────────────────────────────────────────────────────────────────────────
--[ Vulnerability / Observation ]--
Each verification step likely compares parts of the input against expected
values. By analyzing the comparison instructions in the disassembly, we can
extract what values are expected or simply patch the binary to skip the checks.
Key techniques that apply here:
- Static analysis: Decompile with Ghidra/IDA to understand the verification
logic
- Dynamic analysis: Use GDB to step through checks and observe comparisons
- Binary patching: Modify conditional jumps (e.g., jne -> je or jmp) to bypass
checks
- ltrace/strace: Intercept library calls like strcmp, strncmp, memcmp
If strcmp/strncmp is used, the expected password will surface directly in an
ltrace trace; otherwise the comparison instructions in main() reveal the
expected values.
────────────────────────────────────────────────────────────────────────────────
--[ Exploitation / Solution ]--
:: Step 1: Initial reconnaissance
-- bash --
file bypassme.bin
checksec --file=bypassme.bin
strings bypassme.bin | grep -i "pass\|flag\|correct\|wrong\|step\|verify"
:: Step 2: Dynamic analysis with ltrace
-- bash --
# ltrace intercepts library function calls -- if strcmp is used, the
# expected password will appear in the trace output
ltrace ./bypassme.bin
# Enter a test password like "AAAA" and observe strcmp/strncmp calls
:: Step 3: Static analysis with Ghidra/objdump
-- bash --
# Quick disassembly of main
objdump -d -M intel bypassme.bin | less
# Look for cmp, test, je, jne instructions after reads/scanf
In Ghidra:
1. Open bypassme.bin
2. Navigate to main() or search for string references ("Enter password",
"Correct", etc.)
3. Identify the verification function(s)
4. Note the expected values at each comparison
:: Step 4: Bypass via GDB
-- bash --
gdb ./bypassme.bin
# Set breakpoint at each comparison
b *<address_of_first_cmp>
b *<address_of_second_cmp>
run
# At each breakpoint, examine the comparison values:
# x/s $rdi (first arg to strcmp)
# x/s $rsi (second arg to strcmp)
# Or modify the zero flag to force the branch:
# set $eflags |= (1 << 6) # Set ZF to force JE to succeed
continue
:: Step 5: Bypass via binary patching
-- python --
# Patch conditional jumps to unconditional jumps or NOPs
# jne (0x75) -> je (0x74) or jmp (0xEB) or NOP (0x90)
:: Step 6: Run the patched binary or supply the extracted password
-- bash --
./bypassme_patched.bin
# Or: echo "extracted_password" | ./bypassme.bin
────────────────────────────────────────────────────────────────────────────────
--[ Full Exploit Script ]--
-- python --
#!/usr/bin/env python3
"""
Bypass Me - picoCTF 2026
Category: Reverse Engineering | Points: 100
Analyzes and bypasses a password-protected binary (bypassme.bin) that performs
multi-step verification. This script uses multiple approaches:
1. ltrace interception to capture strcmp/strncmp arguments
2. Binary patching to bypass conditional jumps
3. angr symbolic execution as a fallback
Usage:
python3 solve.py [path_to_bypassme.bin]
python3 solve.py # defaults to ./bypassme.bin
"""
import subprocess
import sys
import os
import re
import struct
import tempfile
import shutil
FLAG_PATTERN = re.compile(r"picoCTF\{[^}]+\}")
# ──────────────────────────────────────────────────────────────────
# Method 1: ltrace interception
# ──────────────────────────────────────────────────────────────────
def try_ltrace(binary_path):
"""
Use ltrace to intercept string comparison functions.
If the binary uses strcmp/strncmp/memcmp, the expected password
will appear as an argument in the trace output.
"""
print("[*] Method 1: ltrace interception")
test_input = b"AAAAAAAAAAAAAAAA\n"
try:
proc = subprocess.run(
["ltrace", "-s", "256", "-e", "strcmp+strncmp+memcmp+strcasecmp", binary_path],
input=test_input,
capture_output=True,
text=True,
timeout=10,
)
output = proc.stderr + proc.stdout # ltrace outputs to stderr
# Look for comparison functions with string arguments
# ltrace format: strcmp("user_input", "expected_password") = -1
passwords = []
patterns = [
r'str(?:n?)cmp\(".*?",\s*"([^"]+)"',
r'str(?:n?)cmp\("([^"]+)",\s*".*?"',
r'memcmp\(.*?,\s*"([^"]+)"',
r'strcasecmp\(".*?",\s*"([^"]+)"',
]
for pat in patterns:
matches = re.findall(pat, output)
for m in matches:
if m != test_input.decode().strip() and len(m) > 0:
passwords.append(m)
if passwords:
print(f"[+] Intercepted password(s): {passwords}")
return passwords
else:
print("[-] No passwords intercepted via ltrace")
except FileNotFoundError:
print("[-] ltrace not found, skipping")
except subprocess.TimeoutExpired:
print("[-] ltrace timed out")
return []
# ──────────────────────────────────────────────────────────────────
# Method 2: String extraction
# ──────────────────────────────────────────────────────────────────
def try_strings(binary_path):
"""Search for hardcoded passwords/flags in binary strings."""
print("[*] Method 2: String extraction")
result = subprocess.run(
["strings", "-a", binary_path], capture_output=True, text=True
)
all_strings = result.stdout
# Direct flag check
flag_match = FLAG_PATTERN.search(all_strings.encode())
if flag_match:
print(f"[+] Flag found in strings: {flag_match.group().decode()}")
return flag_match.group().decode()
# Look for password-related strings
interesting = []
for line in all_strings.split("\n"):
line = line.strip()
# Filter for potential passwords (alphanumeric, reasonable length)
if 4 <= len(line) <= 64 and re.match(r'^[a-zA-Z0-9_!@#$%^&*(){}]+$', line):
interesting.append(line)
return interesting
# ──────────────────────────────────────────────────────────────────
# Method 3: Binary patching
# ──────────────────────────────────────────────────────────────────
def try_patching(binary_path):
"""
Patch conditional jumps to bypass verification checks.
Strategy: find all jne/jnz (0x75, 0x0F 0x85) after cmp instructions
near "wrong"/"fail" strings and replace with je/jz or NOP.
"""
print("[*] Method 3: Binary patching")
with open(binary_path, "rb") as f:
data = bytearray(f.read())
original_data = bytes(data)
# Disassemble to find conditional jumps in verification sections
try:
result = subprocess.run(
["objdump", "-d", "-M", "intel", binary_path],
capture_output=True, text=True
)
disasm = result.stdout
except FileNotFoundError:
print("[-] objdump not found")
return None
# Find all conditional jump instructions that could be verification checks
# Pattern: cmp ... followed by jne/jnz (branch to fail) or je/jz (branch to success)
patch_count = 0
# Strategy A: Replace short jne (0x75 XX) with je (0x74 XX)
# This inverts the condition so wrong passwords are accepted
for match in re.finditer(
r'^\s*([0-9a-f]+):\s+75 ([0-9a-f]{2})\s+jne?\s',
disasm, re.MULTILINE
):
addr = int(match.group(1), 16)
# Find this byte sequence in the binary
# We need the file offset, not the virtual address
# For simple binaries, offset ~= addr - base_addr
# Try to find the pattern in the raw data
offset = data.find(b'\x75' + bytes([int(match.group(2), 16)]))
if offset != -1:
# Check context to avoid patching unrelated jumps
data[offset] = 0x74 # jne -> je (invert condition)
patch_count += 1
# Strategy B: Replace long jne (0F 85 XX XX XX XX) with long je (0F 84)
for match in re.finditer(
r'^\s*([0-9a-f]+):\s+0f 85\s',
disasm, re.MULTILINE
):
addr = int(match.group(1), 16)
idx = 0
while True:
idx = data.find(b'\x0f\x85', idx)
if idx == -1:
break
data[idx + 1] = 0x84 # jne -> je
patch_count += 1
idx += 2
if patch_count == 0:
# Try the reverse: patch je to jne (if logic is inverted)
data = bytearray(original_data)
for match in re.finditer(
r'^\s*([0-9a-f]+):\s+74 ([0-9a-f]{2})\s+je?\s',
disasm, re.MULTILINE
):
offset = data.find(b'\x74' + bytes([int(match.group(2), 16)]))
if offset != -1:
data[offset] = 0x75 # je -> jne
patch_count += 1
if patch_count > 0:
print(f"[+] Applied {patch_count} patches")
patched_path = binary_path + ".patched"
with open(patched_path, "wb") as f:
f.write(data)
os.chmod(patched_path, 0o755)
print(f"[+] Patched binary saved to: {patched_path}")
return patched_path
print("[-] No suitable patches found")
return None
# ──────────────────────────────────────────────────────────────────
# Method 4: angr symbolic execution
# ──────────────────────────────────────────────────────────────────
def try_angr(binary_path):
"""Use angr to symbolically find the correct input."""
print("[*] Method 4: angr symbolic execution")
try:
import angr
import claripy
except ImportError:
print("[-] angr not installed (pip install angr)")
return None
proj = angr.Project(binary_path, auto_load_libs=False)
# Create symbolic stdin
sym_input = claripy.BVS("input", 200 * 8)
state = proj.factory.entry_state(
stdin=angr.SimFile("/dev/stdin", content=sym_input),
add_options={
angr.options.ZERO_FILL_UNCONSTRAINED_MEMORY,
angr.options.ZERO_FILL_UNCONSTRAINED_REGISTERS,
}
)
simgr = proj.factory.simulation_manager(state)
# Success/failure strings
find_strs = [b"Correct", b"correct", b"Success", b"success",
b"Access granted", b"Welcome", b"picoCTF", b"flag"]
avoid_strs = [b"Wrong", b"wrong", b"Incorrect", b"incorrect",
b"Denied", b"denied", b"fail", b"Fail", b"Invalid"]
def is_find(s):
out = s.posix.dumps(1)
return any(f in out for f in find_strs)
def is_avoid(s):
out = s.posix.dumps(1)
return any(a in out for a in avoid_strs)
simgr.explore(find=is_find, avoid=is_avoid)
if simgr.found:
solution = simgr.found[0].posix.dumps(0)
solution = solution.split(b"\x00")[0].strip()
stdout = simgr.found[0].posix.dumps(1)
print(f"[+] angr found input: {solution}")
print(f"[+] Program output: {stdout.decode(errors='replace')}")
flag_match = FLAG_PATTERN.search(stdout)
if flag_match:
print(f"[+] FLAG: {flag_match.group().decode()}")
return solution
else:
print("[-] angr could not find a solution")
return None
# ──────────────────────────────────────────────────────────────────
# Method 5: GDB scripted analysis
# ──────────────────────────────────────────────────────────────────
def try_gdb_script(binary_path):
"""Use GDB with a script to break at comparisons and extract expected values."""
print("[*] Method 5: GDB scripted analysis")
gdb_script = """
set pagination off
set confirm off
set follow-fork-mode child
# Break at common comparison functions
catch syscall write
b strcmp
b strncmp
b memcmp
run <<< "TESTPASSWORD123"
# At each breakpoint, print the arguments
while 1
# For strcmp(s1, s2): s1 in $rdi, s2 in $rsi (x86-64 SysV ABI)
printf "RDI (arg1): "
x/s $rdi
printf "RSI (arg2): "
x/s $rsi
continue
end
quit
"""
script_path = "/tmp/gdb_bypass_script.gdb"
with open(script_path, "w") as f:
f.write(gdb_script)
try:
result = subprocess.run(
["gdb", "-batch", "-x", script_path, binary_path],
capture_output=True, text=True, timeout=15
)
output = result.stdout + result.stderr
# Parse GDB output for string comparison values
passwords = []
for match in re.finditer(r'RSI \(arg2\):\s+\S+\s+"([^"]+)"', output):
val = match.group(1)
if val != "TESTPASSWORD123" and len(val) > 0:
passwords.append(val)
for match in re.finditer(r'RDI \(arg1\):\s+\S+\s+"([^"]+)"', output):
val = match.group(1)
if val != "TESTPASSWORD123" and len(val) > 0:
passwords.append(val)
if passwords:
print(f"[+] GDB extracted password(s): {passwords}")
return passwords
except FileNotFoundError:
print("[-] GDB not found")
except subprocess.TimeoutExpired:
print("[-] GDB timed out")
return []
# ──────────────────────────────────────────────────────────────────
# Main solver orchestration
# ──────────────────────────────────────────────────────────────────
def run_binary(binary_path, password):
"""Run the binary with a given password and capture output."""
try:
proc = subprocess.run(
[binary_path],
input=password.encode() if isinstance(password, str) else password,
capture_output=True,
text=True,
timeout=10,
)
return proc.stdout + proc.stderr
except:
return ""
def main():
binary_path = sys.argv[1] if len(sys.argv) > 1 else "./bypassme.bin"
binary_path = os.path.abspath(binary_path)
if not os.path.exists(binary_path):
print(f"[-] Binary not found: {binary_path}")
print("Usage: python3 solve.py [path_to_bypassme.bin]")
sys.exit(1)
print(f"[*] Target: {binary_path}")
print(f"[*] File info: {subprocess.getoutput(f'file {binary_path}')}")
print()
# ── Method 1: ltrace ──
passwords = try_ltrace(binary_path)
if passwords:
for pw in passwords:
output = run_binary(binary_path, pw)
print(f"[*] Trying '{pw}': {output.strip()}")
flag = FLAG_PATTERN.search(output)
if flag:
print(f"\n[+] FLAG: {flag.group()}")
return
# If multi-step, try concatenating passwords
combined = "".join(passwords)
output = run_binary(binary_path, combined)
print(f"[*] Trying combined '{combined}': {output.strip()}")
flag = FLAG_PATTERN.search(output)
if flag:
print(f"\n[+] FLAG: {flag.group()}")
return
print()
# ── Method 2: Strings ──
result = try_strings(binary_path)
if isinstance(result, str) and "picoCTF" in result:
print(f"\n[+] FLAG: {result}")
return
print()
# ── Method 3: Binary patching ──
patched = try_patching(binary_path)
if patched:
print(f"[*] Running patched binary with dummy input...")
output = run_binary(patched, "anything")
print(f"[*] Output: {output.strip()}")
flag = FLAG_PATTERN.search(output)
if flag:
print(f"\n[+] FLAG: {flag.group()}")
return
print()
# ── Method 4: GDB scripted analysis ──
gdb_passwords = try_gdb_script(binary_path)
if gdb_passwords:
for pw in gdb_passwords:
output = run_binary(binary_path, pw)
print(f"[*] Trying '{pw}': {output.strip()}")
flag = FLAG_PATTERN.search(output)
if flag:
print(f"\n[+] FLAG: {flag.group()}")
return
print()
# ── Method 5: angr (slowest, most comprehensive) ──
angr_result = try_angr(binary_path)
if angr_result:
output = run_binary(binary_path, angr_result)
print(f"[*] Output: {output.strip()}")
flag = FLAG_PATTERN.search(output)
if flag:
print(f"\n[+] FLAG: {flag.group()}")
return
print()
print("=" * 60)
print("MANUAL STEPS (if automatic methods failed):")
print("=" * 60)
print("1. Open in Ghidra: ghidra -> Import bypassme.bin -> Analyze")
print("2. Find main() and trace the verification logic")
print("3. In GDB:")
print(" gdb ./bypassme.bin")
print(" b main")
print(" run")
print(" # Step through, watching for cmp/test instructions")
print(" # At each comparison, check register values:")
print(" info registers")
print(" x/s $rdi")
print(" x/s $rsi")
print("4. Patch binary:")
print(" # In radare2: r2 -w bypassme.bin")
print(" # s <addr_of_jne>")
print(" # wa je <target> OR wa nop; nop")
if __name__ == "__main__":
main()
────────────────────────────────────────────────────────────────────────────────
--[ Key Takeaways ]--
- A password-protected binary with sequential checks can be defeated either by
extracting the expected values or by bypassing the conditional logic entirely.
- ltrace is the fastest win when comparisons use libc functions
(strcmp/strncmp/memcmp) — the expected password appears directly as a function
argument.
- Conditional-jump patching (jne 0x75 ↔ je 0x74, or long-form 0F 85 ↔ 0F 84)
inverts or skips a check without ever knowing the password.
- GDB can dump comparison arguments at runtime (x/s $rdi / x/s $rsi) or force a
branch by flipping the zero flag in $eflags.
- angr symbolic execution is a strong fallback: explore toward
"Correct"/"picoCTF" strings while avoiding "Wrong"/"Invalid".