off-by-one(null)
本文最后更新于:2023年12月1日 凌晨
终杀:堆叠万象!
0. 用词规范
在本篇文章里,前向合并指的是向低地址合并堆块,后向合并指的是向高地址合并堆块。
1. off-by-one(null)介绍
off-by-one或者说是off-by-null,应该划归为堆溢出攻击。关于堆溢出,实际上就是向内存块写入数据过长,导致堆块内容溢出到了下一个堆的内容。
由于堆块复用机制,后一个堆块的prev_size一般都是未启用的。也就是说首当其冲的就是size域。
有些极为简单的题目会放大水。这些题会有一个超大的堆溢出。这种只需要直接覆盖下一个已释放堆块的指针域,并且在覆盖时不要破坏size域即可。这篇文章着重介绍堆叠思想,实际上堆叠思想乃至堆攻击的唯一思想,即是UAF(控制被释放的堆块的bin链相关指针)
off-by-one的意思是指堆溢出的溢出范围仅有一个字节。这就意味着我们可以一定程度上影响size的大小和相关的标记位。off-by-null的意思是溢出的字节只能是\x00,相比任意一溢出一字节,这个情况更为苛刻了。
一般来说,off-by-one和off-by-null的攻击主要有两个效果,第一个是引发堆块缩放,第二个是修改size的PREV_INUSE标记。
2. off-by-one 或更大堆溢出引发的堆块放大攻击
堆块缩放攻击里,“放大”是非常易于理解的一个思路。原本glibc中的堆块都是依次相邻排布的。一个堆块size域扩展,就会使其覆盖到后面的堆块。
我们知道堆块的fd bk这些关键的指针都在一个堆块的头部。因此只要实现堆块放大,就很容易控制后续堆块的相关指针。这是堆块重叠的核心思想。
放大未释放堆块
将未释放堆块的size域扩大,使其包裹了下一个堆块的头部,之后将其释放。紧接着就可以通过将其申请出来(或者切片多次申请)得到对于下一个堆块头部的控制权。
free一个堆块,会涉及到对于紧密连接的下一个堆块的检查。不论是放进fastbin还是unsorted bin
1 |
|
1 |
|
以上源码取自2.35,但是似乎前几个版本都是如此。
唯一的特例是tcache,这个是没有检查脚部有没有伪造好后向相邻堆块的。
伪造的fake chunk需要满足size域为一个常规的数值(建议正好能和后面的一个正常堆块接上)。size里的PREV_INUSE位记得置为1
放大已释放堆块
这里建议放大的是unsorted bin chunk。其他bin中的chunk都有size检查,没有太多意义。
被放大的堆块会包裹后面的堆块,很容易造成重叠。
unsorted bin的取出会涉及到对于next_chunk的size位设置,因此你需要提前伪造好被放大堆块的脚部的chunk头信息(注意这里size低位置零)
下面是unsorted bin中完全契合堆块取出过程,在取出之前就涉及到了对于unsorted bin chunk的解链行为。可以看到最后有一个check_malloced_chunk的宏。经过追踪,内部有对于后面一个chunk的prevsize与该chunk的size的对比,以及后一个chunk的size的PREV_INUSE等的检查——总之,还是要伪造一个fake chunk。毕竟这并不困难。
1 |
|
至于堆unsorted bin进行切割,相关chunk不是在unsorted bin中切割,而是先进入small/large bin,再被解链取出切割。切割的过程也会涉及到关于脚部堆块的检查。所以一定记得要在脚上伪造一个堆块。
off-by-null与堆缩打法。
和放大对应的还有缩小。缩小一个堆也是一个打法。这种打法的意义在于创造空洞,即让堆与堆之间隔离,导致对被缩小堆的切割操作不会修改后面一个堆的prev_size。
off-by-null只能实现堆缩,因为它只能把低位覆盖成\x00,所以它只能缩小堆块(或者单纯覆盖PREV_INUSE位)。但off-by-null相对来说是更为常见的一种漏洞,因此学习堆缩打法非常重要。
堆缩——空洞构建
堆缩的打法的简述,即是缩小一个被释放堆块A,使得其与后一个堆块B中间产生空洞。而空洞区域正好提前伪造了一个fake chunk头,可以接住缩小后的堆块。之后切割这个被释放堆块(这要求这个缩小后的堆块能进入unsorted bin),然后将被释放块切割出来的第一个堆块给释放掉。
正常情况下,切割行为会修改后面一个堆块的prev_size域。但是next_chunk宏对于后续堆块的寻址是依据size向后寻址的。因此缩小chunk A后对其切割不会修改B的prev_size,而是会修改空洞中的fake_chunk的prev_size。
之后我们释放B,触发前向合并(这里要注意,B的size位因为A在被缩小之前就是一个被释放块,所以B的size域的P位是0,因此触发了前向合并)会向前寻找到A的地址,然后将A到B中间的所有内存和B一起合并为一个大堆块。
然后我们可以切割这个大堆块拿到中间某段内存的地址,这和上面我们切割被缩小之后的A块拿到的内存区域是有重叠的,于是我们就实现了堆叠,进而可以攻击相关指针。
(缺图待补)
检查增强
上面的过程中涉及到了一个前向合并的操作。前向合并的行为,涉及到对于旧堆的unlink解链行为。但是unlink操作的检查也越来越严格。
2.31后的off-by-null攻击
2.31之后,unlink操作之前加入了一个检查,这个检查一直到现在都没有变动
1 |
|
先比之前的防护,这里加入了一个回溯检查,即根据size尝试找回unlink的触发堆。相比之前检查待解链堆块的脚部的prev_size是否与待解链size相同,这个检查的局限性显然更大
因此,对于这类情况,想触发unlink,就必须要劫持fake_chunk的size位。