│ Category: Reverse Engineering
│ Difficulty: Medium
│ Points: 200
│ Author: Imattas aka Zemi
────────────────────────────────────────────────────────────────────────────────
--[ Challenge Description ]--
│ I made a new password authentication program that even shows you the password
you entered saved in the database! Isn't that cool?
────────────────────────────────────────────────────────────────────────────────
--[ Recon / Initial Analysis ]--
This is a reverse engineering challenge where a binary program implements a
password authentication system. The key hint in the description is that it
"shows you the password you entered saved in the database" -- this suggests the
program transforms your input in some way and displays the result, which we can
use to reverse-engineer the expected password or directly extract the flag.
:: Analysis Tools
- strings: Quick check for readable strings including the flag format
- Ghidra/IDA: Full disassembly and decompilation for understanding the logic
- ltrace/strace: Trace library calls to see strcmp(), memcmp(), etc. with
arguments
- gdb: Dynamic debugging to inspect memory and registers during comparison
- objdump: Quick disassembly of key functions
:: Key Observations
The phrase "shows you the password you entered saved in the database" is the
major clue. This likely means:
- The program echoes back a transformed version of your input
- By observing how input maps to output, we can deduce the transformation
- We can then reverse the transformation on the stored "correct" database entry
to recover the flag
- Alternatively, the program might have a bug where it leaks the actual stored
password
────────────────────────────────────────────────────────────────────────────────
--[ Vulnerability / Observation ]--
The description implies the program:
1. Takes a password as input
2. Performs some transformation/encoding on it
3. Stores/displays the "database" representation
4. Compares against a hardcoded expected value
Several common patterns for this type of challenge:
1. Direct string comparison: The expected password or flag is hardcoded in the
binary and compared against input using strcmp() or a byte-by-byte comparison.
The flag can be extracted with strings or by examining the binary.
2. Character-by-character transformation: The program applies a transformation
(XOR, shift, substitution) to each input character and compares against a stored
array. By reversing the transformation on the stored values, we recover the
password/flag.
3. Format string / buffer display vulnerability: The program "shows you the
password saved in the database" -- it may actually be displaying memory contents
that include the real password or flag if we provide the right input.
4. Obfuscated comparison: The password check is split across multiple functions
or uses anti-debugging tricks, but the core comparison values are still in the
binary.
────────────────────────────────────────────────────────────────────────────────
--[ Exploitation / Solution ]--
1. Initial reconnaissance:
-- bash --
file binary # Check architecture and type
strings binary | grep -i pico # Quick flag search
strings binary | grep -i flag # Look for flag references
checksec binary # Check security mitigations
2. Dynamic analysis with ltrace:
-- bash --
ltrace ./binary # Trace library calls -- look for strcmp/memcmp
If the program uses strcmp(user_input, "picoCTF{...}"), ltrace will show both
arguments.
3. Static analysis: Open in Ghidra and look for:
- The main() function and its control flow
- String references to "password", "database", "correct", "wrong"
- Comparison functions and their arguments
- Hardcoded byte arrays that might be the encoded flag
4. Reverse the transformation: If the binary XORs, shifts, or otherwise encodes
the password before comparison:
- Identify the transformation function
- Extract the target (encoded) values from the binary
- Apply the inverse transformation to recover the plaintext
5. Input-output analysis: Since the program shows the "database" version:
- Input known strings (e.g., "AAAA", "BBBB") and observe the output
- Determine the encoding: is it XOR with a key? Caesar shift? Custom
substitution?
- Once understood, reverse-engineer the expected output back to the flag
────────────────────────────────────────────────────────────────────────────────
--[ Full Exploit Script ]--
-- python --
#!/usr/bin/env python3
"""
Secure Password Database - picoCTF 2026 (Reverse Engineering, 200 pts)
The binary takes a password, transforms it, shows the "database" representation,
and checks it against a stored value. We reverse-engineer the transformation to
extract the flag.
Multiple approaches:
1. Run 'strings' to check for plaintext flag
2. Use ltrace to capture strcmp/memcmp arguments
3. Extract hardcoded comparison values and reverse the transformation
4. Probe the "database display" feature to deduce the encoding
Usage:
python3 solve.py [binary_path]
Dependencies: None required (uses subprocess). Optional: pip install pwntools
"""
import subprocess
import sys
import os
import re
import struct
import string
def find_flag_in_strings(binary_path):
"""Approach 1: Search for the flag directly in binary strings."""
print("[*] Approach 1: Searching binary strings...")
try:
result = subprocess.run(
['strings', binary_path],
capture_output=True, text=True, timeout=10
)
lines = result.stdout.strip().split('\n')
# Search for direct flag
for line in lines:
if 'picoCTF{' in line:
match = re.search(r'picoCTF\{[^}]+\}', line)
if match:
print(f" [+] Flag found in strings: {match.group(0)}")
return match.group(0)
# Collect interesting strings for later analysis
interesting = []
keywords = ['password', 'flag', 'secret', 'correct', 'wrong',
'database', 'auth', 'key', 'enter', 'input']
for line in lines:
if any(kw in line.lower() for kw in keywords):
interesting.append(line)
if interesting:
print(" [*] Interesting strings found:")
for s in interesting[:20]:
print(f" {s}")
# Look for hex-encoded or base64-encoded flag
hex_pattern = re.compile(r'[0-9a-fA-F]{40,}')
for line in lines:
for match in hex_pattern.finditer(line):
try:
decoded = bytes.fromhex(match.group(0)).decode('utf-8', errors='ignore')
if 'picoCTF' in decoded:
flag_match = re.search(r'picoCTF\{[^}]+\}', decoded)
if flag_match:
print(f" [+] Hex-encoded flag: {flag_match.group(0)}")
return flag_match.group(0)
except Exception:
continue
import base64
b64_pattern = re.compile(r'[A-Za-z0-9+/]{20,}={0,2}')
for line in lines:
for match in b64_pattern.finditer(line):
try:
decoded = base64.b64decode(match.group(0)).decode('utf-8', errors='ignore')
if 'picoCTF' in decoded:
flag_match = re.search(r'picoCTF\{[^}]+\}', decoded)
if flag_match:
print(f" [+] Base64-encoded flag: {flag_match.group(0)}")
return flag_match.group(0)
except Exception:
continue
except Exception as e:
print(f" [!] strings search failed: {e}")
return None
def try_ltrace(binary_path):
"""Approach 2: Use ltrace to capture comparison function arguments."""
print("\n[*] Approach 2: Running ltrace to capture comparisons...")
test_inputs = ["test", "AAAA", "picoCTF{test}", "password"]
for test_input in test_inputs:
try:
result = subprocess.run(
['ltrace', '-s', '256', binary_path],
input=test_input + '\n',
capture_output=True, text=True, timeout=10
)
output = result.stderr # ltrace outputs to stderr
if not output:
continue
print(f" [*] Input: '{test_input}'")
# Look for strcmp/memcmp/strncmp calls
for line in output.split('\n'):
if any(fn in line for fn in ['strcmp', 'memcmp', 'strncmp', 'strstr']):
print(f" {line.strip()}")
# Extract the comparison string
match = re.search(r'picoCTF\{[^}]+\}', line)
if match:
print(f" [+] Flag captured from ltrace: {match.group(0)}")
return match.group(0)
# Extract quoted strings from the comparison
str_matches = re.findall(r'"([^"]*)"', line)
for s in str_matches:
if s != test_input and len(s) > 5:
print(f" [*] Comparison value: {s}")
if 'picoCTF' in s:
return s
except FileNotFoundError:
print(" [!] ltrace not found, skipping...")
return None
except subprocess.TimeoutExpired:
print(f" [!] Timeout with input '{test_input}'")
continue
except Exception as e:
print(f" [!] ltrace error: {e}")
continue
return None
def analyze_transformation(binary_path):
"""
Approach 3: Probe the 'database display' to understand the transformation.
Send known inputs and observe the output to reverse the encoding.
"""
print("\n[*] Approach 3: Analyzing input/output transformation...")
# Send test inputs and collect the "database" representations
test_chars = string.ascii_uppercase[:26]
io_map = {}
for char in test_chars:
test_input = char * 8 # Send "AAAAAAAA", "BBBBBBBB", etc.
try:
result = subprocess.run(
[binary_path],
input=test_input + '\n',
capture_output=True, text=True, timeout=5
)
output = result.stdout
if output:
# The "database" output might contain the transformed version
# Try to find the transformed representation
io_map[char] = output
except Exception:
continue
if not io_map:
print(" [!] Could not capture output from binary")
return None
# Analyze the outputs to detect the transformation pattern
print(f" [*] Collected {len(io_map)} input/output pairs")
# Check if it's a simple XOR
print(" [*] Checking for XOR transformation...")
for char, output in list(io_map.items())[:3]:
print(f" Input '{char}' -> Output: {output.strip()[:80]}")
# Try to detect XOR key by sending known values
# If output[i] = input[i] ^ key[i], then key[i] = input[i] ^ output[i]
single_char_tests = {}
for i in range(256):
char = chr(i) if 32 <= i < 127 else None
if char:
single_char_tests[char] = i
# Extract numeric/hex values from output for XOR analysis
for char, output in io_map.items():
hex_values = re.findall(r'(?:0x)?([0-9a-fA-F]{2})', output)
if hex_values:
input_val = ord(char)
for j, hv in enumerate(hex_values):
output_val = int(hv, 16)
xor_key = input_val ^ output_val
print(f" Byte {j}: input=0x{input_val:02x} output=0x{output_val:02x} "
f"XOR key=0x{xor_key:02x} ('{chr(xor_key) if 32 <= xor_key < 127 else '?'}')")
return None
def extract_hardcoded_values(binary_path):
"""
Approach 4: Use objdump to find hardcoded comparison arrays in the binary.
"""
print("\n[*] Approach 4: Extracting hardcoded values from binary...")
try:
# Disassemble and look for immediate values in comparison instructions
result = subprocess.run(
['objdump', '-d', binary_path],
capture_output=True, text=True, timeout=30
)
if not result.stdout:
print(" [!] objdump produced no output")
return None
# Look for cmp instructions with immediate values (character comparisons)
cmp_values = []
lines = result.stdout.split('\n')
for i, line in enumerate(lines):
# x86: cmp $0xXX, ... patterns (byte comparisons)
match = re.search(r'cmp\s+\$0x([0-9a-fA-F]+)', line)
if match:
val = int(match.group(1), 16)
if 0x20 <= val <= 0x7e: # Printable ASCII
cmp_values.append(val)
# x86: mov immediate values that look like ASCII
match = re.search(r'mov\s+\$0x([0-9a-fA-F]+)', line)
if match:
val = int(match.group(1), 16)
# Check if it could be packed ASCII characters
if 0x20 <= val <= 0x7e:
cmp_values.append(val)
elif val > 0xFF:
# Might be multiple characters packed
chars = []
temp = val
while temp > 0:
byte = temp & 0xFF
if 0x20 <= byte <= 0x7e:
chars.append(chr(byte))
temp >>= 8
if chars:
packed_str = ''.join(chars)
if 'pico' in packed_str.lower():
print(f" [!] Packed string found: {packed_str}")
if cmp_values:
# Try to form a string from comparison values
potential_str = ''.join(chr(v) for v in cmp_values if 0x20 <= v <= 0x7e)
print(f" [*] Characters from cmp instructions: {potential_str[:80]}")
match = re.search(r'picoCTF\{[^}]+\}', potential_str)
if match:
return match.group(0)
# Also check .rodata section for data arrays
result2 = subprocess.run(
['objdump', '-s', '-j', '.rodata', binary_path],
capture_output=True, text=True, timeout=10
)
if result2.stdout:
# Parse the hex dump
hex_data = ''
for line in result2.stdout.split('\n'):
match = re.match(r'\s*[0-9a-fA-F]+\s+((?:[0-9a-fA-F]+\s*)+)', line)
if match:
hex_data += match.group(1).replace(' ', '')
if hex_data:
try:
raw_bytes = bytes.fromhex(hex_data)
decoded = raw_bytes.decode('utf-8', errors='ignore')
flag_match = re.search(r'picoCTF\{[^}]+\}', decoded)
if flag_match:
print(f" [+] Flag in .rodata: {flag_match.group(0)}")
return flag_match.group(0)
except Exception:
pass
except Exception as e:
print(f" [!] objdump analysis failed: {e}")
return None
def try_common_xor_decode(binary_path):
"""
Approach 5: Read the binary and try common XOR decoding patterns.
"""
print("\n[*] Approach 5: Trying XOR decode on binary data...")
with open(binary_path, 'rb') as f:
data = f.read()
# Try single-byte XOR keys
for key in range(1, 256):
decoded = bytes(b ^ key for b in data)
if b'picoCTF{' in decoded:
match = re.search(rb'picoCTF\{[^}]+\}', decoded)
if match:
flag = match.group(0).decode()
print(f" [+] XOR key 0x{key:02x}: {flag}")
return flag
# Search for the flag pattern in raw binary
match = re.search(rb'picoCTF\{[^}]+\}', data)
if match:
flag = match.group(0).decode()
print(f" [+] Flag found in raw binary: {flag}")
return flag
# Look for reversed flag
reversed_data = data[::-1]
match = re.search(rb'picoCTF\{[^}]+\}', reversed_data)
if match:
flag = match.group(0).decode()
print(f" [+] Flag found reversed in binary: {flag}")
return flag
print(" [*] No simple XOR decode found")
return None
def try_gdb_debug(binary_path):
"""
Approach 6: Use GDB to set a breakpoint at comparison and read values.
"""
print("\n[*] Approach 6: Automated GDB debugging...")
gdb_commands = """
set pagination off
set confirm off
catch syscall write
run <<< "picoCTF{test_input}"
bt
info registers
x/s $rdi
x/s $rsi
continue
quit
"""
try:
result = subprocess.run(
['gdb', '-batch', '-ex', 'set pagination off',
'-ex', 'break strcmp', '-ex', 'break memcmp',
'-ex', 'break strncmp',
'-ex', 'run <<< "AAAA"',
'-ex', 'x/s $rdi', '-ex', 'x/s $rsi',
'-ex', 'continue',
'-ex', 'quit',
binary_path],
capture_output=True, text=True, timeout=10
)
output = result.stdout + result.stderr
if 'picoCTF' in output:
match = re.search(r'picoCTF\{[^}]+\}', output)
if match:
print(f" [+] Flag from GDB: {match.group(0)}")
return match.group(0)
# Print any interesting output
for line in output.split('\n'):
if any(x in line for x in ['0x', 'strcmp', 'memcmp', 'pico', 'flag']):
print(f" {line.strip()}")
except FileNotFoundError:
print(" [!] GDB not found, skipping...")
except Exception as e:
print(f" [!] GDB error: {e}")
return None
def main():
if len(sys.argv) < 2:
# If no binary specified, look for common names in current directory
common_names = [
'password_db', 'secure_password', 'password_database',
'secure_db', 'auth', 'challenge', 'binary', 'a.out',
'secure_password_database'
]
binary_path = None
for name in common_names:
if os.path.isfile(name):
binary_path = name
break
if binary_path is None:
print("Secure Password Database - picoCTF 2026 (Reverse Engineering, 200 pts)")
print()
print("Usage: python3 solve.py <binary>")
print()
print("This script attempts to extract the flag from the password")
print("authentication binary using multiple approaches:")
print(" 1. String extraction (strings)")
print(" 2. Library call tracing (ltrace)")
print(" 3. Input/output transformation analysis")
print(" 4. Disassembly analysis (objdump)")
print(" 5. XOR brute-force decoding")
print(" 6. Automated GDB debugging")
sys.exit(1)
else:
binary_path = sys.argv[1]
if not os.path.isfile(binary_path):
print(f"[!] File not found: {binary_path}")
sys.exit(1)
# Make sure binary is executable
os.chmod(binary_path, 0o755)
# Get binary info
try:
file_result = subprocess.run(
['file', binary_path], capture_output=True, text=True
)
print(f"[*] Binary: {file_result.stdout.strip()}")
except Exception:
pass
print(f"[*] File size: {os.path.getsize(binary_path)} bytes")
print()
flag = None
# Approach 1: String search
flag = find_flag_in_strings(binary_path)
# Approach 2: ltrace
if flag is None:
flag = try_ltrace(binary_path)
# Approach 3: I/O transformation analysis
if flag is None:
flag = analyze_transformation(binary_path)
# Approach 4: Disassembly analysis
if flag is None:
flag = extract_hardcoded_values(binary_path)
# Approach 5: XOR brute-force
if flag is None:
flag = try_common_xor_decode(binary_path)
# Approach 6: GDB debugging
if flag is None:
flag = try_gdb_debug(binary_path)
# Final output
print()
if flag:
print(f"{'='*50}")
print(f"FLAG: {flag}")
print(f"{'='*50}")
else:
print("[!] Flag not found automatically.")
print()
print("[*] Manual investigation steps:")
print(" 1. Open the binary in Ghidra: ghidra -> New Project -> Import binary")
print(" 2. Find main() and trace the password check logic")
print(" 3. Look for hardcoded arrays/strings used in comparisons")
print(" 4. The 'database display' feature may reveal the encoding:")
print(" - Run the binary with various inputs")
print(" - Note how the 'database' representation changes")
print(" - Reverse the transformation on the expected value")
print(" 5. Try: ltrace -s 256 ./binary")
print(" 6. Try: gdb ./binary, then 'break strcmp' and examine args")
if __name__ == '__main__':
main()
────────────────────────────────────────────────────────────────────────────────
--[ Key Takeaways ]--
- For password-check RE challenges, escalate through cheap-to-expensive
techniques: strings → ltrace (captures strcmp/memcmp arguments live) → static
analysis in Ghidra → dynamic GDB breakpoints.
- A program that "shows you the database representation" is leaking its
transformation — feeding known inputs (AAAA, BBBB) and diffing the output
reveals XOR keys, shifts, or substitutions you can invert.
- Hardcoded comparison values and encoded flags often sit in .rodata; objdump -s
-j .rodata and cmp $0x.. immediates expose them.
- ltrace -s 256 is the single fastest win when the binary compares input against
a hardcoded picoCTF{...} string.