共计 3290 个字符,预计需要花费 9 分钟才能阅读完成。
导读 | 基于虚拟机的软件保护技术不确定是否首先由 vmprotect 提出,但 vmprotect 毫无疑问是将这项技术大力推广至人所周知。现在基于虚拟机的软件保护技术已经成为现代软件安全防护的必备功能之一。 |
本文并不打算对 vmprotect 或其它某款软件安全套件进行深入讨论,而着眼于研究基于虚拟机的软件保护技术的起源、思想和实现。
传统的软件保护技术,根据针对对象不同,可分为反静态调试和反动态调试两大类。反静态调试主要针对对象为反汇编器。反汇编器通过面向特定平台的反汇编引擎(如 PC 平台即为 X86 反汇编引擎),将编译器生成的二进制文件还原成汇编代码,有经验的逆向工程师可以据此还原出算法等核心运算机制。反静态调试主要是通过特定区段加密等方式,将核心信息保护起来,只在运行期才通过解密等算法动态还原,阻碍反汇编器静态地将二进制文件还原成汇编码。
反动态调试主要针对对象为调试器,由于经过静态加密的二进制码最终必须解密才能执行,因此通过 Ollydbg 等动态调试器仍然可以加以查看,反动态调试通过检测调试器和屏蔽调试端口等各种反调试技术阻止逆向工程师通过调试器跟踪软件进程的运行情况,使得软件的运行时状况始终保持处于黑盒状态。
被动型软件保护概念上述两种保护方案均采取主动出击的策略,意图“御敌于国之外”,中心思想是一个“挡字”,阻止逆向工程是窥视软件内部机理,但盾与矛的对抗总是无休止的,并没有任何一种主动型软件保护手段能真正彻底阻断逆向工程,因此另一种“以人为本”的被动型软件保护技术开始走向斗争舞台的中央。
被动型软件保护手段基于一个假设,即逆向工程师已经通过各种办法突破了主动型软件保护措施,可以随心所欲地观察一切,此时“挡”已经挡不住了,只能采取“藏”的策略,通过提高核心代码的隐蔽性来提高逆向工程在阅读反汇编代码阶段的时间成本投入,间接起到软件保护的效果。被动型软件保护手段具体实现方式主要有乱序和混淆。
乱序是指在在程序执行流中添加跳转指令,如 jmp,通过这些跳转指令将一个从上至下执行的完整代码块切分成若干执行先后顺序不一致的代码片段。乱序能够一定程度上增加反汇编代码的阅读难度,但若只是简单地植入无条件跳转很容易被识别和去除,因此工业级的保护产品往往通过采用条件跳转的方式增加识别难度。
混淆是指通过加入无意义代码(又称为花指令或垃圾代码)或者有意义代码,增加反汇编码的理解难度。俗话说要藏好一棵树,最好的地点一定是森林,混淆技术就是通过代码膨胀增加反汇编代码的总量,为隐藏核心代码构造出一片代码“森林”。早期增加的混淆代码为无意义代码,实现类 nop 操作的执行效果,如 push 指令和 pop 指令搭配使用,但这类代码无实际意义,去除后并不会对软件运行产生影响,因此有经验的逆向工程师往往会首先去除这些无意义的混淆代码才开始进行反汇编码的阅读工作,使混淆技术失去效果。为了确保混淆代码不被去除,工业级的保护产品更倾向于采用有意义的混淆代码,核心思想是等价替换,通过多条指令实现核心代码中一条指令的效果,如最简单的赋值指令 moveax,3 可以替换成 xoreax,eax;inceax;inceax;inceax 这四条指令,操作效果一样,但指令数量翻了两番。被动型软件保护技术究竟能否对软件安全起到实质性的作用,业界一直存在争论。反对的观点主要集中在认为被动型软件保护技术只是提高了阅读反汇编代码的难度和数量,让人“眼花”而已,并没有任何实质性的效果。本文认为,软件安全不该简单理解成让软件绝对安全不可攻破,而实际该是攻方与防方、投入与产出的反复博弈的过程,攻方人力的投入自然也是成本之一。一个人单位时间内阅读代码的数量是固定的,因此,提高了阅读反汇编代码的难度和数量,也就提高了阅读反汇编代码的时间,提高了攻方人力成本的投入,对软件安全是有切实效果的。
虚拟机软件保护技术是被动型软件保护技术的分支,具体来说是添加有意义的混淆代码的一种变型使用。
虚拟机技术目前在软件领域应用广泛,根据应用层级不同,基本可分为硬件抽象层虚拟机、操作系统层虚拟机和软件应用层虚拟机。用于保护软件安全的虚拟机属于软件应用层虚拟机,同层的虚拟机还包括高级语言虚拟机,如 java 程序语言运行环境 jvm 和.net 程序语言运行环境 CLR,后者采用虚拟机的原因是便于移植,因此编译器没有直接生成可直接在机器上执行的 nativecode,而改为生成中间代码 byte-code,再通过在不同机器环境下安装对应版本的虚拟机对 byte-code 进行解释执行,从而实现跨平台运行。
用于保护软件安全的虚拟机采用类似的流程。虚拟机保护软件首先会对被保护的目标程序的核心代码进行“编译”——需要注意的是,这里被编译的不是源文件,而是二进制文件——并生成效果等价的 byte-code,然后为软件添加虚拟机解释引擎。用户最终使用软件时,虚拟机解释引擎会读取 byte-code,并进行解释执行,从而实现用户体验完全一致的执行效果。
编译生成 byte-code
要设计一套虚拟机保护软件,首先要设计一套虚拟机指令,也即是 byte-code 的指令集表,生成 byte-code 的过程,实际是将原始机器指令流等价转译成虚拟机指令流的过程。
第一条设计原则是虚拟机指令集表与原始机器指令集表越正交越好,安全系数越高。最坏的情况是虚拟机指令集表与原始机器指令集表为一一对应的关系,采用这种指令集的虚拟机保护程序安全系数趋近与零,对于逆向工程师而言只需要进行简单的换算,即可还原出原始代码。
另一条设计原则是应尽可能地具备图灵完备性,能够完整地表达出原始机器指令的所有可能表达。图灵完备性越好,则虚拟机保护引擎的保护的覆盖范围越广,健壮性越高。理想状态下,虚拟机指令集应完整地实现对原始机器指令集的等价替代,需要完全满足图灵完备性。但实际上完整替代的代价过高甚至不太可能实现,如 x86 指令集的 FCLEX、FPTAN 等指令,仿真难度较高,且核心代码使用这类指令的可能性很小,综合效费比考虑,虚拟机指令集通常并不涵盖这些“生僻”指令。对于不能仿真的指令,可以采取退出虚拟机执行,获取执行结果再进入虚拟机的方法解决。
解释执行 byte-code
在软件运行时,编译产生的 Byte-code 由内嵌入软件可执行文件中的虚拟机解释引擎,采用读取 - 分派的方式解释执行。
虚拟机解释引擎分为两大部分,分别为 Dispatcher 和 handle。
Dispatcher 的中文字面意思为“分派器”,相当于虚拟机解释引擎的 CPU,负责读取 Byte-code,并指派对应的 handle 进行解释执行。Handle 的中文字面意思为“处理”,实际作用为虚拟机指令通过平台 nativecode(如 PC 平台即为 x86 指令)的实现。Handle 的数量与虚拟机指令集的指令数量是一致的。
虚拟机的进入和退出问题
软件保护虚拟机与高级语言虚拟机并不完全一样,主要体现在高级语言虚拟机由始至终均在虚拟机环境下执行,但软件保护虚拟机必须经历本地环境与虚拟机环境的切换,为了保证执行结果的一致性,必须要求虚拟机环境能够正确获取和还原本地环境的执行上下文。较为便捷的方法是采用堆栈机模型,即虚拟机基于堆栈来进行数据操作。进入虚拟机前,先将本地环境压栈,虚拟机直接以栈地址执行指令流操作,退出虚拟机后,再一一出栈,从而保证了上下文在不同执行环境的无缝切换。
基于虚拟机的软件保护技术可以大大增加了逆向工程还原代码的难度,一套设计良好的软件保护虚拟机能够显著增加代码还原所需的时间,从而抬高了逆向工程的成本,达到软件保护的效果。但采用虚拟机并非有百利而无一害,和高级语言虚拟机一样,软件保护虚拟机同样面临会导致执行效率降低的问题,安全和效率总是处于相生相斥的关系,具体偏重只能根据生产环节的要求具体权衡。