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 后,确保以下设置正确:

  1. Project SDK:设置为 JDK 8 或 JDK 11
  2. Maven 自动导入:确保 IDEA 的 Maven 自动导入功能已开启
  3. 编码设置:项目编码设置为 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.solibm.solibdl.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);

常见问题与解决

  1. SO 加载失败:检查 SO 的架构是否与模拟器匹配(armeabi-v7a 对应 ARM32,arm64-v8a 对应 ARM64)
  2. JNI 方法找不到:确保调用的函数签名与 SO 中导出的符号名完全一致
  3. 内存不足:通过 memory.setMemoryBlockFactory() 调整内存分配策略
  4. 无限循环:使用调试器附加断点,检查循环条件和退出逻辑
  5. JNI 函数未实现:继承 AbstractJni 类,重写对应的 JNI 方法实现

Unidbg 是 Android Native 逆向分析中非常强大的工具,掌握它的使用方法可以显著提高分析效率。在实际项目中,建议结合 IDA Pro 进行静态分析,定位关键函数后再用 Unidbg 进行动态验证和算法还原。