译者:興趣使然的小胃
预估稿费:200RMB
投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿
一、前言
传统意义上,(在汇编级别上)对软件进行逆向工程分析是非常繁琐的一个过程,现在的反编译器已经使这一过程的繁琐程度大大简化。反编译器的操作对象仅限于已编译好的机器码(machine code),其目的是恢复出近似源代码级别的代码。
图1. 反编译过程
毋庸置疑,支持反编译功能的反汇编器可以为我们提供科学便利。我们只需轻轻按下按钮,就能将晦涩难懂的“机器码”转换为人类可读的源代码,便于逆向工程分析。
实际情况是,研究者越来越依赖于这类技术,这就导致人们需要直接面对这类技术的缺点。在本文中,我们将探讨一些反编译对抗技术,这类技术可以干扰或针对性地误导依赖反编译器的逆向工程人员。
二、正数SP(堆栈指针)值
第一种技术是经典又略微“嘈杂”的一种方法,该方法可以用来破坏Hex-Rays反编译器的处理流程。对于IDA Pro而言,如果某个函数在返回之前没有清理已分配的堆栈(即平衡堆栈指针(stack pointer,SP)),那么该反编译器将拒绝处理该函数。
图2. IDA在反编译过程中如果检测到正数SP值则会弹出错误信息
这种情况之所以会出现,原因在于IDA无法以合理的方式推断特定函数调用的类型定义。
因此,开发者可以将其作为反编译对抗技术,具体方法是在需要隐藏的函数中,使用不透明谓词技术来破坏堆栈指针的平衡状态,达到干扰效果。
positive_sp_predicate
宏中定义的add rsp, 4
指令在运行时永远不会被执行,然而该指令会破坏IDA的静态反编译分析过程。当我们尝试反编译protected()
函数时,会得到如下结果:
图3. 利用不透明谓词破坏堆栈指针平衡状态
这种技术相对而言较为知名,手动修补(patch)或者修正堆栈偏移即可解决这种问题。
过去我会将这种技术作为一种简单的权宜之计,使新手逆向工程师(如学生)无法跳过反汇编过程,直接利用反编译器的输出结果。
三、返回劫持技术
现在的反编译器一直在追求一个目标,那就是准确识别并抽离编译器生成的低级簿记(bookkeeping )逻辑,这类信息包括函数的预处理代码段(prologues)/结尾代码段(epilogues)或者控制流元数据。
图4. 编译器生成的函数预处理部分通常包含寄存器、为栈帧(stack frame)所分配的空间等信息
反编译器会在输出结果中去掉这类信息,因为源代码级别并不会涉及到寄存器保存、栈帧空间分配管理等概念。
我们可以在函数返回之前,利用被忽略的这种信息来“旋转(pivot)”栈,并且反编译器不会对这种恶意行为发出任何警告或提示信息(这也可称之为Hex-Rays反编译器启发方法的一种缺陷)。
图5. 将堆栈指针(RSP)引导到ROP链上
堆栈旋转(Stack pivoting)技术是二进制利用过程中常用的一种技术,可以实现任意ROP效果。在这个例子中,我们(作为开发者)会利用该技术来劫持程序执行流程,使毫无准备的逆向工程师措手不及。如果人们仅依赖反编译器的输出结果,他们肯定会错过这个信息。
图6. 反编译main函数后会得到带有堆栈旋转技术的deceptive函数
在这个误导实验中,我们将堆栈切换到一个小型ROP链上,这个ROP链已事先嵌入二进制程序中。程序最终会调用对反编译器“不可见”的某个函数。在本例中,我们最终调用的函数的功能是打印出“Evil Code”字符串,以证实该函数的确被程序执行。
图7. 执行带有返回劫持技术的二进制程序
上面这个例子所对应的完整代码如下所示,这段代码可以实现在反编译器中隐藏代码:
四、滥用“noreturn”函数
IDA在处理过程中会自动将某些函数标记为noreturn函数,本文介绍的最后一种技术与这个过程有关。我们经常能看到这类函数,比如标准库中的exit()
或者abort()
函数都属于noreturn
函数。
在生成指定函数的伪代码时,反编译器会忽略调用noreturn
函数后的任何代码。因为反编译器认为,调用类似exit()
函数时,位于这些函数后面的代码永远得不到执行机会。
图8. 调用noreturn函数后的代码对反编译器不可见
如果我们能够欺骗IDA,使IDA认为某个函数为noreturn
函数,那么恶意攻击者就能将代码悄悄隐藏在该函数的调用语句后面。我们可以通过多种方法实现这一效果,某个例子如下所示:
编译上述代码后,我们可以运行基于Binary Ninja的一个后期处理脚本来处理生成的二进制文件,然后交换Procedure Linkage Table(过程链接表,PLT)中已存储的索引数字。程序在运行时会使用这些索引值来解析已导入的库。
图9. 交换ELF头部中的PLT序号
在这个例子中,我们交换了srand()
与exit()
的序号,并调整了某些调用以符合编译结果。这样处理后,IDA会认为在修改版的程序中,deceptive()
函数调用的是noreturn
函数exit()
,但实际上它调用的是srand()
函数。
图10. 反编译main函数后,某些代码隐藏在deceptive函数中的noreturn调用语句后面
我们在IDA中看到程序调用了exit()
,实际上,在运行时程序调用的是srand()
(该语句等同于空指令(no-op))。这种方法给反编译器造成的影响与前面提到的返回劫持技术基本相同。根据执行结果,可以证实程序的确运行了我们的“恶意代码”,但反编译器对此一无所知。
图11. 包含noreturn反编译对抗技术程序的执行结果
虽然这些例子中的恶意代码依然非常明显,然而我们可以在大型函数或者复杂条件代码中利用这些技术,将恶意代码隐藏其中,使之成为攻击者的拿手利器。
五、总结
反编译器的功能令人惊叹,但这种技术仍然存在不足之处。虽然反编译器面对的是不完整的信息,但它们依然竭尽所能为我们人类提供帮助。恶意攻击者可以(并且也将会)将这种冲突作为欺骗方法加以利用。
随着各行各业越来越依赖反编译器的输出结果,反编译对抗技术也会像反调试技术以及反逆向技术一样,得到人们的青睐。