简要回顾
前几天我们讲解了未定义行为、悬空/野指针、内存泄漏、指针与数组混淆、结构体内存对齐、数组越界和空指针解引用。今天我们聚焦格式化字符串漏洞,这是C语言中常见、高危且容易被忽视的安全隐患。
1. 原理与细节逐步讲解
格式化字符串漏洞是由于格式字符串函数(如printf
, fprintf
, sprintf
, snprintf
, vsprintf
, vprintf
, vfprintf
, vsnprintf
等)在使用时,格式字符串参数可控且未正确指定格式化参数,导致攻击者可以通过精心构造的输入,读取甚至修改内存内容。
最常见的错误是直接用用户输入作为格式字符串:
printf(user_input); // 危险
格式字符串中的%x
, %s
, %n
等格式符允许访问栈上数据,甚至可以写任意地址(利用%n
),形成严重的安全漏洞。
格式化字符串函数的工作机制
printf
及相关函数根据第一个参数(格式字符串)解析后续参数并输出。- 如果格式字符串中有格式符(如
%s
),但实际参数不足,函数会继续从栈上取值,造成信息泄露或崩溃。 - 若格式字符串受控,攻击者可以通过格式化指令访问甚至修改内存。
2. 典型陷阱/缺陷说明及成因剖析
典型陷阱
-
直接用用户输入当格式字符串:
char buf[100]; gets(buf); printf(buf); // 格式化字符串漏洞
-
日志记录时未加格式化保护:
syslog(LOG_ERR, user_input); // 也会触发漏洞
成因剖析
- C库的灵活性与危险性:C语言设计时允许格式字符串灵活扩展参数,但没有内建保护。
- 开发者误用:将用户输入、日志内容、外部数据直接用作格式化字符串,导致攻击面暴露。
- 攻击者可控输入:攻击者可输入如
%x %x %x
读取栈数据,甚至%n
写内存。
3. 规避方法与最佳设计实践
-
永远不要将用户输入直接作为格式字符串。
-
始终使用格式化字符串常量,用户输入作为参数传递:
printf("%s", user_input); // 安全
-
日志、安全输出同理:
fprintf(logfile, "%s", user_input);
-
对不确定来源的数据,全部用
%s
明确定义格式。 -
编译器警告:部分编译器可开启
-Wformat
等警告,及时发现格式字符串问题。
4. 错误代码与正确代码对比
错误示例
char buf[100];
fgets(buf, sizeof(buf), stdin);
printf(buf); // 危险:用户输入控制格式字符串
正确示例
char buf[100];
fgets(buf, sizeof(buf), stdin);
printf("%s", buf); // 安全:格式字符串固定
机制差异分析
- 错误代码:
printf(buf)
将用户输入作为格式字符串,攻击者可输入%x %x %x
等触发漏洞。 - 正确代码:
printf("%s", buf)
,格式字符串不受用户控制,用户数据仅作为普通字符串输出。
5. 底层原理说明
- 堆栈访问:格式化字符串函数内部通过遍历格式字符串,在栈上依次消费参数。当格式字符串未预期时,会越界读取、写栈上数据。
- %n漏洞:
%n
指令会将已输出字符数写入对应参数指向的地址,攻击者利用构造格式串可任意写内存(本质为任意写原语)。 - 信息泄露:如
%x
可遍历输出栈上的内容,泄漏程序内部信息(如返回地址、Canary等)。
6. 示意图
<svg width="430" height="100">
<rect x="10" y="40" width="120" height="40" fill="#f9c" stroke="#000"/>
<text x="20" y="65" font-size="13">用户输入: %x %x %s</text>
<rect x="150" y="40" width="120" height="40" fill="#cff" stroke="#000"/>
<text x="160" y="65" font-size="13">printf(buf);</text>
<rect x="290" y="40" width="120" height="40" fill="#ffc" stroke="#000"/>
<text x="300" y="65" font-size="13">泄漏/写内存</text>
<line x1="130" y1="60" x2="150" y2="60" stroke="#000" marker-end="url(#arrow)"/>
<line x1="270" y1="60" x2="290" y2="60" stroke="#000" marker-end="url(#arrow)"/>
<defs>
<marker id="arrow" markerWidth="8" markerHeight="8" refX="4" refY="4"
orient="auto" markerUnits="strokeWidth">
<path d="M0,0 L8,4 L0,8 L2,4 L0,0" fill="#000"/>
</marker>
</defs>
</svg>
7. 总结核心要点与实际建议
- 格式化字符串漏洞是C语言历史上最严重的安全漏洞之一。
- 绝不允许用户输入作为格式字符串参数,始终用常量字符串作为格式模板。
- 定期使用静态分析工具和编译器警告检查代码。
- 格式化日志、错误信息时同样要防范此类漏洞。
建议:养成良好代码习惯,对所有格式化输出严格限定格式字符串,杜绝此类安全隐患!
公众号 | FunIO
微信搜一搜 “funio”,发现更多精彩内容。
个人博客 | blog.boringhex.top