堆溢出崩溃vs栈溢出崩溃的内存越界对比分析
堆溢出崩溃vs栈溢出崩溃的内存越界对比分析
1. 引言
最近在软件开发过程中遇到了两个典型的崩溃问题:堆溢出越界(heap overflow)和栈溢出越界(stack overflow)。先发现的是栈溢出越界崩溃的问题点,后来发现了堆溢出越界崩溃的问题点。
栈溢出越界的问题形如:
char data[32] = { 0 };
...
memcpy(buffer, &data, 1024);
堆溢出越界的问题形如:
char* buffer = (char*)malloc(32);
...
memcpy(buffer, src, 1024);
2. 现象
崩溃现象有点像,都是报错形如:“0x… 写入位置 0x… 时发生访问冲突”,“0x… 读取位置 0x… 时发生访问冲突”。但是崩溃点代码位置距离实际出问题代码的位置距离不太一样。
栈溢出越界的崩溃点代码离实际出问题的代码比较近;
栈溢出越界的崩溃可能会导致函数栈损坏,可能会调用关系函数栈信息丢失;
堆溢出越界的崩溃点代码离实际出问题的代码比较远;
堆溢出越界对函数栈是没有影响的;
3. 疑问
为什么栈溢出时,崩溃点代码离出问题代码近一点?
为什么堆溢出时,崩溃点代码离出问题代码远一点?
是固定如此么?还是和通常情况下的代码有关?也或是一种特例罢了?
栈越界时,造成的破坏好像更大,会影响到函数栈?
4. 分析
对于例子中的heap溢出与stack溢出,都是属于类似与out of range的越界,且是向高地址越界。
对于heap堆来说,它的内存分配是:从低向高分配,所以向高地址越界时,一般情况,会影响到下一块内存申请的变量,但new内存的过程中,无论是缺省分配库glibc,还是使用类似tcmalloc, minimalloc, jemalloc,都并不会按照申请序来分配内存,而是类似先申请一块大一点的,从这个上面分配给外部。所以呢,也并不是一定会地址高低按照次序的,也可能是影响了之前分配的内存内容。
对于stack栈来说,它的内存分配是:从高向低分配,按说会影响到的是已分配的元素;但是由于局部变量(栈的变量)内存分配由编译程序自动生成,编译程序可能申请一大块,然后预留给不同的函数局部变量,所以具体会影响到之前的元素,还是之后使用的元素,不太确定,但整体上来看,是影响之前的元素,甚至会影响到程序入口函数等、调用方函数的栈内变量,所以极易立刻崩溃掉。
综上来看,
- 对于heap来说,向高地址溢出情况,影响在堆内存上;但并不能确定影响的new内存变量,可能是上一个,也可能是下一个,取决于malloc时,找到的空闲内存块。
- 对于stack栈来说,向高地址溢出情况,影响在栈内存上,还可能向上影响到上层函数栈,导致调用关系丢失;受影响的变量也一样,取决于编译器为局部变量分配的地址。
- 根据进程的整体内存地址情况,从heap堆上申请的内存地址,比stack局部变量的内存地址要小;
5. 代码验证
代码验证,都使用的写入数据验证,写入时,越界更容易被感知和定位;另外debug选项使用的去优化,也是为了方便分析。
调用关系:
main->test_overflow->test_stack_overflow();
main->test_overflow->test_heap_overflow();
void test_heap_overflow(int size)
{
char* buffer1 = (char*)malloc(32);
char* buffer2 = (char*)malloc(32);
char* src = (char*)malloc(size);
memset(buffer1, 1, 32);
memset(buffer2, 2, 32);
memset(src, 3, size);
memcpy(buffer1, src, size);
free(buffer1);
free(buffer2);
free(src);
}
void test_stack_overflow(int size)
{
int n1 = 101;
char data1[32] = { 1 };
char data2[32] = { 2 };
int n2 = 102;
char* src = (char*)malloc(size);
memset(src, 3, size);
memcpy(data1, src, size);
free(src);
}
void test_overflow() {
int i = 10;
int j = 20;
int k = 30;
test_heap_overflow(1024);
test_stack_overflow(1024);
}
int main(int argc, char* argv[])
{
test_overflow();
fprintf(stdout, "\npress enter to exit ");
getchar();
return 0;
}
6. 验证1-Stack栈溢出/越界
走读验证代码如下。主要逻辑:把一段size=1024字节的内存复制到栈上局部变量数据data1上,data1上有32字节,会发生写入越界,向栈元素的高地址越界。附加观察变量:n1, n2为int型观察的局部变量,data2为char数据的局部变量,它们的地址都存在栈上。
void test_stack_overflow(int size)
{
int n1 = 101;
char data1[32] = { 1 };
char data2[32] = { 2 };
int n2 = 102;
char* src = (char*)malloc(size);
memset(src, 3, size);
memcpy(data1, src, size);
free(src);
}
test_stack_overflow(1024);
6.1 观察溢出/越界之前
首先观察这几个局部变量的地址情况+堆上变量src的地址:断点在memcpy这一行发生前
可以看到:
- 虽然n1, n2变量定义的代码行位置在data1与data2定义两边,但编译器还是把它们地址放在了低地址;可以看出这块取决于编译器的处理,并不取决于代码行次序。
- 输入变量size的地址放在了高地址,比其它局部变量地址都高;符合stack的特点,stack从高地址往低地址增长,参数先入栈所以在高地址。
- Stack地址从高往低分配使用,实际局部变量地址从高到低依次是:&size, data2, data1, &n2, &n1,对比代码行次序&size, n1, data1, data2, n2,是有显著差异的;
但是注意,这个排序也不是一成不变的,重新编译运行时,编译器可能会调整地址次序,地址次序会发生改变; - 不符合预期的一点:heap堆上变量src的地址,比stack局部变量的地址大一些,这个是不符合预期的。这个是windows系统时的情况,linux下不是如此的。
另外初始值上,可以看到:
n1=101; n2 = 102; data1[0]=1; data2[0]=2; size=1024; src所有字节都是存储0x3。
堆栈情况:
函数堆栈,调用关系正常
6.2 观察溢出/越界之后
观察溢出后情况,局部变量的值情况:断点在memcpy这一行之后
因为memcpy时写出长度超出,发生了写入stack栈内存越界,向栈里高地址越界,可以看到
- data2地址比data1地址高,也写入了0x3的串;
- &size地址也比data1地址高,存储的值从1024,变成了50529027(0x03030303),也成了逐字节的0x3;
- &n1, &n2地址比data1地址低,栈上存储内容不受影响;
- 继续往高地址写的话,就到了上次函数栈的内容,覆盖上层函数栈的内容了,往下观察堆栈情况
堆栈情况:
观察到函数堆栈被破环,调用关系丢失了
基于此,就可以观察到:
栈内溢出/越界的话,对与其它局部变量会造成值覆盖污染的;
溢出/越界较多时,会对函数栈-调用关系覆盖污染,导致程序崩溃,调用关系丢失;
7. 验证2-Heap堆溢出/越界
走读验证代码如下。主要逻辑:把一段size=1024字节的内存复制到stack堆上变量buffer1上,buffer1上有32字节,会发生写入越界,向堆地址的高地址越界。附加观察变量:n1、n2为int型观察的堆上指针变量-4字节,buffer0、buffer1指向char数据的堆上指针变量-32字节,它们的地址都存在堆上。
void test_heap_overflow(int size)
{
int* n1 = (int*)malloc(4);
char* buffer0 = (char*)malloc(32);
char* buffer1 = (char*)malloc(32);
char* buffer2 = (char*)malloc(32);
int* n2 = (int*)malloc(4);
char* src = (char*)malloc(size);
memset(buffer0, 0, 32);
memset(buffer1, 1, 32);
memset(buffer2, 2, 32);
memset(src, 3, size);
memcpy(buffer1, src, size);
free(buffer0);
free(buffer1);
free(buffer2);
free(src);
}
7.1 观察溢出/越界之前
首先观察这几个堆上变量的地址情况+栈上变量size的地址:断点在memcpy这一行发生前
可以看到:
- 虽然n1, n2变量申请堆内存,在buffer0-buffer2申请堆内存的前后两边,但编译器还是把它们地址放在了低地址;可以看出这块取决于编译器的处理,并不取决与代码行次序。
- Heap堆地址从低往高分配使用,实际堆地址从低到高依次是:&n1, &n2, buffer0, buffer1, buffer2, src,对比代码行次序&n1, buffer0, buffer1, buffer2, &n2, src,也是有差异的;
但是注意,这个排序也不是已成不变的,重新编译运行时,malloc出的地址次序会发生改变; - 同上边一样,heap堆上这些变量的地址,也发现比stack局部变量&size的地址大,这个不符合进程内存分布特点;这个是windows系统时的情况,linux下不是如此的。
另外初始值上,可以看到:
n1, n2是申请内存后未赋值,随机值;buffer0初始值存储"\0",bufer1初始值"\1\1…", buffer2初始值"\2\2…", src初始值"\3\3…"。
调用堆栈情况,调用关系正常
7.2 观察溢出/越界之后
观察溢出后情况,堆上变量的值情况:断点在memcpy这一行之后
因为memcpy时写出长度超出,发生了写入堆heap内存越界,向heap堆里高地址越界,可以看到
- buffer2地址比buffer1地址高,也写入了0x3的串;
- buffer1的地址比buffer0地址低,指向内存不受影响;
- n1, n2的地址比buffer0地址低,指向内存数值不受影响;
- 局部变量size存储内容也不会受影响,因为它存储在stack里,和heap越界部分相隔很远;
调用堆栈情况,调用关系依旧正常,不受影响