HackPluto's Blog

Linux堆溢出漏洞--off-by-one

字数统计: 3.1k阅读时长: 12 min
2019/11/06 Share

how2heap

how2heap这是由 shellphish 团队创建的一个仓库,是用来学习堆利用技术广为周知的地方。 且主要针对 glibc。我在这里先放几个网上找的学习笔记,日后我再做以进一步总结。
https://zoepla.github.io/2018/05/how2heap%E7%B3%BB%E5%88%97(%E5%9F%BA%E7%A1%80%E7%AF%87)/
https://xz.aliyun.com/t/2582
http://1nv0k3r.me/2019/02/26/how2heap-%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95/
https://www.anquanke.com/post/id/86808

off-by-one

简介

off-by-one 是指单字节缓冲区溢出,这种漏洞的产生往往与边界验证不严和字符串操作有关,当然也不排除写入的 size 正好就只多了一个字节的情况。其中边界验证不严通常包括使用循环语句向堆块中写入数据时,循环的次数设置错误导致多写入了一个字节。
字符串操作不合适:
一般来说,单字节溢出被认为是难以利用的,但是因为 Linux 的堆管理机制 ptmalloc 验证的松散性,基于 Linux 堆的 off-by-one 漏洞利用起来并不复杂,并且威力强大。 此外,需要说明的一点是 off-by-one 是可以基于各种缓冲区的,比如栈、bss 段等等,但是堆上(heap based) 的 off-by-one 是 CTF 中比较常见的。我们这里仅讨论堆上的 off-by-one 情况。

Linux free函数原理:

由堆块头部形成的隐式链表可知,一个需释放堆块相邻的堆块有两个:前一个块(由当前块头指针加pre_size确定),后一个块(由当前块头指针加size确定)。从而,在合并堆块时会存在两种情况:向后合并、向前合并。当前一个块和当前块合并时,叫做向后合并。当后一个块和当前块合并时,叫做向前合并。

int_free函数中相关代码如下:

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
/* Treat space at ptr + offset as a chunk */
#define chunk_at_offset(p, s) ((mchunkptr) (((char \*) (p)) + (s)))
/* check/set/clear inuse bits in known places */
#define inuse_bit_at_offset(p, s) \
(((mchunkptr) (((char \*) (p)) + (s)))->size & PREV_INUSE)

_int_free (mstate av, mchunkptr p, int have_lock)
{
...
/* consolidate backward \*/ // "向后合并"
if (!prev_inuse(p)) { //如果前一个块为空闲,则进行合并
prevsize = p->prev_size; //获得前一个块大小
size += prevsize; //合并后堆块大小
p = chunk_at_offset(p, -((long) prevsize)); //根据当前块指针和前一个块大小,确定前一个块位置,即合并后块位置
unlink(av, p, bck, fwd); //利用unlink从显式链表Unsorted bin取下前一个块
}

nextchunk = chunk_at_offset(p, size); //根据当前块指针和当前块大小, 确定后一个块位置,
nextsize = chunksize(nextchunk); //获得后一个块大小
nextinuse = inuse_bit_at_offset(nextchunk, nextsize); //根据下一个块的下一个块的PREV_INUSE位,判断下一个块是否空闲
/* consolidate forward \*/ // "向前合并"
if (!nextinuse) { //如果后一个块为空闲,则进行合并
unlink(av, nextchunk, bck, fwd); //使用unlink将后一个块从unsorted bin中取下
size += nextsize; //扩大当前块大小即可完成向前合并
} else
clear_inuse_bit_at_offset(nextchunk, 0);
...
}

unlink 宏中主要的操作如下:
注意:此处的fd、bk指的是显式链表bins中的前一个块和后一个块,与合并块时的隐式链表中的前一个块和后一个块不同
#define unlink(AV, P, BK, FD) {
FD = P->fd; //获取显式链表中前一个块 FD
BK = P->bk; //获取显示链表中后一个块 BK
FD->bk = BK; //设置FD的后一个块
BK->fd = FD; //设置BK的前一个块
}

//由于unlink的危险性,添加了一些检测机制,完整版unlink宏如下
/* Take a chunk off a bin list */
#define unlink(AV, P, BK, FD) { \
FD = P->fd; \
BK = P->bk;
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \
malloc_printerr (check_action, "corrupted double-linked list", P, AV); \
else { \
FD->bk = BK; \
BK->fd = FD; \
if (!in_smallbin_range (P->size) \
&&__builtin_expect (P->fd_nextsize != NULL, 0)) { \
if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0) \
|| __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0)) \
malloc_printerr (check_action, \
"corrupted double-linked list (not small)", \
P, AV); \
if (FD->fd_nextsize == NULL) { \
if (P->fd_nextsize == P) \
FD->fd_nextsize = FD->bk_nextsize = FD; \
else { \
FD->fd_nextsize = P->fd_nextsize; \
FD->bk_nextsize = P->bk_nextsize; \
P->fd_nextsize->bk_nextsize = FD; \
P->bk_nextsize->fd_nextsize = FD; \
} \
} else { \
P->fd_nextsize->bk_nextsize = P->bk_nextsize; \
P->bk_nextsize->fd_nextsize = P->fd_nextsize; \
} \
} \
} \
}

off-by-one 利用思路

由于glibc堆分配器的字节对齐机制,并不是所有的off-by-one漏洞都可以利用。 例如:32位系统,按照8字节进行对齐,如果malloc(512),那么实际会分配512+8=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字节。

在Linux中定义堆的结构体为malloc_chunk

malloc_chunk

1
2
3
4
5
6
7
8
9
10
11
12
struct malloc_chunk {

INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). */
INTERNAL_SIZE_T size; /* Size in bytes, including overhead. */

struct malloc_chunk* fd; /* double links -- used only if free. */
struct malloc_chunk* bk;

/* Only used for large blocks: pointer to next larger size. */
struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
struct malloc_chunk* bk_nextsize;
};

一个使用中的 chunk(使用中,就是指还没有被 free 掉)
在内存中的样子如图所示:

在图中,chunk 指针指向一个 chunk 的开始,一个 chunk 中包含了用户请求的内存区域和相关的控制信息。图中的 mem 指针才是真正返回给用户的内存指针。chunk 的第二个域的最低一位为 P,它表示前一个块是否在使用中,P 为 0 则表示前一个 chunk 为空闲,这时chunk 的第一个域 prev_size 才有效,prev_size 表示前一个 chunk 的 size,程序可以使用这个值来找到前一个 chunk 的开始地址。当 P 为 1 时,表示前一个 chunk 正在使用中,prev_size无效,程序也就不可以得到前一个chunk的大小。不能对前一个chunk进行任何操作。ptmalloc分配的第一个块总是将 P 设为 1,以防止程序引用到不存在的区域。Chunk 的第二个域的倒数第二个位为 M,他表示当前 chunk 是从哪个内存区域获得的虚拟内存。M 为 1 表示该 chunk 是从 mmap 映射区域分配的,否则是从 heap 区域分配的。Chunk 的第二个域倒数第三个位为 A,表示该 chunk 属于主分配区或者非主分配区,如果属于非主分配区,将该位置为 1,否则置为 0。
空闲 chunk 在内存中的结构如图所示:

为了使得 chunk 所占用的空间最小,ptmalloc 使用了空间复用,一个 chunk 或者正在被使用,或者已经被 free 掉,所以 chunk 的中的一些域可以在使用状态和空闲状态表示不同的意义,来达到空间复用的效果。以 32 位系统为例,空闲时,一个 chunk 中至少需要 4个 size_t(4B)大小的空间,用来存储 prev_size,size,fd 和 bk (见上图),也就是 16B,chunk 的大小要对齐到 8B。当一个 chunk 处于使用状态时,它的下一个 chunk 的 prev_size域肯定是无效的。所以实际上,这个空间也可以被当前 chunk 使用。这听起来有点不可思议,但确实是合理空间复用的例子。故而实际上,一个使用中的 chunk 的大小的计算公式应该是:in_use_size = (用户请求大小+ 8 - 4 ) align to 8B,这里加 8 是因为需要存储 prev_size 和 size,但又因为向下一个 chunk“借”了 4B,所以要减去 4。最后,因为空闲的 chunk 和使用中的chunk 使用的是同一块空间。所以肯定要取其中最大者作为实际的分配空间。即最终的分配空间 chunk_size = max(in_use_size, 16)。这就是当用户请求内存分配时,ptmalloc 实际需要分配的内存大小,在后面的介绍中。如果不是特别指明的地方,指的都是这个经过转换的实际需要分配的内存大小,而不是用户请求的内存分配大小。

  • 溢出字节为可控制任意字节:通过修改大小造成块结构之间出现重叠,从而泄露其他块数据,或是覆盖其他块数据。也可使用 NULL 字节溢出的方法
  • 溢出字节为 NULL 字节:在 size 为 0x100 的时候,溢出 NULL 字节可以使得 prev_in_use 位被清,这样前块会被认为是 free 块。(1) 这时可以选择使用 unlink 方法(见 unlink 部分)进行处理。(2) 另外,这时 prev_size 域就会启用,就可以伪造 prev_size ,从而造成块之间发生重叠。此方法的关键在于 unlink 的时候没有检查按照 prev_size 找到的块的后一块(理论上是当前正在 unlink 的块)与当前正在 unlink 的块大小是否相等。

漏洞利用

off-by-one overwrite allocated

在这种情况下堆块布局是这样的

A是发生有off-by-one的堆块,其中B是free状态的块,C是allocated块。而且C是我们的攻击目标块。
我们的目标是能够读写块C,那么就应该去构造出这样的内存布局。然后通过off-by-one去改写块B的size域(注意要保证inuse域的值为1,否则会触发unlink导致crash)以实现把C块给整个包含进来。通过把B给free掉,然后再allocated一个大于B+C的块就可以返回B的地址,并且可以读写块C了。

具体的操作是:

    1. 构成图示的内存布局
    1. off-by-one改写B块的size域(增加大小以包含C,inuse位保持1)
    1. malloc一个B+C大小的块
    1. 通过返回的地址即可对C任意读写

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int main(void)
{
char buf[253]="";
void *A,*B,*C;
void *Overlapped;

A=malloc(252);
B=malloc(252);
C=malloc(128);
memset(buf,'a',252);
buf[252]='\x89'; //把C块包含进来
memcpy(A,buf,253);//A存在off-by-one漏洞

free(B);
Overlapped=malloc(500);
}

这个地方为什么A要申请252的空间,因为在32位下采用8字节对齐,所以在申请252字节情况下,操作系统实际分配的是252+8-4=256字节,256恰好8字节对齐,所以符合off-by-one的条件。

off-by-one overwrite freed

在这种情况下堆块布局依然是这样的

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了。

具体的操作是:

    1. 构成图示的内存布局
    1. off-by-one改写B块的size域(增加大小以包含C,inuse位保持1)
    1. malloc一个B+C大小的块
    1. 通过返回的地址即可对C任意读写
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int main(void)
{
char buf[253]="";
void *A,*B,*C;
void *Overlapped;

A=malloc(252);
B=malloc(252);
C=malloc(128);
free(B);
memset(buf,'a',252);
buf[252]='\x89';
memcpy(A,buf,253);//A存在off-by-one漏洞

Overlapped=malloc(380);
}

off-by-one null byte

在这种情况下溢出的这个字节是一个’x00’字节。这种off-by-one可能是最为常见的,因为诸如:

1
2
3
4
5
buf=malloc(124);
if(strlen(str)==124)
{
strcpy(buf,str);
}

就会产生这种null byte off-by-one,即拷贝一个字符串到一个同样长的缓冲区时,并未考虑到NULL字节。
相比于前两种,这种利用方式就显得更复杂,而且对内存布局的要求也更高了。
首先内存布局需要三个块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int main(void)
{
void *A,*B,*C;
void *B1,*B2;
void *Overlapping;
A=malloc(0x100);
B=malloc(0x208);
C=malloc(0x100);
free(B);
((char *)A)[0x104]='x00';
B1=malloc(0x100);
B2=malloc(0x80);
free(B1);
free(C);
malloc(0x200);
}




Asis CTF 2016 b00ks

https://github.com/ctf-wiki/ctf-challenges/tree/master/pwn/heap/off_by_one/Asis_2016_b00ks

CATALOG
  1. 1. how2heap
  2. 2. off-by-one
    1. 2.1. 简介
    2. 2.2. Linux free函数原理:
    3. 2.3. off-by-one 利用思路
    4. 2.4. 漏洞利用
      1. 2.4.1. off-by-one overwrite allocated
      2. 2.4.2. off-by-one overwrite freed
      3. 2.4.3. off-by-one null byte
    5. 2.5. Asis CTF 2016 b00ks