Bypass sandboxing implementations such as seccomp in CTF pwn challenges with ROP
Environment Setup
OS: Ubuntu 20.04 VM
Tools: pwndbg, pwntools, python3
Target: malloc-testbed(https://github.com/limitedeternity/HeapLAB/tree/main/malloc_testbed)
Introduction
Recent CTF pwn challenges are more and more onerous, probably due to 内卷 among players.
It’s common to find sandboxing implementations such as seccomp in place, that only allows certain syscalls to take place.
Even after hijacking the flow of execution, we won’t be able to simply toss in a one_gadget or system("/bin/sh") to get an interactive shell.
Usually only open read and write syscalls are available to use, and those are enough for us to read the flag.
I’ll document one way to achieve ORW by using the ROP chain near setcontext, and briefly talk about two other ways to bypass seccomp.
In this post, malloc-testbed is configured to use glibc 2.31 with a UAF bug, and libc already leaked.
This technique requires the following conditions to be met:
- libc base address and address of a writeable region leaked(for example: heap)
- control over a function pointer and it’s first argument(for example: __free_hook)
- ability to request enough space to house the rop chain(can be one big chunk or many smaller chunks)
Gadget Hunting
ROP obviously requires ROP gadgets.
I normally hunt for the gadgets with a tool called ROPgadget, and objdumpobjdump gives more control over the gadgets shown in my opinion, since I haven’t read ROPgadget‘s source code yet and have no idea what it’s doing.
However, objdump does not perform misaligned parses on instructions, and thus is likely to miss out certain gadgets.
setcontext gadget:
1 | awae@ubuntu:~/Desktop/HeapLAB_Part2/malloc_testbed$ objdump -Mintel .links/libc.so.6 -d | grep -A30 "setcontext" |
objdump shows the address of the gadget we are interested in, the long series of mov instructions to set many registers based on an address held in the rdx register.
In earlier versions of libc, the setcontext gadget uses rdi to set registers, which is directly controlled by us assuming we overwrote the __free_hook, since the first argument on x86_64 Linux is always stored in the rdi register.
However in more recent versions, the rdx register is used.
In order to control the other registers, we have to find a way to control rdx first.
I’ve seen this gadget being used in other writeups:
1 | mov rdx, [rdi+8] |
but I was unable to find it with objdump or ROPgadget
Instead, I’ll use the following two gadgets found in __rpc_thread_key_cleanup to gain control of the rdx register.
I found them by the command objdump -Mintel .links/libc.so.6 -d | grep -A10 -B10 "call QWORD PTR \[rd".
The logic is, since we have to dereference rdx and control the data there, rdx should point to the heap.
Since we are passing rdi as an argument to free, it should hold an address of the heap too.
The only way we can retain execution flow after the gadget is through either a call or jmp gadget with rdx or rdi dereferenced as an argument.
The two gadgets are:
1 | 122192: 48 8b 47 38 mov rax,QWORD PTR [rdi+0x38] |
and
1 | 12219c: 48 8b 50 08 mov rdx,QWORD PTR [rax+0x8] |
By controlling rdi, we can control the first call instruction to execute the second gadget which sets rdx.
After setting rdx, the second call instruction will call the setcontextgadget.
The rest of the gadgets needed(pop gadgets and maybe syscall if you wish) should be easy to find with ROPgadget.
Note: syscall ; ret gadget can’t be found with ROPgadget for some reason, so use objdump for that.
Exploitation
Since this is just a proof of concept, exploitation is easy.
Tcache poisoning to overwrite __free_hook and ROP to win.
setcontext_rop.py:
1 | #!/usr/bin/python3 |
There are of course many modifications available, such as using library functions instead of syscall, using many small chunks to house the ROP chain, not calling exit and make your exploit ugly etc, but the crux is similar.
Alternatives
Depending on the amount of control, there are many more methods to bypass seccomp.
- If you have an arbitrary write primitive, one method will be to leak stack addresses using the
environvariable, locate the return address and just write ROP chain on the stack. - If the sandbox permits, use an
mprotectcall to mark the heap as executable and pwn it the 90s way. - Social engineer the challenge creators
- Find a remote kernel 0day
Conclusion
Hooks are things of the past! Filestream exploitation and glibc 0day is king
学无止境 谦卑而行