When you only have one jump to an arbitrary address, what can you do?
When I was learning pwning a few months ago doing some ROPs, it troubled me a lot until my instructor introduced me about one_gadget which entirely changed my life. As I become more and more experienced with CTF challenges, I often encounter seccomp challenges, where you cannot simply overwrite some hooks to one_gadget to getshell. Usually, only “open, read, write” syscalls are allowed.
The very first technique I learned is to use setcontext. The attack is based on a syscall called “sigreturn“. In short, when there’s a signal raised by a process, the cpu will pause the process and enter kernel ring to handle something. When the kernel jobs are done, the process will now enter user mode. But the CPU only have one rax so how can it work under kernel mode without affecting user mode registers? Usually the registers will be pushed onto the stack and “poped” back when re-entering user mode. The poping syscall is sigreturn. That is, if you can somehow control the memory that those registers were stored, you can fully control all registers.
In glibc, there’s a function called “setcontext”. The following code is from glibc 2.27:
0000000000052070 <setcontext@@GLIBC_2.2.5>: 52070: 57 push rdi 52071: 48 8d b7 28 01 00 00 lea rsi,[rdi+0x128] 52078: 31 d2 xor edx,edx 5207a: bf 02 00 00 00 mov edi,0x2 5207f: 41 ba 08 00 00 00 mov r10d,0x8 52085: b8 0e 00 00 00 mov eax,0xe 5208a: 0f 05 syscall 5208c: 5f pop rdi 5208d: 48 3d 01 f0 ff ff cmp rax,0xfffffffffffff001 52093: 73 5b jae 520f0 <setcontext@@GLIBC_2.2.5+0x80> 52095: 48 8b 8f e0 00 00 00 mov rcx,QWORD PTR [rdi+0xe0] 5209c: d9 21 fldenv [rcx] 5209e: 0f ae 97 c0 01 00 00 ldmxcsr DWORD PTR [rdi+0x1c0] 520a5: 48 8b a7 a0 00 00 00 mov rsp,QWORD PTR [rdi+0xa0] 520ac: 48 8b 9f 80 00 00 00 mov rbx,QWORD PTR [rdi+0x80] 520b3: 48 8b 6f 78 mov rbp,QWORD PTR [rdi+0x78] 520b7: 4c 8b 67 48 mov r12,QWORD PTR [rdi+0x48] 520bb: 4c 8b 6f 50 mov r13,QWORD PTR [rdi+0x50] 520bf: 4c 8b 77 58 mov r14,QWORD PTR [rdi+0x58] 520c3: 4c 8b 7f 60 mov r15,QWORD PTR [rdi+0x60] 520c7: 48 8b 8f a8 00 00 00 mov rcx,QWORD PTR [rdi+0xa8] 520ce: 51 push rcx 520cf: 48 8b 77 70 mov rsi,QWORD PTR [rdi+0x70] 520d3: 48 8b 97 88 00 00 00 mov rdx,QWORD PTR [rdi+0x88] 520da: 48 8b 8f 98 00 00 00 mov rcx,QWORD PTR [rdi+0x98] 520e1: 4c 8b 47 28 mov r8,QWORD PTR [rdi+0x28] 520e5: 4c 8b 4f 30 mov r9,QWORD PTR [rdi+0x30] 520e9: 48 8b 7f 68 mov rdi,QWORD PTR [rdi+0x68] 520ed: 31 c0 xor eax,eax 520ef: c3 ret ...
The syscall will store all registers into [rdi + n]. Notice from 0x520a5, all registers are restored by
mov reg, [rdi + n]. According to calling convention, rdi is the first parameter of functions. Say you have written
free_hook to this
setcontext+53, then you can control
[rdi + n] therefore all registers.
With some other manipulations, you can execute shellcode to “open read write” flag. I guess this is a good example challenge.
However, this technique is no longer easy after glibc 2.27. Since glibc 2.28, the assembly for setcontext uses
[rdx + n].
00000000000474d0 <setcontext@@GLIBC_2.2.5>: 474d0: 57 push rdi 474d1: 48 8d b7 28 01 00 00 lea rsi,[rdi+0x128] 474d8: 31 d2 xor edx,edx 474da: bf 02 00 00 00 mov edi,0x2 474df: 41 ba 08 00 00 00 mov r10d,0x8 474e5: b8 0e 00 00 00 mov eax,0xe 474ea: 0f 05 syscall 474ec: 5a pop rdx 474ed: 48 3d 01 f0 ff ff cmp rax,0xfffffffffffff001 474f3: 73 5b jae 47550 <setcontext@@GLIBC_2.2.5+0x80> 474f5: 48 8b 8a e0 00 00 00 mov rcx,QWORD PTR [rdx+0xe0] 474fc: d9 21 fldenv [rcx] 474fe: 0f ae 92 c0 01 00 00 ldmxcsr DWORD PTR [rdx+0x1c0] 47505: 48 8b a2 a0 00 00 00 mov rsp,QWORD PTR [rdx+0xa0] 4750c: 48 8b 9a 80 00 00 00 mov rbx,QWORD PTR [rdx+0x80] ...
Thus, you have to choose jump here only when you can control
[rdx + n]. Though hard, you can still find a way to use this gadget. For example, in glibc 2.31, you can combine FSOP and this gadget. In function _IO_str_overflow, you can
[rdx + n] is controllable and will be passed into free(). Here’s a practice challenge to do that.
Another solution to the above practice challenge did not use setcontext. A detailed Chinese writeup and exploit script is here. In his exploit, he used a gadget like this:
mov rbp, [rdi + 0x48] mov rax, [rbp + 0x18] lea r13, [rbp + 0x10] mov DWORD PTR [rbp + 0x10], 0 mov rdi, r13 call [rax + 0x28]
call [[[rdi + 0x48] + 0x18] + 0x28] and
mov rbp, [rdi + 0x18]. If the call jumps to
leave; ret, you have the stack control.
The inspiration for this article is from GACTF 2020 gif2ascii. In this challenge, you can only call an arbitrary address, while you control both the heap and bss and have only libc and elf addresses. Although the libc version is 2.27 where you can use setcontext easily, in this case you don’t have a chunk big enough to control rsp. So I used this gadget:
15b236: 48 8b 6f 48 mov rbp,QWORD PTR [rdi+0x48] 15b23a: 48 8b 45 18 mov rax,QWORD PTR [rbp+0x18] 15b23e: 4c 8d 65 10 lea r12,[rbp+0x10] 15b242: c7 45 10 00 00 00 00 mov DWORD PTR [rbp+0x10],0x0 15b249: 4c 89 e7 mov rdi,r12 15b24c: ff 50 28 call QWORD PTR [rax+0x28]
After the CTF, I was wondering if there’s a better way to find all gadgets like this, as I was manually searching for
mov rbp,QWORD PTR [rdi+ and verify one by one to see if there’s a
So I first use
objdump to dump all functions into a file.
objdump -M intel-mnemonic -d libc-2.27.so > code
grep "mov rbp,QWORD PTR \[rdi" code to list all lines containing the pattern
mov rbp, QWORD PTR [rdi.
Instead of manually check if there’s a call to some area we can control, I write a simple and ugly script to iterate through all results.
import angr import code import logging addrs = [0x356b0, 0x43239, 0x520b3, 0x83677, 0x844f8, 0x850be, 0x8613b, 0x862af, 0x8a7d7, 0x8ae97, 0x8b261, 0x8b898, 0x8b8b0, 0x8b8c0, 0x8ddc3, 0x8dfda, 0x8e1aa, 0x8e3d7, 0x8f050, 0x8fc64, 0x9ccfb, 0xa0c1d, 0xa1444, 0xa188e, 0xa80c6, 0xfab80, 0xfbab0, 0x1011b6, 0x104920, 0x113358, 0x114371, 0x11d227, 0x11dd95, 0x129571, 0x12ab55, 0x12b21a, 0x12bf19, 0x153b19, 0x154e89, 0x1550d8, 0x15a739, 0x15aea9, 0x15b236, 0x199d04] def offset(n): return n + 0x400000 logging.getLogger('angr').setLevel('CRITICAL') logging.getLogger('cle').setLevel('CRITICAL') for x in range(len(addrs)): curr_addr = offset(addrs[x]) print("------") print(hex(addrs[x]), ) project = angr.Project("./libc-2.27.so") st = project.factory.full_init_state( args=["./libc-2.27.so"], addr=curr_addr) st.regs.rdi = 0x4141414141414141 st.regs.rip = curr_addr for x in range(0x20): st.memory.store(0x4141414141414141 + 8 * x, 0x4141414141414141) sm = project.factory.simgr(st) for x in range(5): sm.step() try: # print(sm.active.regs.rip) if (sm.active.regs.rip == 0x4141414141414141).is_true(): print(sm.active.regs.rip) break # code.interact(local=locals()) except Exception: # code.interact(local=locals()) pass
I am still a noob about symbolic execution, so I used a stupid way to see if rip is hijacked. I filled 0x4141414141414141 from 0x4141414141414141 to 0x4141414141414141 + 8 * 0x20. Under most cases, that should result in a
call 0x4141414141414141. It worked well under glibc 2.27 and found 4 gadgets.
I also tested the latest version 2.32 and found only one out of 49(maybe mitigation?). Apparently this code can be improved. For example, it can show some other manipulations to the memory (
mov DWORD PTR [rbp+0x10],0x0). But I guess this needs all registers to first have symbolic values, which is beyond my knowledge about angr.
The only gadget I found in glibc 2.32 is this:
8745b: 48 8b 6f 18 mov rbp,QWORD PTR [rdi+0x18] 8745f: 48 89 fb mov rbx,rdi 87462: 48 2b 6f 10 sub rbp,QWORD PTR [rdi+0x10] 87466: 4c 8b 6f 08 mov r13,QWORD PTR [rdi+0x8] 8746a: 48 89 ea mov rdx,rbp 8746d: 48 01 e8 add rax,rbp 87470: 49 89 ec mov r12,rbp 87473: 48 c1 fa 03 sar rdx,0x3 87477: 48 01 d0 add rax,rdx 8747a: 48 63 57 30 movsxd rdx,DWORD PTR [rdi+0x30] 8747e: 48 8d 6c 10 64 lea rbp,[rax+rdx*1+0x64] 87483: 48 8b 47 38 mov rax,QWORD PTR [rdi+0x38] 87487: 48 39 2f cmp QWORD PTR [rdi],rbp 8748a: 48 0f 4d 2f cmovge rbp,QWORD PTR [rdi] 8748e: f6 47 50 01 test BYTE PTR [rdi+0x50],0x1 87492: 0f 84 d8 00 00 00 je 87570 <_obstack_newchunk+0x120> 87498: 48 8b 7f 48 mov rdi,QWORD PTR [rdi+0x48] 8749c: 48 89 ee mov rsi,rbp 8749f: ff d0 call rax
The staring address 0x8745b is generated by grep. But looking closely, you can find the hijack process can be shorten under some circumstances.
And of course, there must be some other gadgets. I cannot yet think of any methods to break this limitation and find more gadgets. I hope this article can inspire people who have more experience in symbolic execution than me to make a tool to find these gadgets.