2024 Business CTF - Vault of Hope: Rev FlagCasino
Challenge Information
| Attribute | Details |
|---|---|
| Event | 2024 Business CTF - Vault of Hope |
| Category | Reverse Engineering |
| Challenge | Rev FlagCasino |
| Difficulty | Medium |
Summary
Rev FlagCasino is a binary reverse engineering challenge involving a casino-style betting game. The binary uses libc’s rand() function for number generation, and participants must reverse-engineer the binary, extract the expected check values, brute-force the seeding mechanism, and provide the correct sequence of bets to obtain the flag. The challenge demonstrates weaknesses in pseudo-random number generation and binary analysis techniques.
Analysis
Binary Structure:
The casino binary contains:
- Main Game Loop: Prompts for bets and validates them
- Check Array: Embedded array of expected values (28 values)
- Random Number Generation: Uses
libc srand()andrand() - Validation Logic: Compares user input with generated random numbers
Key Vulnerability: The rand() function from libc is predictable when the seed is known. With only 256 possible seeds (0-255), a brute-force approach is feasible.
Extraction Strategy:
- Load the binary with pwntools ELF parser
- Find the
checksymbol in the binary - Extract the 28 32-bit check values from the data section
- Brute-force seeds (0-255) to find which seed produces each expected value
Solution
Step 1: Extract Check Values from Binary
from pwn import *import ctypes
binary_path = './casino'elf = ELF(binary_path)
# Find the check array in the binarycheck_addr = elf.symbols['check']check_size = 28
# Extract check valuescheck_values = []with open(binary_path, 'rb') as f: f.seek(check_addr) for _ in range(check_size): check_values.append(u32(f.read(4)))
print("Extracted check values:")print([hex(value) for value in check_values])Step 2: Seed Brute-Forcing Function
libc = ctypes.CDLL('libc.so.6')
def find_seed_for_value(target): """Find which seed produces the target random value""" for seed in range(256): libc.srand(seed) if libc.rand() == target: return seed return NoneStep 3: Find Correct Bets
correct_bets = []for i, target in enumerate(check_values): print(f"Finding seed for target value {hex(target)}...") seed = find_seed_for_value(target) if seed is not None: correct_bets.append(seed) print(f"Correct bet for check value {i + 1}: {seed}") else: correct_bets.append(None) print(f"No valid seed found for check value {i + 1} within range.")Step 4: Interact with Binary
p = process(binary_path)
# Read the welcome/prompt messagesp.recvuntil(b'[*** PLEASE PLACE YOUR BETS ***]\n')
# Send the correct bets in sequencefor seed in correct_bets: if seed is not None: p.sendline(str(seed).encode()) response = p.recvline() print(response.decode().strip()) if b'CORRECT' not in response: print(f"Error: Expected CORRECT but got {response}") break else: print("Skipping invalid seed")
# Capture final output (flag)final_output = p.recvall().decode('utf-8')print("\nFinal Output:")print(final_output)
p.close()Complete Solver Script:
from pwn import *import ctypes
# Path to the binarybinary_path = './casino'
# Load the binary using pwntoolself = ELF(binary_path)
# Find the address of the check arraycheck_addr = elf.symbols['check']check_size = 28
# Extract the check values from the binarycheck_values = []with open(binary_path, 'rb') as f: f.seek(check_addr) for _ in range(check_size): check_values.append(u32(f.read(4)))
print("Extracted check values:")print([hex(value) for value in check_values])
# Load libc for random number generationlibc = ctypes.CDLL('libc.so.6')
def find_seed_for_value(target): for seed in range(256): libc.srand(seed) if libc.rand() == target: return seed return None
# Find correct bets for each check valuecorrect_bets = []for i, target in enumerate(check_values): print(f"Finding seed for target value {hex(target)}...") seed = find_seed_for_value(target) if seed is not None: correct_bets.append(seed) print(f"Correct bet for check value {i + 1}: {seed}") else: correct_bets.append(None) print(f"No valid seed found for check value {i + 1} within range.")
# Interact with the binaryp = process(binary_path)
# Read the welcome messagesp.recvuntil(b'[*** PLEASE PLACE YOUR BETS ***]\n')
# Send the correct betsfor seed in correct_bets: if seed is not None: p.sendline(str(seed).encode()) response = p.recvline() print(response.decode().strip()) if b'CORRECT' not in response: print(f"Error: Expected CORRECT but got {response}") break else: print("Skipping invalid seed")
# Capture final outputfinal_output = p.recvall().decode('utf-8')print("\nFinal Output:")print(final_output)
p.close()Key Techniques
Binary Analysis:
- ELF symbol extraction with pwntools
- Memory offset calculation
- Static data extraction from binary
Random Number Analysis:
- libc
rand()function behavior - Seed brute-forcing with small keyspace
- Predictable PRNG exploitation
Process Interaction:
- pwntools
process()for binary execution - Sequential I/O handling
- Output parsing and validation
Key Takeaways
- The libc
rand()function is cryptographically insecure and predictable - Small seed spaces (256 possibilities) can be brute-forced in milliseconds
- Binary static data can be extracted and analyzed without execution
- ELF binaries contain symbol information useful for exploitation
- Embedded check values/arrays often contain validation logic that can be reversed
- PRNG seeding with user-controlled values enables pattern prediction
- Struct unpacking (u32) handles multi-byte integer extraction
- Interactive binary exploitation requires careful I/O synchronization
- Deterministic behavior in games/challenges can be exploited through analysis
- Documentation of embedded values makes reverse engineering significantly easier