Day 7:格式化字符串漏洞(Format String Vulnerability)

简要回顾

前几天我们讲解了未定义行为、悬空/野指针、内存泄漏、指针与数组混淆、结构体内存对齐、数组越界和空指针解引用。今天我们聚焦格式化字符串漏洞,这是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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值