IDA2Obj: HITB2021Sin Talk

​ 现在许多高效的 fuzz 引擎(AFL,honggfuzz,syzkaller等)都会收集语料执行时的代码覆盖率,并以此为反馈来指导变异和 fuzz 策略。对于开源的项目,通过设置编译选项,编译器就会自动帮忙插桩了。但是对于闭源的二进制文件,问题就显得比较棘手了。当前主流的做法大多还是 DBI (运行时动态插桩,如 Dynamorio,frida stalker 等)。而与此相对的就是SBI (静态插桩)了。毫无疑问,静态插桩几乎可以获得和编译器插桩一样的执行效率,可以把编译器插桩看成是 SBI 的一个特例。但是目前已有的一些 SBI 方案没有大量运用普及,大都有各自的局限性。

​ SBI 的实现有点像数学里面的某类证明题:命题的表述简洁明了,但是命题的求证过程却并不简单。而我的实现方法没有用到什么高深的理论,一切都只是基于简单的基础知识。

​ 在介绍我的思路之前,先说一下我来趋势之前所做过的一件事:花了差不多一个月的时间,把一个 37M 左右的 exe文件的反汇编全部导出,然后用 VS + MASM 重新生成一个 exe 文件出来,可以和原来的 exe 一样正常运行。

​ 这也是一件看起来很简单做起来却并不简单的事儿,主要原因有两个:

  • 全部导出的反汇编代码大概有 几千万行,MASM 编译这么大的 ASM 文件不知道要等到猴年马月了,况且每次汇编报错都得再等上个猴年马月。因此我将其按照段来进行切割。切割完后,按照符号之间的引用关系给出导出和导入声明。
  • 古老的 MASM 汇编器太弱了,有好多 IDA 反汇编语法需要经过调整才能被 MASM 所接受。因此就陷入了这样一个循环:MASM报错 -> 修复语法格式 -> 再编译。(道路是曲折的,前途是光明的,哈哈,问题解决一个就少一个,考验的只是毅力)。最终把这些坑都填平了,并写出了一个自动调整汇编语法的脚本来做这件事。

​ 因此关于 SBI 的实现,我的第一个想法就是通过将 IDA 里面的反汇编全部导出,拿到一份 汇编级别的“源代码”,然后在此基础上 模拟编译器的插桩 过程,最后再生成一个新的二进制文件出来。

​ 有了之前的努力,这个想法很快就被证明是可行的。但是由于 MASM 的局限性,每次对一个新的二进制文件进行这个操作的时候,都难免会有一些边边角角的语法需要手动微调,这很不适合做成全自动化。

​ 之后我有了第二个想法:

  • 一个二进制文件里面,不是代码就是数据。
  • 从机器码层面来看,代码段可以分为两部分:
    • 没有引用,直接dump, 如:mov eax, 1
    • 有引用,需要修复,如:call sub_xxx; jmp loc_xxx; mov rax, qword_xxx
  • 数据段也可以分为两部分:
    • 没有引用,直接dump, 如:db 0,1,2,3
    • 有引用,需要修复,如:dq offset sub_xxx; dd rva loc_xxx
  • 因此,我可以设计这样一个二进制文件重写算法:
    • 从头到尾遍历每一条汇编指令,记录它们的地址,作为插桩之前的旧坐标系。
    • 在遍历的过程中,输出每条指令的机器码:
      • 不管是代码型指令还是数据型指令
      • 对于固定的部分直接输出
      • 对于需要修复引用的部分,先预留占位符,并记录需要修复的地址坐标和引用类型
    • 在遍历的过程中,顺便在每条基本块指令前插入自定义的跳板指令。因此输出后的临时二进制文件必然变大,地址坐标随之发生变化。
    • 计算每条指令在插桩后的临时二进制文件中的新地址坐标。
    • 根据之前记录的需要修复引用位置的记录表和新的地址坐标系,对之前预留的占位符位置进行引用关系的修复。
    • 最终形成一个新的插过桩的二进制文件。

​ 这个想法从理论上讲是可行的,而且还可以是跨平台的!如果 IDA 能正确分析出所有的交叉引用关系,那么生成的新文件和旧文件就应该是等价的(引用关系就像是人体的支撑骨架)。反之,如果有个别特殊指令的引用关系 IDA 没能识别出来的话,就可能会导致新文件运行时 crash。在后来的实践过程中,确实发现了一些引用关系 IDA 没能识别的情况,但是都可以通过编写脚本来辅助 IDA 识别出来,PE/Macho/ELF 各自都可以实现一套辅助分析的插件来帮助 IDA 分析识别潜在的交叉引用。

​ 这个算法实现起来感觉还是挺麻烦的:有大量的交叉引用位置关系需要手动修复。而我这里的 IDA2Obj 就是这个算法的简单实现,绕过了手动修复的操作。

​ 思考 IDA2MASM 的插桩过程(透过现象看本质):为什么这个过程不需要手动修复?MASM 在这本质上是做了什么?这个生成的过程又发生了什么?

​ 实际上,MASM 就是简单的语法翻译生成obj,ASM 里面通过语法声明的导入导出符号都对应到obj里面了。而 真正进行引用关系修复操作的是链接器! 那么我是否可以绕过汇编器,直接导出obj,然后再利用链接器来帮忙修复引用关系呢?

​ 实践证明是可以的。但是探索的路上确实是踩了不少坑,这次 HITB分享 里面会具体介绍这些坑,以及我是怎样解决的。这些可以帮助将来玩这个 SBI 算法的人少走弯路。当然,我也会在不久以后开源这个 repo,希望感兴趣的研究员可以 pull-request,将这个方案变成一个跨平台的静态插桩方案。

Written on August 13, 2021