教学 > 二进制安全学习路径 > 格式化字符串漏洞
课程进度:65%

格式化字符串漏洞

二进制安全学习路径 | 模块2 | 课程3

1. 引言

格式化字符串漏洞是二进制安全领域中一种非常危险且常见的漏洞类型。当程序没有正确处理包含格式说明符的字符串时,攻击者可以利用这些漏洞读取敏感内存数据,甚至修改程序的执行流程。本课程将深入介绍格式化字符串漏洞的原理、常见利用方式以及防御措施。

学习目标: 理解格式化字符串漏洞的原理和成因,掌握漏洞的检测和利用方法,学习如何编写安全的代码来防御此类漏洞。

2. 格式化字符串基础

2.1 什么是格式化字符串

格式化字符串是包含特殊控制字符(格式说明符)的字符串,用于指定如何格式化和显示数据。在C语言中,printf()、sprintf()、fprintf()等函数使用格式化字符串来控制输出格式。

2.2 常见格式说明符

格式说明符以%开头,常见的包括:

  • %d - 有符号十进制整数
  • %u - 无符号十进制整数
  • %x - 十六进制整数
  • %s - 字符串
  • %c - 字符
  • %p - 指针地址
  • %n - 将已写入的字符数存入对应的整数指针参数
#include int main() { int num = 42; char *str = "Hello World"; printf("整数: %d\n", num); printf("字符串: %s\n", str); printf("十六进制: 0x%x\n", num); printf("指针地址: %p\n", str); return 0; }

3. 格式化字符串漏洞原理

3.1 漏洞成因

格式化字符串漏洞产生的根本原因是:当程序将用户可控制的输入直接作为格式化字符串参数传递给相关函数时,攻击者可以注入格式说明符来获取或修改程序的内存内容。

// 安全代码 printf("%s", user_input); // 正确:用户输入作为格式说明符的参数 // 不安全代码 printf(user_input); // 错误:用户输入直接作为格式化字符串

3.2 漏洞危害

格式化字符串漏洞可以导致:

  • 信息泄露(读取栈上或任意内存的数据)
  • 内存写入(通过%n写入任意地址)
  • 程序崩溃
  • 代码执行(通过覆盖函数指针或返回地址)

安全提示: 格式化字符串漏洞在程序中尤其危险,因为它既可以用于信息泄露,又可以用于代码执行,且利用方式相对简单。

4. 内存结构与格式化函数

4.1 栈与格式化函数的关系

格式化函数(如printf)在解析格式字符串时,会按顺序从栈上获取对应的参数。当格式说明符的数量大于提供的参数数量时,函数会继续从栈上获取值,导致可能泄露栈内容。

格式化字符串与栈的关系图

格式化字符串处理时的栈结构示意图

4.2 参数获取机制

printf族函数如何处理格式说明符:

  1. 解析格式字符串,识别格式说明符
  2. 对于每个格式说明符,从栈上按顺序获取一个参数
  3. 根据说明符类型对参数进行适当的格式化

5. 格式化字符串漏洞利用

5.1 信息泄露

使用%x或%p格式说明符可以泄露栈上的数据:

// 漏洞代码 char buffer[100]; scanf("%s", buffer); printf(buffer); // 漏洞:直接使用用户输入作为格式字符串 // 如果用户输入 "%x %x %x %x",将显示栈上的四个值

5.2 定位输入在栈中的位置

为了更精确地利用格式化字符串漏洞,通常需要定位输入字符串在栈中的位置:

// 输入 "AAAA.%x.%x.%x.%x.%x.%x" // 观察输出,查找 "41414141" (AAAA的十六进制表示) // 确定格式字符串相对于栈的偏移量

5.3 直接参数访问

利用参数索引可以更精确地访问栈上的值:

// 输入 "%6$x" 将直接显示第6个参数 // 这比使用多个 %x 更精确且高效

5.4 使用%n进行内存写入

%n格式说明符可以将已输出的字符数量写入到指定地址:

// 基本原理 int count = 0; printf("Hello%n", &count); // count的值将是5("Hello"的长度) // 攻击示例(在目标程序中) // 1. 在输入中包含目标地址 // 2. 使用%x跳过足够的参数 // 3. 使用%n将数据写入目标地址

5.5 部分写入技术

对于较大的数值,可以使用%hn(写入2字节)或%hhn(写入1字节)进行部分写入:

// 使用 %hn 写入两个字节 // 例如:写入 0x0804abcd 的地址 // 先写入 0xabcd,再写入 0x0804

6. 实际漏洞示例

6.1 基础示例

#include #include void vulnerable_function(char *user_input) { printf(user_input); // 格式化字符串漏洞 } int main(int argc, char *argv[]) { if (argc > 1) { vulnerable_function(argv[1]); } else { printf("请提供一个参数\n"); } return 0; }

6.2 信息泄露示例

// 编译: gcc -fno-stack-protector -no-pie leak_example.c -o leak_example #include #include int main() { char secret[16] = "TopSecretData!"; char buffer[64]; printf("输入你的名字: "); fgets(buffer, sizeof(buffer), stdin); printf("你好, "); printf(buffer); // 格式化字符串漏洞 return 0; } // 攻击者可以输入 "%x %x %x %x %x %x" 来泄露栈上的数据 // 可能会泄露 secret 的内容

6.3 内存写入示例

// 编译: gcc -fno-stack-protector -no-pie write_example.c -o write_example #include #include int main() { int target = 0; char buffer[64]; printf("target的初始值: %d\n", target); printf("target的地址: %p\n", &target); printf("输入字符串: "); fgets(buffer, sizeof(buffer), stdin); printf(buffer); // 格式化字符串漏洞 printf("target的当前值: %d\n", target); if (target == 42) { printf("恭喜! 你成功修改了目标值!\n"); } return 0; } // 攻击者可以构造输入,使用%n格式说明符将42写入target变量

7. 漏洞检测与防御

7.1 代码审计

检查所有格式化函数的使用,确保格式字符串不受用户控制:

  • printf(), fprintf(), sprintf(), snprintf()
  • vprintf(), vfprintf(), vsprintf(), vsnprintf()

7.2 静态分析工具

使用静态分析工具可以帮助检测格式化字符串漏洞:

  • Clang Static Analyzer
  • Flawfinder
  • RATS (Rough Auditing Tool for Security)
  • Fortify Source (GCC/Clang 编译器选项)

7.3 防御措施

保护代码免受格式化字符串攻击的最佳实践:

  • 始终使用格式字符串常量,而不是变量
  • 如果必须使用变量作为格式字符串,先验证其内容
  • 使用 "%s" 作为格式说明符来输出用户提供的字符串
  • 启用编译时保护:-Wformat -Wformat-security -D_FORTIFY_SOURCE=2
// 不安全代码 printf(user_input); // 安全代码 printf("%s", user_input); // 使用snprintf限制长度 char buffer[100]; snprintf(buffer, sizeof(buffer), "%s", user_input); printf("%s", buffer);

8. 高级利用技术

8.1 GOT覆盖

全局偏移表(GOT)是动态链接程序中用于解析函数地址的表。通过格式化字符串漏洞可以覆盖GOT表项,将函数调用重定向到攻击者控制的代码。

8.2 返回地址覆盖

类似于缓冲区溢出攻击,可以使用格式化字符串漏洞覆盖栈上的返回地址,控制程序执行流程。

8.3 ASLR绕过技术

地址空间布局随机化(ASLR)增加了攻击难度,但可以通过以下方式绕过:

  • 使用格式化字符串泄露库函数地址
  • 根据泄露的地址计算偏移
  • 构造攻击链

8.4 多次写入技术

对于需要写入大数值的情况,可以使用多次小数值写入:

// 先写入低字节,再写入高字节 // %<数值>c%<偏移>$hhn - 写入单字节 // 例如: %104c%7$hhn 将向偏移7处写入字节值104(0x68)

9. 真实案例分析

9.1 CTF题目分析

许多CTF竞赛都包含格式化字符串相关的挑战,通常需要利用漏洞来泄露关键信息或修改程序行为以获取flag。

9.2 CVE案例研究

以下是一些著名的格式化字符串漏洞CVE案例:

  • CVE-2012-0809: Sudo格式化字符串漏洞
  • CVE-2015-8617: ProFTPD格式化字符串漏洞
  • CVE-2018-6952: QEMU格式化字符串漏洞

10. 安全编码实践

10.1 开发规范

安全开发规范应包含以下几点:

  • 所有格式化函数调用必须使用静态格式字符串
  • 禁止将用户输入直接传递给格式化函数
  • 使用安全的包装函数

10.2 代码审查清单

代码审查时应关注:

  • 所有printf系列函数的使用
  • 自定义的格式化输出函数
  • 使用变量作为格式字符串的情况

10.3 编译器和工具保护

使用以下编译选项加强保护:

// GCC/Clang编译选项 -Wformat -Wformat-security // 警告不安全的格式字符串用法 -D_FORTIFY_SOURCE=2 // 启用运行时格式字符串检查 -fstack-protector-all // 栈保护(虽不直接防御格式化字符串漏洞,但增加利用难度)

准备好测试您的知识了吗?

完成本课程后,尝试解决与格式化字符串漏洞相关的挑战,巩固您的知识并获得实践经验。

开始挑战