应用安全 --- 模拟独立运行so的方法 Unidbg
非常欢迎来到 Unidbg 的世界!这是 Android 逆向工程中一个极其强大且优雅的工具。下面我将为您提供一份从零开始的 Unidbg 入门指南。
一、Unidbg 是什么?为什么需要它?
想象一下,你在逆向一个 App 时,发现最关键的加密算法不在 Java 层,而是被编译成了原生代码,放在了一个 *.so 动态链接库里。你会怎么办?
-
动态调试:使用 Frida 或 IDA 去 Hook、调试。但问题是,App 可能有强大的反调试、反虚拟机、反 Hook 机制,让你的调试器瞬间崩溃。
-
静态分析:用 IDA 反编译 so 文件。但如果算法逻辑非常复杂或被虚拟化保护(VMP)、混淆了,分析起来将是一场噩梦,耗时耗力。
Unidbg 提供了第三种,也是更优的解决方案:
Unidbg 是一个基于 Java 的“黑盒”模拟执行工具。它可以直接在你的电脑上(无需真机环境)模拟运行 Android 或 iOS 的 so 文件中的函数。
它的核心思想是: 我不需要知道你这个算法具体是怎么实现的(黑盒),我只需要给你提供运行所需的环境(内存、寄存器、JNI接口、系统函数等),让你自己把结果算出来告诉我。
为什么需要 Unidbg?
-
无视反调试:由于是模拟环境,so 文件根本感知不到调试器的存在,所有基于
ptrace、fopen("/proc/self/status")等反调试手段全部失效。 -
高效:一旦编写好调用脚本,算法执行速度极快,非常适合用于大规模、自动化的爬虫工程。
-
可控:可以随时中断、查看寄存器/内存状态,并且输入输出非常清晰。
-
应对虚拟化保护:虽然不能直接还原 VMP 的指令,但可以通过 Unidbg 直接执行得到结果,绕过分析指令本身的过程。
二、Unidbg 的核心工作流程
使用 Unidbg 调用一个 so 文件中的函数,通常分为以下几步:
-
创建模拟器:选择一个 CPU 架构(如
ARM32或ARM64)来初始化模拟环境。 -
加载 so 文件:将目标 so 文件加载到模拟器的内存中。
-
设置运行参数:为 JNI 环境、系统函数(Hook)、内存等做好准备。
-
定位目标函数:找到你想要调用的那个函数的偏移地址或符号名。
-
传入参数:根据函数原型,通过寄存器或栈设置好传入参数。
-
模拟执行:让模拟器从函数入口点开始运行。
-
获取返回值:函数执行完毕后,从寄存器(如 R0, X0)或内存中取出计算结果。
三、环境搭建
-
安装 Java:确保电脑上安装了 JDK 8 或 JDK 11。
-
获取 Unidbg:
-
推荐直接克隆 GitHub 上的项目,这样可以获取最新的示例和代码。
git clone https://github.com/zhkl0228/unidbg.git -
-
导入 IDE:使用 IntelliJ IDEA 打开这个项目。IDEA 对 Java 和 Maven 的支持最好。打开后,Maven 会自动下载所有依赖包。
项目中的 unidbg-android/src/test/java 目录下有海量的示例,这是最好的学习资料。
四、第一个 Unidbg 示例:调用一个无参函数
假设我们有一个 so 文件 libnative-lib.so,里面有一个函数 native_add(),功能是计算 1 + 2。
C 代码原型:
int native_add(int a, int b) {
return a + b;
}
对应的 Unidbg Java 代码:
package com.example;
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Module;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.*;
import com.github.unidbg.memory.Memory;
import java.io.File;
import java.io.IOException;
public class SimpleDemo {
public static void main(String[] args) throws IOException {
// 1. 创建模拟器实例,选择 ARM64 架构
AndroidEmulator emulator = AndroidEmulatorBuilder.for64Bit().build();
// 2. 获取模拟器的内存接口
Memory memory = emulator.getMemory();
// 设置库解析器,用于处理系统库的加载
memory.setLibraryResolver(new AndroidResolver(23)); // API Level 23
// 3. 创建 Android VM (DalvikVM) 实例,这对于运行 JNI 相关的代码很重要
VM vm = emulator.createDalvikVM();
// 加载目标 so 文件到内存中
Module module = vm.loadLibrary(new File("unidbg/src/test/resources/example_binaries/libnative-lib.so"), false); // false 表示不进行初始化调用
// 4. 打印 so 文件加载的基地址
System.out.println("load library base=" + module.base);
// 5. 调用函数
// 5.1 找到函数的偏移地址或符号
// 假设我们通过 IDA 分析,知道 native_add 的偏移地址是 0x1234
Number result = module.callFunction(emulator, 0x1234, 1, 2);
// 也可以尝试通过符号名查找(如果 so 文件未被 strip 掉符号表)
// Number result = module.callFunction(emulator, "native_add", 1, 2);
// 6. 获取并打印结果
// ARM32 下返回值在 R0,ARM64 下在 X0。Unidbg 帮我们封装好了,直接取即可。
System.out.println("Native add result: " + result.intValue());
// 关闭模拟器
emulator.close();
}
}
五、核心概念与高级技巧
-
JNI 交互:
-
很多 so 函数是
Java_com_example_MainActivity_nativeMethod的形式,它需要一个JNIEnv*和jobject参数。 -
Unidbg 的
DalvikVM完美模拟了这一点。你需要创建一个DvmObject来表示this对象,然后使用vm.getJNIEnv()来获取 JNIEnv。 -
示例:
DvmClass mainActivity = vm.resolveClass("com/example/MainActivity"); DvmObject<?> obj = mainActivity.newObject(null); module.callFunction(emulator, "Java_com_example_MainActivity_encrypt", vm.getJNIEnv(), obj.hashCode(), ...);
-
-
Hook 系统函数:
-
so 文件经常会调用
strlen,malloc,fopen等系统函数。Unidbg 需要实现这些函数才能让 so 顺利运行。 -
AndroidResolver已经提供了大部分系统函数的实现。 -
你可以手动 Hook 这些函数来打印日志或修改行为,这对于调试至关重要。
-
示例(Hook
strlen):memory.addHookListener(new HookListener() { @Override public long hook(SvcMemory memory, String library, String symbol, long old) { if ("libc.so".equals(library) && "strlen".equals(symbol)) { return new ReplaceableSvc() { @Override public long handle(Emulator<?> emulator) { // 获取 strlen 的第一个参数,即字符串地址 UnidgPointer pointer = emulator.getContext().getPointerArg(0); String str = pointer.getString(0); System.out.println("[Hook strlen] String: " + str); // 调用原始的系统函数实现并返回结果 return old == 0 ? 0 : emulator.getBackend().nativeSvc.call(emulator, old); } }; } return old; } });
-
-
补全环境:
-
如果 so 文件调用了一个 Unidbg 尚未实现的系统函数,模拟会卡住。你需要根据日志提示,自己实现这个函数(称为 SVC Hook 或 IRQ Hook)。
-
这需要一定的 ARM 汇编和 Linux 系统知识,是 Unidbg 进阶的必经之路。
-
六、学习建议
-
从示例开始:疯狂地阅读、运行、调试
unidbg/android/src/test/java下的官方示例。这是最快的学习路径。 -
结合逆向工具:
-
使用 IDA Pro 或 Ghidra 静态分析 so 文件,确定你要调用的函数偏移地址和参数类型( int? char*?结构体指针?)。
-
使用 Frida 在真机上快速验证函数的功能和参数,然后再用 Unidbg 复现。
-
-
耐心调试:遇到问题别怕,仔细阅读 Unidbg 打印的执行日志,它非常详细,会告诉你执行到了哪里、在哪里卡住了、需要什么函数。
-
查阅文档和社区:GitHub Issues 里有很多宝藏,很多人会遇到和你一样的问题。
Unidbg 的学习曲线前期可能有些陡峭,但一旦掌握,它将成为你逆向武器库中最强大、最优雅的工具之一。它让你真正做到了“以我为主”,让复杂的 Native 代码在为你打工。祝你学习顺利!
浙公网安备 33010602011771号