一、加固的核心目标
应用加固的核心思路是将原始的 DEX(Dalvik Executable)和 SO 文件加密后嵌入壳程序,等应用启动时由壳程序在内存中解密并动态加载执行。这样,即使攻击者获取了 APK,看到的也只是加密后的乱码数据,无法直接通过 jadx 或 baksmali 反编译。
商业加固(如 360 加固、梆梆、爱加密)的原理大体分为三个阶段:打包阶段——将原 DEX 加密后存放在 assets 或 lib 目录下,替换 Application 类为壳的入口类;启动阶段——壳 Application 在 attachBaseContext() 中加载解密 SO 库,从 assets 读取加密 DEX 到内存;执行阶段——解密并通过 DexClassLoader 或 InMemoryDexClassLoader 动态加载,反射调用原 Application 并移交控制权。
一.1 加固的完整生命周期
┌──────────────────────────────────────────────────────┐ |
一.2 壳 classes.dex 的代码结构
加固后的 APK 中,classes.dex 是极简的壳代码:
// 壳的 classes.dex 中通常只有以下几类: |
一.3 关键:ClassLoader 替换机制
这是壳最核心的操作——替换应用的 ClassLoader:
private void replaceClassLoader(Context context, ClassLoader newCl) { |
二、DEX 加密与动态加载技术
二.1 整体加密(第一代)
原始 APK: |
整体加密的缺点:在解密完成后的某个时刻,temporal 文件或内存中包含完整的明文 DEX。攻击者只需在合适的时机(如 Hook DexFile 构造函数)进行一次 dump 即可获取完整代码。
二.2 分段解密(第二代)
DEX 被按类或按方法分段加密: |
二.3 InMemoryDexClassLoader 加载(Android 8.0+)
Android 8.0 引入了 InMemoryDexClassLoader,允许直接从内存中的 ByteBuffer 加载 DEX,无需写入文件系统。这提升了加固的安全性——明文 DEX 不再经过文件系统,减少了被 dump 的机会:
// Android 8.0+ 的内存 DEX 加载 |
二.4 Native 层 DEX 解密示例
// native_dex_decrypt.cpp |
二.5 CRC32 校验 Native 层实现
// native_crc_check.c |
三、SO 保护
加固方案中的 SO 库负责加解密逻辑、反调试和完整性校验。对 SO 的保护手段包括:代码段加密(在 .init_array 中运行时解密)、符号表剥离(strip)、控制流平坦化(OLLVM -mllvm -fla)、字符串加密(编译器插件在编译期替换)。部分加固还会将核心代码以加密形式存放于 .rodata 段的自定义位置,运行时才映射为可执行内存并跳转。
三.1 SO 代码段加密
编译期: |
三.2 SO 自定义 Linker Script
/* custom_shell.ld - 自定义链接脚本片段 */ |
// 函数标记为加密区域 |
四、完整性校验流程
加固 APK 在启动时会执行多轮完整性检查:校验 APK 签名(防止二次打包替换壳包)、校验自身 DEX 和 SO 的 CRC/哈希值(防止静态修改)、检测环境特征(Root、模拟器、hook 框架注入)。这些检查分散在 Native 层多处,形成互相验证的检查网络。
四.1 多轮完整性校验架构
┌─────────────────────────────────────────┐ |
四.2 让校验结果影响后续计算的抗 patch 设计
// 抗 patch 设计:校验结果不作为简单的 bool 返回 |
五、商业加固方案对比
| 特性 | 360加固 | 梆梆加固 | 腾讯乐固 | 网易易盾 | 爱加密 |
|---|---|---|---|---|---|
| 壳 Application | StubApp3580 | AW | StubShell | NQApplication | IjiamiApp |
| 核心 Native 库 | libjiagu.so | libSecShell.so | libtup.so | libnqshield.so | libexec.so |
| VMP 支持 | 是 (独有) | 是 | 否 | 否 | 否 |
| DEX 加密 | 整体+抽取 | 整体 | 整体 | 整体 | 整体 |
| SO 保护 | 代码段加密 | 代码段加密 | 符号剥离 | OLLVM | OLLVM |
| 反调试 | ★★★★★ | ★★★★☆ | ★★★☆☆ | ★★★★☆ | ★★★☆☆ |
| 字符串加密 | ★★★★★ | ★★★★☆ | ★★★☆☆ | ★★★★☆ | ★★★☆☆ |
| 兼容性 | ★★★★☆ | ★★★★☆ | ★★★★★ | ★★★★☆ | ★★★★☆ |
| 脱壳难度 | ★★★★★ | ★★★★☆ | ★★★☆☆ | ★★★☆☆ | ★★★☆☆ |
| 性能开销 | 高 | 中 | 低 | 中 | 中 |
六、加固技术的局限性与未来趋势
六.1 根本局限性
加固技术的根本困境: |
六.2 未来趋势
- VMP 保护深化:将更多业务逻辑下沉到自定义 VM,增加逆向需要理解的新指令集。
- 服务端逻辑迁移:将核心算法放在服务端,客户端仅做展示(适合网络密集型应用)。
- 硬件安全模块(TEE/SE):利用 ARM TrustZone(TEE)或独立安全芯片(SE)执行关键逻辑,代码永不暴露给普通 OS。
- AI 辅助动态保护:运行时 AI 检测异常分析行为(如异常的代码覆盖率、异常的调用频率模式),动态调整防护策略。
- 反 AI 逆向:设计针对大模型代码理解能力的混淆技术(如语义等价转换、逻辑谜题化)。
- 软件证明(Attestation):结合 SafetyNet/Play Integrity API,服务端验证客户端代码的完整性。
七、AOSP 相关源码导读
| 模块 | 源码路径 | 关键内容 |
|---|---|---|
| DexClassLoader | /libcore/dalvik/src/main/java/dalvik/system/DexClassLoader.java |
DEX 类加载器 Java 入口 |
| InMemoryDexClassLoader | /libcore/dalvik/src/main/java/dalvik/system/InMemoryDexClassLoader.java |
内存 DEX 直接加载 |
| DexFile.java | /libcore/dalvik/src/main/java/dalvik/system/DexFile.java |
DEX 文件操作 Java 侧 |
| DexFile (Native) | /art/libdexfile/dex/dex_file.cc |
DEX 解析与加载 Native 实现 |
| DexFile::OpenCommon | /art/libdexfile/dex/dex_file.cc |
DEX 文件打开核心函数(加固主要 Hook 点) |
| ClassLinker | /art/runtime/class_linker.cc |
类链接器(RegisterDexFile, DefineClass) |
| ClassLinker::LoadMethod | /art/runtime/class_linker.cc |
方法加载(抽取型加固指令恢复点) |
| LoadedApk | frameworks/base/core/java/android/app/LoadedApk.java |
保存每个 APK 的 ClassLoader 和资源 |
| Application | frameworks/base/core/java/android/app/Application.java |
Application 生命周期 |
| ContextImpl | frameworks/base/core/java/android/app/ContextImpl.java |
Context 实现类(getPackageInfo → LoadedApk) |
面试常考问题
Q1: 加固为什么不能 100% 防止逆向?有没有理论上绝对安全的保护方案?
A:
根因:任何软件代码最终必须在 CPU 上以明文形式执行。加固的所有保护(加密、混淆、VMP、反调试)只是增加了从 APK 到可理解代码的转换难度,但无法消除”代码必须在内存中明文存在”这个根本事实。攻击者可以在 CPU 执行的路径上设置观测点(通过 Frida、内核模块、JTAG 硬件调试器等)直接获取 CPU 正在执行的指令。
没有理论上绝对安全的纯软件保护方案。这是计算机科学的基本限制——代码需要在不受信任的硬件上执行(即”Untrusted Client Problem”)。如果 CPU 需要执行指令,那么能够访问 CPU 的攻击者就能获取这些指令。
接近”绝对安全”的方案需要硬件参与:
- ARM TrustZone / TEE(可信执行环境):敏感代码在隔离的安全区域执行,普通 OS 无法访问。
- Google Play Integrity API + SafetyNet Attestation:服务端验证客户端完整性,但依赖 Google 服务。
- 完全服务端化的方案:敏感逻辑不在客户端执行(如云端渲染、服务端数据签名)。
但上述方案也各有其局限(需要特定硬件、依赖第三方服务、需要网络连接等)。
加固的目标不是绝对安全,而是:
(1)使逆向的成本超过获取同样功能的合法成本(经济壁垒)。
(2)使逆向的时间超过版本更新周期(时间壁垒)。
(3)使逆向需要的技能组合更加广泛(技术壁垒)。
(4)使攻击者必须使用定制工具而非现有开源工具(工具壁垒)。
AOSP 中 DEX 加载的核心实现:/art/libdexfile/dex/dex_file.cc 的 OpenCommon 函数和 /art/runtime/class_linker.cc 的 RegisterDexFile 函数,是壳解密后加载 DEX 的关键路径,也是脱壳工具的主要 Hook 点。
Q2: 分段解密与整体解密相比有什么优势?如何实现?
A:
分段解密的优势:
(1)内存安全:整体解密在某时刻让所有明文 DEX 同时存在于内存中,攻击者只需抓住一个时间点 dump 即可获取全部代码。分段解密只在方法执行前解密目标区域、执行后立即重新加密(”用完即焚”),使得内存中同时只有极少量的明文代码,大幅提升了 dump 的难度。
(2)抗时序攻击:即使攻击者 Hook 了 DEX 解析函数,每次解密的数据量极小(一个方法甚至一个基本块),需要长时间持续监控才能收集到完整的 DEX 代码。这使得一次性的自动化 dump 工具失效。
(3)抗全量激活:抽取型加固要求攻击者”主动调用所有方法”来触发解密。大型应用可能有数万个方法,主动调用需要构造正确的参数、处理异常、覆盖所有代码路径,工作量极大。
实现方式:
(1)编译期:使用 APK 后处理工具,将每个方法的 code_item 从 DEX 中抽出(替换为指向壳解密函数的桩代码),将原始指令加密后存入单独的索引表。
(2)运行时:通过 Hook ART 的 ClassLinker::LoadMethod 或修改 ArtMethod 的 entry_point,在方法第一次被调用时从索引表中查出对应的加密指令,解密后回填到 code_item,然后执行。
(3)高级变体:”自适应分段”——热点方法(被频繁调用的)保持解密状态以提升性能,冷方法(只调用过一次的)在第一次执行后立即重新加密。
AOSP 中方法加载和执行的源码路径:/art/runtime/class_linker.cc 的 LoadMethod 函数负责将方法从 DEX 加载到 ART 内部结构;/art/runtime/art_method.h 中的 entry_point_from_quick_compiled_code_ 是方法入口点。
Q3: 绕过加固的常见思路有哪些?不同加固类型的绕过策略有何不同?
A:
通用思路:
(1)壳识别:首先通过 apktool 解包检查 so 特征、Manifest 中 Application 类名、或使用 APKiD 工具确定加固厂商和版本。
(2)针对不同加固类型选择策略:
整体型加固:
- Hook
DexFile::OpenCommon(/art/libdexfile/dex/dex_file.cc)在 DEX 文件打开时 dump 参数中的 data 指针。 - Hook
InMemoryDexClassLoader的构造函数获取 ByteBuffer 内容。 - Frida 的
Memory.scan搜索 DEX magic numberdex\n035。 - 监控壳的 Native 库中解密函数(通过 trace dlopen 找到壳的 so → 分析其导出符号 → 定位解密函数 → dump 解密输出)。
抽取型加固:
- 主动调用所有方法来触发指令恢复(通过 Java reflection 或 Frida 枚举所有类的所有方法逐一 invoke)。
- Hook
ClassLinker::LoadMethod在方法首次加载时收集指令。 - 使用 Frida Stalker 追踪代码执行,重建方法的完整控制流。
- 监控应用运行过程中的内存变化,在应用充分运行后做内存 dump(此时被调用过的方法指令已恢复)。
VMP 加固(最难):
- 逆向壳自带的 VM 解释器(通过 IDA Pro/Ghidra 分析 libjiagu_vm.so 中的解释器循环,建立 VMP opcode → 语义 的映射表)。
- Hook VM 解释器的 Dispatch 函数,记录所有执行的 VMP 指令序列(trace-based approach)。
- 符号执行/混合执行(Concolic Execution)VM 解释器,自动推断 opcode 语义。
- 关注 VM 与 ART 的交互边界(VM 最终仍需通过 ART API 执行 field access、method invoke 等),Hook 这些边界函数间接获取程序行为。
(3)绕过壳的防护检测:
- Hook 壳的签名校验函数返回 true。
- Hook ptrace 和 /proc/self/status 读取使反调试失效。
- 使用 Frida 的 spawn 模式在壳初始化前注入。
- 使用 Magisk + Shamiko 隐藏 Hook 框架特征。
Q4: 某个加固后的 APK 在启动时崩溃,如何排查是加固本身的问题还是应用的问题?
A:
排查流程:
(1)获取崩溃日志:adb logcat -b crash > crash.log,查看 AndroidRuntime 的 FATAL EXCEPTION 信息。注意看崩溃发生在 ShellApplication 还是原始 Application。
(2)判断崩溃阶段:
- 如果在
ShellApplication.attachBaseContext()中崩溃 → 概率是加固问题(so 加载失败、解密失败、ClassLoader 替换失败)。 - 如果在原始
Application.onCreate()中崩溃 → 可能是业务代码的兼容性问题或壳的 ClassLoader 替换不完整。
(3)Native 层崩溃排查:
- 使用
addr2line或ndk-stack将 Native 栈回溯符号化:ndk-stack -sym ./symbols/ -dump crash.log。 - 检查是否在壳的 so(libjiagu.so、libSecShell.so 等)中崩溃。
- 使用 IDA Pro 反编译壳 so 中崩溃偏移附近的代码,理解崩溃原因。
(4)Java 层崩溃排查:
- 使用 JADX 反编译壳的 classes.dex,阅读 ShellApplication 的实现。
- 检查是否有 ClassNotFoundException(说明原始 Application 类名配置错误)。
- 检查是否有 UnsatisfiedLinkError(壳的 so 缺少目标架构版本)。
(5)架构兼容性检查:
aapt dump badging target.apk | grep native-code检查 APK 支持的架构。- 确认设备架构与 APK 匹配(如 x86_64 模拟器但 APK 仅含 arm64-v8a)。
- 使用
nativelib目录混淆的可能。
(6)Android 版本兼容性:
- 壳可能不兼容某些 Android 版本(如旧壳不兼容 Android 12+)。
aapt dump badging target.apk | grep sdkVersion检查 minSdkVersion 和 targetSdkVersion。
(7)脱壳验证:
- 如果能看到崩溃日志但无法用 JADX 看代码 → 加固工作正常,问题可能是加固的环境检测误杀。
- 尝试在脱壳后的纯净 DEX 上复现崩溃,对比加固/未加固的表现差异。
Q5: 一个企业如果要为自己的 Android 应用选择加固方案,应该从哪些维度评估?开源加固 vs 商业加固如何选择?
A:
评估维度:
(1)安全性:
- 当前加固方案在该厂商的客户群中有多少已知的 bypass 案例?
- 是否支持 VMP 保护(对金融/游戏等高价值应用至关重要)?
- 发布补丁/更新加固的频率(厂商是否持续对抗新的脱壳技术)?
(2)兼容性:
- 支持的 Android 版本范围(需覆盖目标用户群)。
- 支持的 CPU 架构(armeabi-v7a / arm64-v8a / x86_64)。
- 对 App Bundle(AAB)的支持。
- 对 Android 新版本的跟进速度(Android 14 发布后多久支持)。
(3)性能影响:
- 冷启动时间增加量(通常目标 < 300ms)。
- 运行时 CPU 和内存开销。
- VMP 保护函数的性能降级倍数(通常 5x-20x)。
- 对包体积的影响。
(4)稳定性:
- 在主流设备上的崩溃率。
- 是否引入 ANR(Application Not Responding)。
- 多线程环境下的可靠性。
(5)集成复杂度:
- 是否支持 Gradle Plugin 自动集成(vs 手动使用加固 Web 控制台)。
- 是否支持 CI/CD 流水线集成。
- 是否提供 API 供自动化测试。
(6)商业支持:
- 重大漏洞的响应时间(SLA)。
- 是否提供定期的安全评估报告。
- 定制化需求的支持能力(如特定的加密算法、特定的反调试策略)。
开源 vs 商业加固:
- 开源加固(如 ProGuard/R8 + 自研 SO 保护)适合:中小型应用、非金融/支付类应用、开源项目、技术团队能力强且了解安全攻防的场景。
- 商业加固适合:金融/银行/支付应用、游戏(防止外挂和修改)、有合规要求的应用、技术团队主要聚焦业务而非安全、需要第三方安全背书(向客户/监管证明安全投入)的场景。
选择建议:
- 大多数非金融应用:ProGuard/R8 + 字符串加密 + 基础反调试 + 签名校验 → 足够。
- 金融/支付应用:商业加固 + VMP + 服务端风控 + TEE/SE → 行业标准。
- 游戏:商业加固 + 自研反外挂 + 服务端行为分析 → 纵深防御。

