Off-By-One chunk overlapping
off-by-one
漏洞是一种特殊的溢出漏洞,off-by-one
指程序向缓冲区中写入时,写入的字节数超过了这个缓冲区本身所申请的字节数并且只越界了一个字节。
该漏洞有两种主要的利用方法,其中unlink我还没看,这里只写chunk overlapping
。
前置知识
- 堆是动态分配的,只有在程序中需要时才会分配
- 堆是程序虚拟地址空间的一块连续的线性区域
- 堆的生长方向是从低地址向高地址生长的,而栈是从高地址向低地址生长的
- CPU的字节序是小端序,Off-By-One覆盖的是相邻高地址堆块的低字节
1 | //structure of chunk |
漏洞原理
该漏洞的产生往往与边界验证不严$$^{[1]}$$和字符串操作$$^{[2]}$$有关。
[1]使用循环语句向堆块中写入数据时,循环的次数设置错误导致多写入了一个字节。
(eg.:
for(i=0;i<=len;i++
比应该输入的num次多了一次,造成溢出)[2]字符串操作不合适
(eg.:
strcpy
会将最后截断的\x00
也copy进去)
一般来说,单字节溢出被认为是难以利用的,但是因为 Linux 的堆管理机制
ptmalloc
验证的松散性,基于 Linux 堆的 off-by-one 漏洞利用起来并不复杂,并且威力强大。 此外,需要说明的一点是 off-by-one 是可以基于各种缓冲区的,比如栈、bss 段等等,但是堆上(heap based) 的 off-by-one 是 CTF 中比较常见的。我们这里仅讨论堆上的 off-by-one 情况。
ptmalloc 通过 chunk header 的数据判断 chunk 的使用情况( 仅通过下一块的inuse位 [ 在size域中 ] 来判定当前块是否使用)和对 chunk 的前后块进行定位。简而言之,chunk extend 就是通过控制 size 和 pre_size 域来实现跨越块操作从而导致 overlapping 的。
一个处于使用状态的chunk如下
1 | chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
一个处于释放状态的chunk如下
1 | chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
利用分类
off-by-one中的chunk overlapping可以细分为3种利用方式
off-by-one overwrite allocated
off-by-one overwrite freed
off-by-one null byte
利用条件
off-by-one并不是全都可以达到利用的目的的。首先就要求堆必须以要求的size+0x4字节(x86)的大小进行分配。如果不满足这个条件那么就无法覆盖到inuse位了。这个是由于堆的字节对齐机制造成的,简单的说堆块是以8字节进行对齐的(x64为16字节)。如果malloc(1024),那么实际会分配1024+8=1032字节,这一点很好理解。但是如果是malloc(1020)呢,1020+8=1028字节,而1028不满足8字节对齐,那么实际只会分配1020+4=1024字节,多出的4个字节由下一块的prev_size提供空间。
利用思路
所谓的chunk overlapping是指,针对一个目标堆块。我们可以通过一些操作,使这个目标堆块被我们重新分配到某个我们控制的新的堆块中,这样就可以对目标堆块进行任意的读写了。 具体有以下两种利用思路。
- 溢出字节为可控制任意字节:通过修改大小造成块结构之间出现重叠,从而泄露其他块数据,或是覆盖其他块数据。也可使用 NULL 字节溢出的方法
- 溢出字节为 NULL 字节:在 size 为 0x100 的时候,溢出 NULL 字节可以使得
prev_in_use
位被清,这样前块会被认为是 free 块。、这时prev_size
域就会启用,就可以伪造prev_size
,从而造成块之间发生重叠。此方法的关键在于 unlink 的时候没有检查按照prev_size
找到的块的大小与prev_size
是否一致。
利用方法
off-by-one overwrite allocated
在这种情况下堆块布局是这样的
1 | +--------+--------+--------+ |
A是发生有off-by-one的堆块,其中B和C是allocated状态的块。而且C是我们的攻击目标块。
我们的目标是能够读写块C,那么就应该去构造出这样的内存布局。然后通过off-by-one去改写块B的size域(注意要保证inuse域的值为1,否则会触发unlink导致crash)以实现把C块给整个包含进来。通过把B给free掉,然后再allocated一个大于B+C的块就可以返回B的地址,并且可以读写块C了。
具体的操作是:
构成图示的内存布局
off-by-one改写B块的size域(增加大小以包含C,inuse位保持1)
free掉B块
malloc一个B+C大小的块
通过返回的地址即可对C任意读写
注意,必须要把C块整个包含进来,否则free时会触发check,导致抛出错误。因为ptmalloc实现时的验证逻辑是当前块的下一块的inuse必须为1,否则在free时会触发异常,这一点本来是为了防止块被double free而做的限制,却给我们伪造堆块造成了障碍。
off-by-one overwrite freed
在这种情况下堆块布局依然是这样的
1 | +--------+--------+--------+ |
A是发生有off-by-one的堆块,其中B是free状态的块,C是allocated块。而且C是我们的攻击目标块。
我们的目标是能够读写块C,那么就应该去构造出这样的内存布局。然后通过off-by-one去改写块B的size域(注意要保证inuse域的值为1)以实现把C块给整个包含进来。但是这种情况下的B是free状态的,通过增大B块包含C块,然后再allocated一个B+C尺寸的堆块就可以返回B的地址,并且可以读写块C了。
具体的操作是:
构成图示的内存布局
off-by-one改写B块的size域(增加大小以包含C,inuse位保持1)
malloc一个B+C大小的块
通过返回的地址即可对C任意读写
off-by-one null byte
这种情况就与上面两种有所不同了,在这种情况下溢出的这个字节是一个’\x00’字节。这种off-by-one可能是最为常见的,因为strcpy的使用极易造成这种情况。
就会产生这种null byte off-by-one,即拷贝一个字符串到一个同样长的缓冲区时,并未考虑到NULL字节。
相比于前两种,这种利用方式就显得更复杂,而且对内存布局的要求也更高了。
首先内存布局需要三个块
1 | +--------+--------+--------+ |
其中A,B,C都是allocated块,A块发生了null byte off-by-one
,覆盖了B块的inuse
位,使B块伪造为free。然后在分配两个稍小的块b1、b2,根据ptmalloc的实现,这两个较小块(不能是fastbin)会分配在B块中。然后只要释放掉b1,再释放掉C,就会引发从原B块到C的合并。那么只要重新分配原B大小的chunk,就会重新得到b2。在这个例子中,b2是我们要进行读写的目标堆块。最后的堆块布局如下所示:
1 | +--------+----+----+---+------+ |
布局堆块结构如ABC所示
off-by-one覆盖B,目的是覆盖掉B的inuse位
free B
malloc b1,malloc b2
free C
free b1
malloc B
overlapping b2
这种利用方式成功的原因有两点:
通过prev_chunk()
宏查找前块时没有对size
域进行验证
当B块的size
域被伪造后,下一块的pre_size
域无法得到更新。