查看原文
其他

污点分析挖掘漏洞演示——如何在8小时内从零发现cve2012-0158(word溢出漏洞)

ktkitty 看雪学院 2019-05-26



简介



最近一直在研究漏洞挖掘的东西。


网上能看到的漏洞挖掘的方法,大概就两种,一种是fuzzing,一种是静态分析和动态调试结合,人工审查。


我比较能接受后者,关于这种方法还写过一个帖子,分析网络蚂蚁的栈溢出的。


但是这种方法有一个很大的缺点,当你分析的程序大到一定级别的时候,会变得非常非常非常困难。比如分析word之类的软件....


那么要怎么解决呢?如果你看过我分析网络蚂蚁那个帖子,应该就会发现,我的分析思路,基本上就是人工跟踪数据流,然后看数据流的路径上,有没有异常。


word的漏洞之所以难挖掘,难分析,很大程度就是因为数据流太难跟踪了。那么能不能把这个过程自动化呢?答案是能,利用的就是下面要说的“污点分析”方法。



污点分析简介



我们都知道cpu运行程序的过程,就是按照顺序执行一系列的指令。


指令有操作数,分为源操作数和目的操作数。


操作数又分为内存和寄存器两种。


如果我们把某个源操作数标记为污点数据,那么这条指令执行后,目的操作数也变成了污点数据,这就是污点的传播。


比如下面这个简单的x86汇编程序


1. mov eax, [ebx]
2. mov ecx, eax
3. mov ebx, 0
4. mov edx, ebx

如果ebx对应的内存地址上的数据被标记为污点数据,那么执行完1后,eax也会被标记为污点数据;再执行完2后,ecx被标记为污点数据。


如果我们能把程序执行的指令,都记录下来(比如利用dynamorio之类),然后一条一条解释传播,把源操作数为污点数据的指令记录起来,就可以得到受污点数据影响的程序路径。


比如上面的汇编片段,按照之前的假设,得到的污点路径是1、2。而3、4不受污点数据影响。


分析得到污点路径这个过程就叫污点分析。



溢出漏洞模型



漏洞就是某种特殊的代码执行路径,比如下面这种经典的栈溢出代码 

int taintCnt = ...
char stackBuffer[100];
memcpy(stackBuffer, src, taitnCnt);

显然,如果我们能在受污点数据影响的路径上,发现类似的路径片段,那么几乎就可以认为这部分代码存在溢出漏洞。


但是上面的代码还不够抽象,要提取出一个抽象特征来,才可以在污点路径上去匹配。


对于上面的代码,特征就是我们可以用污点数据控制复制的次数,更本质地说,是循环的次数。


转换成x86汇编,可能就是如下形式


rep movsd, ecx (ecx受控制)

或者


CopyLoop:
mov eax, [esi]
mov [edi], eax
inc esi
inc edi
inc ecx
cmp ecx, taintCnt
jnz CopyLoop



离线调试器



要完全自动分析,准确确定某个地方是不是有漏洞,非常困难;但是如果把有问题的地方标记出来,然后再人工确认,准确率就可以轻松做到很高。


所以要把采集到的指令数据利用起来——在ida中同步显示,也就是离线调试器。


利用离线数据可以很轻松地开启上帝视角,分析起来非常安逸。


下图是ida中的离线调试器客户端界面。(黄色标记代表受污点影响的指令,蓝色标记代表当前指令)


ida中的客户端是用ida的py脚本+Qt实现的。



下图是离线调试器的服务端,ida中的客户端从这里拿数据(http接口)。 



演示开始——利用word生成正常的rtf样本文件


假设时间回到2012年,我们想分析一下word2007中,解析listview控件的时候,有没有漏洞。


从“开发工具”里面,添加listview control。

因为是分析解析过程有没有漏洞,所以往listview里面添加一些数据 



保存为启用宏的文档(*.docm)


再打开那个docm文件,另存为rtf文件



这时候有太多不是我们想分析的数据,把它们都删掉,具体地将是只保留object标记里面的数据。


精简之后的原始样本大小大概是19,149 字节。


采集数据



通过简单的调试(或者利用污点分析也可以),可以知道word读取rtf的地方在wwlib.dll的这个地方



所以我们利用某个类似条件断点的工具,让dynamorio在执行到3126FC0A处后,再开始记录数据。


先记录6千万条试试看。


记录完成大概得到4.07 GB的指令数据。每条指令记录包含的数据有:对应的eip,8个通用寄存器,8个4字节的栈记录。



分析开始



从离线数据中,找出调用ReadFile的地方



ReadFile的参数——缓冲区地址,就是污点源。


从call之后返回的第一条指令开始传播。


比如图中的3126FC2D。



分析1



污点分析结果告诉我们,在偏移为0xCA处,有数据可以控制复制的字节数。 

此处的数据如下图



我们去离线调试器里看看。 rep mov来自msvcr80,往上一层,来到wwlib,可以看到是调用了memmove函数。

可以看到edi的值是0x2200,正好和0xCA附近的值对应起来,所以应该是0xCA附近的几个字节控制复制次数。


dst buffer(push esi)不是栈上缓冲区,所以我们看看它是在哪里分配的,有没有可能溢出。


利用离线调试器的"go to prev call"功能,一层一层往上跳转,可以看到在ole32.dll里面分配内存。


调用GlobalAlloc进行分配,分配的大小和复制的大小一致,所以没有问题。



此外还有一些类似的点,就不全部一一列出来了。


然后下面进入真正的主题。



分析2



一步一步查看,过滤掉没有问题的地方后,我们可以跟到这里。 



在离线调试器里,我们可以看到,dst buffer是栈上的缓冲区 



控制复制次数的ecx来自2B6A偏移处



同时ecx需要满足:和某个值相等 


.text:275C878A                 mov     edi, [ebp+dwBytes]
.text:275C878D                 cmp     [ebp+var_4], edi
.text:275C8790                 jnz     loc_275D3F93

从污点路径中可以发现,[ebp+var_4]来自2B72偏移处(相邻几个字节)。

再往上看看ecx还有没有其他的约束条件。


可以看到,ecx还需要大于或等于8。




同时dst buffer来自

.text:275C8A00                 lea     eax, [ebp+var_8]


也就是只有8字节。


到这里,这部分代码就满足上面的漏洞模型


1. 往栈上缓冲区写入数据

2. 写入大小可控,而且可以超过缓冲区大小(大于或等于8)


把rtf中那两处偏移上的08字符串改成大于08的值,且保持相等,就可以实现栈缓冲区溢出,覆盖返回地址。






本文由看雪论坛 ktkitty 原创

转载请注明来自看雪论坛



往期热门阅读:


扫描二维码关注我们,更多干货等你来拿!


    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存