Table of Contents
缓冲溢出仍然是C编程中最持久和最危险的安全弱点之一。 尽管几十年来,它们仍然引起严重的问题,如数据腐败、系统崩溃和远程代码执行。 写安全的C代码需要深刻理解缓冲溢出是如何发生的,需要有条不紊的预防方法。 本条为编写强健、防溢的C代码提供了全面的指南,涵盖了基本概念、安全功能、验证技术、编译器防护和现代防御措施。
理解缓冲过度
当程序将更多数据写到一个毗连的内存块(一个缓冲)时,会发生缓冲溢出。由于缓冲器居住在堆积或堆积内存中,超过其边界,覆盖了相邻内存位置。这种腐败可能改变程序状态,引入不可预测的行为,或者被攻击者利用来注入和执行任意代码。
后果取决于被覆盖的内容。 覆盖堆栈上的返回地址可以将执行重定向到攻击者控制的代码。 覆盖指针可以导致任意的内存写作。 即使简单的崩溃也可以用来拒绝服务攻击。 理解力学是预防的第一步 。
基于堆叠的重叠流
本地变量,包括已声明的函数内部缓冲器,都存储在堆栈上。堆栈还保存了返回地址、保存的帧指针和其他控制数据。当像 这样的线性缓冲器被超支时,数据会溢出到返回地址及以后。像Morris trim (1988) 这样的经典开发利用堆栈溢出来获取未经授权的访问。
基于堆积的流量
动态分配缓冲器(通过,等)位于堆积层上。 溢出会腐蚀分配器使用的元数据,导致通过堆积喷洒或无用攻击导致坠毁或开采。堆积溢出更难于利用,但同样危险。
共同脆弱功能及其安全替代方案
C 标准库提供几种功能,不进行边框检查。 使用边框是缓冲溢出的最常见原因。 用更安全的对应程序取代边框是基本的最佳做法。
字符串复制和连接
- ]——不安全:复制到一个无效的终止者,没有长度限制.
安全替代:[]——复制到最多n字符;注意如果源长于n,它不会被取消,所以总是手动取消. - 更好的是: ——在BSD和许多Linux系统中可以使用;总是无效终止并返回源字符串的长度用于调试检测.
- ]——不安全:无界限的调和.
安全替代:[]]——最多为n字符的附着,并且总是无终止.
格式化的输出和输入
- ]——不安全:将格式化的输出写入一个缓冲器,不检查大小.
安全替代:[]——将输出限制在大小1字符加上无效的终止符. - []——类似风险;使用]。
- ]——极端危险;从C11标准中删除。
- ]——无边框检查。使用或],并带有字段宽度的指定符。
内存复制和移动
- ]——只有在n被核实不超过目的缓冲大小的情况下才安全.
safer 替代:[]](处理重叠),并始终确保n dest大小. - 一些平台从附件K(C11中的可选)中提供],但采用有限.
验证和大小管理
即使具有安全功能,你也必须验证输入长度,确保适当的缓冲大小,并优雅地处理可能的截断.
检查输入长度
在复制或处理外部输入(用户输入、网络数据、文件内容)之前,确定最大可接受长度,并拒绝或截断超过该长度的数据。例如:
#define MAX_INPUT 255
char buffer[MAX_INPUT + 1]; // +1 for null
if (strlen(user_input) > MAX_INPUT) {
// Handle error: reject or truncate
fputs("Input too long", stderr);
return -1;
}
strncpy(buffer, user_input, sizeof(buffer) - 1);
buffer[sizeof(buffer) - 1] = '\0';
使用有已知限制的固定大小缓冲
只要有可能,请定义具有恒定大小的缓冲器,并在代码中执行。避免可变长度数组(VLA),如果提供大尺寸,则会导致堆叠溢出。而是用明确的大小检查来动态分配。
明确处理断线
函数 [ [FLT: 21] 和 [[FLT: 22]]] 可以截断数据。 要注意返回值, 以检测截断, 并决定截断数据是否可以接受, 或者是否应当提出错误 。 忽略截断会让缓冲器处于出乎意料的状态 。
编译器安全旗和运行时保护
现代编译器提供旗子,用于添加缓冲溢出检测和缓解,而不修改代码。在您的构建系统中启用它们 。
- /]]——在返回地址前插入堆栈金丝雀(随机值). 如果缓冲溢出在修改返回地址前覆盖金丝雀,程序在开发完成前中止.
- ]]——将调用不安全函数,如和],替换为在目标缓冲器太小时中断的选中版本。需要或更高的优化。
- ]——警告格式字符串漏洞,可能导致缓冲溢出或信息泄露.
- ] —地址Sanitize(ASan)仪器代码,用于检测缓冲溢出,在运行时无使用后,以及其他内存错误. 执行缓慢但对于测试非常宝贵.
- ]——避免优化超时检查(谨慎使用)。
操作系统保护
堆叠金丝雀只是一层。现代OSs中的开发性缓解技术包括:
- DAD 执行预防(DEP)/NX bit]——标记堆栈和堆栈作为不可执行,防止 shellcode执行.
- 调整空间布局随机化(ASLR)——随机化内存地址(sack, hap,share library),以更难预测目标.
- 仅重新定位读(RELRO) ——保护Got(全球偏移表)不被覆盖.
允许这些保护(通常默认)提高开发的条条,但不会取代安全编码.
守则审计和静态分析
人文审查与自动静态分析相结合,可以及早发现缓冲溢出的问题。将这些问题纳入您的发展工作流程。
- 手动代码审查——寻找不安全函数的用途,缺少大小检查,以及超出缓冲边界写入的循环.
- static分析工具[——、、]和[]]等工具,可以探测潜在的溢出、使用危险功能和逐个错误。这些工具可在CI管道中运行。
- Fuzzing ——使用libFuzzer,AFL,或其他模糊器,用可能引发溢出量的意外数据自动测试输入处理.
安全守则的实际实例
带“界框”的安全字符串检查复制
#include <stdio.h>
#include <string.h>
int safe_string_copy(char *dest, size_t dest_size, const char *src) {
if (!dest || !src || dest_size == 0) {
return -1; // Invalid parameters
}
size_t src_len = strlen(src);
if (src_len >= dest_size) {
// Source too large; truncation or error
// Option: copy what fits and null-terminate
strncpy(dest, src, dest_size - 1);
dest[dest_size - 1] = '\0';
return 1; // Truncation occurred
}
strncpy(dest, src, dest_size);
// strncpy fills remaining with null, so dest_size fits; no need to null-terminate if src shorter
return 0; // Success, no truncation
}
缓冲大小的安全整数处理
计算大小时, 缓冲溢出也可能是整数溢出的结果。 在分配前总是检查算术 。
#include <stdlib.h>
#include <limits.h>
#include <errno.h>
void *safe_malloc_array(size_t nmemb, size_t size) {
if (nmemb == 0 || size == 0) {
return NULL; // Or handle zero-size allocation
}
if (nmemb > SIZE_MAX / size) {
// Integer overflow would occur
errno = ENOMEM;
return NULL;
}
return malloc(nmemb * size);
}
格式化字符串使用 snprintf
char log_message[256];
int ret = snprintf(log_message, sizeof(log_message),
"User %s logged in from %s", username, ip_address);
if (ret < 0) {
// Output error
} else if ((size_t)ret >= sizeof(log_message)) {
// Truncation occurred; handle if needed
}
其他最佳做法
- 启动缓冲[]——总是零初始化缓冲,以避免泄露未初始化的内存.
- 无约束深度的避免重复——堆积溢出可以从深度重复发生;使用迭代或限制深度.
- 使用]限定词[]——帮助编译器优化,可能捕捉别名问题,虽然不能直接防止溢出.
- 偏好 ]——正确性[]——防止输入字符串的意外修改,执行意图.
- 执行错误处理——不要忽略函数的返回值,如,,等.
供进一步学习的资源
- SEI CERT C编码标准——安全C编码综合规则.
- CWE-120:缓冲复制而不检查输入大小——MITRE对缓冲溢出弱点的分类.
- OWASP缓冲过度流——开放网络应用安全项目提供的实际指导.
- GNU C库手册:字符串和矩阵工具[] — 安全字符串函数的文档.
- Address Sanitize — 一个快速内存错误探测器.
结论
防止缓冲溢出在C并不是可选的;这是任何开发者使用语言的基本责任。通过理解溢出机制、用更安全的替代品取代危险功能、严格验证输入和大小、允许编译器保护、以及采用静态分析和测试,你能够大幅降低这些脆弱性的风险。 没有任何单一技术足够;深入防御 — — 结合编码纪律、编译器旗帜、OS减轻风险和彻底测试 — — 提供了最强的保护。通过这些做法,你可以写出既强大又安全的C代码,能够在关键系统中安全运行。记住:安全编码是一种持续的做法,而不是一次性固定。 随时了解新的脆弱性,并不断审查和改进代码。