站长中国
设为首页 | 站长论坛

站长论坛 站长下载
您所在的位置: 站长中国 > 站长学院 > 安全技术 > 其他相关 >  正文

格式化字符串攻击(Format String Attacks)
  2007年10月15日03:55:54  评论(0条) 字体:[ ]
相关热点:

内容: 
介绍 
什么是格式化字符串攻击? 
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 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机器上完成。如果你到莫桑比克执行任务超过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);


责任编辑:

收藏本文 打印 打印本文  推荐本文 告诉好友 投稿 投稿邮箱
    评论加载中…

站长排行

学院

新闻

专栏

盈利

[揭密网络黄链]中国留学生买凶专破日本
JSP语法(6)
超强弹出窗口代码,什么都挡不住
FLASH视觉特效实例之地震效果
贴吧发帖机使用教程(绝对原创)
关于数据分页(转自www.codeproject.co
ASP实现文件直接下载
Photoshop制作光感超酷效果水晶球
 遍历ASP.NET页面控件
永远的后门[经典]+查不出的后门
淘宝网卖家公然叫卖“艳照门”照片集
驳《百度Hi面世对腾讯有利》
Google绿色专家质疑黑色背景网页节省资
国内各IT企业办公环境揭秘(多图)
阿里妈妈广告卖主全攻略
站长创业源动力 主流站长站赏析
推荐阅读:80年小子的创业道理
Discuz!6.0猛将出击 最强论坛程序酷炫
我的网络,我的团队:专访李文明
百度新闻频道改版十天 流量止跌反弹翻
ECSHOP模板制作参考文档
悬挂阿里妈妈会否被百度惩罚
阿里妈妈是否是中小站长的救世主?
最强网店ECShop发新版 众多酷炫功能给
ECSHOP模板下载
土豆网,优酷网,爆米花等视频网站采集
DedeCms模板安装/制作概述
网上商店系统巅峰对决 ECShop vs ShopE
艰难的走在创业的路上 第一天
编程中国全站采集规则
性福联盟 一个不尊重站长的联盟
大脚:日赚100元—揭露最新firefox欺骗
大脚:垃圾站超级赚钱法之二—突破“站
大脚:垃圾站超级赚钱法之——前言
迅雷联盟、快车联盟收入对比
经理人必看的十个管理网站
Google Adsense的秘密 第二版
西联汇款兑付城市查询
不用SEO取得成功的10个步骤
关于做GOOGLE的五条经验
站长学院  网页设计 建站教程 图形图象 网络编程

Photoshop CS3
Photoshop CS3
不用Photoshop
不用Photoshop

DIV+CSS的开发方式 听听另外的
虚拟主机建站动易里快速生成的
VBScript特效代码 满屏幕乱跑
牛气!一个菜鸟站长的超强网站
创建、维护一个个人博客的“投
让网站流量稳步飙升的秘籍
网站推广的基本思想

新闻线索

如果你有站长界人事变动、重组并购、变革技术出现,以及产品投诉等重要新闻线索,请告诉我们,我们会给予特别关注。
0631-3653338
站长中国编辑部
站长中国24小时新闻热线: 13256307008