众所周知,溢出漏洞从应用形式上可分为远程服务溢出漏洞和客户端(本地)溢出漏洞两类。远程服务溢出漏洞大家很熟悉了,红色代码、冲击波、振荡波等蠕虫都利用了此类漏洞,漏洞的调试和利用有相应的一套方法,前面的文章也有过实例介绍;至于客户端溢出漏洞,这种说法是我暂时从网络上借用来的,IE、 OutLook、Firefox、MSN等存在的漏洞是例子,扩展一下,Office、Acrobar Reader等软件存在的漏洞也归于此类型。远程服务溢出漏洞由于多数协议公开,在协议基础上辅以常用的黑盒测试等技术,利用起来相对容易成功;然而,客户端漏洞通常是由解析某种文件格式的时候缺乏数据边界检查而产生的,而且很多文件格式(如doc、pdf等)厂商都未公开,利用成功的难度明显大一些。
前段时间由于工作要求,我需要对WORD 2000解析doc文件格式时存在的溢出漏洞进行分析与利用。从网上找了找,一共有MS03-035、MS03-050、MS05-023、 MS05-035这四个漏洞公告,影响的都是WORD 2000,(据说WORD 2002可能也受影响)。其中,MS03-050漏洞尽管是03年底发现的漏洞,但是到05年7月底才爆发了针对该漏洞的“万珍”蠕虫,给网络造成了不小的损害,况且现在使用WORD 2000的用户仍然很多,因此觉得这个漏洞目前最有利用的必要。在分析的过程中,我遇到了很多的困难,也走了不少的弯路,不过经过1个多月时间后最终还是比较完美地解决了。在收获信心与提高能力的同时,我当然不会把好东西独享。下面,我就和大家一起来回顾对该漏洞艰苦的调试之旅,希望朋友们看了之后能有所收获。文中讲的东西都更多是基于漏洞调试技术本身及简单的利用,至于如何复杂地利用基本都是与木马技术相结合的,由于工作保密关系就不多谈了,大家可以*发挥。
寻求现成Exploit---无功而返
首先还是看看漏洞公告吧。
根据公告,漏洞原因大家应该大致了解了,WORD在解析doc文件的时候,未对宏的名称进行长度检测,导致了缓冲区溢出。除此以外,公告没有其它任何信息,甚至连它是堆栈还是堆溢出漏洞我们也不确定。对于分析这样的漏洞,我一向采取先解决问题(找Exploit),有空再深入研究的方式。二话不说,打开Google,输入漏洞关键。
在搜索结果中的众多URL中一一浏览,发现这些URL几乎全是关于漏洞概述的,不要说Exploit,就连doc样本文件都找不到一个。一周时间过去了,完全没什么进展。但有一点是肯定的,在面对困难时应该具有充分的黑客精神,绝对不能随意放弃。靠别人不如靠自己,一条路走不同就换另一条,我决定自己分析这个漏洞。
既然该漏洞是WORD在解析doc文件宏名称的时候发生了溢出,那就从构造超长的宏名称开始,可我连WORD宏机制都完全不了解, 看来还只能从这些基础知识入手。下面的内容都是在Windows 2000(或XP SP2) + WORD 2000这样一个平台上进行的。
了解WORD宏机制——测试未果
大概从WORD 97开始,就提供了宏机制。宏是完成一定功能的命令的集合,WORD中各菜单、按钮执行的功能(如新建、打开等),实际上都是用宏的形式对多条命令进行了封装。WORD宏是使用VBA来编写的,目的是使得用户能在WORD中编写完成指定功能的代码,从而扩充了WORD的功能。VBA具体语法和VB一样,我们可以在编写宏的时候调用WINDOWS API,但有些函数VB中没有,宏病毒就是利用宏复制语句来达到感染的目的。宏有两种存储形式,一种是存储doc模板文件中(Normal.dot),而另一种则存储在单个doc文件中。
我们还是来看看实际中是如何使用宏的。打开WORD 2000,创建doc文档并保存为tesd.doc, 然后点击“工具”?“宏”?“宏”(或者按Alt+F8)调出宏。
我们先来尝试新建一个宏。在“宏名”中输入任意名称,必须以字母开头,这里输入“aaaaaaaa”。在“宏的位置” 中选择“test.doc”,然后点击“创建”,调出VBA编辑器。这里的代码只调用API MsgBox函数作测试。
保存以后,运行,弹出了对话框。
当然,这里只是测试,大家还可以通过宏来实现更加复杂的功能,如果要在文件刚打开或保存时执行某功能,则将宏名取为“AutoOpen”或“AutoSave”等。如果宏是由VBA编写的破坏性代码,则可称之为“宏病毒”。
接下来谈谈WORD宏的安全机制,也顺便把我研究这个漏洞的原因解释清楚。宏的安全机制很大程度上是针对“宏病毒”的,WORD 2000通过安全等级来控制宏的执行,安全等级分为无、中、高3级(WORD 2003中有极高),分别代表启用任何宏、用户选择是否启用宏、以及取消任何宏。现在大多数WORD 2000默认的安全等级是中(部分新的WORD 2000版本默认安全级别为高),用户打开带有宏的doc文件后,会弹出如下宏提示框。
如果选择“取消宏”,则doc文件中的宏不会执行,而选择启用宏则执行。当宏安全级别为高的时候,无论怎么样,宏都不会执行。
整理一下,如果WORD宏安全级别为默认设置的话,宏要么需要用户选择启用,要么被取消。这种宏安全机制是可以理解的,否则宏病毒将会泛滥。前面也讲过了,我们如果想让WORD在打开doc文档的时候就执行任意代码的话,如果用宏来实现看来是始终逃不脱其安全机制的,而且用户可能选择“取消宏”。因此,需要研究一种方法,使得不论在哪种安全级别下,不论是否启用宏,只要WORD 将doc文件一打开,我们嵌入的代码就执行。因此出路只有一条,那就是利用溢出漏洞。
在熟悉了宏机制之后,现在回到主题中来,继续分析漏洞。既然是宏名称溢出,那就在建立宏的时候把宏名称写的很长。我把宏名增长到200多个字符,但发现这里是作了限制的,超过255个字符VBA就会报错,但这是WORD正常的错误处理。为了便于测试,我们把test.doc中宏名称设置为176个字符。
保存后再打开该文件,结果发现还是可以执行宏代码,并没有发生什么异常。看来在WORD程序里手动构造超长的宏名称是行不通了。既然如此,那就修改test.doc试试。打开UltraEdit,将test.doc拖入编辑框。
我们的目的很明确,就是找出test.doc中含有176个‘a’的宏名称究竟位于这些二进制数据的哪个位置。仔细看了两遍,发现该文档*有3处保存了宏名称。其中,有两处紧连着,并且是UNICODE形式给出的,分别为全小写和全大写,如图9所示。此外,下面还有一处,保存了完整的ASCII形式的宏名称。
那么,在WORD打开doc文件的时候,究竟是通过哪一处来识别宏名称的呢?可以肯定的是上面3处之一,因此现在我们又需要再次判断了。判断的方法很简单:在UltraEdit中依次把这3处宏名称的第一个字符由‘a’改为‘b’,保存之后再打开test.doc,Alt + F8调用宏,检查宏名称第一个字符是否改为了‘b’,如果改变则说明该处就是WORD解析的宏名称。经过多次测试,发现UNICODE小写形式的宏名称就是我们要找的目标。
根据漏洞公告,我们把宏名称的增长几个‘a’,保存后再打开test.doc,还是出现了图6所示的宏提示框,但无论点击“取消宏”还是“启用宏“,都没有出现什么异常,而是弹出下列提示框。
我选择继续增加‘a’,但还是同样的结果,这可能是由于新增数据破坏了doc文件中其它数据引起的。哎,越来越不清楚了。直觉告诉我,一条漫长的道路就摆在面前---必须分析doc文件格式,然后寻求漏洞解法。后来事实证明,这个时候我离调试成功并不太远了。
分析doc文件格式——小有收获
要知道,WORD 2000 doc文件格式微软并没有公开,要彻底分析无疑有相当的难度。但这条路又不得不走,于是我顺理成章地在网上搜索,先后找到了WORD 6.0和WORD 97的doc 文件格式介绍的复杂的文资料,同时也找到了一些研究人士的简单分析资料。通过这些资料,再结合WORD 2000 与 WORD 97的相似性进行分析,我对doc文件格式有了进一步的了解。以WORD 2000 生成的doc文件为例,这里就简单谈谈:
我们平时见到的文档格式大致分两类,一类是标记型文档,如HTML、XML、PDF等;另一类是流式文档,如doc。Doc文档是复合二进制文档,可以包含文字格式、图片、声音、动态图象、宏等多种格式信息数据,这些数据在doc文件中是以流(Stream)的形式存储的,也就是存储方式与数据具体格式无关。每个文件都以固定8字节“D0 CF 11 E0 A1 B1 1A E1”开头,并以512字节为数据的基本存储单位。这种复合信息数据的存储方式与磁盘文件的组织方式类似,因此doc文档又称为“文件中的文件系统”。大家可以从中得到对比。如图13所示。
图13
对doc文件格式有了这样一个总体认识之后,我们来仔细分析下test.doc。这里给大家介绍一个工具)——DFView.exe。这个工具是专门用来解析文件格式的,位于“vc目录\Common\Tools\”下。VC6下面有这个工具,而.net下没有。为了使用方便,大家可以在桌面上设置该工具的快捷方式。使用DFView.exe需要注意的是它不能解析中文路径,因此大家最好把待解析的doc文件放在英文目录里。这里我用DFView.exe 打开f:\test.doc。
大家可以看到,这个工具把test.doc中的二进制数据按照存储(如Macros)与流(Stream)的形式划分好了。把存储和流一个个展开,发现我们前面找到的UNICODE小写形式的宏名称就在1Table这个流中,该流记录了多种格式信息。
但是这并没有带来什么实质性的进步。不要着急,我们再看看WordDocument这个流,按照WORD 97的分析资料中的说明,WordDocument以一个FIB(File Information Block)数据结构开头,FIB记录了复合文件中各种数据结构信息。
在该结构偏移0X15A处,记录了1Table流中存放的宏信息的位置偏移值,test.doc中该偏移值是0X180。于是我们在1Table流中偏移0X180的地方,找到了FF 01,也就是宏名称信息的“标记”。
当分析到宏名称信息的标记的时候,我想起了前段时间看到的一篇文章叫《寻找客户端漏洞的艺术》。这篇文章网上到处都可以找到,作者分析了IE、 FireFox、Outlook、MSN等客户端程序解析图片文件时出现的溢出漏洞的共性。下面我们先来看看这些漏洞的共性,看了之后大家应该可以想到了什么。
对比同类漏洞模式——柳暗花明
前面所指的共性是指文件中信息存储的格式引起的。无论JGEP、GIF、PNG等图片格式还是doc文件格式,其数据流的有很多段构成(例如宏名称信息就是一个段),而这些段的格式基本都是下面这一个模式:
标记 | 数据长度 | 数据
在客户端程序解析这些文件格式的时候会依据“标记”来确认段,并可能对“数据长度”进行一定的运算,再依据这些“数据长度”来处理随后紧跟的“数据”。以上提到的几个漏洞的产生原因都是在对“数据长度”进行运算的时候没有进行长度确认而导致的。
举个例子。相信大家还记得2004年的JPEG溢出漏洞MS04-028,JPEG格式中的注释段(COM)由0xFFFE开始(标记)+2字节得注释段字节数(数据长度) +注释(数据)构成。因为字节数这个数值包含了本身所占的2字节,所以GDIPLUS.dll在解析JPEG格式文件中的注释段时会把这个值减去2,如果这个值设置成0,1就会产生整数溢出,从而造成了后来的堆溢出漏洞。
回到我们这个WORD漏洞,按照刚才那个格式来对比。其中,上面找到的FF 01是“标记”,那么“数据长度”呢,大家可以看到“Proj…”前面有两字节长度0x00C2,这就是我们要找的“数据长度”,这个数值是包括了 “Project.NewMacros.”之后的宏名称长度。在0x00C2后面,就是真正的宏名称了。通过对比同类漏洞的模式,我们知道了通过增加宏名称数据来造成溢出的方法是不正确的,而是应该将宏名称前面的“数据长度”值增大,WORD在按照改后的“数据长度”拷贝宏名称的时候发生了溢出。好,现在基本清楚了该漏洞的原理,下面继续调试。
坚持不懈---调试成功
将宏名称前面的“数据长度”由0X00C2增大为0X00D0,保存了再打开test.doc,结果发现还是出现了图11所示的提示框。看来头已经比较晕了,但是仍然不能放弃。现在把“数据长度”改得更大一些,改为0X0350。保存再打开test.doc,点击“取消宏”后终于弹出了久违的异常对话框。
呵呵,有希望!看来可能是发生了堆栈溢出,因为如果是堆溢出的话,应该会提示内存不能为“write”。为了验证这一结论,我们使用调试器。平时我都习惯使用Softice为主,在调试这个漏洞的过程中我发现其实VC也是漏洞调试的好工具,前提是机器上必须安装VC。就利用图18弹出的异常对话框,点击“取消”,调出了VC调试器。
这个时候观察下堆栈中的数据,在内存查看框中,输入ESP,回车。
看来宏名称的确被拷贝到了堆栈中,再观察EBP(0012C2EC)指向的内存。
由上面两图,EBP指向的内存位于宏名称后面,ESP指向内存位于宏名称前面,因此明显可以判断发生的异常不是由于问题函数返回到错误地址产生的,而是在返回之前就发生了异常。在这种情况下,我们一般先覆盖SEH结构来尝试漏洞利用。这里就需要用到Softice了,因为VC调试器中不能查看SEH结构在堆栈中的位置。通过Softice,发现SEH结构地址是0X0012FFB0,与当前堆栈指针位置相距了10000多字节,拷贝这么多字节到堆栈中会覆盖掉堆栈中其它线程函数数据,经过测试,覆盖SEH果然是行不通的。那么现在尝试JMP ESP方式,没想到新的问题又不断出现。
尝试利用——频遇难题
在图21中,我们其实已经将问题函数的返回地址在堆栈中的位置找到了,就位于0X0012C2FC, 对应的四字节数据就是图中的阴影部分。
要使用JMP ESP的方式,就必须保证问题函数返回之前不能出现异常。那怎样才能做到呢?看来只能将“数据长度”减小,这里减小到0X0235(测试的实际最大值),保存后打开test.doc。还是提出了异常对话框,点击“取消”,再次进入VC调试器,查看各寄存器。
此时ESP = 0X0012C328,而EBP = 0X86019006。请大家对比观察图21中EBP指向内存的四字节数据,可以得出的结论是问题函数已经返回了,而这里的异常是函数返回之后出现的。函数返回的问题顺利解决了,下一步是判断问题函数为__stdcall还是_cdecl的调用约定。这个问题直接决定了在0X0012C2F4这个地址(函数返回前EBP+8)上放ShellCode还是需要有若干字节的保留地址。检测的方式也很简单,将问题函数EIP改为0XABABABAB,返回后在 VC调试器中观察ESP即可。按照这种方法,发现返回后ESP = 0X0012C328,其实也就是图22中看到的ESP值。EIP保存在0X0012C2F0,那么中间将会有0X0012C328 - 0X0012C2F0 = 0X38字节的保留地址。
OK,将test.doc中EIP保存位置添加JMP ESP指令地址0X7FFA4512,在堆栈数据0X0012C328在文件中的对应处填写指令EB FE,保存后再打开test.doc。结果让人失望,仍然弹出了异常对话框。再将0X7FFA4512改为Windows 2000 SP4上特有的JMP ESP指令地址再测试,还是不行。经过一番分析,发现原来EB FE的位置其实超出了我们0X0235的宏名称极限长度。换句话说,JMP ESP方式在本漏洞中还是行不通!
如果一个漏洞既不能用JMP ESP方式利用,也不能用覆盖SEH结构方式利用的话,大家还有什么办法呢?有的,而且是很原始的——将EIP直接填写为堆栈地址。
反复调试---完美利用
将test.doc中EIP保存位置四字节数据改写为0X0012C2F4,这个地址对于同一文件模板(如test.doc,‘a’的个数不同则该地址不同)在Windows 2000和XP SP0是不变的,在XP SP1-SP2中应该是0X0013C2F4。然后在该位置后面4字节填写EB FE,保存后打开test.doc,呵呵,CPU使用率为100%,成功了!如果大家想构造自己的WORD木马,都可以通过test.doc这个模板来做了。
此外,由于宏名称长度限制在0X0235字节左右,ShellCode必须采用JMP BACK的方式。下一个问题,ShellCode怎么加到doc文件去呢?我用VC写了两个小程序:readdoc和newdoc。readdoc的功能是读取doc数据到指定文件中,而newdoc的作用是将readdoc读出的数据中加入ShellCode,然后生成新的doc文件。这两个程序的实现都很简单,大家可以参考文章附带代码。至于ShellCode的编写,大家可以参考我前面的一些文章,写得很清楚了,只是在测试的时候我发现了一个问题:开始的时候用sub esp, 80H来规划内存,但是每当后面调用LoadLibraryA和GetProcAddress这两个函数的时候,会使后面的指令全部变为无效指令(乱码),费了很大的工夫,结果发现用sub esp, 1000H的时候问题就解决了。
最后,我和大家一起在我本机测试一下我自己制作的一个反弹端口的WORD木马suc_2ksp4.doc。先执行NC –l –p 25,再打开suc_2ksp4.doc。
测试成功!这是在Windows 2000 sp4下的测试,同时我还做了一个针对Windows XP SP2+WORD 2000的版本suc_xpsp2.doc,大家如果有兴趣也可以测试下。
至此为止,这个漏洞就给大家分析完了,它的应用范围是比较广的,可以与木马绑定相结合。