这篇没有从sploitfun中翻译,而是转载了其他一篇文章http://d0m021ng.github.io/2017/03/01/PWN/Linux%E5%A0%86%E6%BC%8F%E6%B4%9E%E4%B9%8Boff-by-one/?source=post_page—–9ba957e27ee8———————-

四种chunk的种类

  1. fastbin

fastbin的范围处于16~64byte,使用单向链表来维护。每次从fastbin中分配堆块时,都会从尾部取出。fastbin块的inuse位永远是置于1的,并且享有最高的优先权,在分配和释放时总会最先考虑fastbin。

  1. unsort bin

unsort bin在bins[]中仅占有一个位置,除了fastbin外的其他块被释放后都会进入到这里来作为一个缓冲,每当进行malloc时会把堆块从unsort bin中取出并放到对于的bins[]中。

  1. small bin

small bin是指大于16byte且小于512byte的堆块,使用双向链表链接,不会有两个相邻的空的small bin块,因为一旦出现这种情况,相邻的块就会被合并成一个块。通常是在调用free函数时触发这一过程。需要注意的是在相邻空块合并时会调用unlink()宏来进行取下操作,但是调用malloc()时的取下操作却没有使用unlink宏。

  1. large bin

超出large bin范围的即为large bin,large bin相比其他块而言具有一条额外的由fd_nextsize和bk_nextsize域组成的链表结构。

0x00. 前言

不同于栈上的off-by-one漏洞,堆上的off-by-one漏洞利用更加复杂灵活。本文参考了一下别人的文章,介绍下Linux堆漏洞off-by-one利用技术,如有错误,欢迎斧正。

0x01. off-by-one原理

off-by-one漏洞即一个字节溢出,这种漏洞一般情况下很难利用,但是在堆上却有很多利用方式。了解Linux glibc堆分配器ptmalloc2的知道,我们可以通过off-by-one覆盖堆块头的size字段,从而改变该堆块的大小或inuse位,利用堆块覆盖或unlink达到想要的目的。

由于glibc堆分配器的字节对齐机制,并不是所有的off-by-one漏洞都可以利用。 例如:32位系统,按照8字节进行对齐,如果malloc(512),那么实际会分配512+42=520字节,其中8字节为prev_size和size字段大小,off-by-one只会覆盖prev_size字段最低字节造成无法利用;如果malloc(508),由于508+8=516字节,516字节不满足8字节对齐,并且考虑到下一块的prev_size字段(4字节)在前一块已分配时,可以填充前一块数据,实际上只会分配508+4=512字节,off-by-one会覆盖size字段最低字节可以利用。同理,64位系统,按照16字节进行对齐,如果malloc(512),那么实际会分配512+82=528字节;如果malloc(504),那么实际会分配504+8=512字节。

0x02. off-by-one利用技巧

如果堆上的off-by-one可以利用,那么有两类利用方式:一类是覆盖size字段inuse位,在free时触发unlink机制;另一类是覆盖size字段最低字节,从而改变堆块大小,使该堆块包含后一块,free掉该堆块之后,再malloc(稍大于该堆块加后一块的大小),就可以对后一块进行读写。

  1. 利用unlink机制

这一类利用方式分两种情况:一种是Small bin unlink,另一种是Large bin unlink。

Small bin unlink: 利用方式已经在Linux堆漏洞之Double-free中介绍过,虽然是不同的漏洞,但是主要利用原理还是类似的,就不介绍了。

Large bin unlink: 利用方式在glibc 2.20版之后已经失效,但还是有必要介绍一下其中一些思路。该攻击最早出现在2014年Google Project Zero项目的一篇文章中The poisoned NUL byte, 2014 edition。在Linux堆漏洞之Double-free中已经讲过unlink宏,其中只讲到unlink Small bin时进行的操作,只需绕过第一层双向循环链表检查就可以利用unlink。如果unlink Large bin,由于Large bin块含有字段fd_nextsize和bk_nextsize,在绕过第一层双向循环链表检查还会进行第二次双向循环链表检查。但是在glibc早期版本(2.19之前),第二次双向循环链表检查只通过断言(assert)形式,属于调试信息,不能真正的对漏洞进行有效的防护。从而可以利用Large bin unlink导致一次任意地址写,然后利用overwriting tls_dtor_list实现漏洞利用。在程序main()函数结束调用exit()函数时,会遍历tls_dtor_list调用一些处理收尾工作的函数,如果通过overwriting tls_dtor_list使其指向伪造的tls_dtor_list,就可以调用自己的函数(如system(‘/bin/sh’))。在当前版本的glibc(2.23)中,unlink宏在unlink Large bin 时会进行双向链表检查,而且在__call_dtors_list中获取tls_dtor_list时也做了一些限制,导致很难利用Large bin unlink。 Overwriting tls_dtor_list是一个很好的利用点,但是目前我还没有找到如何利用。

  1. 利用堆块覆盖

这一类攻击主要是获取对目标堆块的读写,利用方式分两种情况:一种是覆盖最低字节为任意数(off-by-one overwrite freed or allocated),另一种是覆盖最低字节为NULL(off-by-one NULL byte)。

off-by-one overwrite freed or allocated: 如图1所示,堆块A、B、C,其中堆块A已分配且含有off-by-one漏洞,堆块B已释放,堆块C为目标堆块,需要对堆块C可读写。可以通过堆块A的off-by-one漏洞覆盖堆块B size字段的最低字节(不改变inuse位),使堆块B的长度可以包含堆块C。然后在malloc(B+C),就可以获取堆块B的原来指针,从而可以对目标堆块进行读写。

如果堆块A、B、C都是已分配,可以释放掉堆块B,将问题转化为前面一种情况,同样可以解决。

      _____________________                          ______|_____B_______|______
     |      |      |       |                        |      |    |    |   |     |
     |  A   |   B  |   C   |                        |  A   | B1 | B2 |   | C   |
     |______|______|_______|                        |______|____|____|___|_____|
图1  overwrite freed or allocated                      图2  overwrite null byte

off-by-one overwrite NULL byte : 这类漏洞在实际中很常见,如使用strcpy()进行复制时未考虑字符串长度。如图2所示,堆块A、B、C,其中堆块A已分配且含有off-by-one漏洞,堆块B、C已分配,堆块B2为目标堆块,需要对堆块B2可读写。利用方法:先释放掉堆块B,然后通过堆块A的off-by-one漏洞覆盖堆块B size字段的最低字节为NULL,减小堆块B的size字段值 (如果堆块B size字段未改变,再次分配时,堆块C的prev_size字段会改变,造成漏洞无法利用) ;再申请两个较小的堆块B1和B2(B1+B2<B),这时堆块C的prev_size大小仍然是堆块B的大小,释放掉堆块B1和堆块C时就会导致堆块B和堆块C进行合并,然后再malloc(B+C)大小的堆块就可以得到原来堆块B的地址,从而可以对堆块B2进行读写。

下面分享一下off-by-one NULL byte 漏洞代码,今后遇到这类漏洞,再补充一个实例。

#include <stdio.h>
#include <string.h>
#include <malloc.h>

int main(int argc, char* argv[])
{
    void *A,*B,*C;
    void *B1,*B2;
    void *Overlapping;
    A = malloc(0x100-8);
    B = malloc(0x200);
    C = malloc(0x100);
    printf("chunk B address: %x,  C address: %x\n", B, C);

    free(B);
    ((char *)A)[0x100 - 8] = '\x00';    // off-by-one NULL byte

    B1=malloc(0x100);
    B2=malloc(0x80);
    printf("chunk B1 address: %x,  B2 address: %x\n", B1, B2);
    free(B1);
    free(C);
    Overlapping = malloc(0x300);  
    printf("new malloced chunk: %x\n", Overlapping);
    return 0;
}