了解最新技术文章
相对新颖,代码虚拟化可能是目前最有效的保护技术之一1。随之而来的是相对严重的缺点,例如执行速度受阻2以及难以对生产代码进行故障排除。与其他更传统的软件保护技术相比,其优势在于提高了逆向工程的障碍。
代码保护上下文中的虚拟化意味着:
生成虚拟机M1
将要在机器M0 3上执行的原始代码对象C0转换为要在M1上运行的语义等效的代码对象C1。
虽然M1的一般特性可能是固定的(例如,所有代的M1都是具有这样那样特性的堆栈机器),但M1的指令集架构 (ISA)可能不一定是固定的。例如,操作码、微码及其实现可能因代而异。至于C1,一代的特性只受转换器能力的限制。不用说,标准的混淆技术可以应用于C1。虚拟化过程可能是递归的(C1可以是实现机器M2规范的 VM ,执行代码对象C2,模拟C0的原始行为等)。
总而言之,在实践中,这使得M1和C1独一无二且难以逆向工程。
注意:所有标识符名称都已被混淆。为了清晰和理解,它们被重命名。
下面,该类VClass
被发现是“虚拟化的”。虚拟化类意味着所有非构造函数(除了<init>(*)V
and之外的所有<clinit>()V
方法)都被虚拟化了。
方法d(byte[])byte[]
是虚拟化的:
它被转换为一个解释器循环,上面有两个大型 switch 结构,这些结构在存储在本地 array 中的伪代码条目上分支pcode
。
添加了一个PCodeVM
类。它是一个修改后的基于堆栈的虚拟机(更多内容见下文),它执行基本的加载/存储操作、自定义加载/存储以及一些算术、二进制和逻辑操作。
p-code VM 类的片段。此处的完整代码,还包含虚拟化类。
有关“通用”VM 操作的完整列表,请参阅上面链接的要点。三个示例,其中一个表明操作并不像术语所暗示的那样通用:
如前所述,负操作码表示特定于虚拟化方法的自定义操作,包括控制流更改。一个例子:
通过对该代码的分析以及在其他二进制文件中发现的虚拟化方法,可以推断出应用保护程序生成的 p-code VM 的特征:
VM 是一个混合堆栈机器,它使用 5 个相同高度的并行堆栈,存储在以下数组中:
java.lang.Object(容纳所有对象,包括数组)
int(容纳所有小整数,包括布尔值和字符)
长
漂浮
双倍的
对于上述 5 种堆栈类型中的每一种,VM 使用两个额外的寄存器来存储和加载
使用了两个堆栈指针:一个指示堆栈 TOP,另一个似乎使用更自由,类似于 peek 寄存器
堆栈有一个保留区域来存储虚拟化方法参数(包括this
如果方法是非静态的)
ISA 编码很简单:每条指令都是一个字长,它是要执行的 p 码指令的操作码。指令中没有嵌入寄存器、索引或立即值的概念,就像大多数堆栈机 ISA 所具有的那样。
因为 ISA 非常简单,所以指令语义的实现几乎完全取决于 p 代码处理程序。因此,它们被分为两类:
半通用 VM 操作(加载/存储、算术、二进制、测试)由 VM 类处理并具有正 ID。(虚拟化类中的每个虚拟化方法都使用一个 VM 对象。)
特定于给定虚拟化方法的操作(例如,方法调用)使用负 ID,并在虚拟化方法本身内处理。
虽然PCodeVM
操作码都是“有用的”,但虚拟化方法(负 id)的许多特定操作码只能执行在语义上等同于NOP
or的代码GOTO
。
下面,我们将解释用于重建虚拟化方法的过程。呈现的 CFG 是dexdec 4管道使用的 IR-CFG(中间表示) 。请注意,与gendec的 IR 5不同,dexdec的 IR 不公开,但其文本表示大多是不言自明的。
总的来说,一个虚拟化的例程,一旦像任何其他例程一样被 dexdec 处理,看起来如下所示: 一个循环 p-code 条目(存储在x8
下面),a()
首先在 0xE 处处理,或者由大型例程切换处理。
该例程a()
是 PCodeVM.exec(),其优化的 IR 归结为一个大的单个开关。6
非虚拟化器需要识别关键项目才能开始,例如 p-code 条目、用作 p-code 数组索引的标识符等。一旦它们被收集,虚拟化例程的 concolic 执行成为可能,并且允许重建原始执行流程的原始版本。需要注意多个注意事项,例如 p 代码内联、分支或流终止。在当前状态下,非虚拟化器忽略异常控制流。
下面是未展平 CFG 的原始版本。请注意,所有操作都是基于堆栈的;此时代码本身尚未修改,它仍然由基于 VM 堆栈的操作组成。
dexdec 的标准 IR 优化通道(死代码删除、常量和变量传播、折叠、算术简化、流程简化等)基本上清理了代码:
在这个阶段,所有操作都是基于堆栈的。从上面生成的高级代码将非常笨拙且难以分析,尽管比原来的双开关要好得多。
下一阶段是分析基于堆栈的操作,以恢复堆栈槽的使用并将其转换回标识符(可以看作是虚拟寄存器;本质上,我们实现了基于堆栈的操作到基于寄存器的操作的转换)。堆栈分析可以通过多种方式完成,例如,使用定点分析。同样,需要注意一些警告,正确识别堆栈及其索引的需要对于此操作至关重要。
经过另一轮优化:
堆栈分析完成后,我们可以将堆栈槽访问替换为标识符访问。
经过一轮优化:
此时,“原始”CFG 基本上被重建,并且可以应用其他高级反混淆通道(例如,基于仿真的反混淆器)。
高级代码生成产生一个干净的、非虚拟化的例程:
反转后,似乎是修改后的 RC4 算法。请注意添加到键的 +3/+4。
所有版本的 JEB 都检测虚拟化方法和类:在 APK/DEX 上运行Global Analysis(GUI 菜单:Android )并查找那些特殊事件:
JEB Pro版本 3.22 7附带 unvirtualizer 模块。
提示:
确保启用混淆器,并启用 Unvirtualization(在选项中默认启用)。
必须禁用 try-blocks 分析才能使类取消虚拟化。(使用 MOD1+TAB 重新编译,取消勾选“Parse Exception Blocks”)。
在第一次反编译之后,识别guard0/guard1、重命名和重新编译可能会更容易,否则OP 混淆仍然存在并使代码不必要地难以阅读。(请参阅本系列的第 1 部分,了解将这些字段重命名为这些特殊名称的含义和检测到受保护应用程序时的作用。)
我们希望您喜欢关于代码(非)虚拟化的第三部分。
本系列的本机代码保护可能会有第四章也是最后一章。直到下一次!
—
就个人而言,我第一次涉足基于 VM 的保护可以追溯到 2009 年,当时我分析了 Trojan.Clampi,这是一种受 VMProtect 保护的 Windows 恶意软件
尽管有人可能会争辩说,对于当前的硬件(快速 x64/ARM64 处理器)和软件(JIT'er 和 AOT 编译器),这个缺点可能不像以前那么重要了。
这里的机器可以理解为物理机或虚拟机dexdec 是 JEB 的 dex 反编译引擎
gendec 是 JEB 的通用反编译管道
注意与通过 chenxification 和类似技术展平的 CFG 的相似之处。这里的一个关键区别是下一个块可以使用 p-code 数组而不是关键变量来确定,在每次操作后更新。即,是 FSM——控制下一个状态(= 下一个基本块)是什么——嵌入在扁平化代码本身中,或者实现为 p 代码数组。
JEB Android和 JEB 演示版本不提供 unvirtualizer 模块。我最初编写这个模块是为了不打算发布的概念验证,但最终决定将它提供给我们拥有合法(非恶意)用例的专业用户,例如代码审计和黑盒评估。