问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

堆溢出崩溃vs栈溢出崩溃的内存越界对比分析

创作时间:
作者:
@小白创作中心

堆溢出崩溃vs栈溢出崩溃的内存越界对比分析

引用
CSDN
1.
https://blog.csdn.net/chunyexiyu/article/details/140524431

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栈来说,它的内存分配是:从高向低分配,按说会影响到的是已分配的元素;但是由于局部变量(栈的变量)内存分配由编译程序自动生成,编译程序可能申请一大块,然后预留给不同的函数局部变量,所以具体会影响到之前的元素,还是之后使用的元素,不太确定,但整体上来看,是影响之前的元素,甚至会影响到程序入口函数等、调用方函数的栈内变量,所以极易立刻崩溃掉。

综上来看,

  1. 对于heap来说,向高地址溢出情况,影响在堆内存上;但并不能确定影响的new内存变量,可能是上一个,也可能是下一个,取决于malloc时,找到的空闲内存块。
  2. 对于stack栈来说,向高地址溢出情况,影响在栈内存上,还可能向上影响到上层函数栈,导致调用关系丢失;受影响的变量也一样,取决于编译器为局部变量分配的地址。
  3. 根据进程的整体内存地址情况,从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这一行发生前

可以看到:

  1. 虽然n1, n2变量定义的代码行位置在data1与data2定义两边,但编译器还是把它们地址放在了低地址;可以看出这块取决于编译器的处理,并不取决于代码行次序。
  2. 输入变量size的地址放在了高地址,比其它局部变量地址都高;符合stack的特点,stack从高地址往低地址增长,参数先入栈所以在高地址。
  3. Stack地址从高往低分配使用,实际局部变量地址从高到低依次是:&size, data2, data1, &n2, &n1,对比代码行次序&size, n1, data1, data2, n2,是有显著差异的;
    但是注意,这个排序也不是一成不变的,重新编译运行时,编译器可能会调整地址次序,地址次序会发生改变;
  4. 不符合预期的一点:heap堆上变量src的地址,比stack局部变量的地址大一些,这个是不符合预期的。这个是windows系统时的情况,linux下不是如此的。

另外初始值上,可以看到:
n1=101; n2 = 102; data1[0]=1; data2[0]=2; size=1024; src所有字节都是存储0x3。

堆栈情况:
函数堆栈,调用关系正常

6.2 观察溢出/越界之后

观察溢出后情况,局部变量的值情况:断点在memcpy这一行之后

因为memcpy时写出长度超出,发生了写入stack栈内存越界,向栈里高地址越界,可以看到

  1. data2地址比data1地址高,也写入了0x3的串;
  2. &size地址也比data1地址高,存储的值从1024,变成了50529027(0x03030303),也成了逐字节的0x3;
  3. &n1, &n2地址比data1地址低,栈上存储内容不受影响;
  4. 继续往高地址写的话,就到了上次函数栈的内容,覆盖上层函数栈的内容了,往下观察堆栈情况

堆栈情况:
观察到函数堆栈被破环,调用关系丢失了

基于此,就可以观察到:
栈内溢出/越界的话,对与其它局部变量会造成值覆盖污染的;
溢出/越界较多时,会对函数栈-调用关系覆盖污染,导致程序崩溃,调用关系丢失;

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这一行发生前

可以看到:

  1. 虽然n1, n2变量申请堆内存,在buffer0-buffer2申请堆内存的前后两边,但编译器还是把它们地址放在了低地址;可以看出这块取决于编译器的处理,并不取决与代码行次序。
  2. Heap堆地址从低往高分配使用,实际堆地址从低到高依次是:&n1, &n2, buffer0, buffer1, buffer2, src,对比代码行次序&n1, buffer0, buffer1, buffer2, &n2, src,也是有差异的;
    但是注意,这个排序也不是已成不变的,重新编译运行时,malloc出的地址次序会发生改变;
  3. 同上边一样,heap堆上这些变量的地址,也发现比stack局部变量&size的地址大,这个不符合进程内存分布特点;这个是windows系统时的情况,linux下不是如此的。

另外初始值上,可以看到:
n1, n2是申请内存后未赋值,随机值;buffer0初始值存储"\0",bufer1初始值"\1\1…", buffer2初始值"\2\2…", src初始值"\3\3…"。

调用堆栈情况,调用关系正常

7.2 观察溢出/越界之后

观察溢出后情况,堆上变量的值情况:断点在memcpy这一行之后

因为memcpy时写出长度超出,发生了写入堆heap内存越界,向heap堆里高地址越界,可以看到

  1. buffer2地址比buffer1地址高,也写入了0x3的串;
  2. buffer1的地址比buffer0地址低,指向内存不受影响;
  3. n1, n2的地址比buffer0地址低,指向内存数值不受影响;
  4. 局部变量size存储内容也不会受影响,因为它存储在stack里,和heap越界部分相隔很远;

调用堆栈情况,调用关系依旧正常,不受影响

© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号