2024 Business CTF - Vault of Hope: Rev FlagCasino

Challenge Information

AttributeDetails
Event2024 Business CTF - Vault of Hope
CategoryReverse Engineering
ChallengeRev FlagCasino
DifficultyMedium

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:

  1. Main Game Loop: Prompts for bets and validates them
  2. Check Array: Embedded array of expected values (28 values)
  3. Random Number Generation: Uses libc srand() and rand()
  4. 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:

  1. Load the binary with pwntools ELF parser
  2. Find the check symbol in the binary
  3. Extract the 28 32-bit check values from the data section
  4. 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 binary
check_addr = elf.symbols['check']
check_size = 28
# Extract check values
check_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 None

Step 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 messages
p.recvuntil(b'[*** PLEASE PLACE YOUR BETS ***]\n')
# Send the correct bets in sequence
for 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 binary
binary_path = './casino'
# Load the binary using pwntools
elf = ELF(binary_path)
# Find the address of the check array
check_addr = elf.symbols['check']
check_size = 28
# Extract the check values from the binary
check_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 generation
libc = 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 value
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.")
# Interact with the binary
p = process(binary_path)
# Read the welcome messages
p.recvuntil(b'[*** PLEASE PLACE YOUR BETS ***]\n')
# Send the correct bets
for 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
final_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