Android APK 加固原理(二):从 DEX 解密到 ART 加载,如何缩短代码明文暴露窗口

59 阅读12分钟

Android APK 加固原理(二):从 DEX 解密到 ART 加载,如何缩短代码明文暴露窗口

基于 XopProtector 源码架构的技术分析 项目地址:github.com/xopJack/Xop…

在上一篇《Android APK 加固原理(一):Native Shell 如何隐藏和恢复 DEX》中,我们分析了 Android APK 加固的第一道核心防线:

通过 ProxyApplication 接管应用启动,再由 Native Shell 控制受保护 DEX 的恢复过程。

但问题并没有结束。

因为无论 APK Shell 做得多复杂,只要应用最终需要执行,代码就必须经历一个过程:

密文
 ↓
解密
 ↓
恢复
 ↓
ART 加载
 ↓
执行

这里存在一个 Android APK 加固中非常关键的问题:

从 DEX 被解密,到真正进入 ART Runtime 执行,中间会不会出现完整的明文代码?

如果答案是:

那么攻击者就有机会:

Hook
Dump
Copy
Monitor

最终重新获得原始 DEX。

因此,真正的 APK 加固,并不仅仅研究:

如何把 DEX 加密。

更重要的是研究:

DEX 解密之后,如何尽可能减少明文代码暴露的时间和空间。

这就是本文要讨论的核心:

Plaintext Window —— 代码明文暴露窗口

结合 XopProtector 的 DEX Runtime 设计,本文将重点分析:

  • 什么是 DEX 明文暴露窗口;
  • 为什么传统 DEX 解密方案容易被 Dump;
  • XopProtector 为什么采用 Payload + Restore 的设计;
  • 什么是 Hollow Restore;
  • 什么是 Class-Batch Restore;
  • 为什么要在 ART Map 之前进行预处理;
  • Cold Start 与 Warm Start 为什么不能采用完全相同的策略;
  • 如何在安全性和启动性能之间寻找平衡。

一、什么是代码明文暴露窗口

先来看一个最简单的 DEX 加密方案。

构建阶段:

classes.dex
     │
     ▼
Encrypt
     │
     ▼
encrypted.dex

APK 中保存:

encrypted.dex

应用启动后:

encrypted.dex
      │
      ▼
Decrypt
      │
      ▼
classes.dex
      │
      ▼
Write To Disk
      │
      ▼
DexClassLoader
      │
      ▼
ART

从功能角度来说,这完全可以运行。

但安全问题也非常明显。

因为在:

Decrypt
   ↓
Plain classes.dex

之后,完整 DEX 已经重新出现。

我们可以把这个阶段称为:

Plaintext Window

也就是:

从代码恢复为明文,到代码重新进入更受控的 Runtime 状态之间,攻击者能够观察、读取或复制明文代码的时间窗口。

可以简单表示为:

┌─────────────────────────────────────────────┐
│                                             │
│              Plaintext Window               │
│                                             │
│  DEX 已经恢复为明文,但仍可能被外部截获       │
│                                             │
└─────────────────────────────────────────────┘

传统方案:

Encrypted DEX
      │
      ▼
Decrypt
      │
      ▼
┌───────────────────┐
│                   │
│   Plain DEX File  │  ← 风险区域
│                   │
└───────────────────┘
      │
      ▼
ART Load

如果这个 DEX:

  • 写入磁盘;
  • 长时间存在;
  • 存在完整副本;
  • 可以被 Hook;
  • 可以被内存 Dump;

那么前面的 DEX 加密价值就会大幅下降。


二、为什么“解密后写入文件”并不安全

假设一个加固方案采用:

assets/code.dat
       │
       ▼
Decrypt
       │
       ▼
/data/data/package/cache/classes.dex
       │
       ▼
DexClassLoader

对于攻击者来说,只需要关注:

/data/data/<package>/

或者:

cache/
code_cache/
files/

在应用启动之后寻找:

classes.dex
classes2.dex
classes3.dex

如果存在完整明文文件,那么攻击过程可能重新变成:

启动 APK
   │
   ▼
等待 Shell 解密
   │
   ▼
提取 classes.dex
   │
   ▼
JADX

这样,Shell 最终只是增加了一步操作。

攻击成本并没有真正提高多少。

因此,一个更合理的保护方案,需要避免:

把完整的原始 DEX 长时间以普通文件形式重新暴露出来。

XopProtector 的源码设计中明确围绕这一问题进行了优化,其中包括:

  • no plaintext zip
  • plaintext-window shrink
  • Class-batch hollow restore
  • Parallel file prepatch before ART map

这些设计的核心目标,都是减少传统“完整 DEX 解密 → 明文长期存在”的攻击面。


三、真正需要保护的不是 DEX 文件,而是 DEX 生命周期

这是理解 APK 加固的一个重要转变。

很多人认为:

DEX 加密
=
保护 DEX 文件

实际上,更准确的理解应该是:

DEX Protection
=
保护代码生命周期

一个 DEX 从构建到执行,大致会经历:

Build
  │
  ▼
Original DEX
  │
  ▼
Protect
  │
  ▼
Protected Payload
  │
  ▼
APK
  │
  ▼
Application Launch
  │
  ▼
Decrypt
  │
  ▼
Restore
  │
  ▼
ART Mapping
  │
  ▼
Execute

真正需要控制的是:

Original DEX
       ↓
Protected Payload
       ↓
Runtime Restore
       ↓
ART

中的每一个关键节点。

因此,XopProtector 的思路并不是单纯:

Encrypt
↓
Decrypt

而是逐步控制:

  • Payload 如何保存;
  • 什么时候解密;
  • 解密哪些数据;
  • 是否需要完整恢复;
  • 是否需要生成完整 DEX;
  • 如何在 ART Mapping 前完成处理;
  • 如何减少明文数据的停留时间。

四、XopProtector 的 DEX Payload 设计

从 XopProtector 的保护资产结构可以看到,受保护 APK 中包含:

code.bin
dexes.zip
config.json

其中:

dexes.zip

采用项目定义的:

PDX1

格式承载 DEX 相关数据。

而:

code.bin

则用于保存运行时恢复所需要的代码数据。

整体可以理解为:

Original APK
      │
      ▼
XopProtector Packer
      │
      ├── DEX Processing
      │
      ├── Code Extraction
      │
      ├── Payload Generation
      │
      └── Shell Injection
      │
      ▼
Protected APK
      │
      ├── Shell DEX
      ├── libprotector.so
      │
      └── Protected Assets
              │
              ├── code.bin
              ├── dexes.zip
              └── config.json

这种设计带来的第一个变化就是:

APK 中的业务代码不再只依赖普通 classes.dex 作为唯一存储形式。

攻击者面对的不再是:

classes.dex
 ↓
JADX

而是:

Shell
 ↓
Payload
 ↓
Native Runtime
 ↓
Restore

因此,攻击链首先被向后推进了一层。


五、为什么不能简单地“全部解密”

假设一个 APK 有:

classes.dex
classes2.dex
classes3.dex
classes4.dex

最直接的方式是:

Application Start
       │
       ▼
Decrypt All
       │
       ▼
Restore All
       │
       ▼
ART Load

这存在两个问题。


问题一:完整代码同时暴露

如果一次性恢复:

classes.dex
classes2.dex
classes3.dex
classes4.dex

攻击者一旦找到正确的 Dump 时机,就可能一次获得:

完整业务代码

例如:

┌──────────────────────┐
│ classes.dex          │
│ classes2.dex         │
│ classes3.dex         │
│ classes4.dex         │
└──────────────────────┘

全部处于可访问状态。


问题二:启动性能下降

如果应用启动时:

Decrypt All
+
Restore All
+
Patch All

那么:

CPU
IO
Memory

都会产生额外开销。

最终表现可能就是:

点击 APP
   ↓
黑屏
   ↓
等待
   ↓
进入首页

对于商业应用来说:

安全不能以严重破坏用户体验为代价。

因此,加固系统必须考虑:

Security
+
Performance
+
Compatibility

三者之间的平衡。


六、什么是 Hollow Restore

XopProtector 的 DEX Runtime 中包含:

Hollow Restore

从技术思路上理解,Hollow 可以看成:

保留 DEX 的整体结构或运行时定位能力,将需要保护的实际代码数据抽离到独立 Payload 中。

原始代码:

Class A
├── method1
├── method2
└── method3

Class B
├── method1
└── method2

保护之后,可以抽象理解为:

DEX Structure
│
├── Class A
│     ├── Method Slot
│     ├── Method Slot
│     └── Method Slot
│
└── Class B
      ├── Method Slot
      └── Method Slot

真正的代码则进入:

Protected Payload
│
├── Class A.method1 Code
├── Class A.method2 Code
├── Class A.method3 Code
├── Class B.method1 Code
└── Class B.method2 Code

运行时:

Hollow Structure
       │
       ▼
Locate Protected Code
       │
       ▼
Restore
       │
       ▼
ART

这样做的关键价值在于:

APK 中不再简单保存一份完整、直接可被反编译的业务 DEX。

攻击者即使打开:

classes.dex

看到的也可能只是部分结构信息,而真正的 Code Item 或受保护代码数据需要经过 Runtime Restore 才能恢复。


七、从 Hollow Restore 到 Class-Batch Restore

如果恢复粒度仍然是:

全部恢复

那么问题依然存在。

因此 XopProtector 进一步引入:

Class-Batch Hollow Restore

也就是:

以 Class 或 Class Batch 为单位进行恢复。

传统方式:

Protected Payload
       │
       ▼
Restore Everything
       │
       ▼
Complete DEX

Batch Restore:

Protected Payload
       │
       ├── Batch 1
       │
       ├── Batch 2
       │
       ├── Batch 3
       │
       └── Batch N

恢复流程:

Application Startup
       │
       ▼
Load Required Batch
       │
       ▼
Restore
       │
       ▼
ART Mapping
       │
       ▼
Continue Startup

从安全角度来看:

一次性完整恢复

意味着:

攻击者只需要找到一个正确时机。

而:

分批恢复

则意味着攻击者需要:

理解 Restore Strategy
        ↓
寻找多个恢复时机
        ↓
定位不同 Code Data
        ↓
重新组织数据

攻击复杂度明显提高。


八、缩短明文窗口的核心思想

所谓:

Plaintext-window shrink

并不是说:

DEX 永远不会出现明文。

这是不现实的。

因为最终 ART 需要执行代码。

真正的目标是:

减少暴露时间
+
减少完整副本
+
减少落地文件
+
减少无意义缓存

可以理解为:

传统:

Decrypt
   │
   ▼
Plain DEX
   │
   │  ← 长时间存在
   │
   │
   ▼
ART

优化后的思路:

Decrypt
   │
   ▼
Restore Required Data
   │
   ▼
ART Mapping
   │
   ▼
Execute

目标就是:

让代码从受保护状态进入 Runtime 的路径尽可能短。

也就是:

Protected
    ↓
Restore
    ↓
ART

中间尽量避免:

Plain File
    ↓
Long-term Storage
    ↓
Copy
    ↓
Extra Cache

九、为什么要避免 Plaintext ZIP

传统打包方式中,一个简单的思路可能是:

DEX Files
   │
   ▼
ZIP
   │
   ▼
Encrypt

运行时:

Encrypted ZIP
       │
       ▼
Decrypt
       │
       ▼
Plain ZIP
       │
       ▼
Extract DEX

这样会产生两个暴露对象:

Plain ZIP

以及:

Plain DEX

因此:

Encrypted Payload
        ↓
Plain ZIP
        ↓
Plain DEX

实际上增加了一层中间明文。

XopProtector 在 DEX Runtime 优化中明确提出:

no plaintext zip

其核心思路就是:

避免在恢复过程中产生不必要的完整明文压缩包。

更合理的流程是:

Protected Payload
        │
        ▼
Decrypt Required Data
        │
        ▼
Direct Extract / Restore
        │
        ▼
ART

这样可以减少:

中间文件
完整副本
磁盘残留

十、为什么 ART Mapping 是一个关键节点

Android 应用最终需要通过 ART 来执行代码。

因此:

ART Map

可以看成 DEX 保护流程中的一个重要边界。

完整流程:

Protected Code
       │
       ▼
Native Restore
       │
       ▼
DEX Ready
       │
       ▼
ART Mapping
       │
       ▼
ART Runtime
       │
       ▼
Execution

在 ART Mapping 之前:

Native Shell 仍然可以控制代码数据的恢复和预处理。

进入 ART Runtime 之后:

代码已经开始进入 Android Runtime 的管理体系。

因此,XopProtector 在优化中加入:

Parallel file prepatch before ART map

可以理解为:

在 ART 真正映射之前,提前完成必要的恢复和 Patch 工作。

整体:

Protected Payload
       │
       ├──────────────┐
       │              │
       ▼              ▼
Decrypt         Prepatch
       │              │
       └──────┬───────┘
              │
              ▼
           ART Map
              │
              ▼
           Execute

这样可以减少:

ART 已经开始加载
       ↓
Native Runtime 仍在大量处理

带来的性能问题。


十一、Parallel Prepatch 为什么重要

假设所有工作都是串行的:

Start
 │
 ▼
Decrypt
 │
 ▼
Restore
 │
 ▼
Patch
 │
 ▼
ART Map
 │
 ▼
Application

整个启动时间可以表示为:

T =
Decrypt
+
Restore
+
Patch
+
ART Map

如果其中一部分工作可以提前或者并行:

┌── Decrypt ──┐
Start ────┤              ├── ART Map
          └── Prepatch ─┘

那么关键启动路径就可能缩短。

这就是:

Parallel file prepatch before ART map

的意义。

它不仅是性能优化。

从安全角度来说,也意味着:

减少恢复后的代码处于“已经明文,但尚未进入 ART”的等待时间。

因此它同时服务于:

Performance
+
Plaintext Window Shrink

十二、Cold Start 与 Warm Start 为什么需要不同策略

这是商业 APK 加固中非常现实的问题。

第一次启动应用:

Cold Start

通常需要完成更多工作:

Read Payload
     ↓
Decrypt
     ↓
Extract
     ↓
Restore
     ↓
Prepare Runtime

因此 XopProtector 中存在:

cold-start decrypt → extract pipeline

的优化思路。

而应用后续再次启动:

Warm Start

如果仍然重复:

Read
↓
Decrypt
↓
Copy
↓
Write

那么会产生大量不必要的:

IO
Memory Copy
CPU

因此 XopProtector 的优化中还提出:

warm skip code.bin re-copy

也就是说:

冷启动和后续启动不必采用完全相同的恢复路径。

可以抽象为:

Cold Start

Protected Payload
       │
       ▼
Decrypt
       │
       ▼
Extract
       │
       ▼
Restore
       │
       ▼
ART

Warm Start

Prepared Runtime Data
       │
       ▼
Skip Unnecessary Copy
       │
       ▼
Restore Required State
       │
       ▼
ART

这种设计体现了一个重要原则:

安全系统不能只考虑第一次保护成功,还必须考虑长期运行成本。


十三、启动性能与安全性之间的平衡

一个 APK 加固方案可能非常安全:

每次启动
   ↓
全部 DEX
   ↓
强加密
   ↓
完整解密
   ↓
完整校验
   ↓
重新加载

但如果最终:

启动时间
+ 3 秒

那么用户体验会明显下降。

反过来,如果为了性能:

第一次解密
   ↓
保存完整 DEX
   ↓
以后直接加载

那么安全性又会下降。

因此真正的商业 APK 加固系统,需要寻找:

Security
                 ▲
                 │
                 │
                 │
Performance ─────┼───── Compatibility

三个方向之间的平衡。

XopProtector 的:

  • Class-Batch Restore;
  • Parallel Prepatch;
  • Cold Start Pipeline;
  • Warm Start Skip;
  • No Plaintext ZIP;

本质上都是围绕这个目标进行优化。

不是简单追求:

“最强加密”。

而是:

在应用能够正常运行的前提下,提高攻击成本,同时控制启动性能。


十四、从攻击者角度看 Plaintext Window

如果没有控制明文窗口,攻击者的流程可能非常简单:

APK
 │
 ▼
Run
 │
 ▼
Hook Decrypt
 │
 ▼
Dump Memory
 │
 ▼
Get classes.dex

而加入 Shell + Payload + Restore 后:

APK
 │
 ▼
Analyze Shell
 │
 ▼
Locate Native Runtime
 │
 ▼
Analyze Payload
 │
 ▼
Understand Restore Flow
 │
 ▼
Locate ART Mapping
 │
 ▼
Find Correct Dump Point
 │
 ▼
Reconstruct Code

如果进一步采用:

Class-Batch Restore

那么攻击者可能还需要处理:

Batch A
Batch B
Batch C
Batch N

重新组织恢复结果。

攻击从:

拿到文件

逐渐变成:

理解整个 Runtime

这就是明文窗口缩短带来的真正价值。


十五、完整的 XopProtector DEX Restore 流程

结合 XopProtector 的整体设计,可以将整个过程抽象为:

Original APK
                     │
                     ▼
          XopProtector Packer
                     │
        ┌────────────┼────────────┐
        │            │            │
        ▼            ▼            ▼
    Shell DEX   Native Runtime  Payload
        │            │            │
        └────────────┼────────────┘
                     │
                     ▼
                Protected APK
                     │
                     ▼
                  App Launch
                     │
                     ▼
              ProxyApplication
                     │
                     ▼
              Native Bootstrap
                     │
                     ▼
             Read Protected Data
                     │
                     ▼
                  Decrypt
                     │
                     ▼
          ┌──────────────────────┐
          │  Class-Batch Restore │
          └──────────────────────┘
                     │
                     ▼
             Prepatch / Prepare
                     │
                     ▼
                  ART Map
                     │
                     ▼
                 Execution

整个过程中,核心目标始终没有变化:

让代码以尽可能短的路径,从受保护状态进入可执行状态。

而不是:

Decrypt
↓
生成一份完整明文
↓
放在那里
↓
等 ART 加载

十六、总结:DEX 加密真正的难点,不在加密,而在恢复

很多人第一次做 APK 加固时,会把重点放在:

AES
RC4
XOR
ChaCha20

到底使用哪一种算法。

但实际上,对于 APK Runtime Protection 来说,更重要的问题是:

解密之后怎么办?

如果:

Encrypted DEX
      ↓
Decrypt
      ↓
Plain classes.dex
      ↓
攻击者复制

那么无论前面使用多强的加密算法,最终仍然可能被绕过。

因此,真正的 DEX 加固应该关注:

何时恢复?
恢复多少?
在哪里恢复?
是否产生完整副本?
如何进入 ART?
明文存在多久?

XopProtector 在 DEX Runtime 层中的设计方向,正是围绕这些问题展开。

通过:

  • Protected Payload;
  • PDX1;
  • code.bin
  • Hollow Restore;
  • Class-Batch Restore;
  • Plaintext Window Shrink;
  • No Plaintext ZIP;
  • ART Map 前 Prepatch;
  • Cold Start Pipeline;
  • Warm Start Optimization;

尝试重新控制 DEX 从:

Protected State

到:

ART Execution

之间的整个生命周期。

最终,APK 加固不再只是:

“给 DEX 加密。”

而是:

“控制代码什么时候恢复、恢复到什么粒度,以及明文代码在系统中存在多久。”

这也是 Native Shell 技术进一步发展的重要方向。

在 XopProtector 的整体保护体系中,DEX Shell 和 Runtime Restore 解决的是:

如何隐藏代码。

而接下来进一步进入的技术,则是:

即使攻击者成功定位到代码,也让其难以直接理解代码逻辑。

下一篇,我们将继续分析:

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

将从 XopProtector 的 PVM1 实现思路出发,分析:

  • 什么是方法级代码抽取;
  • 什么是 Virtualized Packing;
  • 为什么 PVM1 不等于真正的 Native Interpreter;
  • Dalvik Code 如何被抽取;
  • Runtime 如何恢复方法代码;
  • PVM1 与传统 DEX Shell 的区别;
  • PVM1 与 PVM2 真虚拟化的区别。

从 DEX 文件级保护,进一步进入:

Method-Level Code Protection —— 方法级代码保护。