相信多年后想起2020的夏天还是会会心一笑 。
似乎永远花不完的夜宵券,腾云三楼铁板烧窗口,每周上新的免费雪糕承担了体重上升的借口,
亲身参与中国队伍首次DEFCON CTF夺冠,见识过顶级选手的神仙操作,多熬了几个通宵也是值得,
没有比赛的周末会和阿良到豫园站旁边的ChaletPlus喝可以续杯的Tequila Sunrise或Sea Breeze。
This is a challenge from QiangWangBei Quals this year. QiangWangBei is considered to be one of the top CTF game in China.The challenge is a http server in MIPS64, only two solved during the game (from 0ops and eee). I’ve found the vulnerability and constructed a PoC during the game, but didn’t have enough time to build a system-mode emulation environment to finish the expliot. Also, because unfamiliar with uclibc, I didn’t come up with a viable method to hijack the control flow.
After the game finished, I recalled how to build the QEMU debugging environement with buildroot. I shared this knowledge with my colleague @ruan, he wrote a detailed writeup (in chinese) about the building steps. So in this writeup I’ll focus on the challenge itself. I finished this challenge with some hints from @Himyth. Thanks @Himyth and @ruan!
Debug Environment
You can download the challenge from here. The challenge designer provides a httpd
MIPS64 binary with ld64-uClibc-1.032.so
and libuClibc-1.0.32.so
. With these binaries, it is easy to launch the program in user-mode emulation. We can copy the library files ld64-uClibc-1.0.32.so
-> ./lib/ld64-uClibc.so.0
and libuClibc-1.0.32.so
-> ./lib/libc.so.0
. Then launch the httpserver with qemu-mips64
:
1 | qemu-mips64 -L ./ httpd # without gdbserver |
In this way, there is no ASLR and the base addresses of each sections are quite different from the system-mode emulation. For example, in user-mode, libc address will be 0x4000xxxxxx
, but the correct address will be 0xfff7xxxxxx
in system-mode. Aftering leaking some addresses from the remote server, we confirmed that the remote service is running in system emulation. Such difference will make it hard to develop a stable exploit against remote service, for example leaking a libc address with puts
in user-mode will fail because of the 00
in the middle of the address, but we don’t have such trouble in system-mode. So it is better to have a system-mode debugging environment.
Reversing and Vulnerability
Since our favorite reverse engineering tool IDA pro does not support decompiling mips64 yet. We tried Ghidra and it works surprisingly well. A flaw here is Ghidra could not identify some sections’ base addresses correctly, so it could not process the data references.
The tip here is to fix the memory mapping, you could click the “memory map” button at toolbox, then find “.rodata” section, then click the “move the block to anthor address” button on top right. Now you could change the start address from 0x13cc0
to 0x3cc0
, and the data references should be good.
Then we can see the logic of this program clearly. It accepts http request from stdin and print response on stdout. There is a serve_file
which can read static files from “htdocs” and return their contents, but it sanitizes “..” token, so we could not escape the directory.
It also has a handle
function, which provides a interface like common menu style heap challenge. We can use POST
method to create or edit an item.
1 | def post(idx, len, content): |
And it provides show
and delete
functions:
1 | def show(idx): |
It turns out these logic are bug-free. The vulnerability is when dealing with incorrect Content-Length. When processing a POST request, it will get at most 0x400 bytes of request content and try to parse the Content-Length. If the Content-Length value is converted to a negative number, the error value as well as its size will be passed to error_request
.
1 | iVar3 = strcasecmp(local_38,"POST"); |
error_request
will first copy the error value into global pointer info
. But the memset(info, 0, 0x200)
suggests that the size of info
buffer is only 0x200
bytes.
1 | void error_request(void *param_1,size_t param_2) |
If we trace the info
pointer and we can see that it was allocated in init
function, it was on the heap and in the size of 0x200 bytes.
1 | int init(EVP_PKEY_CTX *ctx) |
Now we can construct a PoC like:
1 | def error_post(idx, content): |
Heap-Overflow and uclibc
The problem becomes how to exploit a heap overflow vulnerability in uclibc. In recent version of uclibc, the allocator is based on early version of ptmalloc. So some common heap exploitation techniques in glibc can be applied, and the good news is there are much fewer security hardening measures in uclibc.
The attack plans is first leaking a libc address from the chunk inside unsortedbin, with the heap-overflow and the show function, this is very easy. Then we can perform fastbin attack
and get aribitrary write primitive. Using the heap overflow to modify the fd
of freed chunk in fastbin is also easy, and we don’t need to care about selecting a fake size, because there is no size check.
The only question here is how to hijack the control flow with this arbitrary write. If you look at the source code of uclibc or open its binary with IDA Pro, there is no such thing like __free_hook
or __malloc_hook
.
We look at the disassembly of free
function, and realize that the calling instruction is perfromed with $gp
and $t9
registers. $gp
is pointing to an address from data section, when calling another libc function, it first load the address with offset to $t9
and then jalr $t9
to finish the calling operation. For example, when calling __pthread_cleanup_push_defer
on line 24, it first loads the target function address with ld $t9, -0x6890($gp)
on line 19, and then jalr $t9
to call the target function.
1 | .text:000000000005D7B8 .globl free |
We examined the $gp
value in gdb, and calculate -0x6890 $(gp)
, the address is 0xa9c70
. This address is located in .got
. And it is writable.
1 | .got:00000000000A9C70 _pthread_cleanup_push_defer_ptr:.dword _pthread_cleanup_push_defer |
We tried to overwrite this address with gdb command: set {unsigned long long} $libc+0xa9c70 = 0xdeadbeef
, and request a delete operation, control flow hijacked. So the conclusion is that the libc function calling is finished via some got-like mechanism, we can overwrite the got entry and hijack the control flow.
But hijacking _pthread_cleanup_push_defer
could not control the first argument. At last we chose to hijack the munmap
function at the end of free
, so that we can control the first argument. By modifying munmap
to system
and setting up chunk content to /bin/sh
, we can spawn a shell. Of course, we need to faking the chunksize to fullfill the condition of chunk_is_mmapped
and get into the munmmap branch, because IS_MMAPPED
is 0x2, so a fake size like 0x20002
will do the trick.
Exp
1 | from pwn import * |