32、安全编程:缓冲区溢出防范、格式化字符串漏洞、文件路径遍历攻击
说实话,安全编程这个话题,我年轻时真没当回事。那时候写C代码,满脑子都是「跑起来就行」。直到有一次,我负责的一个网络服务模块在线上被人搞崩了——对方就是利用了一个小小的缓冲区溢出。嗯,从那以后,我再也不敢轻视这些安全细节了。
今天咱们就聊聊三个最常见的C语言安全陷阱:缓冲区溢出、格式化字符串漏洞、文件路径遍历攻击。这三个问题,说白了都是「输入不可信」导致的。你想想看,如果每个用户输入你都当敌人来防,很多漏洞根本不会出现。
核心原则:永远不要信任外部输入。无论是用户键盘敲的、网络传来的、还是文件里读的,只要不是你自己写的,就得先过安检。
一、缓冲区溢出:老牌杀手
缓冲区溢出,说白了就是你往一个杯子里倒水,杯子满了还继续倒。C语言里最典型的就是 strcpy()、sprintf()、gets() 这些不检查边界的函数。
我在项目中遇到过这样一个场景:一个日志记录模块,用 sprintf() 拼接用户输入的字符串。结果用户输入了一个超长的用户名,直接把栈上的返回地址给覆盖了。程序崩溃还算好的,更可怕的是攻击者可以精心构造输入,让程序跳转到恶意代码。
1.1 危险函数一览
| 危险函数 | 问题 | 安全替代 |
|---|---|---|
| gets() | 不检查输入长度 | fgets() |
| strcpy() | 不检查目标缓冲区大小 | strncpy() 或 strlcpy() |
| sprintf() | 不检查输出长度 | snprintf() |
| scanf("%s") | 不限制读取长度 | scanf("%Ns") 或 fgets() |
| strcat() | 不检查目标剩余空间 | strncat() |
1.2 安全写法示例
我个人习惯是:能用 snprintf() 绝不用 sprintf()。来看个对比:
// ❌ 危险写法
char buf[64];
sprintf(buf, "用户输入: %s", user_input); // 如果user_input超过48字节,就溢出了
// ✅ 安全写法
char buf[64];
snprintf(buf, sizeof(buf), "用户输入: %s", user_input); // 自动截断,不会溢出
小技巧:我写代码时,会在每个缓冲区定义旁边加个注释,标明预期最大长度。比如 char name[32]; // 最大31字符 + '\0'。这样自己回头维护时也不会搞错。
二、格式化字符串漏洞:隐蔽的陷阱
这个漏洞我第一次听说时,觉得「怎么可能有人犯这种错?」结果后来自己review代码时,还真发现了一处。说白了,就是你把用户输入直接当成了格式化字符串的参数。
// ❌ 危险写法
printf(user_input); // 如果user_input包含 "%x %x %x",就会泄露栈上数据
// ✅ 安全写法
printf("%s", user_input); // 只当作普通字符串输出
为什么会这样?因为 printf() 家族函数的第一个参数是格式化字符串。如果这个字符串来自用户,攻击者可以用 %x 读取栈内存,用 %n 写入内存。我曾经见过一个案例,攻击者用 %n 修改了程序的权限检查变量,直接拿到了root权限。
2.1 防范要点
- 永远不要把用户输入直接作为格式化字符串的第一个参数
- 使用
%s明确指定输出格式 - 日志函数也要注意:
syslog()同样有这个问题 - 编译时开启
-Wformat-security警告
注意:即使你用了 snprintf(),如果第一个参数来自用户输入,同样存在格式化字符串漏洞。安全的关键在于「谁控制了格式化字符串」,而不是「输出到哪里」。
三、文件路径遍历攻击:别让用户乱跑
这个漏洞我印象特别深。有一次我写一个文件下载功能,用户传文件名,我直接拼到路径后面去读文件。结果测试同事传了个 ../../../etc/passwd,直接把系统密码文件给读出来了。嗯,当时脸都绿了。
文件路径遍历攻击,说白了就是攻击者用 ../ 跳出你限定的目录,去访问不该访问的文件。
3.1 典型攻击示例
// ❌ 危险写法
char path[256];
snprintf(path, sizeof(path), "/data/files/%s", user_filename);
FILE *fp = fopen(path, "r"); // 如果user_filename是"../../etc/passwd",就出大事了
// ✅ 安全写法
// 方法1:检查路径中是否包含".."
if (strstr(user_filename, "..") != NULL) {
// 拒绝访问
return -1;
}
// 方法2:使用realpath()规范化路径后检查前缀
char real_path[PATH_MAX];
if (realpath(user_filename, real_path) == NULL) {
// 路径无效
return -1;
}
if (strncmp(real_path, "/data/files/", strlen("/data/files/")) != 0) {
// 不在允许的目录内
return -1;
}
3.2 更完善的防范策略
我个人建议采用多层防御:
- 白名单机制:只允许访问预定义的文件列表,而不是让用户随便传文件名
- 路径规范化:用
realpath()解析出绝对路径,再检查前缀 - 拒绝特殊字符:过滤
..、/、\0等 - 最小权限原则:程序只以最低权限运行,即使被攻击,损失也有限
避坑指南:我曾经只用 strstr() 检查 ..,结果攻击者用 ....// 绕过了——因为 ....// 在Windows下等价于 ../。所以别只检查一种写法,最好用 realpath() 做最终校验。
四、知识体系总览
下面这张图,我把三个漏洞的核心要点串在了一起。你看一眼就能明白它们之间的共性和差异:
五、总结与个人心得
这三个漏洞,说白了都是「信任了不该信任的东西」。我做了这么多年C语言开发,最大的体会就是:写安全代码不是靠技巧,而是靠习惯。
- 每次用
printf()时,多问自己一句:第一个参数是固定的吗? - 每次拼接路径时,多问自己一句:用户能控制哪一部分?
- 每次拷贝字符串时,多问自己一句:目标缓冲区够大吗?
养成这些习惯,比你记住一百个漏洞案例都管用。嗯,今天就聊到这儿。这些坑我都踩过,希望你别再踩一遍。
一句话总结:安全编程不是加几个if判断就完事了,它是一种思维方式——时刻假设输入是恶意的,时刻假设自己的代码有漏洞。只有这样,你写出来的代码才能真正经得起考验。
公众号:蓝海资料掘金营,微信deep3321