漏洞概述
- 在C语言中,常用的输出函数有printf、fprintf、vprintf、vfprintf、sprintf等。对于这些输出函数,Format String是其第一个参数,一般称之为格式化字符串
- 格式化字符串在输出函数中的解析过程:printf接受变长的参数,其中第一个参数为格式化字符串,后面的参数在实际运行时将与格式化字符串中特定格式的子字符串进行对应,将格式化字符串中的特定子串,解析为相应的参数值。
-
格式化字符串漏洞原理
:格式化函数是一种变长参数函数,后面的参数需要根据栈的参数传递来进行释放,x86的参数全在栈上,x64的参数从第4个开始放在栈上。这些格式化函数遇到格式化说明符的关键字符之后,会按照传参规则去寻找参数来进行替换或者修改,并不会关心真实的传参情况。所以如果实际参数数量小于所需的参数数量,则其依然会将对应位置的数值当成参数进行转换,从而引发格式化字符串漏洞。 - 格式化字符串漏洞既能泄漏信息、又能修改信息,功能比较强大。
接下来,调试2段代码,来初步理解这个漏洞
代码1
#include <stdio.h>
int main()
{
char *name="prettyX";
printf("My name is %s");
return 0;
}
采取如下的编译命令
gcc -no-pie -fstack-protector-all -m32 -o printf printf.c
上述代码,在编译时,会报一个Warning
下面开始调试
gdb printf
disass main
经过前期对齐、将Canary压入栈的操作,函数正式开始运行
我们在黄框处,设置断点(b *0x080484ae),并运行(r)
在黄框处,调用了printf函数,那么在调用之前,需要压入参数
n单步执行,查看此时的edx内容
在下图中,下一步即调用printf函数,观察此时栈中的参数
观察上面源码,printf函数是这样写的
printf("My name is %s");
而正常的逻辑是
printf("My name is %s",name);
即,name作为最右侧参数先入栈,接着是“My name is %s”入栈
printf函数也会按照这个逻辑,将对应位置的数值按照指定的格式输出
在这里,就是将 esp+4 位置的数值,当做name变量的值
再来回顾一下,该程序正常执行的结果
下面,我们要做的是,将上上图中,esp+4 位置的值修改,修改为 esp+18 处的值
c继续执行,查看运行结果
可以看到,运行结果符合我们的预期
代码2
#include <stdio.h>
#include <stdlib.h>
int main()
{
char str[0x10];
read(0,str,0x20);
printf("PrettyX : %s");
}
同样使用上面的编译命令
gcc -no-pie -fstack-protector-all -m32 -o printf2 printf2.c
gdb printf2
disass main
同样,我们将断点设置在将Canary压入栈之后,即 main+40 处,r
部分操作和上面例子相同,这里就不再一一截图
从上图,黄框处,可以看到,是将read函数的3个参数依次从右至左压入栈中
我们n单步执行
read函数,从命令行中输入,输入串“lovesmile”,发现串已经读入
在stack区,一个是在read函数中的值,一个则是在main函数中的值
接下来
add esp,0x10 恢复栈
sub esp,0xc 又为栈开辟空间
我们n单步执行,观察栈中变化
发现运行过main+59后,此时,esp指向的值,为我们当时read函数栈中的值
即,通过esp的加减调整栈帧,但是栈中数据并未发生变化
继续运行2步
运行后,我们发现,将printf函数的格式化部分加入栈中
在栈中的位置也是很巧合,继续运行会发生什么?
确实将串打印出来了
好了,这两段代码的调试今天就先到这里了
通过对两段简单的代码调试,基本了解格式化字符串的漏洞原理,并熟练了gdb的操作
下两节将针对格式化字符串漏洞做更深入的学习
这一系列的文章,是对自己的学习的一个记录,其中难免有错误,多批评指正,感谢
加油
参考
Roger师傅的课程
《CTF特训营》