unidbg的使用汇总

2025/03/11 sec 共 6498 字,约 19 分钟

Unidbg 简介

Unidbg 是一个基于 Unicorn 引擎的 Android 模拟执行框架,能够让我们在 无需真实 Android 设备 或 不启动 APK 的情况下执行 SO 库中的 JNI 方法。它主要用于:

  • 逆向分析:解密、协议分析、破解加密算法。
  • 自动化测试:在 PC 端模拟 JNI 调用,提高效率。
  • 安全研究:分析恶意 APK 代码,检测安全漏洞。

核心概念

AndroidEmulator

AndroidEmulator 是 Unidbg 提供的 Android 进程模拟器,它负责创建 ARM 架构的运行环境,支持 32/64 位。

AndroidEmulator emulator = AndroidEmulatorBuilder.for32Bit()
        .setProcessName("com.example.test")
        .build();

Memory(虚拟内存管理)

Unidbg 通过 Memory 模块管理 so 库的加载和 Android 进程的内存映射。

Memory memory = emulator.getMemory();
memory.setLibraryResolver(new AndroidResolver(23)); // 23 代表 Android 版本

DalvikVM(虚拟机)

DalvikVM 是 Unidbg 提供的 Java 层虚拟机,用于 JNI 交互。

VM vm = emulator.createDalvikVM();
vm.setJni(new AbstractJni() {});
vm.setVerbose(true); // 打开日志输出

DalvikModule Module(SO模块)

  • DalvikModule 负责加载 Java 层 DEX 代码,同时支持解析 JNI 调用。
  • Module 代表一个已加载的 so 文件,支持解析导出函数及调用。
DalvikModule dm = vm.loadLibrary(new File("path\\libxyz.so"), false);
Module module = dm.getModule();

##

如何编译

使用Maven编译,下载解压到本地目录后,进入到unidbg的源码目录,执行编译命令:

D:\maven\apache-maven-3.9.9\bin\mvn.cmd  clean install -DskipTests

编译出错:

--- gpg:1.5:sign (default) @ unidbg ---
'gpg.exe' 不是内部或外部命令,也不是可运行的程序或批处理文件。

修改 unidbg根目录下的 pom.xml 文件,删除如下内容后重新编译即可。

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-gpg-plugin</artifactId>
    <version>1.5</version>
    <executions>
        <execution>
            <phase>verify</phase>
            <goals>
                <goal>sign</goal>
            </goals>
        </execution>
    </executions>
</plugin>

首次使用

AndroidStudio先随便创建一个安卓应用的工程,然后为工程添加模块,选择 Java or Kotlin Library,根据向导填写基本信息,即可创建一个Java应用程序。

添加依赖库:

dependencies {
    implementation 'com.github.zhkl0228:unidbg:0.9.8'
}

模拟调用

例如有一个so文件libxyz.so,它有一些注册函数,其中函数targetFunc是我们想要模拟调用的。

首先使用IDA查看该函数的偏移地址,如下,该偏移地址是:0x6A60。

.data:0001A6B8                 DCD atargetFunc         ; "targetFunc"
.data:0001A6BC                 DCD aLandroidConten_6   ; "(Landroid/content/Context;I[B[B)I"
.data:0001A6C0                 DCD sub_6A60

编写代码:

package com.bigsing.unidbgdemo;

import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Module;
import com.github.unidbg.arm.backend.Unicorn2Factory;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.AbstractJni;
import com.github.unidbg.linux.android.dvm.DalvikModule;
import com.github.unidbg.linux.android.dvm.DvmClass;
import com.github.unidbg.linux.android.dvm.DvmObject;
import com.github.unidbg.linux.android.dvm.VM;
import com.github.unidbg.linux.android.dvm.array.ByteArray;
import com.github.unidbg.memory.Memory;
import com.github.unidbg.virtualmodule.android.AndroidModule;
import com.sun.jna.Pointer;

import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;

public class MyClass extends AbstractJni {
    final AndroidEmulator emulator;
    final VM vm;
    final Module module;
    final DalvikModule dalvikModule;

    MyClass() {
        // 创建 Android ARM 32位模拟器
        emulator = AndroidEmulatorBuilder.for32Bit()
                .setProcessName("com.example.test")
                .addBackendFactory(new Unicorn2Factory(true))
                .build();

        // 获取模拟内存并设置 Android 版本解析器
        Memory memory = emulator.getMemory();
        memory.setLibraryResolver(new AndroidResolver(23)); // 23 代表 Android 版本

        // 创建 Android 虚拟机
        vm = emulator.createDalvikVM();
        vm.setJni(this);
        vm.setVerbose(true);

        // 注册 Android 模块,确保 Unidbg 能正确解析系统库
        new AndroidModule(emulator, vm).register(memory);

        // 加载目标 so 文件
        dalvikModule = vm.loadLibrary(new File("E:\\APPs\\libxyz.so"), false);
        module = dalvikModule.getModule(); // 获取加载后的模块对象
    }

    public static void main(String[] args) {
        String input = "这里是加密字符串..."; 
        MyClass test = new MyClass();
        test.callFuncByOffset(input);
        test.callFuncByRegister(input);
    }

    // 通过 JNI 注册的方法调用
    public void callFuncByRegister(String input) {
        byte[] inData = input.getBytes(StandardCharsets.UTF_8);
        final byte[] outData = new byte[4096];
        ByteArray inByteArray = new ByteArray(vm, inData);
        ByteArray outByteArray = new ByteArray(vm, outData);

        // 执行 JNI_OnLoad 以初始化 JNI 相关的符号
        dalvikModule.callJNI_OnLoad(emulator);
        DvmClass nativeClass = vm.resolveClass("com/xyz/app/abcd");

        try {
            // 调用 targetFunc 方法
            int len = nativeClass.callStaticJniMethodInt(emulator,
                    "targetFunc(Landroid/content/Context;I[B[B)I",
                    null,
                    1,
                    vm.addLocalObject(inByteArray),
                    vm.addLocalObject(outByteArray));

            if (len > 0) {
                byte[] out = outByteArray.getValue();
                System.out.println("[Register Call] Decoded Text: " + new String(out, StandardCharsets.UTF_8));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    // 通过 native 层函数偏移调用
    public void callFuncByOffset(String input) {
        Pointer env = vm.getJNIEnv(); // 获取 JNI 环境指针

        byte[] inData = input.getBytes(StandardCharsets.UTF_8);
        final byte[] outData = new byte[4096];
        ByteArray inByteArray = new ByteArray(vm, inData);
        ByteArray outByteArray = new ByteArray(vm, outData);

        // 获取目标 Java 类对象的引用
        DvmClass nativeClass = vm.resolveClass("com/xyz/app/abcd");
        DvmObject<?> nativeClassObject = nativeClass.newObject(null);
        int jclassRef = vm.addLocalObject(nativeClassObject);  // 获取 Java 层的 jclass 句柄,也可以填 0

        // 构造参数列表
        ArrayList<Object> list_arg = new ArrayList<>();
        list_arg.add(env);          // JNIEnv*
        list_arg.add(jclassRef);    // jclass (静态方法调用)
        list_arg.add(null);         // Context (可选参数)
        list_arg.add(1);            // int 参数
        list_arg.add(vm.addLocalObject(inByteArray));  // byte[] 输入数据
        list_arg.add(vm.addLocalObject(outByteArray)); // byte[] 输出数据

        // 调用目标 native 层函数,偏移地址 0x6A60
        Number result = module.callFunction(emulator, 0x6A60, list_arg.toArray());
        int len = result.intValue();

        if (len > 0) {
            byte[] out = outByteArray.getValue();
            System.out.println("[Offset Call] Decoded Text: " + new String(out, StandardCharsets.UTF_8));
        }
    }
}

代码使用了两种方法来调用目标函数:按照函数偏移地址调用callFuncByOffset)、按照注册的jni函数调用callFuncByRegister)。

因为我们的目标函数恰巧被它的JNI_OnLoad进行了注册,所以可以按照调用jni函数的方式进行调用,但是这个方法不够通用,特别是在目标函数没有被注册的情况下,这个时候就要通过函数偏移进行调用了。

函数的偏移地址已经要分析正确,否则调用肯定是不会成功的,这需要在IDA里进行分析。因为这个函数是被注册成jni函数了,通过运行日志也可以看出它的偏移是0x6a60,也可以佐证在IDA里获取的地址是否正确。

JNIEnv->FindClass(com/xyz/app/abcd) was called from RX@0x40004e60[libxyz.so]0x4e60
JNIEnv->NewGlobalRef(class com/xyz/app/abcd) was called from RX@0x40004e80[libxyz.so]0x4e80
JNIEnv->FindClass(java/lang/String) was called from RX@0x40004ec0[libxyz.so]0x4ec0
JNIEnv->NewGlobalRef(class java/lang/String) was called from RX@0x40004ee0[libxyz.so]0x4ee0
JNIEnv->RegisterNatives(com/xyz/app/abcd, RW@0x4001a6a0[libxyz.so]0x1a6a0, 3) was called from RX@0x40004f2c[libxyz.so]0x4f2c
RegisterNative(com/xyz/app/abcd, targetFunc1(Landroid/content/Context;II)I, RX@0x40005cbc[libxyz.so]0x5cbc)
RegisterNative(com/xyz/app/abcd, targetFunc2(Landroid/content/Context;ILjava/lang/String;Ljava/lang/String;[BII)J, RX@0x4000646c[libxyz.so]0x646c)
RegisterNative(com/xyz/app/abcd, targetFunc(Landroid/content/Context;I[B[B)I, RX@0x40006a60[libxyz.so]0x6a60)
Find native function Java_com_xyz_app_abcd_targetFunc => RX@0x40006a60[libxyz.so]0x6a60
JNIEnv->GetArrayLength([B@496bc455 => 231) was called from RX@0x40006aa0[libxyz.so]0x6aa0

文档信息

Search

    Table of Contents

    目录