格式化字符串漏洞是printf函数比较常见的漏洞,常见的漏洞形式就是printf(s);如果s字符是我们可控的话,那就会导致任意位置读和写等严重的后果。

首先如下代码段

1
2
3
char s[100];
gets(s);
printf(s);

逻辑看上去十分简单,就是输入什么就输出什么。避免这类漏洞的方式也很简单,固定第一个参数就可以了,但是现在第一个参数我们可控的话就要介绍一些平时用不到的格式化字符串的标识符了。

%d %f %x %s这些烂大街的相信都不用讲也会,讲讲其它的标识符

%p:输出第n个参数的十六进制值,目前测试与%x不一样的地方就是输出会带0x,并且%后面带的整型参数表示第几个参数而非参数长度。

%n:将已打印字符串长度输入到下一个参数所表示的地址去,%后面带的整型参数表示要输出的第几个参数。

注:以上结论皆是在Ubuntu18.0.4环境下使用gcc编译器得到的

那么通过以上两个较生僻格式化的标识符就可以达到任意读和任意写了。

%p达到任意读的目的

现在有以下测试程序

1
2
3
4
5
6
7
8
9
10
#include<stdio.h>
int main(){
char buf[30];
while(buf[0]!='\0'){
gets(buf);
printf(buf,1,2,3,4,5);
printf("\n");
}
return 0;
}

由于我们在64位的环境下编译,所以刚好给6个参数占满寄存器,那么此时我输入一个%6$p就可以直接输出%6$p这个字符串所对应的long的值(因为64位栈的宽度为8字节,所以被迫输出long型整数)

这里可以很清楚看到70243625就是字符串%6$p小端输出的结果,如果量一下缓冲区到rbp的距离我们还可以把栈的地址泄露出来,这就是任意读,地址一旦泄露那么可以做的事情就非常多了,如果加了canary甚至可以无视canary溢出。

%n达到任意写目的

我们的测试程序是这样写的

1
2
3
4
5
6
7
8
9
10
11
#include<stdio.h>
int main(){
char buf[30];
int a=0,b=0,c=0,d=0,e=0;
while(buf[0]!='\0'){
gets(buf);
printf(buf,&a,&b,&c,&d,&e);
printf("\n%d %d %d %d %d\n",a,b,c,d,e);
}
return 0;
}

在这个测试程序略微化简了一点,要修改的变量地址直接传参进去了,在实际攻击的过程中可不会这么给你提供,你需要算偏移然后将地址给格式化字符串定点修改某个变量的值,这就是所谓的任意写。

这里可以看到,通过我的输入成功改变了三个变量的值

这就是格式化字符串漏洞的利用。。