*以下内容为本人的学习笔记,如需要转载,请声明原文链接 微信公众号「ENG八戒」https://mp.weixin.qq.com/s/IwSVImp5cOB3gZbaf0YiPw
写过 C/C++ 的都知道,内存允许程序员自主分配,用完了这些资源也得释放出来,这种在系统运行过程中动态申请的内存,称为动态内存。
常言道,借东西好借好还,下次再借也不难,但是有的人有时候还真的忘了还回去。这要是发生在程序运行时,申请的内存没正常释放,没管理好,就避免不了会面对内存报错的问题。
内存都允许你自由操纵了,灵活性是真的大,恰恰这也是它的弊端。
今天就来聊聊 C/C++ 的报错 double free or corruption
C 语言提供了两个函数用于分配和释放内存 malloc 和 free,需要引用头文件 <stdlib.h>。<stdlib.h> 是 C 标准库头文件 为 C 语言程序员提供可靠、高效的函数,以实现动态内存分配、数据类型转换、伪随机数生成、过程控制、搜索和排序、数学以及多字节或宽字符函数,还包括一些常用常数,目的是促进组织和平台间的代码标准化。
#include <stdlib.h>
#include <stdio.h>
int main()
{
int *ptr = malloc(sizeof(int));
*ptr = 100;
printf("%d", *ptr);
free(ptr);
return 0;
}
输出:
100
调用 malloc 会分配一块内存空间,并将这块内存空间的首地址返回。调用时,需要传入目标内存空间的大小,单位按照字节(Byte)算,而返回的地址数据类型是 void*,所以,根据目标空间的具体用途转换即可。
这块内存空间在分配之后还属于未初始化的状态,如果对内存空间的使用比较复杂,建议先用 memset 初始化一下。
内存空间使用完,需要使用 free 释放掉,避免闲置浪费,否则就算是内存泄漏了。内存泄露会直到程序进程结束为止。
在其它的高级语言里,比如 Java、Python 等,出于内存安全的考虑,都不会允许用户自己管理内存,而 C++ 是个例外,这可能来自于 C 语言的传承。
C++ 里同样提供了 malloc 和 free,但是引用的头文件变成了
另外 C++ 还提供了两个额外的操作符用于分配和释放内存,分别是 new 和 delete。
#include <iostream>
using namespace std;
int main()
{
int *ptr = new int;
*ptr = 100;
cout << *ptr << endl;
delete ptr;
return 0;
}
输出:
100
关键词 new 后接上一个数据类型,然后分配和数据类型 int 对应大小的内存空间,并返回首地址。对应地,new 申请的内存空间被使用完不再需要时,应该使用关键词 delete 释放,delete 直接操作内存空间首地址。
借来的钱用得可以很爽,是的,常人都这样。不过,每到要还钱的时候就特别不情愿,要么推三推四,要么直接抵赖,一不留神就忘了是否有还过这事。
比如,张三本来一直在外租房将就着过日子,随着家里人口逐渐增多,就和老婆合计着从银行贷了一笔资金准备买房嘛,贷了款之后,银行贷款经理就告诉他,“张先生,你们家以后每月就得由一名代表人来还贷款,不需要几个人同时还的,记住了哈!”
好了,这个故事给了我们什么启发呢?就是资金或者资源的借入借出需要有一个管理人,这样可以避免混乱进而出错。
同样的,在 C/C++ 的编程里边,经常会出现一些内存资源管理混乱而出现的报错甚至运行时崩溃的问题,比如 double free or corruption。
#include <iostream>
using namespace std;
int main()
{
int *ptr = new int;
*ptr = 100;
cout << *ptr << endl;
delete ptr;
delete ptr;
return 0;
}
执行
100
free(): double free detected in tcache 2
Aborted (core dumped)
程序执行崩溃并报错 double free,根本原因是对同一内存地址调用了多次的 free 或 delete 执行释放,这会导致应用的内存管理数据结构被损坏,甚至会允许恶意用户在内存任意区域写入数据。这类损坏会导致程序崩溃或者程序的部分执行流程被改变。如果攻击者这个时候特意覆盖特定的寄存器或者内存区域来引导执行他们的代码,进而可以产生提升权限的交互式 shell,这样就完全被破防了。
这也算是内存泄漏的一种,系统一旦检测到 double free 也会终止进程继续执行(Aborted)。
一块内存被释放之后,空闲的内存会被放入链表中,用于重新管理和组合不同的空闲内存碎片,便于将来用于分配更大的内存空间。这个链表属于双向链表,每个空闲的内存空间都可以往前和往后查找其它内存空间。
那么攻击者可以利用这个过程吗?
答案是肯定的。当 free 被调用时,攻击者可以让原本需要被链表管理的空闲内存取消链接,覆盖寄存器值并从缓冲区载入shell代码,最终往内存写入任意值。
上面的示例代码简单演示了 double free 的触发,平常出现这种报错的条件并不比上面的情形要复杂多少。比如,释放同一块内存的动作在相隔了几百甚至更多行的位置执行,有的还发生在不同源码文件,这就会让程序员容易多次释放。下面尝试总结一下,来看一下常见的犯错情形:
其实,细看一下上面总结的几种常见犯错情形,我们也可以很好地避免低级错误。
有个最佳实践是,分配的内存地址存储变量 ptr 在定义声明时就应该初始化为 NULL,内存被释放后应立刻将 ptr 置为 NULL,使用这块内存或者释放前应该遵循先判断内存空间是否有效的原则,简单点可以用 (ptr != NULL)。
另外,负责释放的管理责任方应该尽量单一,即使横跨多个源文件或模块。这里有个道理就是避免”多龙治水“。
中国在过去一直是个农业大国,有着重农轻商的历史,各种典故都有着农业的影子。
相传,几龙治水、几牛耕地那是对当年农业收成的预示,不妨翻一下老黄历看看? “龙”是管雨的神,以五龙治水可获风调雨顺,因东南西北中都有神龙,各施其职。龙少了当年就要发大水;龙多了当年将要天大旱。原因是管雨的龙神少了怕管不过来,就忙忙碌碌四处播雨以至大涝;管雨的龙神多了呢,就像“三个和尚无水吃”一样以至大旱。至于涝到什么程度还看治水的龙少到什么程度,龙越少涝得越严重。旱的程度亦一样。
因此就有了“龙多不下雨”的谚语。
计算机编程说到底还是程序员的思维体现,人情世故也会反映在代码的逻辑上。