公司动态
Android APK 加固原理(三):方法级代码抽取——PVM1 虚拟化打包到底是什么?
Android APK 加固原理三方法级代码抽取——PVM1 虚拟化打包到底是什么系列文章第一篇《Android APK 加固原理一Native Shell 如何隐藏和恢复 DEX》第二篇《Android APK 加固原理二从 DEX 解密到 ART 加载如何缩短代码明文暴露窗口》第三篇《Android APK 加固原理三方法级代码抽取——PVM1 虚拟化打包到底是什么》第四篇《Android APK 加固原理四真正的代码虚拟化——PVM2 Native Interpreter 技术解析》第五篇《Android APK 加固原理五SO.text段加密、ELF 加载与运行时动态解密》第六篇《Android APK 加固原理六RASP 运行时安全防护——如何检测 Frida、Hook 与运行时攻击》第七篇《从 APK 加密到代码虚拟化XopProtector 多层 Android 应用保护体系解析》项目地址https://github.com/xopJack/XopProtector一、前言为什么有了 DEX 加密还需要 PVM1在前两篇文章中我们已经分析了 Android APK 加固最基础的一层防护第一层是把原始 DEX 从 APK 中拿走。第二层是把 DEX 加密让逆向工具无法直接从 APK 中得到完整的 classes.dex。但是仅仅做到 DEX 加密并不能解决所有问题。因为 Android 应用最终还是需要运行。无论 DEX 在 APK 中如何加密应用启动以后代码最终还是需要进入 Android Runtime也就是 ART 的执行体系。因此攻击者真正关心的问题会逐渐从“APK 里面有没有完整 DEX”转变为“运行时能不能把 DEX 恢复出来”进一步又会变成“能不能只针对关键方法进行分析”这也是方法级保护存在的意义。XopProtector 在整体保护体系中增加了 PVM1原始 DEX ↓ 定位目标方法 ↓ 抽取方法 Dalvik 指令 ↓ PVM1 编码 ↓ AES-GCM 加密 ↓ 原方法体替换成安全占位 Stub ↓ 生成 code.bin ↓ 运行时 Native Shell 解密 ↓ 恢复 Dalvik 指令 ↓ 写回 DEX ↓ ART 执行因此PVM1 的核心思想并不是“让代码永远不出现”。而是把关键方法从正常 DEX 的 code_item 中抽离出来让静态 DEX 分析工具看到的只是一个占位方法真正的方法实现被移动到独立的加固数据区中并在运行时恢复。XopProtector 源码明确将--vmp-prefix定义为 PVM1并特别说明它是 **“decode → write Dalvik”**而不是 Interpreter。二、先搞清楚PVM1 到底是什么很多人看到“VMP”“Virtual Machine”“虚拟化”这些词第一反应就是原始代码 ↓ 虚拟指令 ↓ Virtual Machine ↓ Interpreter这种理解对于 XopProtector 的PVM2才成立。对于 PVM1并不是这样。XopProtector 的VmCodec.java对 PVM1 的注释非常直接Lightweight method-level VM packing (PVM1) Dalvik → non-Dalvik image Runtime unpacks inside .bitcode before writing DEX. This is virtualized packing, not a full bytecode interpreter.也就是说PVM1 更准确的技术定义应该是Method-Level Virtualized Packing方法级虚拟化打包而不是True Code Virtualization真正的代码虚拟化。这两个概念一定要区分。三、PVM1 和 PVM2 到底有什么区别这是整个系列最容易混淆的地方。可以直接用下面这张表理解特性PVM1PVM2保护粒度方法级方法级是否抽取原始指令是是是否生成虚拟化数据是是是否存在 Interpreter否是是否恢复 Dalvik是否是否写回 DEX是否最终执行者ARTNative Interpreter核心目的提高静态逆向成本改变代码执行模型对运行时 Hook 的抵抗能力中等更强性能开销相对较小更高实现复杂度较低很高XopProtector README 对两者的定义非常明确--vmp-prefix PVM1 unpack → write Dalvik not an interpreter --true-vmp-prefix PVM2 JNI trampoline native interpret因此PVM1 是“代码搬家 编码保护 运行时恢复”。而PVM2 是“代码翻译 自定义指令集 Native Interpreter”。这也是为什么 XopProtector 把 PVM1 和 PVM2 设计成两个不同阶段。四、PVM1 的核心从“类级保护”下降到“方法级保护”传统 DEX 加密往往是classes.dex ↓ 整体加密运行时整个 DEX ↓ 整体解密 ↓ 交给 ART这种方式的缺点很明显一旦整个 DEX 被恢复攻击者就获得了大量可分析代码。而 PVM1 改变了保护粒度。它不再简单地把整个 DEX 看成一个整体而是进一步进入DEX ├── Class A │ ├── method A() │ ├── method B() │ └── method C() │ ├── Class B │ ├── method D() │ └── method E() │ └── Class C └── method F()然后选择其中关键方法method B() method D() method F()进行抽取。最终形成DEX ├── 普通方法 → 正常保留 │ ├── PVM1 方法 → 抽空 │ ↓ │ code.bin │ └── PVM2 方法 → 后续真正虚拟化因此PVM1 的第一个重要思想就是保护从“DEX 级”进一步下降到了“Method 级”。五、第一步找到需要保护的方法XopProtector 的 Packer 是在构建阶段工作的。源码中的PackerMain会读取 DEX并遍历其中的 ClassDef、ClassData 以及 DirectMethods / VirtualMethods。核心流程可以抽象为APK ↓ 解包 ↓ classes.dex ↓ 解析 DEX ↓ 遍历 ClassDef ↓ 定位目标 Class ↓ 遍历 DirectMethod ↓ 遍历 VirtualMethod ↓ 提取 code_item源码中的walkDexMethods()就承担了这一职责。它会遍历dex.classDefs()然后读取classData.getDirectMethods() classData.getVirtualMethods()再逐个调用extractOne(...)处理方法。六、PVM1 并不是所有方法都保护这是一个非常重要的设计。XopProtector 并不是无脑把所有方法都转换成 PVM1。源码中存在vmpPrefixes对应--vmp-prefix也就是说可以按照类描述符前缀选择需要进行 PVM1 处理的代码。例如--vmp-prefix Lcom/example/security/那么Lcom/example/security/Foo; Lcom/example/security/Pay; Lcom/example/security/License;等类中的方法就可能进入 PVM1 流程。源码中明确维护了vmpPrefixes trueVmpPrefixes hollowPrefixes三个不同维度。这实际上构成了普通代码 ↓ Hollow PVM1 代码 ↓ PVM1 Packing PVM2 代码 ↓ True VMP因此可以针对不同代码价值选择不同保护等级。七、第二步读取方法真正的 Dalvik 指令找到目标方法以后PVM1 并不是简单地复制整个 MethodId。它真正需要保护的是方法的 code_item 中的 Dalvik instruction stream。源码中com.android.dex.Codecodedex.readCode(method);然后short[]unitscode.getInstructions();再根据units.length * 2计算实际指令区域大小。之后通过method.getCodeOffset()16定位到 code_item 中真正的 instructions 区域。然后raf.seek(insnsOffset);raf.readFully(original);把原始 Dalvik 指令读取出来。所以这里可以把 PVM1 的第一核心动作总结成Method ↓ CodeItem ↓ instructions ↓ byte[]也就是把原方法的 Dalvik 指令从 DEX 中物理抽取出来。八、第三步PVM1 编码到底做了什么现在进入 PVM1 最核心的VmCodec。XopProtector 的 PVM1 数据拥有一个非常明显的 MagicPVM1源码privatestaticfinalbyte[]MAGIC{P,V,M,1};编码后的数据结构可以简单理解成---------------- | PVM1 | ---------------- | encoded byte 0 | ---------------- | encoded byte 1 | ---------------- | encoded byte 2 | ---------------- | ... | ----------------值得注意的是PVM1 编码后的数据长度基本等于原始 Dalvik 指令长度 4 字节 Magic。它并没有像真正的虚拟机那样把一条 Dalvik 指令重新编译成复杂的 VM 指令流。这也是为什么称它为Virtualized Packing而不是True Virtualization源码明确说明 PVM1 编码结果是same length 4即原始长度加上PVM1四字节头。九、PVM1 的第一层编码基于 Method Index 的 Key StreamPVM1 的编码并不是简单byte ^ 0x55它会根据methodIdx以及byte offset生成一个简单的动态字节流。源码keystream(methodIdx, i)其核心计算为(methodIdx * 131 i * 17 0xA5) 0xff因此Key f(methodIndex, byteOffset)然后encodedByte originalByte ^ key这样不同 Method Index 的编码结果就不会完全相同。十、PVM1 的第二层Nibble Swap除了 XORPVM1 还做了一层非常轻量的字节变换。源码中if((i1)0){b((b4)0xf0)|((b4)0x0f);}也就是对偶数位置的字节进行高低 4 bit 交换。例如原始 1010 0011经过 Nibble Swap0011 1010所以 PVM1 的实际编码逻辑可以抽象为Dalvik Byte ↓ XOR KeyStream ↓ 偶数位置 Nibble Swap ↓ PVM1 Blob解码时反过来PVM1 Blob ↓ Nibble Swap ↓ XOR KeyStream ↓ 原始 Dalvik Byte因此这个过程本质上是为了破坏原始 Dalvik 指令的线性特征让静态扫描器无法直接把这一段数据当作正常 DEX 指令流解析。十一、但是 PVM1 真正的安全边界并不在这个 XOR这一点非常重要。如果只看methodIdx i 131 17 0xA5 XOR你会发现这并不是现代密码学意义上的强加密。实际上 XopProtector 的真正安全边界来自后面那一层AES-GCMPVM1 编码只是Dalvik ↓ PVM1 Transform ↓ AES-GCM源码中extractOne()的流程非常清楚original Dalvik instructions ↓ VmCodec.encode() ↓ PVM1 blob ↓ CryptoUtils.aesGcmEncrypt() ↓ stored也就是说PVM1 负责改变数据形态AES-GCM 负责真正的数据机密性。十二、第四步原始方法代码被真正“抹掉”这是 PVM1 最关键的一步。如果只是复制一份代码那么原 DEX 里面仍然存在原始代码。保护就没有意义。所以 XopProtector 在提取完方法指令以后会直接修改原来的 DEX。流程原始 Method Code ↓ 读取 ↓ 保存到 PVM1 ↓ 原位置写入 Stub源码中writeReturnStub(...)会把原始 instructions 替换成一个与返回类型匹配的占位代码。例如void → return-void int → const/4 v0, 0 → return v0 object → const/4 v0, 0 → return-object v0剩余空间则用nop类指令填充。十三、为什么不能直接把方法体全部清零这是 Android ART 加载过程中的一个关键问题。DEX 并不是Class Method Code随便写什么都可以。ART 在加载、验证以及后续执行过程中会检查 Method 的结构和 code_item。如果直接code_item 0或者破坏整个 CodeItem很容易造成DEX 验证失败 VerifyError Class loading failure Crash所以加固系统常见的思路是不破坏方法结构只替换真正的业务指令。XopProtector 也是这样做的。例如原方法intadd(inta,intb){returnab;}原来的 Dalvik 指令可能类似add-int returnPVM1 后变成const/4 v0, 0 return v0 nop nop ...真正的add-int已经被拿走。这就是所谓Hollow / Method Hollowing也就是方法空洞化。十四、因此 PVM1 最重要的结构变化是这样的加固之前classes.dex Method A ↓ CodeItem ↓ 真正业务 Dalvik 指令加固之后classes.dex Method A ↓ CodeItem ↓ 安全 Stub与此同时assets/protector/code.bin ↓ PVM1 ↓ AES-GCM ↓ 真正的 Dalvik 指令形成┌─────────────────────┐ │ classes.dex │ │ │ │ Method A │ │ ↓ │ │ Stub / Hollow │ └──────────┬──────────┘ │ │ runtime restore ↓ ┌─────────────────────┐ │ code.bin │ │ │ │ Method Index │ │ Plain Size │ │ Flags │ │ AES-GCM Blob │ └─────────────────────┘十五、PVM1 的第五步生成 code.bin方法被抽取以后需要一个地方保存这些方法。XopProtector 使用code.bin作为运行时方法代码仓库。源码中的writeCodeBin()会将不同 DEX 中的保护方法进行组织。当前代码使用的是code.bin v4。其结构可以抽象为Header ↓ version ↓ dex count ↓ dex offsets ↓ Dex Blob每个方法记录大致包含methodIndex plainInsnsSize storedInsnsSize flags insns也就是Method Index ↓ 告诉 Runtime “这个代码属于哪个方法” Plain Size ↓ 解密以后需要恢复多少字节 Stored Size ↓ 当前加密数据长度 Flags ↓ 告诉 Runtime PVM1 / PVM2 / 其他类型 Insns ↓ AES-GCM 加密后的方法数据源码中writeCodeBin()明确写入了methodIndex plainInsnsSize insns.length flags insns并且支持多 DEX。十六、为什么必须保存 Method Index这是整个方法级保护体系的“索引核心”。DEX 中的方法是通过method_ids进行编号的。例如method_id #100 method_id #101 method_id #102PVM1 不需要在code.bin中保存一套完整的 Java/Kotlin 方法名。它可以直接利用methodIndex关联DEX Method ↕ code.bin Record因此 Runtime 可以实现methodIndex 102 ↓ 找到 code.bin 中 #102 ↓ AES-GCM decrypt ↓ PVM1 decode ↓ 得到真实 Dalvik instructions ↓ 恢复 Method #102这就是 PVM1 的“方法级映射关系”。十七、PVM1 运行时到底发生了什么这部分是整个机制最值得分析的地方。很多人会误以为启动 APP ↓ 整个 code.bin 解密 ↓ 整个 DEX 恢复实际上从源码结构来看XopProtector 的 Runtime 是围绕code.bin code_map Method ART Hook组织起来的。Native Shell 启动以后会先定位dexes.zip code.bin config.json然后加载相关 Key。之后code.bin ↓ read_file() ↓ codeitem::parse() ↓ state.code_mapRuntime 将保护方法建立成内部映射。源码protector::codeitem::parse(...)解析完成以后state.code_map就成为运行时的方法保护索引。十八、Runtime 为什么要 Hook ART这里就涉及 Android 加固真正困难的地方。如果Method A在 DEX 中已经被替换成return 0那么 ART 自己执行的时候自然只会执行return 0它不知道真正代码在哪里。所以必须在ART 加载 / 定义 Class / Method的关键路径上进行干预。XopProtector 在初始化阶段会protector::hook::install_hooks();然后再解析和应用code.bin源码明确说明在解析 / 应用 code.bin 前安装 ART hooks以便 DefineClass 时进行 patch。因此整个体系实际上形成DEX ↓ ART ↓ Hook ↓ 识别被保护 Method ↓ 查 code_map ↓ 恢复真实 Dalvik ↓ 交给 ART十九、PVM1 的运行时恢复流程可以把它完整画成App 启动 │ ▼ Native Shell 初始化 │ ▼ 读取 code.bin │ ▼ codeitem::parse │ ▼ 建立 code_map │ ▼ 安装 ART Hook │ ▼ ART 加载目标 Class │ ▼ 检查 Method 是否受保护 │ ┌──────┴──────┐ │ │ 否 是 │ │ ▼ ▼ 正常执行 查询 code_map │ ▼ AES-GCM 解密 │ ▼ PVM1 Decode │ ▼ 得到真实 Dalvik 指令 │ ▼ Patch CodeItem │ ▼ ART 执行这就是 PVM1 最核心的运行时机制。二十、PVM1 的“虚拟化”到底体现在哪里现在回到最开始的问题PVM1 到底算不算虚拟化答案是算但属于非常轻量级的“虚拟化打包”而不是完整 VM 虚拟化。因为它确实把原始代码Dalvik instructions转换成了PVM1 image原始代码不再直接位于原 Method CodeItem 中。但是它没有Dalvik opcode ↓ PVM opcode ↓ VM Register ↓ VM Stack ↓ Dispatcher ↓ Handler这一整套机制。因此PVM1 Dalvik → protected image → Dalvik而PVM2 Dalvik → custom VM bytecode → Native Interpreter这才是真正意义上的Code Virtualization二十一、为什么 PVM1 比真正虚拟化简单很多假设原始代码intcalc(inta,intb){intxab;returnx*10;}PVM1 的目标只是隐藏 add-int mul-int return然后运行时恢复 add-int mul-int returnART 继续执行。所以 PVM1 并不需要理解add-int mul-int if goto invoke new-instance monitor try/catch的语义。它只需要保存 ↓ 解码 ↓ 恢复因此实现成本相对较低。二十二、真正的 PVM2 为什么会复杂得多假设 PVM2 也拿到add-int mul-int return它不会把这些指令恢复到 DEX。而是转换成自己的VM_ADD VM_MUL VM_RETURN然后Native Interpreter switch(opcode) { case VM_ADD: ... case VM_MUL: ... case VM_RETURN: ... }于是ART ↓ JNI Trampoline ↓ Native Interpreter ↓ VM Opcode ↓ Handler整个执行模型都发生改变。XopProtector 当前 PVM2 文档也明确说明PVM2 方法不会恢复到 Dalvik而是通过 JNI trampoline 进入 Native Interpreter读取code.bin中的 PVM2 image。这就是下一篇文章真正需要讨论的内容。二十三、PVM1 对静态逆向的意义假设没有 PVM1classes.dex ↓ jadx ↓ Java/Kotlin-like code攻击者可以直接看到calculateToken()validateLicense()checkSignature()generateKey()而加入 PVM1 后classes.dex ↓ 目标方法 ↓ Stub静态分析工具看到的可能只是return0;或者returnnull;真实逻辑已经不在 Method CodeItem 中。所以静态反编译结果 ≠ 真实业务逻辑这就是 PVM1 最直接的价值。二十四、但是 PVM1 并不是“不可逆”这一点在技术文章中必须客观说明。PVM1 的最终执行路径仍然是PVM1 ↓ Decode ↓ Dalvik ↓ ART所以从攻击者角度静态分析 ↓ 难度增加 运行时动态分析 ↓ 仍然可以观察 最终恢复后的 Dalvik ↓ 仍然存在因此PVM1 的目标不是让代码永远无法获得而是增加静态分析和自动化脱壳的成本。这也符合 XopProtector 项目本身的定位加固的作用是提高逆向成本而不是保证应用绝对不可破解。二十五、PVM1 最大的价值其实是“方法级明文控制”如果把保护过程画成时间轴APK │ │ 加密状态 ▼ App 启动 │ ▼ Runtime 解密 │ ▼ PVM1 Method Decode │ ▼ Dalvik Method 明文 │ ▼ ART 执行关键区别是以前整个 DEX ↓ 大量代码同时明文PVM1Method A ↓ 需要时恢复 Method B ↓ 需要时恢复 Method C ↓ 需要时恢复因此保护对象从“整个代码包”变成“一个个关键方法”。这就是方法级加固最重要的工程价值。二十六、从源码看PVM1 实际上是“三层保护叠加”如果把 XopProtector 的 PVM1 单独拆开可以得到第一层 Method Hollowing ↓ 从 DEX 中移除真实实现 第二层 PVM1 Transform ↓ XOR Nibble Swap ↓ 破坏原始 Dalvik 数据特征 第三层 AES-GCM ↓ 真正的数据机密性所以它并不是PVM1 XOR也不是PVM1 AES而是Method │ ▼ ┌───────────────┐ │ Method Extract│ └───────┬───────┘ ▼ ┌───────────────┐ │ PVM1 Transform │ │ XOR Nibble │ └───────┬───────┘ ▼ ┌───────────────┐ │ AES-GCM │ └───────┬───────┘ ▼ code.bin而 DEX 中只留下Stub二十七、PVM1 和传统“代码抽取”有什么区别如果只说“PVM1 就是把代码抽出来。”其实不够准确。因为普通代码抽取可能只是DEX ↓ 抽取 Method ↓ 保存到其他文件但是 PVM1 还增加了Method Index PVM1 Encoding AES-GCM Method Hollowing ART Runtime Restore所以完整体系是代码抽取 代码变形 加密存储 原位置空洞化 运行时恢复这才构成完整的 PVM1。二十八、PVM1 的完整生命周期把整个源码实现浓缩成一条链【构建阶段】 APK │ ▼ 解包 DEX │ ▼ 遍历 ClassDef │ ▼ 找到目标 Method │ ▼ 读取 CodeItem │ ▼ 提取 Dalvik Instructions │ ▼ VmCodec.encode │ ▼ PVM1 Blob 生成 │ ▼ AES-GCM 加密 │ ▼ code.bin │ ├───────────────┐ │ │ ▼ ▼ 原 Method 被抹掉 Stub │ ▼ DEX 重写 │ ▼ APK 重新打包 【运行阶段】 App 启动 │ ▼ Native Shell │ ▼ 读取 code.bin │ ▼ codeitem::parse │ ▼ 建立 code_map │ ▼ 安装 ART Hook │ ▼ ART 加载 Class │ ▼ 找到受保护 Method │ ▼ 查询 code_map │ ▼ AES-GCM 解密 │ ▼ PVM1 Decode │ ▼ 得到真实 Dalvik │ ▼ Patch Method │ ▼ ART │ ▼ 正常执行这条链基本就是 XopProtector PVM1 的核心原理。二十九、PVM1 真正解决了什么问题可以总结成四个字静态不可见。更准确地说1. 静态 DEX 中不再存在完整方法实现攻击者拿到 DEX 后目标方法已经被替换成 Stub。2. 方法实现被移动到独立数据区真实代码进入code.bin而不是继续留在原来的 CodeItem 中。3. PVM1 破坏原始 Dalvik 数据形态通过XOR KeyStream Nibble Swap让数据不再表现为正常 Dalvik instruction stream。4. AES-GCM 提供真正的机密性即使攻击者找到code.bin也不能简单通过strings dexdump jadx直接获得原始代码。三十、但 PVM1 还有一个天然弱点这也是为什么 XopProtector 后面还需要 PVM2。PVM1加密 ↓ 恢复 ↓ Dalvik ↓ ART所以最终真实 Dalvik还是要出现。因此攻击者如果把分析重点从APK 静态分析转向Runtime ↓ ART Hook ↓ Method Restore ↓ Memory Dump就可能重新获取真实方法。所以 PVM1 的安全模型是提高静态分析成本 增加脱壳复杂度 缩短攻击者直接获得代码的路径 但是 最终仍然回到 ART Dalvik 执行体系这也是 PVM1 与 PVM2 最根本的安全边界。三十一、PVM1 → PVM2是 Android 加固的一次质变可以把整个演进过程理解为第一阶段 DEX 加密 攻击者 APK ↓ 找到解密点 ↓ 得到 DEX 第二阶段 PVM1 攻击者 APK ↓ 找到 code.bin ↓ 找到 Runtime Restore ↓ 获取 Dalvik 第三阶段 PVM2 攻击者 APK ↓ 找到 JNI Trampoline ↓ 找到 Native Interpreter ↓ 理解自定义 VM ↓ 分析 VM Opcode ↓ 恢复原始语义所以难度是逐层提升的。三十二、PVM1 最值得学习的工程设计从工程实现角度看PVM1 并没有试图一步做到极端复杂。它采取的是一种非常实用的思路DEX 加密 ↓ 解决“APK 静态暴露” PVM1 ↓ 解决“关键方法暴露” PVM2 ↓ 解决“恢复后仍然是 Dalvik” SO 加密 ↓ 解决 Native 代码暴露 RASP ↓ 解决运行时攻击也就是说不同保护技术解决不同攻击面。这比单纯依赖一种“超级加密算法”更加符合商业 APK 加固系统的工程思路。三十三、源码层面的关键文件如果你准备继续深入研究 XopProtector那么 PVM1 最值得看的几个源码位置是packer/ └── src/main/java/com/yqsh/protector/packer/ │ ├── PackerMain.java │ ├── walkDexMethods() │ ├── extractOne() │ ├── writeReturnStub() │ └── writeCodeBin() │ └── VmCodec.java ├── encode() └── decode()其中PackerMain.java负责扫描 DEX ↓ 定位 Method ↓ 抽取指令 ↓ 替换 Stub ↓ 生成 code.binVmCodec.java负责PVM1 Encode PVM1 DecodeNative 侧则对应native/src/main/cpp/vm/ └── vm_codec.cpp负责运行时PVM1 DecodeNative Runtimenative/src/main/cpp/runtime/ └── engine.cpp负责初始化 ↓ 加载 code.bin ↓ 建立 code_map ↓ 安装 Hook ↓ 进入运行时保护流程这些源码结构可以非常清晰地证明PVM1 并不是一个独立的 VM而是 Packer Native Runtime ART Hook 三者协同完成的方法级保护机制。三十四、最终总结一句话理解 XopProtector PVM1如果只用一句话解释PVM1 就是在构建阶段把关键方法的 Dalvik 指令从 DEX 中抽出来经过 PVM1 变换和 AES-GCM 加密后保存到code.bin原 Method 只留下与返回类型匹配的安全 Stub应用运行时由 Native Shell 解析code.bin通过 ART Hook 找到目标 Method解密并恢复真实 Dalvik 指令再交给 ART 执行。整个过程可以最终浓缩成PVM1 ┌───────────────────┐ │ 原始 Method │ └─────────┬─────────┘ │ ▼ 提取 Dalvik Code │ ▼ PVM1 Encode XOR NibbleSwap │ ▼ AES-GCM │ ▼ code.bin │ │ ┌─────────▼─────────┐ │ 原 DEX Method │ │ │ │ 真实 Code 被移除 │ │ ↓ │ │ Stub │ └───────────────────┘ Runtime Native Shell │ ▼ code.bin │ ▼ code_map │ ▼ ART Hook │ ▼ 找到目标 Method │ ▼ AES-GCM Decode │ ▼ PVM1 Decode │ ▼ 恢复 Dalvik │ ▼ Patch Method │ ▼ ART │ ▼ 执行代码所以PVM1 不是“让代码不再执行”而是让代码不再以正常 DEX Method 的形式存在。这就是它和传统 DEX 加密最大的区别。而下一阶段真正值得研究的问题就是如果连“恢复 Dalvik”这一步都不要了能不能让被保护方法从始至终都不回到 DEX而是直接由 Native 自己解释执行答案就是PVM2。PVM2 不再是加密 → 解密 → 恢复 Dalvik → ART而会变成原始 Dalvik ↓ PVM2 Compiler ↓ 自定义 VM Image ↓ JNI Trampoline ↓ Native Interpreter ↓ VM Opcode ↓ 执行XopProtector 当前源码中的 PVM2 已经进一步加入了多 ISA、Opcode Morphing、RASP Gate、解释执行以及 PVM2 Image等机制。PVM2 v3 还会为每个 APK 生成 opcode 映射并根据isa_id选择不同 Native dispatch 入口。这才是真正意义上的 Android 代码虚拟化。下一篇《Android APK 加固原理四真正的代码虚拟化——PVM2 Native Interpreter 技术解析》将重点拆解Dalvik ↓ PVM2 Compiler ↓ VM Opcode ↓ PVM2 Image ↓ JNI Trampoline ↓ Native Interpreter ↓ Dispatcher ↓ Opcode Handler ↓ 寄存器 / 对象 / Field / Method ↓ 最终执行并重点解释为什么 PVM2 和 PVM1 已经不是同一个层级的加固技术。参考源码本文分析以 XopProtector 当前公开源码为基础重点涉及packer/PackerMain.javapacker/VmCodec.javanative/vm/vm_codec.cppnative/runtime/engine.cppPVM2 设计文档项目公开 README 明确将--vmp-prefix定义为 PVM1将--true-vmp-prefix定义为 PVM2。