使用 Frida 脱壳加固 App 并修复 SO 文件

使用 Frida 脱壳加固 App 并修复 SO 文件

📚 前置知识

本配方涉及以下核心技术,建议先阅读相关章节:

问题场景

你遇到了什么问题?

  • 用 jadx 打开 APK,发现代码被混淆或看不到关键逻辑
  • 🛡️ APK 使用了加固(加壳)保护,无法静态分析
  • 📦 SO 文件被加密,IDA Pro 无法正确加载
  • 你想获取 App 运行时真正的 DEX 文件
  • 🧩 需要分析 Native 层代码,但 SO 文件已被加壳

本配方教你:使用 Frida 动态脱壳加固 App、Dump 内存中的 DEX 和 SO 文件、修复文件格式以供分析。

核心理念

💡 动态脱壳:在运行时获取已解密的代码

  • 加壳只是静态保护,运行时必然会解密
  • Frida 可以在 DEX/SO 加载时 dump 内存
  • 修复文件格式后即可用传统工具分析
  • 绕过所有加固方案的通用方法

预计用时: 30-60 分钟


工具清单

必需工具

  • Frida - 动态插桩框架
  • Android 设备(已 Root)或模拟器
  • Python 3.7+ - 运行 Frida 脚本
  • jadx-gui - 分析脱壳后的 DEX

可选工具

  • IDA Pro / Ghidra - 分析 SO 文件
  • frida-dexdump - 自动化 DEX 脱壳
  • FRIDA-DEXDump - 另一个流行的脱壳工具
  • SoFixer - 修复 dump 的 SO 文件

前置条件

✅ 确认清单

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 1. Frida 正常运行
frida-ps -U

# 2. Python 环境
python3 --version

# 3. jadx-gui 已安装
jadx-gui --version

# 4. 检查设备 root 状态
adb shell su -c 'id'
# 应该显示 uid=0(root)

识别 App 是否加固

方法 1:jadx 查看

打开 APK,如果看到:

  • 只有几个类和方法
  • StubAppProxyApplication 等字样
  • MainActivity 逻辑异常简单

方法 2:查看 SO 文件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 解压 APK
unzip app.apk -d app_unzipped

# 查看 lib 目录
ls app_unzipped/lib/arm64-v8a/

# 常见加固壳 SO 文件名
# libjiagu.so (360加固)
# libDexHelper.so (梆梆加固)
# libtup.so (腾讯加固)
# libexec.so (爱加密)

解决方案

第 1 步:使用 frida-dexdump 脱壳(10 分钟)

1.1 安装 frida-dexdump

1
2
3
4
5
6
# 克隆项目
git clone https://github.com/hluwa/frida-dexdump.git
cd frida-dexdump

# 安装依赖
pip3 install frida frida-tools

1.2 运行脱壳

1
2
3
4
5
6
7
8
9
# -f: 启动应用
# --no-pause: 不暂停,立即运行
python3 main.py -U -f com.example.app

# 脚本会自动:
# 1. 启动应用
# 2. Hook DEX 加载函数
# 3. 导出所有已加载的 DEX 文件
# 4. 保存到当前目录

输出示例

1
2
3
4
5
6
[DEXDump] Dumping DEX file: 0x7abc000000, size: 4562314
[DEXDump] Saved: com.example.app_classes.dex
[DEXDump] Found DEX: /data/app/.../base.apk!classes2.dex
[DEXDump] Dumping DEX file: 0x7abc500000, size: 2314567
[DEXDump] Saved: com.example.app_classes2.dex
[DEXDump] Total: 2 DEX files dumped

1.3 验证脱壳结果

1
2
# 用 jadx 打开
jadx-gui com.example.app_classes.dex

第 2 步:使用 FRIDA-DEXDump(备选方案)(10 分钟)

如果 frida-dexdump 不工作,可以尝试 FRIDA-DEXDump:

1
2
3
4
5
6
# 安装
git clone https://github.com/lasting-yang/frida_dump.git
cd frida_dump

# 运行命令
python3 dump_dex.py -U -f com.example.app

第 3 步:手动脚本脱壳(高级)(15 分钟)

如果自动化工具失败,可以编写自定义 Frida 脚本。

3.1 Hook OpenCommon(适用于 Android 8.0+)

dex_dump.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
function dumpDex() {
  Java.perform(function () {
    console.log("[*] DEX Dumper started");

    // 查找 libart.so
    var libart = Process.findModuleByName("libart.so");
    if (!libart) {
      console.log("[-] libart.so not found");
      return;
    }

    // Hook OpenCommon (Android 8.0+)
    // 符号名称因版本而异,需要用 nm 或 readelf 确认
    var OpenCommon = null;

    // 尝试常见符号
    var symbols = [
      "_ZN3art7DexFile10OpenCommonEPKhjS2_jRKNS_10OatDexFileEbbPS1_",
      "_ZN3art7DexFile10OpenCommonEPKhmS2_jRKNS_10OatDexFileEbbPS1_NS_6Handle",
    ];

    for (var i = 0; i < symbols.length; i++) {
      OpenCommon = Module.findExportByName("libart.so", symbols[i]);
      if (OpenCommon) {
        console.log("[+] Found OpenCommon:", OpenCommon);
        break;
      }
    }

    if (!OpenCommon) {
      console.log("[-] OpenCommon not found");
      return;
    }

    // Hook
    Interceptor.attach(OpenCommon, {
      onEnter: function (args) {
        // args[0] = base (DEX 内存地址)
        // args[1] = size (DEX 文件大小)

        var base = args[0];
        var size = args[1].toInt32();

        console.log("[*] 检测到 DEX!");
        console.log("    基址: " + base);
        console.log("    大小: " + size);

        // 读取 DEX 文件头,验证魔数
        var magic = base.readCString(4);
        if (magic === "dex\n") {
          console.log("    Magic: " + magic + " ✓");

          // Dump DEX
          var dexBytes = base.readByteArray(size);
          var fileName = "/sdcard/" + size + ".dex";

          var file = new File(fileName, "wb");
          file.write(dexBytes);
          file.close();

          console.log("[+] DEX dumped to: " + fileName);
        } else {
          console.log("    Invalid magic: " + magic);
        }
      },
    });

    console.log("[*] Hooks installed, waiting for DEX load...");
  });
}

setImmediate(dumpDex);

使用方法

1
2
3
4
5
# 运行脚本
frida -U -f com.example.app -l dex_dump.js --no-pause

# 拉取到本地
adb pull /sdcard/*.dex .

3.2 查找正确的符号名

1
2
3
4
5
Module.enumerateExports("libart.so").forEach(function (exp) {
  if (exp.name.includes("DexFile") && exp.name.includes("Open")) {
    console.log(exp.name, exp.address);
  }
});

第 4 步:Dump SO 文件(10 分钟)

4.1 查看已加载的 SO

1
2
# 查看进程加载的 SO 文件
frida -U -f com.example.app

在 REPL 中输入:

1
2
3
4
5
6
7
8
Process.enumerateModules().forEach(function (m) {
  if (m.name.includes("native") || m.name.includes("encrypt")) {
    console.log(m.name, m.base, m.size);
  }
});

// 输出示例:
// libnative-lib.so 0x7abc000000 0x50000

4.2 Dump SO 内存

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function dumpSo(moduleName) {
  var module = Process.findModuleByName(moduleName);
  if (!module) {
    console.log("[-] Module not found: " + moduleName);
    return;
  }

  console.log("[+] 找到模块:", moduleName);
  console.log("    基址: " + module.base);
  console.log("    大小: " + module.size);

  // 导出整个模块
  var buffer = module.base.readByteArray(module.size);
  var fileName = "/sdcard/" + moduleName;

  var file = new File(fileName, "wb");
  file.write(buffer);
  file.close();

  console.log("[+] 已导出到: " + fileName);
}

// 使用
dumpSo("libnative-lib.so");

拉取文件

1
adb pull /sdcard/libnative-lib.so .

4.3 使用 frida-all-in-one(推荐)

1
2
3
4
5
6
7
8
# 克隆项目
git clone https://github.com/hookmaster/frida-all-in-one.git
cd frida-all-in-one

# 运行命令
python3 dump_so.py -U com.example.app libnative-lib.so

# 会自动导出并修复 SO 文件

第 5 步:修复 SO 文件(10 分钟)

从内存 dump 的 SO 文件可能缺少 ELF 头信息,需要修复。

5.1 使用 SoFixer

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 下载
git clone https://github.com/F8LEFT/SoFixer.git
cd SoFixer

# 编译(需要 CMake)
mkdir build && cd build
cmake ..
make

# 使用
./SoFixer ../libnative-lib.so ../libnative-lib_fixed.so

输出示例

1
2
3
4
5
6
[+] Detected architecture: ARM64
[+] Rebuilding ELF header...
[+] Fixing section table...
[+] Fixing dynamic symbols...
[+] Output file: libnative-lib_fixed.so
[+] Done!

5.2 验证修复结果

1
2
3
4
5
6
7
# 检查文件类型
file libnative-lib_fixed.so
# 应该显示: ELF 64-bit LSB shared object, ARM aarch64...

# 用 IDA Pro 打开
# 或用 readelf 查看
readelf -h libnative-lib_fixed.so

原理深入

DEX 脱壳原理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
App 启动
壳代码加载
解密原始 DEX(在内存中)
调用 DexFile::OpenCommon 加载 DEX ← Frida Hook 点
Frida 读取内存中的 DEX 数据
保存到文件

OpenCommon 函数签名

1
2
3
4
5
6
// Android 8.0+ 的 OpenCommon 签名(简化)
static std::unique_ptr<DexFile> OpenCommon(
    const uint8_t* base,  // DEX 内存基址
    size_t size,          // DEX 大小
    ...
)

SO Dump 原理

1
2
3
4
5
6
7
8
内存中的 SO 布局:
[ELF Header]   ← 可能被壳破坏
[.text 段]
[.data 段]
[.rodata 段]
...
[Symbol Table] ← 需要重建
[String Table] ← 需要重建

直接 dump 内存只能获取段数据,缺少完整 ELF 结构,所以需要 SoFixer 修复。

常见加固壳识别

加固厂商SO 文件名特点
360 加固libjiagu.so整体加密
梆梆加固libDexHelper.so方法抽取
腾讯加固libtup.soVMP 保护
爱加密libexec.so多层加密
网易易盾libnesec.so云端保护

通用策略:所有加固都需要在运行时解密,Frida 脱壳对所有方案都有效!


常见问题

❌ 问题 1: frida-dexdump 报错 “Failed to spawn”

症状:无法启动应用

解决

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 1. 确认应用已安装
adb shell pm list packages | grep example

# 2. 确认包名正确
# 从 AndroidManifest.xml 获取准确包名

# 3. 尝试 Attach 模式
# 先手动启动应用
adb shell am start -n com.example.app/.MainActivity

# 再附加
python3 main.py -U com.example.app

❌ 问题 2: Dump 的 DEX 无法用 jadx 打开

可能原因:DEX 头部损坏

解决步骤

  1. 检查魔数

    1
    2
    
    xxd dumped.dex | head -1
    # 应该看到: 64 65 78 0a (dex\n)
  2. 验证 DEX 大小

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    # 验证 DEX 大小
    with open('dumped.dex', 'rb') as f:
        f.seek(32)  # 跳到 file_size 字段
        size = int.from_bytes(f.read(4), 'little')
        print(f"DEX 声明的大小: {size}")
    
    import os
    actual_size = os.path.getsize('dumped.dex')
    print(f"实际文件大小: {actual_size}")
  3. 使用 dexrepair 修复

    1
    2
    
    git clone https://github.com/anestisb/dexrepair.git
    python3 dexrepair/dexrepair.py dumped.dex fixed.dex

❌ 问题 3: Hook 点没有触发

检查步骤

  1. 确认 libart.so 已加载
    1
    2
    
    var libart = Process.findModuleByName("libart.so");
    console.log("libart found:", libart !== null);
  2. 列出所有 OpenCommon 符号
    1
    2
    3
    4
    5
    
    Module.enumerateExports("libart.so").forEach(function (exp) {
      if (exp.name.includes("OpenCommon")) {
        console.log(exp.name);
      }
    });
  3. 尝试其他 Hook 点
    1
    2
    3
    4
    5
    
    // Android 7.0-
    var OpenMemory = Module.findExportByName(
      "libart.so",
      "_ZN3art7DexFile10OpenMemoryEPKhjRKNSt3__112basic_stringIcNS3_11char_traitsIcEENS3_9allocatorIcEEEEjPNS_6MemMapEPKNS_10OatDexFileEPS9_"
    );
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

### ❌ 问题 4: SO 文件修复后 IDA 仍无法分析

**症状**IDA 打开后只显示数据,没有函数

**解决**

1. **手动定义函数**
     IDA 中:
    - 光标移到疑似函数起始处
    -  'P' 键创建函数
    -  'C' 键转换为代码
2. **使用符号恢复工具**
    ```bash
    # 如果原始 SO 有符号表
    readelf -s original.so > symbols.txt

    # 用 IDA 脚本导入符号
    ```
3. **检查是否有 OLLVM 混淆**
    - 如果看到大量跳转和无意义的代码块
    - 可能是 OLLVM 控制流平坦化
    - 参考:[OLLVM 反混淆](../Analysis/ollvm_deobfuscation.md)

### ❌ 问题 5: App 检测到 Frida 并崩溃

**症状**:启动后立即退出,logcat 显示反调试提示

**解决**

参考 [Frida 反调试绕过](../Anti-Detection/frida_anti_debugging.md)

快速方法:

```bash
# 使用 Magisk Hide
# 或使用修改版 Frida 服务器
wget https://github.com/hluwa/strongR-frida-android/releases/download/xxx/frida-server

相关链接

相关配方

工具深入

项目资源

项目说明链接
frida-dexdump自动化 DEX 脱壳https://github.com/hluwa/frida-dexdump
FRIDA-DEXDump深度 DEX 脱壳https://github.com/lasting-yang/frida_dump
SoFixerSO 文件修复https://github.com/F8LEFT/SoFixer
frida-all-in-one综合工具集https://github.com/hookmaster/frida-all-in-one

理论基础


快速参考

脱壳工具对比

工具类型难度特点
frida-dexdump自动化简单简单,支持多版本
FRIDA-DEXDump自动化简单深度搜索,更全面
手动脚本定制中等灵活,适合特殊情况
objection交互式简单多功能,含脱壳

一键脱壳脚本

auto_unpack.sh

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#!/bin/bash

PACKAGE=$1

if [ -z "$PACKAGE" ]; then
    echo "用法: $0 <package_name>"
    exit 1
fi

echo "🔓 开始脱壳: $PACKAGE"

# 1. Dump DEX
echo ""
echo "📦 导出 DEX 文件..."
python3 ~/tools/frida-dexdump/main.py -U -f $PACKAGE

# 2. Dump SO
echo ""
echo "📚 导出 SO 文件..."
frida -U -f $PACKAGE -l dump_all_so.js --no-pause

sleep 5

# 3. 拉取文件
echo ""
echo "📥 拉取文件..."
adb pull /sdcard/*.dex .
adb pull /sdcard/*.so .

# 4. 清理
adb shell rm /sdcard/*.dex
adb shell rm /sdcard/*.so

echo ""
echo "✅ 完成! 文件已保存到当前目录"
ls -lh *.dex *.so

dump_all_so.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
function dumpAllSo() {
  var modules = Process.enumerateModules();
  console.log("[*] 找到 " + modules.length + " 个模块");

  modules.forEach(function (module) {
    // 只导出 .so 文件
    if (!module.name.endsWith(".so")) {
      return;
    }

    // 排除系统库
    if (module.path.startsWith("/system") || module.path.startsWith("/apex")) {
      return;
    }

    console.log("[+] 导出: " + module.name);
    console.log("    路径: " + module.path);
    console.log("    基址: " + module.base);
    console.log("    大小: " + module.size);

    try {
      var buffer = module.base.readByteArray(module.size);
      var fileName = "/sdcard/" + module.name;
      var file = new File(fileName, "wb");
      file.write(buffer);
      file.close();
      console.log("    已保存: " + fileName);
    } catch (e) {
      console.log("    错误: " + e);
    }
  });

  console.log("[*] 完成!");
}

setImmediate(dumpAllSo);

💡 提示: 脱壳是一个反复试错的过程。如果一种方法不起作用,尝试其他方法或工具。大多数加固方案都可以通过 Frida 动态脱壳绕过。


相关内容

如果这篇文章对你有帮助,请我喝杯咖啡吧~
+5 支付宝支付宝
+5 微信微信