pwnfreemedium

hunting-field

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.

$ ls tags/ techniques/
array_pointer_underflowstack_layout_reconstructionlittle_endian_integer_overwritegame_state_manipulation

$ cat /etc/rate-limit

Rate limit reached (20 reads/hour per IP). Showing preview only — full content returns at the next hour roll-over.

hunting-field — TJCTF 2026

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

...

$ grep --similar

Similar writeups