Format String Attacks

本文不对原文进行逐字句的翻译,只保留原文基本原理部分,加了一点自己的理解,希望能讲清楚。写了另外一篇博客,记录和格式化字符串攻击相关的小实验: http://blog.csdn.net/aqifz/article/details/49704287。

原文信息:作者Tim Newsham ,2000.9. 下载地址:http://forum.ouah.org/FormatString.PDF

摘要
讨论格式化字符串漏洞的原因和影响,结合实际的例子阐述原理。
介绍
本文介绍格式化字符串攻击的方方面面,让不了解的人有一个基本的认识。
什么是格式化字符串攻击?
    格式化字符串漏洞和很多其他安全漏洞一样,来自于编程者的偷懒。比方说,编程者正在写代码,其任务是打印一个字符串或是将它复制到缓冲区,本应该写成类似于printf("%s", str);的语句,但他懒得去打6个字符,写成了 printf(str);是啊,何乐而不为呢?为什么要费心注意多余的printf参数,还要费时去解析这些麻烦的格式? 不管怎样 printf的第一个参数都是一个要打印的字符串!然而编程者并不知道他打开了一个安全漏洞,使得攻击者可以控制程序的执行,这就是格式化字符串漏洞的根本原因。
编程者到底做错了什么?他传了一个希望原样打印的字符串,但是printf将这个字符串解析成了格式化字符串,然后扫描特殊的格式符(比如%d之类的),找到之后从栈中获取一个参数。显而易见的后果是攻击者可以通过打印栈中的值窥探到程序内存,而不太容易发现的是攻击者可写入任意值到程序的内存中。
Printf——学校忘记告诉你的东西
    假定你已经知道了printf的一些基本特性,这里介绍 和格式化字符串攻击相关的一些特性。
    格式化字符串中任何点都可以获取当前字符输出的数目,当解析到%n时,在%n之前所输出字符的个数将会写到对应的参数中,比如说获取两个格式化数之间的位置偏移:
int pos, x = 235, y = 93;
printf("%d %n%d\n", x, &pos, y);
printf("The offset was %d\n", pos);
    注意,%n格式符返回的是应该已经输出的字符个数,而不是实际输出的字符个数。当格式化一个字符串到固定大小的缓冲区中去时,输出的字符串会被截断,尽管如此,%n返回的仍是如果字符串不被截断的情况下的偏移。举例来阐明,下例将输出100而不是20。
char buf[20];
int pos, x = 0;
snprintf(buf, sizeof buf, "%.100d%n", x, &pos);
printf("position: %d\n", pos);
一个简单的例子
    通过下面的具体示例来说明格式化字符串的原理。
/*
 * fmtme.c
 * Format a value into a fixed-size buffer
 */
#include 
int
main(int argc, char **argv)
{
 char buf[100];
 int x;
 if(argc != 2)
 exit(1);
 x = 1;
 snprintf(buf, sizeof buf, argv[1]);
 buf[sizeof buf - 1] = 0;
 printf("buffer (%d): %s\n", strlen(buf), buf);
 printf("x is %d/%#x (@ %p)\n", x, x, &x);
 return 0;
}
    首先,这段代码的目的很简单:将命令行传入的值格式化到一个固定长度的缓冲区中,对长度进行了限制以免溢出,对格式化字符串格式化后进行输出,第二个整数值被设置然后输出,这个变量稍后将作为攻击的目标,但现在它的值一直是1。注意, 本文的所有例子都是在x86 BSD/OS 4.1 box上运行,不同的系统可能会有所不同。
(snprintf的函数原型是:int snprintf(char *str, size_t size, const char *format, ...),格式化字符串后面应该还有具体参数。正常的调用应该是snprintf(buf, sizeof buf, "%s",argv[1]); )
FORMAT ME!
    现在尝试一些攻击,开始使用一些正常的参数调用程序,如下。这没什么特别的,程序格式化字符串到缓冲区中去然后打印出来长度和值,然后打印出x的十进制/16进制值以及存储的地址0x804745c。
    然后传入一些格式符,比如打印出栈中的整数:
    对程序进行快速分析就可以得到main函数在调用snprintf函数时程序的栈布局:
Format String Attacks_第1张图片
(按照x86的栈结构,栈从高地址往低增长,先是返回地址,保存的ebp,再可能是保存的其他寄存器以及canary,然后函数中分配局部变量空间,然后是子函数snprintf参数,传参时按从右往左的顺序一次压栈。表中依次即buf地址,sizeof buf,argv[1]指针,x,buf缓冲区。这里没有给出各格式符%x应该对应的参数,但是snprintf并不知道,只是往高地址挨个读取栈内容当作参数。)
    前面测试输出的4个值是栈中格式化字符串后面的4个参数:变量x,从未初始化的buf变量中取出的3个整数。
    重点来了,作为攻击者,我们控制了缓冲区中的值。这些值也可以用作snprintf的参数,通过一个快速的测试来验证:
    前4个a字符被复制到了缓冲区的开头,然后被snprintf解析成了整数参数,其值为0x61616161(a的ascii值)。(原本是一般的字符串复制,因此"aaaa "被复制到buf中,然后snprintf解析到两个%x,因为 前面已经有三个参数,这两个%x格式符分别要取对应 第4、5个参数,因此读到了x和buf缓冲区前4个字节作为这两个参数,此时buf前4个字节已经填充了"aaaa",然后1和"aaaa"的16进制表示接在"aaaa "字符后写入到buf中。)
确切的点
    现在从被动地探测转为积极地改变程序状态,还记得变量x吗?下面来改变它的值,需要通过一个参数来进入其地址,这里将跳过第一个参数:变量x,然后使用%n格式符写指定的地址。(这里使用PERL执行程序,在命令行参数中放置任意的字符)
(这里的x地址发生了变化,不清楚是不是因为地址随机化的作用,还是作者修改了程序代码导致布局略有变化。)
x的值变化了,到底发生什么?snprintf的参数实际如下:
snprintf(buf, sizeof buf, "\x58\x74\x04\x08%d%n", x, 4 bytes from buf);
    开始snprintf将第一个4字节复制到buf(\x58\x74\x04\x08打印出来应当是4个乱码),然后扫描%d格式符并打印出x的值,最后到了%n格式符,将栈中下一个值即buf的前4个字节作为参数,这四个字节已经被填充为"\x58\x74\x04\x08"(解析为整数即0x08047458),snprintf将已经输出的字节数写入这个地址。而这个地址就是x的地址,这并不是巧合,我们通过事先检查程序选择了0x08047458值。这个案例中,程序有助于打印出我们想得知的地址。更典型的是,这个值能通过调试程序得知。
    现在可以选择一个任意的地址并写入值(近乎是任意的,只要这个地址不包含NUL字符)。我们能否写入有用的值?snprintf目前只能写入字符的数目,如果我们想写一个小的值(>4,至少目的地址要占4字节),解决方法比较简单:填充格式化字符串直到正确的值。如果是更大的值呢?可以利用一个特性:在没有截断时,%n计算的是应该输出的字符数目:
    %n写入x的值为504,远远大于实际写入buf的99字符数,因此可以通过指定一个大的域宽来写入任意的大值(这在几个版本的glibc printf中有一个实现缺陷,当指定大的域宽时,printf将会下溢内部缓冲区导致程序crash。因此,在某些版本的Linux中,攻击程序不能使用大于几千的域宽。比如说printf(“%.9999d”, 1);将会导致段错误。)。那小值呢?可以通过拼接几次写操作来构造任意值(甚至是0),如果我们以字节为偏移写4个数,可以通过四个最不重要的字节构造任意整数。为了说明情况,考虑一下四次写:
Format String Attacks_第2张图片
    在4次写完成后,0x44332211留在了地址A中,由4次写的最不重要字节组成,这个技术可以灵活选择写入的值,但是有些缺点:需要四次写来设置值,覆盖了目标地址相邻的4个字节,而且需要3次非对齐的写操作。有些架构并不支持非对齐写,因此这个技术不能广泛应用。
那又怎样?
    你问那又怎样?你可以写任意的值到内存中任意地址!无疑可以好好利用它:
    改写程序存储的UID,用于降低和提升特权。
    改写执行的命令。
    改写返回地址,指向包含shellcode的缓冲区。
    简单来说:你拥有了程序。
那么,今天学到了什么?
    printf比你预想的要强大。
    偷工减料永远不会有好报的。
    一个看起来无意的疏漏可以给攻击者提供足够的攻击手段来毁了你的一天。
    有足够的空闲时间、精力、一个输入字符串,你可以让某人的小错误变成一个全国联合新闻故事。

你可能感兴趣的:(Articles)