Unidbg 使用和加载 SO 文件的方法
Unidbg 使用和加载 SO 文件的方法
在 Android 逆向工程中,分析 Native 层的 SO 文件是一项核心任务。传统方式需要真机或模拟器配合 Frida 等工具进行 hook,但这种方式存在设备依赖、环境复杂、调试不便等问题。Unidbg 作为一个基于 Unicorn 的 Android Native 模拟执行框架,能够在纯 PC 环境中模拟执行 SO 文件中的函数,极大地简化了逆向分析流程。
Unidbg 项目介绍
Unidbg 是 GitHub 上的开源项目,它的核心能力是在不需要 Android 真机或模拟器的情况下,模拟执行 ARM/ARM64 架构的 Native 函数。它的主要特点包括:
- 无需真机:在 PC 上直接运行,不依赖 Android 设备或模拟器
- 支持 ARM/ARM64:可以模拟执行 32 位和 64 位的 SO 文件
- JNI 环境模拟:内置了完整的 JNI 函数表模拟,能处理 Java 层和 Native 层的交互
- 调试友好:支持断点、单步执行、寄存器查看等调试功能
- 可扩展性强:可以自定义虚拟文件系统、模拟 Java 对象、替换 JNI 函数实现
Unidbg 底层基于 Unicorn 引擎进行 CPU 指令模拟,同时封装了 ELF 解析、SO 加载、JNI 环境搭建等高级功能,使得逆向分析人员可以将精力集中在算法还原本身。
环境搭建
基础环境要求
使用 Unidbg 需要以下基础环境:
- JDK 8 或更高版本:Unidbg 是 Java 项目,需要 Java 运行环境
- Maven 或 Gradle:用于依赖管理
- IntelliJ IDEA(推荐):作为开发 IDE
创建 Unidbg 项目
最快捷的方式是直接克隆 Unidbg 官方仓库,它自带了示例代码和依赖配置:
git clone https://github.com/zhkl0228/unidbg.git
cd unidbg
如果你想在已有项目中集成 Unidbg,可以在 Maven 的 pom.xml 中添加依赖:
<dependency>
<groupId>com.github.zhkl0228</groupId>
<artifactId>unidbg-android</artifactId>
<version>0.9.7</version>
</dependency>
对于 Gradle 用户,在 build.gradle 中添加:
implementation 'com.github.zhkl0228:unidbg-android:0.9.7'
IDEA 配置
将项目导入 IDEA 后,确保以下设置正确:
- Project SDK:设置为 JDK 8 或 JDK 11
- Maven 自动导入:确保 IDEA 的 Maven 自动导入功能已开启
- 编码设置:项目编码设置为 UTF-8,避免中文注释乱码
创建 Unidbg 模拟器实例
Unidbg 提供了两种模拟器实现:AndroidARMEmulator 用于模拟 32 位 ARM 环境,AndroidARM64Emulator 用于模拟 64 位 ARM64 环境。
32 位模拟器
import com.github.unidbg.AndroidEmulatorBuilder;
import com.github.unidbg.LibraryResolver;
import com.github.unidbg.linux.android.AndroidEmulatorBuilderARM32;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.DvmClass;
import com.github.unidbg.linux.android.dvm.StringObject;
import com.github.unidbg.memory.Memory;
public class UnidbgTest extends AbstractJni {
// 创建模拟器实例
private final AndroidEmulatorBuilderARM32 builder;
private final AndroidARMEmulator emulator;
private final VM vm;
private final Memory memory;
private final Module module;
public UnidbgTest() {
// 创建 ARM32 模拟器
builder = AndroidEmulatorBuilder.for32Bit();
emulator = builder.build();
// 获取虚拟机
vm = emulator.createDalvikVM();
// 获取内存管理器
memory = emulator.getMemory();
// 设置 Android SDK 版本
LibraryResolver resolver = new AndroidResolver(23);
memory.setLibraryResolver(resolver);
}
}
64 位模拟器
// 创建 ARM64 模拟器
AndroidARM64Emulator emulator = AndroidEmulatorBuilder.for64Bit()
.setProcessName("com.example.app")
.build();
VM vm = emulator.createDalvikVM();
64 位模拟器的使用方式与 32 位基本一致,主要区别在于寄存器宽度和调用约定。
加载 SO 文件的方法
加载单个 SO 文件
Unidbg 通过 loadLibrary 方法加载 SO 文件,它会自动完成 ELF 解析、内存映射、重定位等操作:
// 加载 SO 文件(方式一:从文件路径加载)
Module module = emulator.loadLibrary(
new File("lib/armeabi-v7a/libnative.so"),
false
);
// 加载 SO 文件(方式二:从类加载路径加载)
Module module = vm.loadLibrary("native", false);
loadLibrary 的第二个参数 false 表示不立即调用 JNI_OnLoad。如果设置为 true,Unidbg 会在加载完成后自动调用 JNI_OnLoad。
加载依赖库
许多 SO 文件依赖于系统库(如 libc.so、libm.so、libdl.so 等)。Unidbg 内置了这些系统库的模拟实现,但如果目标 SO 依赖了其他第三方 SO,需要手动加载:
// 先加载依赖库
emulator.loadLibrary(new File("lib/armeabi-v7a/libssl.so"), false);
emulator.loadLibrary(new File("lib/armeabi-v7a/libcrypto.so"), false);
// 再加载目标 SO
Module targetModule = emulator.loadLibrary(
new File("lib/armeabi-v7a/libsign.so"),
false
);
加载顺序很重要,必须先加载被依赖的库,再加载依赖它的库。
调用 JNI_OnLoad
对于需要动态注册 JNI 方法的 SO 文件,必须在加载后调用 JNI_OnLoad:
// 加载 SO
Module module = emulator.loadLibrary(
new File("lib/armeabi-v7a/libsign.so"),
false
);
// 手动调用 JNI_OnLoad
emulator.callLibraryInit(module, vm);
或者使用 loadLibrary 的第二个参数设为 true,自动调用 JNI_OnLoad:
Module module = emulator.loadLibrary(
new File("lib/armeabi-v7a/libsign.so"),
true // 自动调用 JNI_OnLoad
);
调用 SO 中的导出函数
调用导出函数是 Unidbg 的核心功能。对于静态注册的 JNI 函数,可以直接通过符号名调用:
// 通过符号名调用导出函数
Number result = module.callFunction(emulator,
"Java_com_example_app_NativeUtils_sign",
vm.getJNIEnv(), // JNIEnv* 指针
0, // jobject(this)
vm.addLocalObject(new StringObject(vm, "hello_world")) // jstring 参数
);
System.out.println("签名结果: " + result);
处理不同类型的参数和返回值
Unidbg 通过 vm.addLocalObject() 将 Java 对象传递给 Native 层:
// 传递字符串
StringObject input = new StringObject(vm, "input_data");
Number result = module.callFunction(emulator, "nativeFunc",
vm.getJNIEnv(), 0,
vm.addLocalObject(input)
);
// 传递字节数组
ByteArray byteArray = new ByteArray(vm, dataBytes);
Number result = module.callFunction(emulator, "nativeFunc",
vm.getJNIEnv(), 0,
vm.addLocalObject(byteArray)
);
对于返回字符串的情况:
// 如果 Native 函数返回 jstring
DvmObject<?> result = (DvmObject<?>) module.callFunction(emulator,
"Java_com_example_app_NativeUtils_encrypt",
vm.getJNIEnv(), 0,
vm.addLocalObject(new StringObject(vm, "test"))
);
String output = result.getValue().toString();
System.out.println("加密结果: " + output);
Unidbg 执行 Native 函数的完整流程
下面给出一个完整的示例,从创建模拟器到调用 SO 函数的全流程:
import com.github.unidbg.AndroidEmulatorBuilder;
import com.github.unidbg.Module;
import com.github.unidbg.linux.android.AndroidARMEmulator;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.*;
import com.github.unidbg.linux.android.dvm.array.ByteArray;
import com.github.unidbg.memory.Memory;
import java.io.File;
public class SignHelper extends AbstractJni {
private final AndroidARMEmulator emulator;
private final VM vm;
private final Module module;
public SignHelper() {
// 1. 创建模拟器
emulator = AndroidEmulatorBuilder.for32Bit()
.setProcessName("com.example.app")
.build();
// 2. 创建虚拟机
vm = emulator.createDalvikVM();
vm.setVerbose(true); // 打印调用日志
// 3. 设置库解析器
Memory memory = emulator.getMemory();
memory.setLibraryResolver(new AndroidResolver(23));
// 4. 加载 SO 文件
module = emulator.loadLibrary(
new File("lib/armeabi-v7a/libsign.so"),
true
);
}
public String getSign(String data) {
// 5. 调用 Native 函数
DvmObject<?> result = module.callFunction(emulator,
"Java_com_example_app_NativeUtils_sign",
vm.getJNIEnv(),
vm.resolveClass("com/example/app/NativeUtils").newObject(null),
vm.addLocalObject(new StringObject(vm, data))
);
return result != null ? result.getValue().toString() : null;
}
public static void main(String[] args) {
SignHelper helper = new SignHelper();
String sign = helper.getSign("hello");
System.out.println("签名结果: " + sign);
}
}
处理 SO 文件的依赖库
当 SO 文件依赖了 Unidbg 未内置的库时,会出现加载失败的错误。常见的解决方法:
使用 stub 库
Unidbg 支持加载 stub(桩)库,即只有符号表但没有实际实现的库。可以创建一个空的 stub 库来满足链接需求:
// 加载 stub 库(只提供符号,不提供实现)
Module stub = emulator.loadLibrary(
new File("lib/armeabi-v7a/libstub.so"), false
);
替换缺失的系统调用
某些 SO 文件会直接调用系统调用(SVC 指令),Unidbg 默认处理了大部分常见系统调用。如果遇到未实现的系统调用,可以手动添加:
emulator.getSyscallHandler().addHandler(
new SyscallHandler() {
@Override
public boolean handle(Emulator<?> emulator, long syscall, ...) {
if (syscall == /* 自定义系统调用号 */) {
// 处理逻辑
return true;
}
return false;
}
}
);
调试技巧
开启详细日志
Unidbg 提供了丰富的日志输出,帮助分析执行过程:
// 开启详细日志
vm.setVerbose(true);
// 开启 JNI 调用日志
Debugger debugger = emulator.attach(DebuggerType.CONSOLE);
断点调试
使用 Unidbg 内置的调试器设置断点:
// 在指定地址设置断点
emulator.attach(DebuggerType.CONSOLE)
.addBreakPoint(module.base + 0x1234);
// 在符号处设置断点
emulator.attach(DebuggerType.CONSOLE)
.addBreakPoint(module.findSymbolByName("encrypt").getAddress());
内存查看和修改
Memory memory = emulator.getMemory();
// 读取指定地址的内存
byte[] data = memory.readBytes(address, length);
// 写入内存
memory.writeBytes(address, data);
// 查看内存映射
memory.showMemoryMap();
单步执行
// 单步执行模式
emulator.attach(DebuggerType.CONSOLE)
.setStepMode(true);
常见问题与解决
- SO 加载失败:检查 SO 的架构是否与模拟器匹配(armeabi-v7a 对应 ARM32,arm64-v8a 对应 ARM64)
- JNI 方法找不到:确保调用的函数签名与 SO 中导出的符号名完全一致
- 内存不足:通过
memory.setMemoryBlockFactory()调整内存分配策略 - 无限循环:使用调试器附加断点,检查循环条件和退出逻辑
- JNI 函数未实现:继承
AbstractJni类,重写对应的 JNI 方法实现
Unidbg 是 Android Native 逆向分析中非常强大的工具,掌握它的使用方法可以显著提高分析效率。在实际项目中,建议结合 IDA Pro 进行静态分析,定位关键函数后再用 Unidbg 进行动态验证和算法还原。