news 2026/8/28 22:14:27

Android APK 加固原理(三):方法级代码抽取——PVM1 虚拟化打包到底是什么?

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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.Codecode=dex.readCode(method);

然后:

short[]units=code.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 数据拥有一个非常明显的 Magic:

PVM1

源码:

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 Stream

PVM1 的编码并不是简单:

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

除了 XOR,PVM1 还做了一层非常轻量的字节变换。

源码中:

if((i&1)==0){b=((b<<4)&0xf0)|((b>>4)&0x0f);}

也就是:

对偶数位置的字节进行高低 4 bit 交换。

例如:

原始: 1010 0011

经过 Nibble Swap:

0011 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-GCM

PVM1 编码只是:

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){returna+b;}

原来的 Dalvik 指令可能类似:

add-int return

PVM1 后变成:

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 #102

PVM1 不需要在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_map

Runtime 将保护方法建立成内部映射。

源码:

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){intx=a+b;returnx*10;}

PVM1 的目标只是:

隐藏: add-int mul-int return

然后运行时:

恢复: add-int mul-int return

ART 继续执行。

所以 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 对静态逆向的意义

假设没有 PVM1:

classes.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 ↓ 大量代码同时明文

PVM1:

Method 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.bin

VmCodec.java

负责:

PVM1 Encode PVM1 Decode

Native 侧则对应:

native/src/main/cpp/vm/ └── vm_codec.cpp

负责运行时:

PVM1 Decode

Native Runtime:

native/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.java
  • packer/VmCodec.java
  • native/vm/vm_codec.cpp
  • native/runtime/engine.cpp
  • PVM2 设计文档

项目公开 README 明确将--vmp-prefix定义为 PVM1,将--true-vmp-prefix定义为 PVM2。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/28 22:05:54

纯CSS美食网站设计实战:从变量系统到响应式布局

简介&#xff1a;CSS作为前端开发的核心技术&#xff0c;通过层叠样式表实现对网页视觉呈现的精准控制。其工作原理基于选择器、属性和值的组合&#xff0c;通过盒模型、定位、浮动等机制进行页面布局。CSS的技术价值在于分离内容与表现&#xff0c;提升开发效率与维护性&#…

作者头像 李华
网站建设 2026/8/28 22:04:48

R语言非参数回归在保险定价中的应用:LOESS、GAM与样条回归实战

1. 项目概述&#xff1a;当精算遇见“自由形态” 在保险定价这个传统上由精算公式和严格假设主导的领域&#xff0c;我们常常面临一个核心矛盾&#xff1a;如何平衡模型的准确性与可解释性&#xff1f;经典的广义线性模型&#xff08;GLM&#xff09;凭借其清晰的参数结构和统计…

作者头像 李华
网站建设 2026/8/28 22:04:01

2026年武汉市职称申报详细流程+注意事项来咯

武汉市职称申报详细流程和注意事项&#xff1a;1.武汉市职称申报网站: 湖北省专业技术人员职称评审管理信息系统2.注册账号&#xff0c;登录网站进入报名&#xff0c;先绑定自己的工作单位&#xff08;输入单位如果绑定不了&#xff0c;那就是单位还没有开通账户&#xff0c;需…

作者头像 李华
网站建设 2026/8/28 22:01:10

出货量一年涨776%,退货率60%:AI眼镜的冰火两重天

过去一年&#xff0c;AI眼镜出货量涨了776%&#xff0c;是消费电子里最猛的一条赛道&#xff1b;可就在这周&#xff0c;挪威宣布要立法管它&#xff0c;微博把它骂上"隐私重灾区"热榜&#xff0c;行业里还传着"退货率60%"的阴影。卖爆了&#xff0c;为什么…

作者头像 李华