gaem
Challenge
Imported from local notes.md.
Solution
Original Notes
gaem
Challenge Summary
- Given: a static x86_64 game binary
gaemreachable atchalls.squ1rrel.dev:5001. - Goal: exploit the game and read the remote
flag.txt. - Constraints: static binary, no PIE, canary present, NX enabled, line-oriented menu interface.
Initial Recon / Triage
- Observations:
- The binary is non-PIE and not stripped, so the game-specific symbols are visible.
- Pets are heap objects created by
spawn_pet, and each pet stores a callback pointer at offset0x20. - The rename path reads
0x28bytes into a stack buffer and then copies them into the pet name field with an unchecked string copy.
- File identification:
starting_files/gaemis a static x86_64 ELF.
- Entry points:
mainspawn_petpet_near- pet talk callbacks
cat_purr/dog_purr
Hypotheses & Approach
- Hypothesis 1: renaming a nearby pet overflows past the 32-byte pet name field and overwrites the talk callback pointer.
- Hypothesis 2: replacing that callback with
printfgives a format-string primitive that can rewritemain's saved return state and pivot into a staged ROP chain.
Both were correct.
spawn_petallocates0x30bytes and stores:- name at
+0x00 - callback at
+0x20 - x/y at
+0x28/+0x2c
- name at
- The rename action reaches a nearby pet, reads
0x28bytes into a stack buffer, and copies them into the pet object, so bytes 32-39 replace the callback pointer. - After overwriting the callback with
printf, talking to the pet gives a format-string write using attacker-controlled arguments embedded after aNULin the same talk buffer.
Execution Steps (Reproducible)
Stage 1
Commands:
cd /root/squ1rrel2026CTF/gaem
file starting_files/gaem
checksec --file=starting_files/gaem
nm -n starting_files/gaem | egrep 'main|spawn_pet|pet_near|cat_purr|dog_purr'
objdump -d -Mintel starting_files/gaem --disassemble=main --disassemble=spawn_pet --disassemble=read_line
Results:
- The hero starts at
(2,2)andWhiskersis reachable via move scriptssdr. - The rename path can replace the pet callback with
printf@0x4056b0by sending 32 filler bytes followed by the low 3 nonzero bytes of the function pointer. - Talking to the pet with
%7$pleaks a stable stack pointer insidemain's frame.
Stage 2
Commands:
cd /root/squ1rrel2026CTF/gaem
python3 artifacts/solve.py remote
Results:
- The solver renames
Whiskersso its callback becomesprintf. %7$pleaks the stack buffer used by the talk/rename path.- Two short
%hnwrites patchmain's saved return path:- saved RIP ->
pop rsp ; ret - next qword -> the staged talk buffer
- saved RIP ->
- The final talk message begins with
A\0soprintfis harmless, then contains a compact ROP chain starting at byte 2:pop rdi ; ret-> pointer to/bin/shpop rsi ; pop r15 ; ret->rsi = 0pop rdx ; xor eax, eax ; pop rbx ; pop r12 ; pop r13 ; pop rbp ; ret->rdx = 0pop rax ; ret->rax = 59- direct
syscall
- After sending
qalone, the process returns through the pivot intoexecve("/bin/sh", NULL, NULL). - A second write sends
cat flag.txtandexit, yielding the remote flag:squ1rrel{nptl_my_beloved_y0u_m4k3_th1s_g4m3_s0_ez}
Artifacts Produced
artifacts/solve.py- final exploit script for local/remote use.
Flag
squ1rrel{nptl_my_beloved_y0u_m4k3_th1s_g4m3_s0_ez}