百科知识库-中国实用知识供应者
网站地图设为首页加入收藏
百科知识库
您当前的位置:百科知识库科学技术电脑技术网管天地浅析格式化串漏洞
知识导航
浅析格式化串漏洞

浅析格式化串漏洞 浅析格式化串漏洞

 作者:isno (isno@sina.com)


  -----------------目录-------------------

  一.前言

  二.基础知识简介

  三.格式化串漏洞原理

  (1)参数个数不固定造成访问越界数据

  (2)利用%n格式符写入跳转地址

  (3)利用附加格式符控制跳转地址的值

  (4)总结

  四.对wu-ftp 6.0格式化串漏洞的分析

  (1)问题出现在哪里

  (2)wu-ftp漏洞的利用

  五.后记

  ----------------------------------------

一.前言

  最近许多软件被发现存在格式化串漏洞,其中最著名的是wu-ftp6.0和rpc.statd,由于
相当多的网站缺省安装了这两种软件,而且网上针对这两个漏洞的攻击程序很多很好用,所
以由这两个漏洞而被攻破的网站非常之多。因此非常有必要认真研究一下格式化串漏洞,但
网上介绍格式化串漏洞的中文文章却特别少,就我知道的只有一篇warning3写的和另一篇
xuzq翻译的文章,我又参考了几篇英文文章,费了半天工夫看的头疼了才搞明白这种漏洞的
机理。

  由于那几篇文章写的较为深奥,像我这样的普通初学者看起来很费劲,我想就我的理解
写一篇浅显一点的文章,使其他像我一样的菜鸟免受头疼之苦,同时也把这篇文章作为备忘
材料,等我以后忘了再回过头来看看:-)由于本人水平有限,谬误之处再所难免,欢迎多多
指教。

二.基础知识简介

  在了解格式化串漏洞之前有必要复习一下关于堆栈的基础知识,网上介绍缓冲区溢出的
文章很多,其中大多都介绍了堆栈的知识,读者可以自行参考那些文章,我在这里只是简单
的介绍一下。

  一个程序的动态数据通过一块叫做堆栈的区域来存放。堆栈处于内存的高端,它有个特
性:后进先出。当程序中调用子函数时,计算机首先把参数依次压入堆栈,然后把指令寄存
器(EIP)中的内容做为返回地址(RET)压入堆栈,第三个压入堆栈的是基址寄存器(EBP),然后
把当前的栈顶指针(ESP)拷贝到EBP,做为新的基地址。最后把ESP减去一定的数值,用来为本
地变量留出一定空间。
 
  普通的缓冲区溢出就是利用了堆栈生长方向和数据存储方向相反的特点,用后存入的数
据覆盖先前压栈的数据,一般是覆盖返回地址,从而改变程序的流程,这样子函数返回时就跳到了
黑客指定的地址,就可以按照黑客意愿做任何事情了。
 
  格式化串漏洞和普通的缓冲溢出有相似之处,但又有所不同,它们都是利用了程序员的
疏忽大意来改变程序运行的正常流程。下面详细介绍格式化串漏洞的原理,最后对wu-ftp6.0
格式化串漏洞进行一下分析。

三.格式化串漏洞原理
 
  所谓格式化串,就是在*printf()系列函数中按照一定的格式对数据进行输出,可以输出
到标准输出,即printf(),也可以输出到文件句柄,字符串等,对应的函数有fprintf,sprintf,
snprintf,vprintf,vfprintf,vsprintf,vsnprintf等。能被黑客利用的地方也就出在这一系列
的*printf()函数中,可能有人会问:这些函数只是把数据输出了,怎么能造成安全隐患呢?
在正常情况下当然不会造成什么问题,但是*printf()系列函数有三条特殊的性质,这些特殊
性质如果被黑客结合起来利用,就会形成漏洞。

(注:以下测试环境为RedHat Linux 6.0)

#可以被黑客利用的*printf()系列函数的三个特性:

(1)参数个数不固定造成访问越界数据

  首先第一个可以被利用的性质是:*printf()系列函数的参数的个数是不固定的。拿printf()
函数举例来说,如果我们要依次输出3个整型数据和1个字符串,可以用以下程序:

#include
int main(void)
{
int i=1,j=2,k=3;
char buf[]="test";
printf("%s %d %d %d\n",buf,i,j,k);
return 0;
}

  这是正常的使用方法,程序会输出:

test 1 2 3

  这个printf()函数共有5个参数,第一个是格式化串"%s %d %d %d\n",第二个是字符串buf的
地址,%s对应buf,其后的三个%d分别对应i,j,k,这样就把数据输出了。但是如果我们减少printf()
函数的参数个数,写成这样:

printf("%s %d %d %d\n",buf,i,j);

  格式化输出符号仍然是4个,但对应的数据却只剩下3个了(buf,i,j)了,那么情况会怎样呢?
我们编译运行一下看看,这个程序输出:

test 1 2 1953719668

  我们可以清楚的看到,尽管没有给最后一个%d提供对应的数据,但是它还是输出了一个10位的
整数1953719668,这个大整数到底是什么呢?我们再修改源程序,把输出的语句改为:

printf("%s %d %d %x\n",buf,i,j);

  即按照16进制输出最后一个参数,这时输出的结果就是:

  

test 1 2 74736574

  也就是说,当没有给printf()函数的格式化串提供足够的对应参数时,printf()并没有报错,而
是把内存中某个4字节的内容打印了出来,这四个字节的内容是74736574。

  那么74736574究竟是什么玩意呢?如果你对ASCII码熟悉的话应该可以反映过来,字符串在内存
当中是以ASCII码的形式存储的,它们有如下对应关系:

十六进制 十进制字符
  74 ----------> 116--------->t
  73 ----------> 115--------->s
  65 ----------> 101--------->e

  74736574对应的字符串恰好是tset,由于字符串在内存当中是以反序排列的,74736574对应的
实际字符串应该是:test。是不是看起来有点眼熟?翻回前面再看看那个程序,对了,就是我们在程序
中定义的字符串buf[]的内容。这决不是偶然的,回忆一下前面说过的堆栈的工作流程,我们可以想象
到这个程序在堆栈中的情况:

i)调用main()函数之前首先把返回地址压栈;
ii) 然后压入的是EBP,并把ESP拷贝到EBP;
iii)把ESP减去一定的数量,也就是把堆栈扩大,给变量i,j,k,buf留出空间;
iv) 开始调用printf(),把printf()的4个参数j,i,buf和格式串"%s %d %d %x\n"依次压入堆栈;
v)压入printf()的返回地址;
vi) 压入此时的EBP;
vii)开始执行printf()。

  这时候的堆栈看起来应该是这个样子的:

栈顶栈底
  --------------------------------------------------------------------------
  | EBP | EIP | 格式串| buf地址| i | j |buf内容| \0 | k | j | i | EBP | EIP|
  --------------------------------------------------------------------------

  看到堆栈的实际内容,就不难理解为什么会打印出74736574即"test"了,printf()首先找到第
一个参数格式串"%s %d %d %x\n",然后就开始按照对应关系依次打印前面堆栈中内容,%s对应
buf地址,也就打印出了buf[]的内容,第一个%d对应i,第二个%d对应j,%x本来是应该对应k的,可是
由于我们提供给printf()的参数中没有k,而j前面正好是buf内容,所以就把buf的内容作为16进制
数输出了,也就是我们看到的74736574。可以预测,如果提供给printf()的格式串中再多几个%x的话,
printf()还会继续打印前面堆栈里的"\0"(buf的结束符),k,j,i,EBP,EIP等内容。

  说到这里,已经把产生格式化串漏洞的根源揭露出来了:因为*printf()系列函数的参数的个数
是不固定的,如果其第一个参数即格式串是由用户来提供的话,那么用户就可以访问到格式串前面的
堆栈里的任何内容了。

  之所以会出现格式化串漏洞,就是因为程序员把printf()的第一个参数即格式串,交给用户来
提供,如果用户提供特定数量的%x(或%d,%f,随你的便啦),就可以访问到特定地址的堆栈内容。

  有些人会说:"靠!你费了这么半天劲,就只是为了打印出了前边堆栈里的内容啊?"我们当然不
只是为了看看堆栈里的内容,我们是要改变堆栈的内容,改变返回地址,使程序跳去执行我们提供的
代码,这就需要联系上*printf()系列函数的第二个特殊的性质。

(2)利用%n格式符写入跳转地址

  到目前为止我们都只是显示内存的内容而没有改变它,但是利用*printf()的一个特殊的格式符
%n,我们就向内存中写入内容。

  %n是一个在编程中不经常用到的格式符,它的作用是把前面已经打印的长度写入某个内存地址,
为了搞清其具体用法和性质,我们看一看下面的例程:

#include
int main(void)
{
int num;
int i=1,j=2,k=3;
printf("%d%d%d%n\n",i,j,k,&num);
printf("%d\n",num);
return 0;
}

运行显示:
123
3

  可以看出,%n的作用就是把已经打印出来字符的数量保存到对应的内存地址当中,这里是num当中。
注意,这里必须对应一个内存地址,%n把字符数写入到这个地址的内存。如果把上述语句改成:

printf("%d%d%d%n\n",i,j,k,num);

  这样就会出现段访问错误。

    

 
百科知识库 版权所有

Copyright © 2007-2009 www.zsku.net, All Rights Reserved

本站所收集信息资料为网络转载 版权属各作者 并已著明作者 旨在资源共享、交流、学习之用,请勿用于商业用途,本站并不保证所有信息、文本、图形、链接及其它内容的绝对准确性和完整性,故仅供访问者参照使用。