24、格式化字符串:printf格式化字符串的漏洞与安全写法
格式化字符串,说白了就是 printf 那一套。很多新手觉得它简单,不就是 %d、%s 嘛。但我要告诉你,这个看似人畜无害的小东西,曾经搞出过多少安全漏洞,甚至让整个系统被攻破。
我个人习惯,每次看到代码里出现 printf(user_input) 这种写法,心里就咯噔一下。嗯,今天我们就来聊聊这个坑。
24.1 格式化字符串漏洞的本质
先看一个最简单的例子:
// 错误写法
char *name = getUserInput();
printf(name); // 直接把用户输入当格式化字符串
// 正确写法
printf("%s", name); // 用户输入只作为数据,不作为格式控制
为什么会这样?因为 printf 的第一个参数是格式化字符串,它决定了后面怎么解析参数。如果你把用户输入直接放进去,用户就可以在输入里塞 %x、%n 这些格式符,从而读取甚至改写内存。
24.2 漏洞能造成什么后果?
我记得有一次做代码审计,看到一个老项目里有这样的代码:
void log_message(char *msg) {
char buffer[256];
snprintf(buffer, sizeof(buffer), msg); // 又一个漏洞!
printf("%s", buffer);
}
你想想看,如果 msg 的内容是 "%x %x %x %x %x %x",那 snprintf 就会把栈上的数据当作参数打印出来。攻击者可以用这个手法泄露栈上的返回地址、canary 值,甚至通过 %n 写入任意内存。
具体来说,格式化字符串漏洞可以做到:
- 信息泄露:用
%x、%p读取栈上数据 - 内存读取:用
%s配合地址偏移,读取任意地址内容 - 内存写入:用
%n向指定地址写入已打印字符数 - 代码执行:通过改写 GOT 表或返回地址,劫持控制流
核心原理:printf 不知道你给了多少个参数。它只按格式化字符串里的 % 标记,从栈上依次取数据。用户控制了格式化字符串,就等于控制了 printf 的「眼睛」和「手」。
24.3 安全写法的黄金法则
我在项目中遇到过好几次类似的隐患,后来总结了一套规则,分享给你:
规则一:永远指定格式符
// 不安全
printf(buf);
fprintf(stderr, buf);
snprintf(out, size, buf);
// 安全
printf("%s", buf);
fprintf(stderr, "%s", buf);
snprintf(out, size, "%s", buf);
规则二:使用带长度限制的函数
// 不安全
sprintf(buffer, "Name: %s", name); // 可能缓冲区溢出
// 安全
snprintf(buffer, sizeof(buffer), "Name: %s", name);
规则三:警惕可变参数中的类型不匹配
// 错误:类型不匹配
int x = 42;
printf("%lld", x); // 未定义行为!x 是 int,但期望 long long
// 正确
printf("%lld", (long long)x);
-Wformat 和 -Wformat-security 警告。GCC 和 Clang 能帮你发现大部分格式化字符串问题。
24.4 实战中的避坑指南
我曾经接手过一个日志模块,里面大量使用了这种模式:
void log_error(const char *fmt, ...) {
va_list args;
char buffer[1024];
// 错误:先格式化到 buffer,然后直接 printf(buffer)
vsnprintf(buffer, sizeof(buffer), fmt, args);
printf(buffer); // 漏洞!
// 正确:直接 printf 带格式符
vprintf(fmt, args);
}
你看,即使 vsnprintf 本身是安全的,但后续的 printf(buffer) 又把格式化字符串的控制权交给了外部数据。这种「二次格式化」的问题,在真实项目中非常常见。
还有一个容易忽略的点:国际化与本地化。有些项目会把翻译后的字符串直接传给 printf:
// 危险!翻译字符串可能包含 % 符号
printf(gettext("File not found: %s"), filename);
如果翻译文件里不小心写了 %s 以外的格式符,或者翻译人员误加了 %n,那就出大事了。我建议所有国际化字符串也要经过格式化安全检查。
24.5 格式化字符串的 SVG 知识图谱
下面这张图总结了格式化字符串的核心知识点,你可以对照着梳理思路:
24.6 总结与检查清单
好了,我们来快速过一遍今天的关键点:
- 不要直接 printf 用户输入——这是铁律,没有例外
- 指定格式符——用
printf("%s", buf)而不是printf(buf) - 警惕二次格式化——先 snprintf 再 printf 也可能出问题
- 类型要匹配——
%d配 int,%ld配 long,别乱用 - 开启编译器警告——
-Wformat -Wformat-security是你的好帮手 - 代码审查时重点看——所有
printf、sprintf、fprintf的第一个参数
一句话总结:格式化字符串漏洞,说白了就是「把数据当代码执行了」。你只要记住——printf 的第一个参数是「代码」,后面的参数才是「数据」。代码和数据永远要分开。
嗯,格式化字符串这块就聊到这儿。内容不多,但每次写代码时多留个心眼,就能避免很多麻烦。
公众号:蓝海资料掘金营,微信deep3321