Diaphora源码分析——二进制文件对比工具

本博客由闲散白帽子胖胖鹏鹏胖胖鹏潜力所写,仅仅作为个人技术交流分享,不得用做商业用途。转载请注明出处,未经许可禁止将本博客内所有内容转载、商用。 

0x00 Diaphora介绍

      Diaphora(διαφορά, 希腊语中的“different”)是一个IDA插件,用来帮助二进制文件对比。他和其他的比较工具很类似,有一个著名的开源工具就叫做Zynamics BinDiff。还有其他的开源工具,比如DarunGrim或者是TurboDiff。但是从实际使用结果上来看,这些Diaphora能够进行更多的操作,并且能够更好地识别效果。Diaphora源码下载。

      这里我们使用Diaphora对库函数文件以及固件进行对比,从而进行库函数的识别工作。其实也不算是识别,只是进行比对。而我们在使用IDA过程中,使用过FLIRT进行函数签名,同时也使用过Bindiff进行二进制对比,但是其效果都不如Diaphora的效果好。所以我们这次以Diaphora为例,分析其进行函数签名及比对的算法,之后再对算法进行改进,获得我们想要的功能。

0x01 Diaphora文件结构

       各位如果有兴趣看官方文档的话,我还是推荐先看一遍官方的帮助文档。虽然我这里面也会进行一部分的翻译,但是我觉得原汁原味的还是最好的。帮助文档。

——Diaphora.py:IDAPython 插件。包含了所有启发,图形显示,输出接口等等
——jkutils/kfuzzy.py:这是未经修改的kfuzzy.py库版本,他是DeepToad项目的一部分,该项目主要对二进制文件进行模糊哈希(fuzzy hash)。 因为Diaphora对伪代码进行模糊哈希,所以我们引用了这个文件
——jkutils/factor.py:这是经过修改的基于图论的私有恶意软件集群工具(a private malware clusterization toolkit)。这个库提 供了在Python内快速factor数字的功能,并且能够对比素factor的数组。Diaphora使用该工具进行模糊AST哈希对比以及调用流图模糊哈希对比,这两个对比是基于小素数产品的( small-primes-products,BinDiff作者的思想,可以参考论文

Pygments:此目录包含了未经修改的pygments库,是一个生成高亮代码的小工具,说英语代码保管,论坛,wiki以及其他需要美化源码的场景。

这其中,我们更关心两个文件Diaphora.py以及factor.py,因为我们其实主要是想了解Diaphora进行函数签名的算法以及对比的算法,这样我们才方便进行迁移,而一些代码美化和结果输出的内容,有兴趣开发IDA插件或者感兴趣的同学可以自行研究~

0x02 Diaphora的匹配规则

      这里我们只针对Best match进行分析。顾名思义,Best Match是可信度最高的匹配结果,基本上能够达到95%以上的可信度。因为我们在进行二进制分析的时候,需要的是确定正确的匹配结果,如果使用了不可靠的匹配,将导致分析结果的错误。我们舍弃了部分匹配的结果,因此造成了较高的漏报;如果加入了部分匹配结果,将造成误报;从人工分析的角度来看漏漏报造成的影响将小于误报。比如你需要确定malloc函数的位置,进而分析哪个函数调用了malloc函数。而使用best match能够得到1个确定的结果,使用partial match得到了3个可能的结果,但是这3个函数实际上并不是malloc函数,这将增加我们的二进制分析工作,甚至导致分析结果的谬误。因此我们只选择完全匹配的函数。

      好了,我们来看下官方文档怎么说明的。“diaphora首先会为两个二进制文件生成不同的的数据库,并且对比两个数据库中的数据是否相同,甚至是主键值都要相同。如果相同的话,数据库就认为是100%相同,并且不进行多余的比较。”那么我们都需要比较那些键值呢?

伪代码:IDA生成的伪代码应该相同,它能够匹配x86,x86_64和ARM指令集
汇编代码:两个函数的汇编必须完全相同
byte哈希和名字:每条汇编指令的第一个字节要相同,同时对应的真实名字,而不是IDA自动生成的伪指令名,要相同
相同的地址、节点、边界和内存:数据库中存储的基本块的数量、快的地址、边界的名字和内存都要相同
RVS和hash:RVA(Relative Virtual Address)相关虚拟地址以及字节哈希都要相同
相同的顺序和hash:两个函数应该具有相同的hash并且在IDA中的顺序应该相同(比如同为第100个函数)
函数hash:两个函数的hash相同。hash = MD5(所有指令的字节)
Byte hash:两个函数的Byte hash应该相同。这个hash是计算了所有指令字节的集合,但是和上面的函数hash不同,他去除了有地址依赖的指令,比如重定位和jump以及relative call。
Byte 数目:这两个函数的长度(也就是字节数)应该完全相同

      从这些比对的条件来看,diaphora是一个比较严格的二进制文件对比工具,其实用作于补丁对比更合适,并不完全适合于库函数识别,并且大量使用了函数特征、block、反汇编和伪代码的内容,我们如果想要融入Angr还是需要大量的修改的,同时我们也并不是很确定他能兼容的很好。(鹏鹏小声bb:改什么改呀,直接使用IDA进行文件对比,然后根据生成的比对结果,从中抽选出best match,直接导入Angr不是更方便么)(理智的鹏鹏:我们当然可以这样做,但是这将导致自动化分析工具的不完整性)

0x04 FLIRT 的函数匹配算法介绍

      使用过IDA的同学都知道,IDA自带了一个FLIRT工具,该工具能够自动生成函数的签名。我们既然说他不是很好用,就需要首先了解FLIRT是如何进行函数签名的。一下内容节选自官方文档

      FLIRT的识别原则基于以下几条。FLIRT只考虑C/C++(我们做的固件分析也主要是这两个语言);FLIRT并不试图进行完美的函数识别,因为某些函数完整的识别将导致不可遇见的结果(比如C/C++中两个不同名字的函数具有相同的代码,这种情况很常见);我们只识别代码段的函数(这个可以理解);只识别函数,不管参数和函数行为。(以上几条,我们也都是同意的。)

      同时我们应该遵守以下约束:1.我们应该极力避免误报,理想的情况下误报应该为0(前文我们阐述了);2.识别的函数必须只是用有限的寄存次和内存资源;3.生成的签名必须是无处理器依赖的,能够跨平台使用;4.main函数应该识别出来并且被合理的标注。(注:我们和FLIRT的需求是有一些相关性的,上述两端的要求,我们基本上是同意的,但是我们并不满足于这些要求,我们期望得到更高的准确率和0误报率,我想这也就是FLIRT的局限性;同时,即使是FLIRT采用了避免误报的手段,我们在实际使用中还是会出现很多漏报,当函数比较短时,曾经有过46个函数误识别)。

        FLIRT列出了很多库函数识别时遇到的阻碍。主要有:

        1.很多字节是难以确定的。比如动态加载地址,某些地址是需要动态加载的时候进行重新纠正的,而连接的时候并不能知道。这些地址有external 名字,所以一定程度上缓解了这种情况。优化技术同时还引入了新的问题,就是可能导致常量字节不同。

               0000: 9A........        call    far ptr xxx
       替换成了
               0000: 90                nop
               0001: 0E                push    cs
               0002: E8....            call    near ptr xxx
        2.某些函数功能相同,但是调用方式不同,比如strcmp和fstrcmp在大内存模型中相同。问题在于,我们在进行识别的时候不想忽略这些函数,但是这对用户很重要,而我们却不能分辨出他们。
3.另外一个问题
                call    xxx
                ret
       或者
                jmp     xxx
      第一眼没有什么异常,但是问题在于目前标准库中存在着大量这样的代码。直接对比这些函数不会有什么结果,识别出这些函数的唯一方法就是找到他们调用的函数。通常来说,所有的端函数(2-3行指令)很难识别,并且误识别率很高。然而不识别他们也不行,可能会导致雪崩式错误:你没有识别出这个函数,调用他的函数你也识别不了。
       IDA的解决方案:
      IDA为我们行识别的函数库中的所有函数创建了一个数据库。IDA检查每个程序开头的byte是否被反汇编了,以及是否能够作为标准库函数的开始标志。识别算法所需要的信息都写在了一个 signature(签名)文件中,每个函数书都以pattern的形式表示。pattern是一个函数的前32字节,并且所有的变量字节都被标记出来。
      558BEC0EFF7604..........59595DC3558BEC0EFF7604..........59595DC3 _registerbgidriver
      558BEC1E078A66048A460E8B5E108B4E0AD1E9D1E980E1C0024E0C8A6E0A8A76 _biosdisk
      558BEC1EB41AC55604CD211F5DC3.................................... _setdta
      558BEC1EB42FCD210653B41A8B5606CD21B44E8B4E088B5604CD219C5993B41A _findfirst
      在pattern中,"."表示的是变量byte。一些函数的起始字节队列是相通的。因此,树形结构似乎特别适合于这些函数的存储。上面的paatern可以修改为:
558BEC
      0EFF7604..........59595DC3558BEC0EFF7604..........59595DC3 _registerbgidriver
      1E
        078A66048A460E8B5E108B4E0AD1E9D1E980E1C0024E0C8A6E0A8A76 _biosdisk
      B4
        1AC55604CD211F5DC3                                       _setdta
        2FCD210653B41A8B5606CD21B44E8B4E088B5604CD219C5993B41A   _findfirst
      树中的节点就是byte队列。在此例中,树的根节点包含队列"558BEC",并且派生出三个字数,分别以"0E","1E","B4"开头。以B4开头的子树又派生出了两个子树。每个子树都以叶子节点结束。函数名字就保存在叶子节点中(上述例子中尽有函数名可见)
树形数据结构主要完成了两个目标:
       1.我们在树种存储了多个函数共有的几个byte,因此了内存使用。当然这种接生的效果和函数数量成正比。
       2.他非常适合快速模式匹配。如果是将某个函数与函数库中所有函数进行匹配,进行匹配的次数将随着特征文件中的函数数量成对数增长。O(n)=log(n)
      那么根据函数的前32byte进行匹配就是一件明智的事情了。正如前文所述,二进制库中有很多函数的开头几个byte是相同的:
558BEC
      56
        1E
          B8....8ED8
                   33C050FF7608FF7606..........83C406
                                                      8BF083FEFF
                    0. _chmod   (20 5F33)
                    1. _access  (18 9A62)
      但是当两个函数有超过32字节向同师,我们将这两个叶子都存储下来。为了解决这个问题,我们计算33字节到第一个变量字节的CRC16值。CRC存储在签名文件中。进行CRC签名的bytre同样需要存储,因为它随着函数不同而不同。在上面例子中,CRC16是对——chmod函数的从33到52这20个字节进行计算的,对于_access函数计算18个字节。
      当然,第一个变量字节在33字节处也是有可能的,用于计算CRC16的字节长度为0.实际上这很少发生,而且这种算法给出误识别的概率非常低。
      有时候,函数的前32字节的pattern是相同的,CRC16也是相同的。比如下面的例子。
      05B8FFFFEB278A4606B4008BD8B8....8EC0
                0. _tolower (03 41CB) (000C:00)
                1. _toupper (03 41CB) (000C:FF)
      我们这里只使用3个字节计算CRC16,他在两个函数中是一样的。在这种情况下,我们将尝试找到一个位置,其中叶子中的所有函数有不同字节。在这个例子中,是32+3+000c
但即使这样,我们仍不能识别出所有的函数,比如:
... (partial tree is shown here)
                0D8A049850E8....83C402880446803C0075EE8BC7:
                  0. _strupr (04 D19F) (REF 0011: _toupper)
                  1. _strlwr (04 D19F) (REF 0011: _tolower)
      这两个函数在没有变量字节的情况下是相同的,而是根据调用他们的函数而产生了不同。在这个例子红,唯一的办法就是识别出在偏移11处使用他们的函数的名称。这个算法的缺点是,识别_strupr()和_strlwr依赖于识别出__toupper和_tolower,而在后者未识别之前,我们就要推迟识别;这就需要二次识别,但是好在只有几个函数需要二次识别。
最后我们还会遇到两个函数名字不同,但是代码相同的情况。这种情况在C++代码中很常见,我们将这种情况成为“碰撞”。看来只有人工智能能够解决此问题,FLIRT还在等待后续开发。
    总结:FLIRT进行函数识别算法如下。
       1.采用树形结构存储函数的前32字节,并且识别函数时进行逐一比对;
   2.当32字节出现冲突时,使用CRC16对从第33字节开始到第一个变量字节处的byte串进行计算;
   3.让CRC16相同时,尝试寻找一个每个函数的byte都不相同的位置;
   4.当函数字节完全相同,只有调用者不同时,先记录下来,随后进行二次检查;

   5.当代码相同名字不同时,等待人工智能的应用。

   总之FLIRT还是很智能的,进行库函数识别的也很好,但是我想Diaphora应该做的不会比FLIRT差,我们先分析下算法在进行讨论。

猜你喜欢

转载自blog.csdn.net/zhuzhuzhu22/article/details/80814951