64位格式化字符串漏洞


#何为格式化字符串漏洞

格式化字符串具有如下形式,之后我们会频繁的使用该形式中的名称: 如格式化字符串 %7\(-8.7Lf 从左到右来看,7\)意思是输出第7个参数;‘-’的意思是左对齐(缺省情况下是右对齐);8的意思是输出占据8个字符的位置;.7声明精读为保留7位小数;L对应length,指对于浮点类型来说,printf期待一个long double类型的数据;最后的f则是指数据类型为浮点数。对于格式化字符串漏洞来说,最需要关注的组成部分是parameter部分。 格式化字符串漏洞通常出现于程序员在printf一族的函数中采用形如下面的使用方式:

1
2
scanf("%s", name);
printf(name);

这种写法看似是将用户的输入输出了一遍,但实际上当用户输入格式化字符串时,printf函数会试图寻找并不存在的参数(这属于一种未定义行为),而printf函数参数的存放位置又遵循着一定规律,因此产生了漏洞。漏洞的利用可以分为内存泄露和指定位置写两种。

#基本思路

我们首先需要了解printf函数存放其参数的规律。在GCC编译为64位可执行文件的情况下,printf函数的前六个参数分别存放在rdi,rsi,rdx,rcx,r8,r9寄存器中,其余的参数与32位类似,按照从右到左的顺序存放在栈中,这就为我们泄露栈中的内容提供了机会。上面提到,parameter n\(可以指定输出第几个参数,因此只需指定n的值,我们就可以获得栈中特定位置的内容。 然而仅仅是读还不够,格式化字符串漏洞指定位置写的利用方式是通过一个特殊的type实现的,即n。n不同于f,d等用于读取的type,n的作用是将已经成功输出的字符个数写入对应的整型指针参数所指的变量。因此结合parameter n\),field width以及type n我们可以实现任意位置写,但在具体的操作中也有一些需要注意的点,下面将用例子来说明。

#内存泄露

来看一个简单的程序:

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main(int argc, char * argv[])
{
char a[1024];
memset(a, '\0', 1024);
read(0, a, 1024);
printf(a);
return 0;
}

使用GCC编译为名为vuln的可执行文件后,我们使用一个shell脚本来将此程序运行在本地的10001端口上:

1
2
3
4
#!/bin/sh
while $True; do
nc -l -p 10001 -e vuln
done

下面两张图片分别显示了调用printf函数时的实际栈结构和对于printf函数的参数来说时的栈结构

可以看到,对于printf函数来说,第8个参数的位置刚好是原本read函数存储输入的字符串的位置。那是不是意味着我们可以利用格式化字符串 %8$llx 来泄露我们输入的字符串呢?我们可以试验一下:

可以看到,实验结果很成功,但这还不够,有没有一个办法可以输出任意位置的内存呢?这时就应该想到指针了,进而想到与指针有联系的type,即s。经过简单的构造,我们得到下面这个可以输出addr处内存(直到\x00为止)的字符串:addr%8$s。 现在假设题目中没有给出可执行文件,只告诉我们可以从127.0.0.1:10001访问到该程序,且程序已关闭PIE。那么如何利用格式化字符串漏洞将可执行文件dump下来呢?

64位可执行文件由于其庞大的虚拟地址空间,在开启PIE后很难泄露特定位置处的内存,然而对于32位可执行文件来说,可以采用爆破的方式避免PIE的干扰。实际上,PIE仅仅是将内存页随机化,一般内存页的大小是0x1000字节,即地址的后三位是没有被随机化的。


← Prev 《程序员的自我修养》- 虚拟地址空间 | 未定义行为:一个printf函数引发的思考 Next →