前几天去Intel面试时,遇到了一个问题:printf("%s", s)
与printf(s)
有何区别?面试官还提示我从安全的角度回答这个问题,然而当时并没有想出答案来……:( 回来后仔细研究了下这个问题,才发现pritnf(s)
这种写法是存在严重安全漏洞的,这被称为printf格式化字符串漏洞攻击。
printf
函数支持不定参数的原理在此不多说,可参考stdarg.h
头文件的相关介绍。核心就是,函数使用栈传递参数,且根据cdecl函数调用约定,压栈顺序是从最后一个参数开始逆序进行的,而printf
函数的第一个参数是确定的字符串,故可以从这个字符串中判断出栈中还有多少个数据,每个数据的类型是什么,进而依次取出各个数据。
正常情况下,格式化字符串中格式化字符%
是和之后的参数一一对应的,然而我们可以考虑一种情况,若格式化字符%
比参数多会怎么样呢,答案是,会取到栈中其他的数据。这就是printf(s)
这种写法有问题的原因了,这里使用s
而不是一个字符串字面常量,而s
传入什么内容其实是不可控的,若传入字符中存在%
,就会输出栈中其他一些内容。要是s
还是可以由外部输入的,那就可以通过巧妙的构造s
的形式来实现访问栈中本来没有权限访问的内容,这就是所谓的格式化字符串漏洞攻击。
下面来看一个实际的例子:
1 | int main() { |
这段程序中由用户输入一个整数,存放在security
这个变量中,之后再输入一个字符串str
,并使用printf(str)
这样的形式将其打印出来。针对这个例子,我们可以构造一个特定的输入字符串将security
的内容给打印出来。
使用GCC 4.9.3编译以上程序,优化等级-O2
,得到的程序用objdump
反汇编一下,可以找出对应的汇编代码为:
1 | 00408350 <_main>: |
408350
40835a
部分为GCC自动生成的main
函数入口处代码,此处不用管它;40835f
408372
为第一个scanf
函数,读入security
;408377
408382
为第二个scanf
函数,读入str
;408387
40838a
调用printf
输出str
。根据反汇编结果,我们可以画出408372
行时栈的结构:
scanf
函数反向压栈,故%esp
指针指向的元素$0x40a0c0
应该就是字符串字面量"%d"
的首地址,而%esp+0x04
就是栈中的下一个元素,这就是security
变量的地址,从汇编代码中可以看到,这个地址是一个%esp
的偏移量,%esp+0x18
。由此,我们可以分析出security
在栈中的位置,而之后%esp
指针没有改变,故我们可以构造以下这个特殊的字符串:%d%d%d%d%d_____security_is:%d
,这个字符串会连续取出栈中的前6个int
,第6个int
的地址就是%esp+0x18
,这正是security
变量的地址。
实际运行程序测试一下:
可以看到,我们成功读到了security
的内容。
为了预防这个问题,我们需要确保在使用printf
函数时第一个参数必须是一个字符串字面量。其实,以上写法编译器也会产生warning:format string is not a string literal (potentially insecure).
最后,其实配合一个比较少用的printf
参数%n
还可以利用此漏洞实现对栈中内容的修改,在此就不展开了,可参考第三篇参考资料。
参考资料:
printf(string) vs. printf(“%s”, string)
What is the underlying difference between printf(s) and printf(“%s”, s)?
格式化字符串漏洞简介