Linux堆漏洞之off-by-one

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+4*2=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+8*2=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是一个很好的利用点,但是目前我还没有找到如何利用。

  2. 利用堆块覆盖
    这一类攻击主要是获取对目标堆块的读写,利用方式分两种情况:一种是覆盖最低字节为任意数(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,将问题转化为前面一种情况,同样可以解决。

    1
    2
    3
    4
    5
    6
          _____________________                          ______|_____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进行读写。

0x03. off-by-one漏洞实例

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#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;
}

0x04. 参考文献

1. 从一字节溢出到任意代码执行-Linux下堆漏洞利用
2. Off-By-One Vulnerability (Heap Based)