格式化字符串漏洞
格式化字符串介绍
常见格式化字符串函数
函数 | 基本介绍 |
---|
printf | 输出到stdout | fprintf | 输出到指定FILE流 | vprintf | 根据参数列表格式化输出到stdout | vfprintf | 根据参数列表格式化输出到FILE流 | sprintf | 输出到字符串 | snprintf | 输出指定字节数到字符串 | vsprintf | 根据参数列表格式化输出到字符串 | vsnprintf | 根据参数列表格式化输出指定字节到字符串 |
常用格式化字符串形式
%[parameter][flags][field width][.precision][length]type
- parameter:n$,获取格式化字符串中的指定参数
- field width:输出的最小宽度
- precision:输出的最大长度
- length,输出的长度
- type
- d/i,有符号整数
- u,无符号整数
- x/X,16进制
- o,8进制
- s,所有字节
- c,char类型单个字符
- p,void * 型,输出对应变量的值。printf("%p",a) 用地址的格式打印变量 a 的值,printf("%p", &a) 打印变量 a 所在的地址。
- n,不输出字符,但是把已经成功输出的字符个数写入对应的整型指针参数所指的变量。
原理验证
示例程序:
#include<stdio.h>
int main() {
char s[100] = "aaaa.%p.%p.%p.%p.%p.%p.%p";
printf(s);
return 0;
}
32位
编译命令:
gcc test.c -g -m32 -o test
输出结果:
aaaa.0xf7ffc988.0xffffcf2a.0x56555595.0xffffcf2a.0xf7ffc984.0x61616161.0x2e70252e
栈结构:
00:0000│ esp 0xffffcee0 —? 0xffffcef8 ?— 'aaaa.%p.%p.%p.%p.%p.%p.%p'
01:0004│ 0xffffcee4 —? 0xf7ffc988 (_rtld_global_ro+136) ?— 0x8e
02:0008│ 0xffffcee8 —? 0xffffcf2a ?— 0x0
03:000c│ 0xffffceec —? 0x56555595 (main+24) ?— add ebx, 0x1a3f
04:0010│ 0xffffcef0 —? 0xffffcf2a ?— 0x0
05:0014│ 0xffffcef4 —? 0xf7ffc984 (_rtld_global_ro+132) ?— 0x6
06:0018│ eax 0xffffcef8 ?— 'aaaa.%p.%p.%p.%p.%p.%p.%p'
自上而下依次是参数0~6,参数0为格式化字符串地址,而格式化字符串前4字节又作为参数6(由于栈结构不同,需要视情况而定)。因此如果将格式化字符串合适的位置设置为目标地址就可以对该地址的数据进行操作。
64位
编译命令:
gcc test.c -g -m64 -o test
输出结果:
aaaa.0x7fffffffde78.0x70.0x555555554770.0x7ffff7dced80.0x7ffff7dced80.0x2e70252e61616161.0x70252e70252e7025
寄存器:
RAX 0x0
RBX 0x0
RCX 0x555555554770 (__libc_csu_init) ?— push r15
RDX 0x70
RDI 0x7fffffffdd20 ?— 'aaaa.%p.%p.%p.%p.%p.%p.%p'
RSI 0x7fffffffde78 —? 0x7fffffffe21b
R8 0x7ffff7dced80 (initial) ?— 0x0
R9 0x7ffff7dced80 (initial) ?— 0x0
R10 0x0
R11 0x0
R12 0x5555555545a0 (_start) ?— xor ebp, ebp
R13 0x7fffffffde70 ?— 0x1
R14 0x0
R15 0x0
RBP 0x7fffffffdd90 —? 0x555555554770 (__libc_csu_init) ?— push r15
RSP 0x7fffffffdd20 ?— 'aaaa.%p.%p.%p.%p.%p.%p.%p'
RIP 0x555555554747 (main+157) ?— call 0x555555554580
栈结构:
00:0000│ rdi rsp 0x7fffffffdd20 ?— 'aaaa.%p.%p.%p.%p.%p.%p.%p'
01:0008│ 0x7fffffffdd28 ?— '%p.%p.%p.%p.%p.%p'
02:0010│ 0x7fffffffdd30 ?— '.%p.%p.%p'
03:0018│ 0x7fffffffdd38 ?— 0x70 /* 'p' */
04:0020│ 0x7fffffffdd40 ?— 0x0
由于64位程序先使用rdi、rsi、rdx、rcx、r8、r9寄存器作为函数参数的前六个参数,多余的参数会依次压在栈上,因此前6个输出的为寄存器中的值(aaaa 看做是格式化字符串参数),格式化字符串前8个字节作为参数6。
泄露内存
泄露栈变量内存
泄露栈变量的值
获取栈中被视为第
n
+
1
n+1
n+1 个参数的值:%n$x
注意:%x其实只是%d的16进制输出,对应的是32位也就是4字节;在64位操作系统下,只会截区后32位;%p和系统位数关联没有问题,因此建议用%p。
泄露栈变量对应对应地址的内容
获取栈中被视为第
n
+
1
n+1
n+1 个参数对应地址的内容:%n$s
泄露任意地址内存
获取地址addr对应的值(addr为第k个参数):addr%k$s
覆盖内存
注意:覆盖内存只能覆盖某地址对应的内存,而不是第几个参数,对于开启ASLR的程序覆盖栈上某个值要事先泄露栈地址。
pwntools生成payload
对于格式化字符串payload,pwntools也提供了一个可以直接使用的类Fmtstr,具体文档见http://docs.pwntools.com/en/stable/fmtstr.html,我们较常使用的功能是
fmtstr_payload(offset, {address:data}, numbwritten=0, write_size='byte')
offset 表示格式化字符串的偏移numbwritten 表示已经输出的字符个数write_size 表示写入方式,是按字节(byte)、按双字节(short)还是按四字节(int),对应着hhn、hn和n,默认值是byte,即按hhn写。
注意:部分题目会限制时间,导致pwntools生成的payload失效。一般这一类题目可以通过仅修改低地址等操作减小输出长度,这时需要手动构造payload。
手动构造payload
覆盖小数字
对于小于机器字长的数字,如果把地址放在格式化字符串前面会使得已输出字符个数大于数字大小,因此要将地址放在后面。
以数字2为例:aa%k$n[padding][addr]
覆盖大数字
直接一次性输出大数字个字节来进行覆盖时间过长,因此需要把大数字拆分成若干个部分,分别进行覆盖。比如hhn 按字节写或hn 按双字写。
以hhn 写入32bit数为例,payload形式为:[addr][addr+1][addr+2][addr+3][pad1]%k$hhn[pad2]%(k+1)$hhn[pad3]%(k+2)$hhn[pad4]%(k+3)$hhn
|