游戏开发论坛

 找回密码
 立即注册
搜索
查看: 5009|回复: 5

格式化字符串攻击[转]

[复制链接]

16

主题

172

帖子

177

积分

注册会员

Rank: 2

积分
177
QQ
发表于 2003-10-31 16:25:00 | 显示全部楼层 |阅读模式
昨天无意中发现一篇文章不错 值得看...希望大家能够喜欢
[em7] [em4] [em7] [em4]

什么是格式化字符串攻击?

Printf-学校忘记教给你的东西
简单的例子
来格式化吧!(format Me!)
X MARKS THE SPOT(X是本文示例程序中我们试图重写的一个变量)
怎么着(So what)?

摘要
本文讨论格式化字符串漏洞的成因和含义,并给出实际的例子来解释原理。

介绍
我知道在某些时候对于你我和我们大家而言,下面这种情况总会发生。在一个时下流行的晚餐会上,夹杂在同事们大呼小叫的声音里,你听到了"格式化字符串攻击"这只言片语。
"格式化字符串攻击?什么是格式化字符串攻击?"你心说。由于害怕在同事们面前显露出自己的无知,你决定停止不自然的微笑,而频频点头以示自己对这玩艺了如指掌。如果一切顺利,大家会共饮鸡尾酒,谈话仍将继续,但是没人明白这究竟是怎么回事。现在不用再害怕什么了,本文会提供你想知道而又不好意思问的所有内容。


什么是格式化字符串攻击?
格式化字符串漏洞同其他许多安全漏洞一样是由于程序员的懒惰造成的。当你正在阅读本文的时候,也许有个程序员正在编写代码,他的任务是:打印输出一个字符串或者把这个串拷贝到某缓冲区内。他可以写出如下的代码:

printf("%s", str);
但是为了节约时间和提高效率,并在源码中少输入6个字节,他会这样写:
printf(str);
为什么不呢?干嘛要和多余的printf参数打交道,干嘛要花时间分解那些愚蠢的格式?printf的第一个参数无论如何都会输出的!程序员在不知不觉中打开了一个安全漏洞,可以让攻击者控制程序的执行,这就是不能偷懒的原因所在。

为什么程序员写的是错误的呢?他传入了一个他想要逐字打印的字符串。实际上该字符串被printf函数解释为一个格式化字符串(formatstring)。函数在其中寻找特殊的格式字符比如"%d"。如果碰到格式字符,一个变量的参数值就从堆栈中取出。很明显,攻击者至少可以通过打印出堆栈中的这些值来偷看程序的内存。但是有些事情就不那么明显了,这个简单的错误允许向运行中程序的内存里写入任意值。

Printf-学校忘记教给你的东西
在说明如何为了自己的目的滥用printf之前,我们应该深入领会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 values into a fixed-size buffer
*/
#include <stdio.h>
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机器上完成。如果你到莫桑比克执行任务超过20年时间可能会对x86不熟悉,这是一个little-endian机器。这决定在例子中多精度数字的表示方法。在这里使用的具体数值会因为系统的差异而不同,这些差异表现在不同体系结构、操作系统、环境甚至是命令行长度。经过简单调整,这些例子可以在其他x86平台上工作。通过努力也可以在其他体系结构的平台上工作。

来格式化吧!(format Me!)
现在是我们戴上黑帽子开始以攻击者方式思考问题的时候了。我们现在手头有一个测试程序。知道这个程序有一个漏洞并且了解程序员是在哪里犯错误的(直接把用户输入的命令行参数作为snprintf的格式化参数)。我们还拥有关于printf函数深入的知识,知道如何运用这些知识。让我们开始修补我们的程序吧。

从简单的开始,我们通过简单的参数调用程序。看这儿:
% ./fmtme "hello world"
buffer (11): hello world
x is 1/0x1 (@ 0x804745c)
现在这儿还没有什么特别的事情发生。程序把我们输入的字符串格式化输出到缓冲区里,然后打印出它的长度和数值。程序还告诉我们变量x的值是1(以十进制和十六进制分别显示),x的存储地址是0x804745c。

接下来我们试着使用一些格式指令。在下面的例子中我们打印出在格式化字符串之上栈堆中的整型数值:
% ./fmtme "%x %x %x %x"
buffer (15): 1 f31 1031 3133
x is 1/0x1 (@ 0x804745c)
对这个程序的快速分析可以揭示在调用snprintf函数时程序堆栈的规划:
Address Contents Description
fp+8 Buffer pointer 4-byte address
fp+12 Buffer length 4-byte integer
fp+16 format string 4-byte address
fp+20 Variable x 4-byte integer
fp+24 Variable buf 100 characters
(补充:我参考了"缓冲区溢出机理分析"一文,才看明白上面的内容。简单介绍一下:当程序中发生函数调用时,计算机做如下操作:首先把参数压入堆栈;然后保存指令寄存器(IP)中的内容做为返回地址(RET);第三个放入堆栈的是基址寄存器(FP);然后把当前的栈指针(SP)拷贝到FP,做为新的基地址;最后为本地变量留出一定空间,把SP减去适当的数值。
----------------------------------------------------------------------

当调用函数snprintf ()时,堆栈如下:
低内存端 高内存端
函数局部变量 sfp ret buf sizeof(buf) argv[1] x和buf
<- [ ] [ ] [ ] [ ] [ ] [ ] 数据区
栈顶 栈底

前一个测试运行结果的四个输出值(1 f31 1031
3133)是在格式化字符串后面堆栈中接下来的四个参数:变量x和3个4字节整型(未经初始化)。
现在该主角出场了。作为一个攻击者,我们要控制储存在缓冲区中的变量。这些值也是传递给snprintf调用的参数!让我们看看这个测试:
% ./fmtme "aaaa %x %x"
buffer (15): aaaa 1 61616161
x is 1/0x1 (@ 0x804745c)
耶!我们提供的这四个'a'字符被拷贝到buffer的起始处,然后被snprintf作为整型参数解释成0x61616161 ('a' is
0x61 in ASCII)。

X MARKS THE SPOT
所有的工作准备就绪了,是时候把我们的攻击从被动探测转为主动改变程序的状态了。还记得变量"x"吗?让我们试着改变它的值。为了完成这个任务,我们必须跳过snprintf的第一个参数,它就是变量x,最后使用%n格式写入我们指定的地址。这听起来比实际情况复杂。用一个例子可以解释清楚。

【注意:我们在这里使用PERL来执行程序,这可以让我们方便地在命令行参数中放置任意字符】:
% perl -e 'system "./fmtme", "\x58\x74\x04\x08%d%n"'
buffer (5): X1
x is 5/x05 (@ 0x8047458)
x的值被改变了,但是究竟发生了什么?传给snprintf的参数看起来如下所示:
snprintf(buf, sizeof buf, "\x58\x74\x04\x08%d%n", x, 4 bytes from
buf)
起先snprintf把头四个字节拷入buf。接下来扫描%d格式并打印出x的值。最后遇到%n指令。这个指令从栈堆中取出下一个值,该值来自buf的头四个字节。这四个字节是刚才填入的"\x58\x74\x04\x08",或者解释成一个整型0x08047458。Snprintf然后写入到目前为止输出的字节数目,5,到

这个地址(0x08047458)。这个地址就是变量x的地址。这不是巧合。我们通过先前对程序的检查仔细选择了数值0x08047458。在这里,程序打印出我们感兴趣的地址是十分有帮助的。更普遍的情况是这个值要通过debugger的帮助来获取

好棒耶!我们可以选取任意地址(几乎是任意地址;长度和不带NULL字符的地址一样长)并且可以写入一个值。但是我们能写入一个有用的值吗?snprintf仅能写入到目前为止输出的字符数目。如果我们想要写入一个比四大的小值,解决方法很简单:按照实际需要的数值填充格式化字符串

直到我们得到正确的值。但是如果是大数值怎么办?这里我们可以利用一个事实:%n会计数不考虑截短情况应该输出的字符个数:
% perl -e 'system "./fmtme", "\x54\x74\x04\x08%.500d%n"
buffer (99): %0000000 ... 0000
x is 504/x1f8 (@ 0x8047454)
%n写入x的值为504,比buf的长度限制99要长多了。我们可以通过指定一个大的域宽值[1]
(field
width)提供任意大的值。但是对于小值怎么办呢?我们可以通过多次写入的组合来构造任意数值(甚至是0)。如果我们每次以一个字节的偏量写出四个数字,我们可以构造任意整数而不仅限于至少四个字节(地址通常用四字节表示)。为了说明这一点,考虑下面的四次写操作:

Address A A+1 A+2 A+3 A+4 A+5 A+6
Write to A: 0x11 0x11 0x11 0x11
Write to A+1: 0x22 0x22 0x22 0x22
Write to A+2: 0x33 0x33 0x33 0x33
Write to A+3: 0x44 0x44 0x44 0x44
Memory: 0x11 0x22 0x33 0x44 0x44 0x44 0x44
在四次写操作完成后,整型值0x44332211留在地址为A的内存中。由四次写入操作的有效字节构成。这个技术使得我们更灵活地选择数值写入,但是这种方法是有缺点的:赋一个值要用四次写操作。而且会覆盖目标地址临近的三个字节。它还要进行三次非对齐的写操作,这项技术并不是通用的。


怎么着(So what)?
So what? So what!? SO WHAT!#@??
你可以向内存中的任意地址写入任意值(几乎是任意的)!!!你肯定可以想出利用这一点的好方法。让我们看看
* 覆盖一个程序储存的UID值,以降低和提升特权
* 覆盖一个执行命令
* 覆盖一个返回地址,将其重定向到包含shell code的缓冲区中
更通俗地讲:你拥有这个程序(为所欲为)
今天我们都学到了什么?
* printf 比你以前想象的功能更强大
* 抄近路从来都是没有回报的(raphaelzl(小飞熊))
* 一个看起来很微小的错误会给攻击者一个有力的杠杆用来毁掉你的生活(raphaelzl
(小飞熊))
* 拥有足够的时间、努力和一个复杂的输入字符串,你可以把某人的简单错误变成全国性的新闻事件

[1]
在某些版本的glibc中printf的实现有缺陷。当指定一个大的域宽时,printf会导致一个内部缓冲区的下溢出(?underflow)并且导致程序崩溃。因此,在某些版本的linux下不可能使用大于几千的域宽值来攻击程序。例如:下面的代码会在有这个缺陷的系统上导致segmentation

fault:
printf("%.9999d", 1);  

59

主题

1104

帖子

1199

积分

金牌会员

Rank: 6Rank: 6

积分
1199
发表于 2003-10-31 18:12:00 | 显示全部楼层

Re:格式化字符串攻击[转]

呵呵,其实要演示没这么复杂的。。
举个简单的例子。
#include <stdio.h>
int function(int a, int b, int c) {
     char buffer[14];
     int sum;
     int *ret;
     // 如果是在gcc 3.2.2版本下,这里则应该是:ret = buffer + 28;
     ret = (int*)(buffer + 20);
     (*ret) += 0xa;
     sum = a + b + c;
     return sum;
}

main()
{
    int x;
    x = 0;
    function(1,2,3);
    x = 1;
    printf("%d\n",x);
    return 0;
}
呵呵,大家会发现打印出来的结果是0,而不是1,当然,在WIN2K的环境下,运行时会报一个warning,不过别管它。

原因就是因为buffer[14]实际上占用的空间是16,在WIN2K下,先压入栈的是函数的返回地址,然后接着就是第一个本地参数了。那么这个时候访问buffer + 20,就是返回地址,然后通过看汇编代码可以发现,x = 1这一句和上一句的地址差是10,那么就是0xa。所以加上0xa,这个时候执行完function就会返回到x = 1的下一句开始执行了。

为什么在LINUX下是28呢?因为压栈的先后顺序不一样,会先把previous ebp压进栈,那个家伙有8个字节,所以说要8 + 4= 12个字节,再加上开始的16个字节,就是28了。

59

主题

1104

帖子

1199

积分

金牌会员

Rank: 6Rank: 6

积分
1199
发表于 2003-10-31 18:36:00 | 显示全部楼层

Re: 格式化字符串攻击[转]

呵呵,如果大家对这个感兴趣的话,俺就再讲讲如何通过数据溢出来运行自己的代码。

既然大家修改函数的返回地址,那么也就可以在指定的地址写入自己的代码来使其运行,实际计算机运行的都是一串0和1代码,那么如果你是在WINDOWS下的话,先用VC写好一段你想运行的代码,然后打开调试模式,打开调试汇编窗口,看到第一条语句的地址和最后一条语句的地址,然后再打开内存窗口,输入你的第一条语句的地址,然后把那段十六进制的东西拷下来,然后把函数指针的返回处写入你的代码,哈哈~想干什么都行了。。。

那么在LINUX下呢,用GDB也可以,也是先写好你的代码,编译成可执行文件,然后gdb xxxx,再敲入disassemble xxx,注,此处xxx为你的代码的函数名,然后也可以找到地址,然后再输入x /(yy)xb xxxx,注,这里的yy为要显示的字节数,xxxx为你代码的开始的地址。然后,呵呵,也可以找到那些代码了吧,然后拷贝下来,干嘿嘿嘿的事吧~HOHO。。

对了,有一点大家需要注意,就是此类攻击代码一般都是使用字符串的一些漏洞,但是呢,在字符串拷贝的时候,碰到\0就会自动终止,所以大家需要修改修改自己的代码,让其生成的最终代码中不要有0出现。

呵呵,欢迎对此感兴趣的朋友一起讨论~

16

主题

172

帖子

177

积分

注册会员

Rank: 2

积分
177
QQ
 楼主| 发表于 2003-11-1 20:44:00 | 显示全部楼层

Re:格式化字符串攻击[转]

听课中...

1

主题

40

帖子

48

积分

注册会员

Rank: 2

积分
48
发表于 2004-3-11 19:50:00 | 显示全部楼层

Re:格式化字符串攻击[转]

学习中

3

主题

23

帖子

23

积分

注册会员

Rank: 2

积分
23
QQ
发表于 2004-3-11 20:48:00 | 显示全部楼层

Re: Re:格式化字符串攻击[转]

的确,printf 和 scanf 是最难的~~我也好头疼 [em4] [em4] [em4]
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

作品发布|文章投稿|广告合作|关于本站|游戏开发论坛 ( 闽ICP备17032699号-3 )

GMT+8, 2025-2-25 19:28

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表