【缺陷周话】第13期:二次释放
字数 1649 2025-08-18 11:38:28
二次释放(Double Free)漏洞详解与防护指南
1. 二次释放概述
二次释放(Double Free)是指对同一个指针指向的内存释放了两次的操作。这种内存管理错误主要出现在C/C++程序中,是常见的安全漏洞类型之一。
1.1 基本概念
- C语言中的表现:对同一个指针进行两次
free()操作 - C++中的表现:通常由浅拷贝操作不当引起,当两个对象共享动态内存时,析构函数可能多次释放同一块内存
1.2 相关标准
- CWE ID: 415 (Double Free)
- 危害等级: 中等到严重,可导致程序崩溃或拒绝服务
2. 二次释放的危害
二次释放可能导致以下严重后果:
- 应用程序崩溃:破坏内存管理数据结构
- 拒绝服务攻击:使关键服务不可用
- 潜在的安全漏洞:可能被利用执行任意代码
2.1 实际漏洞案例
| CVE编号 | 受影响软件/版本 | 漏洞位置 | 影响 |
|---|---|---|---|
| CVE-2018-18751 | GNU gettext 0.19.8 | read-catalog.c | 拒绝服务 |
| CVE-2018-17097 | SoundTouch 2.0 | WavFile.cpp | 拒绝服务 |
| CVE-2018-16425 | OpenSC <0.19.0-rc1 | pkcs15-sc-hsm.c | 应用程序崩溃 |
| CVE-2018-16402 | elfutils 0.173 | elf_end.c | 二次释放和崩溃 |
3. 代码示例分析
3.1 缺陷代码示例
// CWE415_Double_Free__malloc_free_char_17.c
void bad()
{
char * data;
data = (char *)malloc(100*sizeof(char)); // 第32行:内存分配
if (data == NULL) {return;}
free(data); // 第36行:第一次释放
for(int i = 0; i < 1; i++)
{
free(data); // 第38行:第二次释放 - 缺陷点
}
}
问题分析:
- 第32行使用
malloc()分配内存 - 第36行第一次释放内存
- 第38行在循环中再次释放同一块内存,导致二次释放
3.2 修复后的代码
void good()
{
char * data;
data = (char *)malloc(100*sizeof(char)); // 内存分配
if (data == NULL) {return;}
free(data); // 释放内存
/* 不再对已释放的内存进行二次释放 */
}
修复要点:
- 保持内存分配和释放的对称性
- 避免对已释放的指针再次调用
free()
4. 二次释放的常见场景
4.1 C语言中的典型场景
- 显式多次调用free():直接对同一指针多次调用free
- 函数间传递已释放指针:一个函数释放后,另一个函数不知情再次释放
- 错误处理路径:在不同错误处理分支中都释放同一指针
4.2 C++中的特殊场景
-
浅拷贝问题:
- 两个对象共享动态内存
- 析构时各自尝试释放同一块内存
-
赋值运算符重载不当:
- 未正确处理自我赋值
- 未正确管理资源所有权
-
移动语义使用不当:
- 移动后源对象仍尝试释放资源
5. 防护措施与最佳实践
5.1 基本防护策略
-
设置指针为NULL:
free(ptr); ptr = NULL; // 防止后续误用 -
使用静态分析工具:
- 360代码卫士等工具可自动检测二次释放问题
-
代码审查重点:
- 检查所有free/delete调用
- 跟踪指针的生命周期
5.2 C++特定防护
-
深拷贝替代浅拷贝:
class MyClass { public: // 深拷贝构造函数 MyClass(const MyClass& other) { data = new int(*other.data); } // 深拷贝赋值运算符 MyClass& operator=(const MyClass& other) { if (this != &other) { delete data; data = new int(*other.data); } return *this; } private: int* data; }; -
使用智能指针:
#include <memory> void safe_function() { std::shared_ptr<int> ptr(new int(42)); // 无需手动释放,自动管理生命周期 } -
实现移动语义:
class Resource { public: // 移动构造函数 Resource(Resource&& other) noexcept : data(other.data) { other.data = nullptr; // 确保源对象不再拥有资源 } // 移动赋值运算符 Resource& operator=(Resource&& other) noexcept { if (this != &other) { delete data; data = other.data; other.data = nullptr; } return *this; } private: int* data; };
5.3 高级防护技术
-
自定义内存分配器:
- 跟踪所有分配和释放操作
- 检测重复释放尝试
-
内存调试工具:
- Valgrind
- AddressSanitizer (ASan)
-
防御性编程:
#define SAFE_FREE(p) do { if(p) { free(p); p = NULL; } } while(0)
6. 测试与验证
6.1 测试用例设计
-
基本测试:
- 对同一指针连续调用free两次
- 验证程序行为
-
边界条件测试:
- 释放NULL指针
- 释放未分配内存的指针
-
多线程测试:
- 并发释放同一指针
- 验证线程安全性
6.2 静态分析工具使用
使用360代码卫士等工具进行检测:
-
检测流程:
- 扫描整个代码库
- 识别所有内存管理操作
- 分析指针生命周期
-
典型输出:
- 缺陷位置定位
- 风险等级评估
- 修复建议
7. 总结
二次释放是C/C++程序中常见且危险的内存管理错误,开发者应当:
- 遵循"谁分配谁释放"原则
- 释放后立即置空指针
- 在C++中使用RAII和智能指针
- 定期使用静态分析工具检查代码
- 在关键代码路径添加额外保护
通过系统性的防护措施和严格的代码规范,可以有效地预防二次释放漏洞,提高软件的安全性和稳定性。