HackPluto's Blog

Linux堆溢出unlink攻击

字数统计: 1.9k阅读时长: 8 min
2019/11/11 Share

引入

先来看一段漏洞程序

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdlib.h>
#include <string.h>
int main(int argc, char *argv[]){
char *first, *second;
first = malloc(666);
second = malloc(12);
if (argc != 1)
strcpy(first, argv[1]);
free(first);
free(second);
return 0;
}

程序含义很简单,申请了两块内存,然后对第一块进行字符串复制,这里存在溢出,然后是释放掉两个地址。
上述程序在分配完堆后,堆内存分布如下图所示。

漏洞原理

释放堆时会判断当前 chunk 的相邻 chunk 是否为空闲状态,若是则会进行堆合并。合并时会将空闲 chunk 从 bin 中 unlink,并将合并后的 chunk 添加到 unsorted bin 中。堆合并分为向前合并和向后合并。

malloc_chunk

Ptmalloc 采用边界标记法将内存划分成很多块,从而对内存的分配与回收进行管理。在ptmalloc 的实现源码中定义结构体 malloc_chunk 来描述这些块

1
2
3
4
5
6
7
8
9
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 是否空闲,即检查当前 chunk 的 PREV_INUSE(P)位是否为 0。若为空闲,则将其合并。合并时,改变当前 chunk 指针指向前一个 chunk,使用 unlink 宏将前一个空闲 chunk 从 bin 中移除,最后更新合并后 chunk 的大小。

malloc.c 中向后合并的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
INTERNAL_SIZE_T hd = p->size; /* its head field */
INTERNAL_SIZE_T sz; /* its size */
INTERNAL_SIZE_T prevsz; /* size of previous contiguous chunk */
sz = hd & ~PREV_INUSE;
/* consolidate backward */
if (!(hd & PREV_INUSE))
{
prevsz = p->prev_size;
p = chunk_at_offset(p, -(long)prevsz);
sz += prevsz;
unlink(p, bck, fwd);
}
set_head(p, sz | PREV_INUSE);

向前合并

首先判断下个 chunk 是否空闲,即检查下下个 chunk(相对当前 chunk)的 PREV_INUSE(P)位是否为 0,若为 0 表明下个 chunk 是空闲的,则进行合并。合并时使用 unlink 宏将下个 chunk 从它的 bin 中移除,并更新合并后的 chunk 大小。

malloc.c 中向前合并的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* check/set/clear inuse bits in known places */
#define inuse_bit_at_offset(p, s)\
(((mchunkptr)(((char*)(p)) + (s)))->size & PREV_INUSE)
INTERNAL_SIZE_T hd = p->size; /* its head field */
INTERNAL_SIZE_T sz; /* its size */
sz = hd & ~PREV_INUSE;
next = chunk_at_offset(p, sz);
nextsz = chunksize(next);
/* consolidate forward */
if (!(inuse_bit_at_offset(next, nextsz)))
{
sz += nextsz;
...
unlink(next, bck, fwd);
next = chunk_at_offset(p, sz);
}
set_head(p, sz | PREV_INUSE);
next->prev_size = sz;

当前释放的堆与前一个或后一个空闲 chunk 进行合并时,会把空闲 chunk 从 bin 中移除,移除过程使用 unlink 宏来实现。unlink 宏的定义如下:

1
2
3
4
5
6
7
/* Take a chunk off a bin list */
#define unlink(P, BK, FD) { \
FD = P->fd; \
BK = P->bk; \
FD->bk = BK; \
BK->fd = FD; \
}

unlink 即为将 P 从链表中删除的过程。

unlink 攻击

上述例子中,传入的字符串参数长度大于 666 字节时 strcpy 会使 first chunk 溢出,可覆盖 second chunk 的头部字段为如下值:

1
2
3
4
prev_size = 偶数(PREV_INUSE=0)
size = -4
fd = free@got - 12
bk = shellcode address

因为 second chunk 的大小覆盖为 -4,所以下下个 chunk 在 second chunk 偏移为 -4 的位置,因此 malloc 把 second chunk 的 prev_size 当做下下个 chunk 的 size。而 prev_size 已被覆盖为偶数(PREV_INUSE位为0),malloc 会将 second chunk 当作空闲 chunk。
释放 first chunk 时会将 second chunk 从 bin 中 unlink,并将其合并到 first chunk。这个过程会触发 unlink(second),此时 P = second chunk ptr,unlink 过程如下:

1
2
3
4
1)FD = second chunk ptr->fd = free@got – 12
2)BK = second chunk ptr->bk = shellcode address;
3)FD->bk = BK,即(free@got – 12)->bk = shellcode address; free@got – 12 + 12= shellcode address;free@got = shellcode address;
4)BK->fd = FD,即shellcode address->fd = free@got – 12

即 free 的 GOT 表项被修改为了 shellcode 地址。因此,程序在执行第二个 free 时就会执行 shellcode。同理,步骤4)中将 shellcode addr + 8 处 4 个字节覆盖为 free@got - 12,所以在编写 shellcode 时应跳过这 4 个字节。

绕过安全校验

以下为 glibc-2.19 中 unlink 宏的部分代码,在删除 P 节点之前会检查 FD->bk != P || BK->fd != P 是否成立,即检查当前 chunk 前一个 chunk 的 bk 与后一个 chunk 的 fd 是否指向当前 chunk。若当前 chunk 的 fd 和 bk 被修改则无法通过这项检查,FD->bk = BK 与 BK->fd = FD 不会执行,导致 unlink 攻击不能进行。

1
2
3
4
5
6
7
8
9
10
11
12
/* Take a chunk off a bin list */
#define unlink(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); \
else { \
FD->bk = BK; \
BK->fd = FD; \
...
} \
}

攻击者可在指针 ptr 指向的内存中伪造一个空闲 chunk P,根据 ptr 构造合适的地址覆盖 chunk P 的 fd 和 bk,使得 FD->bk == P && BK->fd == P 成立。具体如下:

1
2
P->fd = ptr - 0xC
P->bk = ptr - 0x8

原因跟上面的构造理由一样

1
2
3
4
5
6
1)FD = P->fd = ptr - 0xC
2)BK = P->bk = ptr - 0x8
// FD->bk = ptr - 0xC + 0xC = ptr; BK->fd = ptr -0x8 + 0x8 = ptr;
// 由于 ptr 指向 P,可成功绕过指针校验
3)FD->bk = BK,即 ptr = ptr - 0x8;
4)BK->fd = FD,即 ptr = ptr - 0xC

实例分析

通过调试网上找的一个例子来具体分析 unlink 利用及其安全机制的绕过。

程序中有一个全局指针数组用于存储每一个 malloc 所分配堆块返回的指针。

buf地址为0x08049D60

申请空间

先申请3个大小为 0x80 的堆(small chunk),之所以要申请0x80大小的堆是因为,如果申请的太小free之后会进入fastbin不能直接利用,也不能申请太大的,所以申请small chunk最为合适。

查看堆中的内容,程序会将 malloc 返回的用户空间指针 ptr_mem 存放在全局指针数组 buf[n] 中

填充字符串

申请好堆后,使用 set 功能把字符串 “/bin/sh” 写入到 chunk3 中,为后面执行 system 函数做准备。

使用 set 功能编辑 chunk0 的内容可溢出并覆盖 chunk1,在 chunk0 中伪造一个大小为 0x80 的空闲 chunk P,将其 fd 和 bk 设置为 &buf[0]-0xc 和 &buf[0]-0x8,并且修改 chunk1 的 prev_size 和 size 字段。

从图中可以看到在0x804ad是0x81,最后一位表示前一个堆快在使用,所以堆的大小为0x80,fd 和 bk 分别是0x08049D60-0xc 和 0x08049D60-0x8。

free堆

接着使用 delete 释放 chunk1,由于相邻的 chunk P 为空闲块,会触发 unlink(P) 把 chunk P 从 smallbins 中解除,并与 chunk1 合并为大小为 0x108 的空闲块。unlink 过程中可绕过 “指针破坏” 检测,并实现写内存。最终会把 buf[0]的内容 修改为 &buf[0]-0xC。

在这种情况下我们已经具有了对于&buf[0]地址写的权限了,可以将我们想读写的内容直接输入在&buf[0]然后调用print_chunk和set_chunk。

获取system函数地址

修改 buf[0],控制其指向的内存。可将其修改为 free@got,接着使用 print 输出 chunk0 的内容,可泄露出内存中 free 函数的地址,从而可计算得到 system 函数的地址。

再次编辑 chunk0 的内容,把 system 的地址写入 free@got 中。写完后可查看 free@got 已指向 system 函数。

当使用 delete 删除 chunk3 时执行的 free(chunk3) 实际上是 system(“\bin\sh”),从而成功 getshell。








CATALOG
  1. 1. 引入
  2. 2. 漏洞原理
    1. 2.1. malloc_chunk
    2. 2.2. 向后合并
    3. 2.3. 向前合并
    4. 2.4. unlink
  3. 3. unlink 攻击
    1. 3.1. 原始的 unlink 攻击
    2. 3.2. 绕过安全校验
  4. 4. 实例分析
    1. 4.1. 申请空间
    2. 4.2. 填充字符串
    3. 4.3. free堆
    4. 4.4. 获取system函数地址