目录
  1. 1. 一、加固的核心目标
    1. 1.1. 一.1 加固的完整生命周期
    2. 1.2. 一.2 壳 classes.dex 的代码结构
    3. 1.3. 一.3 关键:ClassLoader 替换机制
  2. 2. 二、DEX 加密与动态加载技术
    1. 2.1. 二.1 整体加密(第一代)
    2. 2.2. 二.2 分段解密(第二代)
    3. 2.3. 二.3 InMemoryDexClassLoader 加载(Android 8.0+)
    4. 2.4. 二.4 Native 层 DEX 解密示例
    5. 2.5. 二.5 CRC32 校验 Native 层实现
  3. 3. 三、SO 保护
    1. 3.1. 三.1 SO 代码段加密
    2. 3.2. 三.2 SO 自定义 Linker Script
  4. 4. 四、完整性校验流程
    1. 4.1. 四.1 多轮完整性校验架构
    2. 4.2. 四.2 让校验结果影响后续计算的抗 patch 设计
  5. 5. 五、商业加固方案对比
  6. 6. 六、加固技术的局限性与未来趋势
    1. 6.1. 六.1 根本局限性
    2. 6.2. 六.2 未来趋势
  7. 7. 七、AOSP 相关源码导读
  8. 8. 面试常考问题
【逆向安全技术-防护篇】应用加固原理

一、加固的核心目标

应用加固的核心思路是将原始的 DEX(Dalvik Executable)和 SO 文件加密后嵌入壳程序,等应用启动时由壳程序在内存中解密并动态加载执行。这样,即使攻击者获取了 APK,看到的也只是加密后的乱码数据,无法直接通过 jadx 或 baksmali 反编译。

商业加固(如 360 加固、梆梆、爱加密)的原理大体分为三个阶段:打包阶段——将原 DEX 加密后存放在 assets 或 lib 目录下,替换 Application 类为壳的入口类;启动阶段——壳 Application 在 attachBaseContext() 中加载解密 SO 库,从 assets 读取加密 DEX 到内存;执行阶段——解密并通过 DexClassLoaderInMemoryDexClassLoader 动态加载,反射调用原 Application 并移交控制权。

一.1 加固的完整生命周期

┌──────────────────────────────────────────────────────┐
│ 加固阶段(打包服务器) │
├──────────────────────────────────────────────────────┤
│ 1. 接收原始 APK │
│ 2. 解析 AndroidManifest.xml → 记录原始 Application │
│ 3. 提取所有 classes*.dex │
│ 4. 对每个 DEX 进行加密 (AES/自定义) │
│ 5. 将加密后的 DEX 放入 assets/ 或 lib/*.so 的数据段 │
│ 6. 创建壳 classes.dex (仅含 ShellApplication 等) │
│ 7. 修改 AndroidManifest: 替换 Application 为壳的 │
│ 8. 添加壳的 lib/*.so 文件 │
│ 9. 添加壳的配置文件 (壳需要知道如何解密) │
│ 10. 重新打包 → 签名 → 输出加固后的 APK │
└──────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────┐
│ 运行阶段(用户设备) │
├──────────────────────────────────────────────────────┤
│ 1. 系统加载壳 classes.dex │
│ 2. 系统创建 ShellApplication │
│ 3. ShellApplication.attachBaseContext() 被调用 │
│ ├── 加载壳的 Native 库 (System.loadLibrary) │
│ ├── 从 assets/ 或 APK 中读取加密 DEX │
│ ├── 在 Native 层解密 DEX │
│ ├── 创建 DexClassLoader / InMemoryDexClassLoader │
│ ├── 反射替换 LoadedApk.mClassLoader │
│ └── 反射创建原始 Application 并替换 │
│ 4. 系统调用原始 Application.onCreate() │
│ 5. 应用正常运行(业务代码从解密后的 DEX 执行) │
└──────────────────────────────────────────────────────┘

一.2 壳 classes.dex 的代码结构

加固后的 APK 中,classes.dex 是极简的壳代码:

// 壳的 classes.dex 中通常只有以下几类:
// 1. ShellApplication - 继承 android.app.Application
// 2. ShellClassLoader 或 ShellDexClassLoader - 继承 ClassLoader
// 3. 若干 helper 类 - 工具函数

// ShellApplication 的典型实现(简化版)
package com.shell.wrapper; // 壳公司特有的包名

public class ShellApplication extends Application {
static {
System.loadLibrary("shell"); // 壳的 native 库
}

private String[] mRealDexPaths;
private Application mRealApplication;

@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);

// Step 1: 在 Native 层解析 APK 并解密 DEX
String[] dexPaths = nativeExtractDex(
base.getApplicationInfo().sourceDir // APK路径
);
mRealDexPaths = dexPaths;

// Step 2: 创建新的 ClassLoader
ClassLoader newClassLoader = createClassLoader(dexPaths);

// Step 3: 替换应用的 ClassLoader
replaceClassLoader(base, newClassLoader);

// Step 4: 创建原始 Application 实例
try {
Class<?> realAppClass = newClassLoader.loadClass(
getMetaData("real_application_class"));
mRealApplication = (Application) realAppClass.newInstance();

// Step 5: 调用原始 Application 的 attachBaseContext
Method attachMethod = Application.class.getDeclaredMethod(
"attach", Context.class);
attachMethod.setAccessible(true);
attachMethod.invoke(mRealApplication, base);

} catch (Exception e) {
throw new RuntimeException("Failed to load real app", e);
}
}

@Override
public void onCreate() {
super.onCreate();
// 调用原始 Application 的 onCreate
if (mRealApplication != null) {
mRealApplication.onCreate();
}
}

// Native 方法声明
private native String[] nativeExtractDex(String apkPath);
private native void nativeDecryptDex(byte[] input, byte[] output);
}

一.3 关键:ClassLoader 替换机制

这是壳最核心的操作——替换应用的 ClassLoader:

private void replaceClassLoader(Context context, ClassLoader newCl) {
try {
// 获取 LoadedApk 对象
// LoadedApk 是系统保存每个 APK 状态的核心类
ContextImpl contextImpl = (ContextImpl) context;
Object loadedApk = contextImpl.getClass()
.getMethod("getPackageInfo").invoke(contextImpl);

// 替换 mClassLoader 字段
Field classLoaderField = loadedApk.getClass()
.getDeclaredField("mClassLoader");
classLoaderField.setAccessible(true);
classLoaderField.set(loadedApk, newCl);

// 在 Android 8.0+ 中可能还需要替换 Application 的 ClassLoader
// 因为某些系统路径直接使用 Application.getClassLoader()

// 注:不同 Android 版本 LoadedApk 字段名可能有细微差异
// 商业加固需要适配多个 Android 版本

} catch (Exception e) {
throw new RuntimeException("ClassLoader replacement failed", e);
}
}

二、DEX 加密与动态加载技术

二.1 整体加密(第一代)

原始 APK:
classes.dex → 包含全部业务逻辑,直接可用

加固后 APK:
classes.dex → 壳代码(体积很小, 20-100KB)
assets/classes.dat → 加密的原始 classes.dex
assets/classes2.dat→ 加密的 classes2.dex (若有)
lib/libshell.so → 解密引擎

运行时:
libshell.so 读取 classes.dat → AES 解密 → 写入临时文件
→ DexClassLoader(tmp_file) → 加载

整体加密的缺点:在解密完成后的某个时刻,temporal 文件或内存中包含完整的明文 DEX。攻击者只需在合适的时机(如 Hook DexFile 构造函数)进行一次 dump 即可获取完整代码。

二.2 分段解密(第二代)

DEX 被按类或按方法分段加密:

classes.dex (壳)
assets/
├── dex_header.dat → DEX header(部分真实,部分 dummy)
├── class_data_1.dat → 类1~100的加密代码
├── class_data_2.dat → 类101~200的加密代码
├── ...
├── method_code_1.dat → 方法代码块1
└── method_code_2.dat → 方法代码块2

运行时解密策略:
Class A 首次加载 → 解密 class_data_1 中对应部分
Class A.method1() 首次调用 → 解密 method_code_1 中对应指令
方法执行完毕 → 可选:重新加密该方法的代码区域

二.3 InMemoryDexClassLoader 加载(Android 8.0+)

Android 8.0 引入了 InMemoryDexClassLoader,允许直接从内存中的 ByteBuffer 加载 DEX,无需写入文件系统。这提升了加固的安全性——明文 DEX 不再经过文件系统,减少了被 dump 的机会:

// Android 8.0+ 的内存 DEX 加载
public ClassLoader createMemoryClassLoader(byte[] decryptedDex) {
// 直接包装解密后的字节数组
ByteBuffer dexBuffer = ByteBuffer.wrap(decryptedDex);

// 创建 InMemoryDexClassLoader
ClassLoader parent = getClassLoader();
ClassLoader dexLoader = new InMemoryDexClassLoader(
new ByteBuffer[]{dexBuffer},
parent
);

return dexLoader;
}

// 或者通过 DexFile 直接接受 ByteBuffer
// DexFile dexFile = new DexFile(byteBuffer);

二.4 Native 层 DEX 解密示例

// native_dex_decrypt.cpp
#include <jni.h>
#include <string.h>
#include <android/log.h>

#define TAG "ShellDEX"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__)

// 简单 XOR 解密(商业加固使用 AES/RSA/自定义算法)
void decrypt_dex_xor(uint8_t* data, size_t len, const uint8_t* key,
size_t key_len) {
for (size_t i = 0; i < len; i++) {
data[i] ^= key[i % key_len];
}
}

JNIEXPORT jbyteArray JNICALL
Java_com_shell_wrapper_ShellApplication_nativeDecryptDex(
JNIEnv* env, jobject thiz, jbyteArray encrypted, jbyteArray key) {

// 获取加密数据
jsize enc_len = (*env)->GetArrayLength(env, encrypted);
jbyte* enc_data = (*env)->GetByteArrayElements(env, encrypted, NULL);

// 获取密钥
jsize key_len = (*env)->GetArrayLength(env, key);
jbyte* key_data = (*env)->GetByteArrayElements(env, key, NULL);

// 解密(直接在原数组上修改,避免额外内存分配)
decrypt_dex_xor((uint8_t*)enc_data, (size_t)enc_len,
(uint8_t*)key_data, (size_t)key_len);

// 创建返回的字节数组
jbyteArray result = (*env)->NewByteArray(env, enc_len);
(*env)->SetByteArrayRegion(env, result, 0, enc_len, enc_data);

// 释放资源并清零敏感数据
memset(enc_data, 0, enc_len); // 清零原始数据
(*env)->ReleaseByteArrayElements(env, encrypted, enc_data, JNI_ABORT);
(*env)->ReleaseByteArrayElements(env, key, key_data, JNI_ABORT);

return result;
}

二.5 CRC32 校验 Native 层实现

// native_crc_check.c
// Native 层 CRC32 校验(检测代码段是否被篡改)

#include <jni.h>
#include <elf.h>
#include <stdio.h>
#include <string.h>
#include <zlib.h>

#define EXPECTED_TEXT_CRC 0xABCD1234 // 编译期预埋的 .text 段 CRC

// 从 /proc/self/maps 中获取当前 SO 的基地址
void* get_module_base(const char* soname) {
FILE* fp = fopen("/proc/self/maps", "r");
if (!fp) return NULL;

char line[512];
void* base = NULL;

while (fgets(line, sizeof(line), fp)) {
if (strstr(line, soname)) {
// 行格式: "7a000000-7b000000 r-xp 00000000 fd:03 12345 libxxx.so"
base = (void*)strtoul(line, NULL, 16);
break;
}
}
fclose(fp);
return base;
}

// 获取 ELF section 的名称(通过 section header string table)
const char* get_section_name(void* base, Elf64_Shdr* shdr) {
Elf64_Ehdr* ehdr = (Elf64_Ehdr*)base;
char* shstrtab = (char*)base +
((Elf64_Shdr*)((char*)base + ehdr->e_shoff) +
ehdr->e_shstrndx)->sh_offset;
return shstrtab + shdr->sh_name;
}

bool verify_text_section() {
// 获取当前 SO 的基址
void* base = get_module_base("libshell.so");
if (!base) return false;

Elf64_Ehdr* ehdr = (Elf64_Ehdr*)base;

// 验证 ELF magic
if (memcmp(ehdr->e_ident, ELFMAG, SELFMAG) != 0) {
return false;
}

// 遍历所有 section header
Elf64_Shdr* shdr_start = (Elf64_Shdr*)((char*)base + ehdr->e_shoff);
for (int i = 0; i < ehdr->e_shnum; i++) {
Elf64_Shdr* shdr = &shdr_start[i];
const char* name = get_section_name(base, shdr);

if (strcmp(name, ".text") == 0) {
// 计算 .text 段的 CRC32
uint32_t crc = crc32(0L, Z_NULL, 0);
crc = crc32(crc,
(uint8_t*)base + shdr->sh_offset,
shdr->sh_size);

// 与编译期预埋值比较
return crc == EXPECTED_TEXT_CRC;
}
}
return false;
}

三、SO 保护

加固方案中的 SO 库负责加解密逻辑、反调试和完整性校验。对 SO 的保护手段包括:代码段加密(在 .init_array 中运行时解密)、符号表剥离(strip)、控制流平坦化(OLLVM -mllvm -fla)、字符串加密(编译器插件在编译期替换)。部分加固还会将核心代码以加密形式存放于 .rodata 段的自定义位置,运行时才映射为可执行内存并跳转。

三.1 SO 代码段加密

编译期:
1. 正常编译生成 libshell.so
2. 使用链接脚本将 .text 段的某些函数标记为加密区域
3. 使用后处理工具对标记区域加密(AES/XOR)
4. 在 .init_array 中添加解密代码
5. 预埋解密密钥分散在 .rodata 中

运行时(.init_array 执行流程):
1. 从分散的位置收集密钥片段,组合出密钥
2. 通过 /proc/self/maps 找到当前 SO 加载基址
3. 定位加密的 .text 区域
4. mprotect 将区域改为 PROT_READ | PROT_WRITE
5. 执行解密
6. mprotect 恢复为 PROT_READ | PROT_EXEC
7. 清零密钥数据(防止内存 dump 获取密钥)
8. cacheflush 刷新指令缓存

三.2 SO 自定义 Linker Script

/* custom_shell.ld - 自定义链接脚本片段 */

SECTIONS
{
/* 加密的代码段(放在 .encrypted_text section) */
.encrypted_text : {
__encrypted_start = .;
*(.encrypted_text*)
__encrypted_end = .;
} > LOAD_SEGMENT

/* 解密密钥(分散存储) */
.key_fragment_1 : {
*(.key_fragment_1)
}
.key_fragment_2 : {
*(.key_fragment_2)
}
.key_fragment_3 : {
*(.key_fragment_3)
}
}
// 函数标记为加密区域
__attribute__((section(".encrypted_text")))
void sensitive_algorithm(const uint8_t* input, uint8_t* output) {
// 该函数在 SO 文件中是加密的
// 运行时由 .init_array 解密后执行
// ...
}

__attribute__((section(".encrypted_text")))
bool native_signature_check(JNIEnv* env, jobject ctx) {
// 签名校验逻辑
// ...
}

四、完整性校验流程

加固 APK 在启动时会执行多轮完整性检查:校验 APK 签名(防止二次打包替换壳包)、校验自身 DEX 和 SO 的 CRC/哈希值(防止静态修改)、检测环境特征(Root、模拟器、hook 框架注入)。这些检查分散在 Native 层多处,形成互相验证的检查网络。

四.1 多轮完整性校验架构

┌─────────────────────────────────────────┐
│ 校验网络(分散 + 交叉验证) │
├─────────────────────────────────────────┤
│ │
│ 校验点 1: Java 层签名校验 │
│ ↓ 返回结果 encrypted(result) │
│ 校验点 2: SO A 的 .text CRC 校验 │
│ ↓ 返回结果 encrypted(result) │
│ 校验点 3: SO B 中的 DEX 哈希校验 │
│ ↓ 返回结果 encrypted(result) │
│ 校验点 4: 隐藏在正常业务逻辑中的校验 │
│ ↓ 返回结果 encrypted(result) │
│ ... │
│ ↓ │
│ 汇总模块: 组合所有分散的结果 │
│ 如果任一校验失败 → 触发保护 │
│ 保护动作: 延迟退出 / 写入异常数据 │
│ / 服务端标记异常 │
│ │
└─────────────────────────────────────────┘

四.2 让校验结果影响后续计算的抗 patch 设计

// 抗 patch 设计:校验结果不作为简单的 bool 返回
// 而是作为后续计算的一部分,使得即使 patch 了校验
// 后续业务逻辑也会因错误的值而无法正常工作

static uint8_t g_integrity_state = 0; // 用多个 bit 存储分布式校验结果

// 分散在校验网络中的各个校验点
void check_point_1() {
if (verify_signature()) {
g_integrity_state |= 0x01; // bit 0
}
}

void check_point_2() {
if (verify_text_crc()) {
g_integrity_state |= 0x02; // bit 1
}
}

void check_point_3() {
if (!is_debugging()) {
g_integrity_state |= 0x04; // bit 2
}
}

// 关键:校验结果用于"生成解密密钥"的一部分
// 而不是简单的 if (result) return 的形式
uint8_t derive_key_fragment() {
// 如果校验全通过,g_integrity_state == 0x07
// 任何校验失败导致少一个 bit,生成的关键片段就不同
// 后续解密就会失败(导致业务异常,而非立即崩溃)
return g_integrity_state ^ 0x07; // 全通过返回 0x00
}

// 在真正的业务逻辑中使用 derived key
void compute_sensitive_data() {
uint8_t key_fragment = derive_key_fragment();

// key_fragment 直接影响解密密钥
// 如果校验被 patch 但 g_integrity_state 被绕过
// 这里生成的密钥就会错误 → 解密得到垃圾数据
// 导致应用行为异常但不直接崩溃(迷惑攻击者)
do_business_logic_with_key(key_fragment);
}

五、商业加固方案对比

特性 360加固 梆梆加固 腾讯乐固 网易易盾 爱加密
壳 Application StubApp3580 AW StubShell NQApplication IjiamiApp
核心 Native 库 libjiagu.so libSecShell.so libtup.so libnqshield.so libexec.so
VMP 支持 是 (独有)
DEX 加密 整体+抽取 整体 整体 整体 整体
SO 保护 代码段加密 代码段加密 符号剥离 OLLVM OLLVM
反调试 ★★★★★ ★★★★☆ ★★★☆☆ ★★★★☆ ★★★☆☆
字符串加密 ★★★★★ ★★★★☆ ★★★☆☆ ★★★★☆ ★★★☆☆
兼容性 ★★★★☆ ★★★★☆ ★★★★★ ★★★★☆ ★★★★☆
脱壳难度 ★★★★★ ★★★★☆ ★★★☆☆ ★★★☆☆ ★★★☆☆
性能开销

六、加固技术的局限性与未来趋势

六.1 根本局限性

加固技术的根本困境:
任何代码最终必须在 CPU 上以明文形式执行。
加密 → 解密 → 执行 → 内存中必然出现明文代码。
攻击者总能在"解密后、执行时"这个时间窗口截获代码。

这就是"DRM困境"在移动端的体现:
- 代码必须对 CPU 可读 → 必然可以被 dump
- 加密/解密有时序 → 存在时间窗口
- CPU 架构固定 → 无法硬件级保护

六.2 未来趋势

  1. VMP 保护深化:将更多业务逻辑下沉到自定义 VM,增加逆向需要理解的新指令集。
  2. 服务端逻辑迁移:将核心算法放在服务端,客户端仅做展示(适合网络密集型应用)。
  3. 硬件安全模块(TEE/SE):利用 ARM TrustZone(TEE)或独立安全芯片(SE)执行关键逻辑,代码永不暴露给普通 OS。
  4. AI 辅助动态保护:运行时 AI 检测异常分析行为(如异常的代码覆盖率、异常的调用频率模式),动态调整防护策略。
  5. 反 AI 逆向:设计针对大模型代码理解能力的混淆技术(如语义等价转换、逻辑谜题化)。
  6. 软件证明(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.ccOpenCommon 函数和 /art/runtime/class_linker.ccRegisterDexFile 函数,是壳解密后加载 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.ccLoadMethod 函数负责将方法从 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 number dex\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 层崩溃排查:

  • 使用 addr2linendk-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 → 行业标准。
  • 游戏:商业加固 + 自研反外挂 + 服务端行为分析 → 纵深防御。
打赏
  • 微信
  • 支付宝

评论