26、安全编程:缓冲区溢出防范、整数溢出检查、格式化字符串安全
说实话,安全编程这个话题,我每次讲都觉得特别沉重。为什么?因为我在项目里踩过的坑,十个有八个跟这三个问题有关:缓冲区溢出、整数溢出、格式化字符串漏洞。嗯,今天咱们就把这三个"老朋友"彻底聊透。
一、缓冲区溢出:C语言的头号杀手
缓冲区溢出,说白了就是往一个杯子里倒水,结果水满了还继续倒。C语言不检查边界,这是它的"原罪"。我当年刚入行时,就因为这个被客户骂过——一个字符串拷贝没控制长度,直接把堆栈给冲垮了。
核心原则:永远不要相信输入的长度。永远。
1.1 危险函数清单
这些函数,我建议你直接拉黑:
| 危险函数 | 问题 | 安全替代 |
|---|---|---|
| gets() | 不检查输入长度 | fgets() |
| strcpy() | 不检查目标缓冲区大小 | strncpy() 或 strlcpy() |
| strcat() | 不检查拼接后总长度 | strncat() |
| sprintf() | 不检查输出长度 | snprintf() |
| scanf("%s") | 不限制字符串长度 | scanf("%Ns") 或 fgets() |
1.2 实战代码:错误 vs 正确
先看一个我经常在面试题里看到的反面教材:
// ❌ 错误写法
void process_input(char *user_input) {
char buffer[64];
strcpy(buffer, user_input); // 如果输入超过63字符,直接崩
printf("处理结果: %s\n", buffer);
}
你想想看,如果攻击者输入了128个字符,会发生什么?栈上的返回地址被覆盖,程序的控制权就丢了。我在一个IoT项目里见过类似的漏洞,黑客通过这个拿到了设备的root权限。
正确的写法应该是这样:
// ✅ 安全写法
void process_input(char *user_input) {
char buffer[64];
strncpy(buffer, user_input, sizeof(buffer) - 1);
buffer[sizeof(buffer) - 1] = '\0'; // 手动加终止符
printf("处理结果: %s\n", buffer);
}
我的习惯:每次用 strncpy 之后,我都会手动加 '\0'。因为 strncpy 在源字符串长度 >= n 时不会加终止符,这个坑我踩过两次。
二、整数溢出:看不见的边界
整数溢出比缓冲区溢出更隐蔽。为什么?因为程序不会崩,但结果完全错了。我曾经在一个嵌入式设备上调试了三天,最后发现是一个 uint8_t 变量自增到255后变成了0。
2.1 溢出场景分析
常见的溢出场景有这几种:
- 无符号整数回绕:0 - 1 = 4294967295(对 uint32_t 来说)
- 有符号整数溢出:INT_MAX + 1 = 负值(未定义行为)
- 截断溢出:把大整数赋值给小类型,高位被丢弃
看个实际例子:
// ❌ 有漏洞的代码
int copy_data(char *dst, char *src, size_t len) {
if (len > 1024) return -1; // 检查了,但不够
memcpy(dst, src, len); // 如果 len 是负数呢?
return 0;
}
这里有个问题:len 是 size_t(无符号),但 if 判断里用了有符号比较。如果传入的 len 是 0xFFFFFFFF,它会被当作 -1 处理,比较通过,然后 memcpy 拷贝了4GB数据——缓冲区直接炸了。
正确的做法:
// ✅ 安全写法
int copy_data(char *dst, char *src, size_t len) {
if (len == 0 || len > 1024) return -1; // 明确检查范围
memcpy(dst, src, len);
return 0;
}
2.2 乘法溢出陷阱
这个我印象特别深。有一次做内存池管理,需要分配 n * sizeof(element) 字节。如果 n 很大,乘法结果可能溢出,导致分配的内存远小于预期。
// ❌ 危险
void *allocate_elements(size_t n) {
return malloc(n * sizeof(MyStruct)); // n * sizeof 可能溢出
}
// ✅ 安全
void *allocate_elements(size_t n) {
if (n > SIZE_MAX / sizeof(MyStruct)) {
return NULL; // 溢出检查
}
return malloc(n * sizeof(MyStruct));
}
避坑指南:我曾经在代码里用 int 存了文件大小,结果文件超过2GB后,大小变成了负数。从那以后,所有跟大小、长度相关的变量,我统一用 size_t。
三、格式化字符串安全:printf 也能被攻击?
很多人觉得 printf 就是个打印函数,能有什么安全问题?嗯,我当年也这么想,直到我亲眼看到一个同事的代码被 %x 和 %n 玩坏。
3.1 漏洞原理
格式化字符串漏洞的核心是:格式化参数和实际参数不匹配。看这个:
// ❌ 高危写法
printf(user_input); // 用户输入里如果有 %s %x %n,就完了
// ✅ 安全写法
printf("%s", user_input); // 只把用户输入当字符串打印
为什么危险?因为 %n 可以把已经输出的字符数写入一个地址。攻击者精心构造输入,就能往任意内存地址写值。我在一个网络设备上见过这种攻击,攻击者通过 HTTP 请求的 User-Agent 字段注入了格式化字符串,直接拿到了 shell。
3.2 防御策略
防御其实很简单,就两条:
- 永远不要把用户输入直接作为 printf 的第一个参数
- 永远使用
printf("%s", input)而不是printf(input)
另外,我建议在编译时开启 -Wformat -Wformat-security 警告,GCC 会帮你检查格式字符串是否匹配。
四、知识体系总览
下面这张图,是我自己总结的安全编程核心逻辑。你把它记牢了,能避开90%的坑。
五、总结与建议
安全编程说白了就一句话:别偷懒,别侥幸。我见过太多人觉得"这个输入是内部传来的,没问题",结果出了问题。嗯,我自己也犯过这种错。
最后给你三个实操建议:
- 编译时开启所有安全相关警告:
-Wall -Wextra -Wformat -Wformat-security -D_FORTIFY_SOURCE=2 - 代码审查时重点检查这三个点:字符串拷贝、整数运算、printf 调用
- 使用静态分析工具:比如 Coverity、Clang Static Analyzer,能自动发现很多问题
我的经验:每次提交代码前,我会自己过一遍这三个检查清单。花不了5分钟,但能省下后面几天的调试时间。真的。
公众号:蓝海资料掘金营,微信deep3321