Unidbg 模拟执行框架指南

Unidbg 模拟执行框架指南

Unidbg 是一个基于 Java 开发的 Android/iOS 原生库 (.so/.dylib) 模拟执行框架。它能够在 PC 上模拟完整的 ARM 执行环境,使你可以像调用本地 Java 方法一样直接调用和调试原生库中的函数。


核心思想与应用场景

Unidbg 的核心思想是**“欺骗”**——让 .so 文件认为自己正运行在一个真实的 Android 设备上:

  • 模拟文件系统: 创建虚拟文件系统,放入应用的数据和配置文件
  • 模拟内存空间: 加载 .so 及其依赖的系统库到模拟内存中
  • 模拟 JNI 环境: 实现大部分 JNI 函数,拦截 .so 对 Java 层的调用
  • Hook 系统调用 (SVC): 拦截底层系统调用,返回预设结果

主要应用场景

场景说明
算法复现直接调用加密/解密函数,无需逆向算法本身
绕过环境检测Hook Root、模拟器、设备 ID 等检测点
绕过反调试ptrace 等反调试手段在模拟环境中天然无效
批量计算编写脚本批量调用目标函数,爆破参数或生成签名
调用非导出函数只要知道偏移就可以直接调用任何函数

Unidbg vs. Frida

特性UnidbgFrida
执行环境PC 端 (模拟执行)移动设备端 (真机/模拟器)
工作模式.so 当作黑盒库来调用侵入正在运行的应用进程进行 Hook
依赖仅需要 .so 文件及其依赖库需要一个完整的、能运行的 APK
反调试天然免疫需要编写脚本来对抗反调试
性能较低 (全模拟)较高 (代码在设备上原生运行)
适用性适合纯算法分析适合分析与系统、UI 强相关的逻辑

环境搭建

Maven 项目配置

 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
<project>
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <artifactId>unidbg-demo</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <unidbg.version>0.9.7</unidbg.version>
    </properties>

    <repositories>
        <repository>
            <id>jitpack.io</id>
            <url>https://jitpack.io</url>
        </repository>
    </repositories>

    <dependencies>
        <dependency>
            <groupId>com.github.zhkl0228</groupId>
            <artifactId>unidbg-android</artifactId>
            <version>${unidbg.version}</version>
        </dependency>
        <dependency>
            <groupId>com.github.zhkl0228</groupId>
            <artifactId>unidbg-api</artifactId>
            <version>${unidbg.version}</version>
        </dependency>
        <dependency>
            <groupId>commons-logging</groupId>
            <artifactId>commons-logging</artifactId>
            <version>1.2</version>
        </dependency>
    </dependencies>
</project>

Gradle 配置

1
2
3
4
5
6
7
8
repositories {
    mavenCentral()
    maven { url 'https://jitpack.io' }
}
dependencies {
    implementation 'com.github.zhkl0228:unidbg-android:0.9.7'
    implementation 'com.github.zhkl0228:unidbg-api:0.9.7'
}

推荐项目结构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
unidbg-demo/
├── pom.xml
├── src/main/
│   ├── java/com/example/demo/
│   │   ├── SignEmulator.java        <-- 模拟器主类
│   │   └── AbstractApp.java        <-- 公共基类
│   └── resources/
│       ├── apk/target-app.apk      <-- 目标 APK
│       └── so/libnative-lib.so     <-- 目标 SO
└── target/rootfs/default/           <-- 虚拟文件系统
    └── proc/self/maps

基本使用流程

完整代码模板

 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
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;

public class MyEmulator extends AbstractJni {

    private final AndroidEmulator emulator;
    private final Module module;
    private final VM vm;

    public MyEmulator() {
        // 1. 创建模拟器 (for32Bit=armeabi-v7a, for64Bit=arm64-v8a)
        emulator = AndroidEmulatorBuilder
                .for32Bit()
                .setProcessName("com.example.target")
                .build();

        // 2. 初始化内存,设置 API 级别 (23=Android 6.0)
        final Memory memory = emulator.getMemory();
        memory.setLibraryResolver(new AndroidResolver(23));

        // 3. 创建 DVM,传入 APK 以自动解析 classes.dex
        vm = emulator.createDalvikVM(new File("src/main/resources/apk/target.apk"));
        vm.setJni(this);
        vm.setVerbose(true);

        // 4. 加载目标 SO (true = 执行 .init 和 .init_array)
        DalvikModule dm = vm.loadLibrary(
                new File("src/main/resources/so/libnative-lib.so"), true);
        module = dm.getModule();

        // 5. 调用 JNI_OnLoad (触发动态注册)
        dm.callJNI_OnLoad(emulator);
    }

    // 方式一:通过导出符号名调用
    public String callBySymbol(String input) {
        DvmObject<?> context = vm.resolveClass("android/content/Context").newObject(null);
        DvmObject<?> result = vm.callJniMethod(emulator,
                "Java_com_example_NativeLib_sign(Landroid/content/Context;Ljava/lang/String;)Ljava/lang/String;",
                context, new StringObject(vm, input));
        return result.getValue().toString();
    }

    // 方式二:通过偏移地址调用 (偏移从 IDA/Ghidra 获取)
    public int callByOffset(int arg1, int arg2) {
        // ARM 模式地址不变,Thumb 模式地址需要 +1
        Number[] result = module.callFunction(emulator, 0x1A3C, arg1, arg2);
        return result[0].intValue();
    }

    // 方式三:通过 DvmClass 调用 JNI 静态方法
    public String callByDvmClass(String input) {
        DvmClass dvmClass = vm.resolveClass("com/example/NativeLib");
        DvmObject<?> result = dvmClass.callStaticJniMethodObject(emulator,
                "getSign(Ljava/lang/String;)Ljava/lang/String;",
                new StringObject(vm, input));
        return result != null ? result.getValue().toString() : null;
    }

    public static void main(String[] args) {
        MyEmulator emu = new MyEmulator();
        System.out.println("Result: " + emu.callBySymbol("test_data"));
        emu.emulator.close();
    }
}

提示: 如果目标 APK 同时包含 armeabi-v7aarm64-v8a,优先尝试 32 位,模拟更成熟稳定。


JNI 环境模拟

当 SO 通过 JNI 回调 Java 层代码时,需要在 AbstractJni 子类中手动实现这些回调。

方法重写示例

 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
public class MyEmulator extends AbstractJni {

    @Override
    public DvmObject<?> callObjectMethod(BaseVM vm, DvmObject<?> dvmObject,
                                          String signature, VarArg varArg) {
        switch (signature) {
            case "android/content/Context->getPackageName()Ljava/lang/String;":
                return new StringObject(vm, "com.example.target");

            case "android/content/Context->getSharedPreferences(Ljava/lang/String;I)Landroid/content/SharedPreferences;":
                return vm.resolveClass("android/content/SharedPreferences").newObject(null);

            case "android/content/SharedPreferences->getString(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;":
                String key = varArg.getObjectArg(0).getValue().toString();
                return new StringObject(vm, "device_id".equals(key) ? "abc123" : "");
        }
        return super.callObjectMethod(vm, dvmObject, signature, varArg);
    }

    @Override
    public DvmObject<?> callStaticObjectMethod(BaseVM vm, DvmClass dvmClass,
                                                String signature, VarArg varArg) {
        switch (signature) {
            case "android/util/Base64->encodeToString([BI)Ljava/lang/String;":
                byte[] data = (byte[]) varArg.getObjectArg(0).getValue();
                return new StringObject(vm,
                        java.util.Base64.getEncoder().encodeToString(data));
        }
        return super.callStaticObjectMethod(vm, dvmClass, signature, varArg);
    }

    @Override
    public int callIntMethod(BaseVM vm, DvmObject<?> dvmObject,
                             String signature, VarArg varArg) {
        if ("java/lang/String->length()I".equals(signature)) {
            return dvmObject.getValue().toString().length();
        }
        return super.callIntMethod(vm, dvmObject, signature, varArg);
    }

    @Override
    public DvmObject<?> getObjectField(BaseVM vm, DvmObject<?> dvmObject,
                                        String signature) {
        switch (signature) {
            case "android/content/pm/PackageInfo->signatures:[Landroid/content/pm/Signature;":
                DvmObject<?> sig = vm.resolveClass("android/content/pm/Signature")
                        .newObject("fake_signature_hex");
                return new ArrayObject(sig);
        }
        return super.getObjectField(vm, dvmObject, signature);
    }

    @Override
    public int getStaticIntField(BaseVM vm, DvmClass dvmClass, String signature) {
        if ("android/os/Build$VERSION->SDK_INT:I".equals(signature)) return 23;
        return super.getStaticIntField(vm, dvmClass, signature);
    }
}

JNI 签名速查表

Java 类型JNI 签名示例
voidVdoSomething()V
booleanZisValid()Z
intIgetCount()I
longJgetTimestamp()J
byte[][BgetData()[B
StringLjava/lang/String;getName()Ljava/lang/String;
int[][IgetIds()[I

系统调用处理

虚拟文件系统

 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
public class MyEmulator extends AbstractJni implements IOResolver<AndroidFileIO> {

    public MyEmulator() {
        // ... 其他初始化 ...
        emulator.getSyscallHandler().addIOResolver(this);
    }

    @Override
    public FileResult<AndroidFileIO> resolve(Emulator<AndroidFileIO> emulator,
                                              String pathname, int oflags) {
        switch (pathname) {
            case "/proc/self/status":
                // TracerPid=0 表示未被调试(反调试关键)
                return FileResult.success(new ByteArrayFileIO(oflags, pathname,
                    ("Name:\ttarget\nTracerPid:\t0\nUid:\t10086\n").getBytes()));

            case "/system/build.prop":
                return FileResult.success(new ByteArrayFileIO(oflags, pathname,
                    ("ro.product.model=Pixel 2\nro.product.brand=google\n").getBytes()));

            case "/proc/cpuinfo":
                return FileResult.success(new ByteArrayFileIO(oflags, pathname,
                    ("processor\t: 0\nHardware\t: Qualcomm MSM8998\n").getBytes()));
        }
        return null;  // 使用默认处理
    }
}

系统调用处理流程

1
2
3
4
5
6
7
8
9
+------------------+     +------------------+     +------------------+
| SO 执行代码      |     | Unicorn 引擎     |     | Unidbg Handler   |
|                  |     |                  |     |                  |
|  open("/proc/    | --> | 执行 SVC #0      | --> | SyscallHandler   |
|   self/maps")    |     | 产生中断         |     | .handle()        |
|                  |     |                  |     |                  |
|  <-- fd=3        | <-- | 写入 r0=3        | <-- | IOResolver       |
|                  |     |                  |     | .resolve()       |
+------------------+     +------------------+     +------------------+

Hook 功能

HookZz (Inline Hook)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import com.github.unidbg.hook.hookzz.*;

IHookZz hookZz = HookZz.getInstance(emulator);

hookZz.wrap(module.base + 0x1A3C, new WrapCallback<HookZzArm32RegisterContext>() {
    @Override
    public void preCall(Emulator<?> emulator,
                        HookZzArm32RegisterContext ctx, HookEntryInfo info) {
        int arg0 = ctx.getIntArg(0);
        String arg1 = ctx.getPointerArg(1).getString(0);
        System.out.println("[preCall] arg0=" + arg0 + ", arg1=" + arg1);
        ctx.push(arg1);  // 保存供 postCall 使用
    }

    @Override
    public void postCall(Emulator<?> emulator,
                         HookZzArm32RegisterContext ctx, HookEntryInfo info) {
        int retVal = ctx.getIntArg(0);
        String saved = ctx.pop();
        System.out.println("[postCall] input=" + saved + ", ret=" + retVal);
        ctx.setR0(0);  // 修改返回值
    }
});

xHook (PLT Hook)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import com.github.unidbg.hook.xhook.IxHook;

IxHook xHook = XHookImpl.getInstance(emulator);

// Hook libnative-lib.so 中对 strlen 的调用
xHook.register("libnative-lib.so", "strlen", new ReplaceCallback() {
    @Override
    public HookStatus onCall(Emulator<?> emulator, HookContext context, long originFunction) {
        String str = context.getPointerArg(0).getString(0);
        System.out.println("[xHook] strlen(\"" + str + "\")");
        return HookStatus.RET(emulator, originFunction);  // 调用原函数
    }
});
xHook.refresh();  // 必须调用 refresh 使 Hook 生效

指令级追踪

1
2
3
4
5
6
// 追踪指定范围内的所有指令
emulator.traceCode(module.base + 0x1000, module.base + 0x2000);

// 监控内存读写
emulator.traceRead(0x40001000, 0x40001100);
emulator.traceWrite(0x40001000, 0x40001100);

Hook 方式对比

Hook 方式原理适用场景性能影响
HookZzInline HookHook 任意地址的函数
xHookPLT/GOT HookHook 导入的外部函数极低
traceCode指令回调追踪执行流、分析算法逻辑
Breakpoint断点中断调试特定位置、检查寄存器状态

调试与排错

常见错误及解决方案

1. Invalid memory read/write

1
UnicornException: Invalid memory read (UC_ERR_READ_UNMAPPED) at 0x00000000

原因:空指针引用。检查传入参数和 JNI 回调返回值是否有效。

2. UnsupportedOperationException

1
2
UnsupportedOperationException:
  android/telephony/TelephonyManager->getDeviceId()Ljava/lang/String;

原因:未处理的 JNI 回调。在 callObjectMethod 等方法中补充对应签名的处理。

3. 缺少 SO 依赖

1
FileNotFoundException: resolve library: libcrypto.so failed

解决:手动预加载依赖库:

1
2
vm.loadLibrary(new File("path/to/libcrypto.so"), false);  // 先加载依赖
vm.loadLibrary(new File("path/to/libtarget.so"), true);   // 再加载目标

4. 系统调用未实现

1
Unsupported syscall: 0x14e (334)

解决:查找 syscall 编号对应的功能,在 SyscallHandler 中补充实现。

调试技巧

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 开启详细日志
vm.setVerbose(true);

// 设置断点
Debugger debugger = emulator.attach();
debugger.addBreakPoint(module.base + 0x1A3C, (emu, addr) -> {
    Arm32RegisterContext ctx = emu.getContext();
    System.out.println("R0=0x" + Long.toHexString(ctx.getR0Int()));
    System.out.println("R1=0x" + Long.toHexString(ctx.getR1Int()));
    return true;  // true=继续执行
});

// 内存 Dump
byte[] data = emulator.getBackend().mem_read(address, size);
Inspector.inspect(data, "Memory dump");

排错流程

1
2
3
4
5
6
SO 执行失败
  +-- JNI 调用未处理?     --> 在 AbstractJni 中补充实现
  +-- 系统调用未实现?     --> 在 SyscallHandler 中补充
  +-- 文件访问失败?       --> 在 IOResolver 中添加文件模拟
  +-- 内存访问错误?       --> 检查参数传递和返回值
  +-- 以上都不是?         --> 开启 traceCode,结合 IDA 定位

实战案例

💡 思路一句话: Unidbg 的核心思路是「在 PC 上模拟 Android 环境调用 SO 函数」— 不需要真机、不需要 root,直接在 Java 代码中加载 SO → 补齐 JNI 环境 → 调用目标函数 → 获取返回值。适合需要大量调用签名函数的场景。

模拟执行某 App 的 generateSign() 签名函数的完整流程。

目标

1
2
3
4
5
// com.example.security.SignUtil (从 Jadx 反编译获得)
public class SignUtil {
    static { System.loadLibrary("security"); }
    public static native String generateSign(Context ctx, String params, long timestamp);
}

完整实现

 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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
public class SignEmulator extends AbstractJni implements IOResolver<AndroidFileIO> {

    private final AndroidEmulator emulator;
    private final VM vm;
    private final DvmClass signUtilClass;

    public SignEmulator() {
        emulator = AndroidEmulatorBuilder.for32Bit()
                .setProcessName("com.example.security")
                .build();
        Memory memory = emulator.getMemory();
        memory.setLibraryResolver(new AndroidResolver(23));
        emulator.getSyscallHandler().addIOResolver(this);

        vm = emulator.createDalvikVM(new File("src/main/resources/apk/target-app.apk"));
        vm.setJni(this);
        vm.setVerbose(true);

        DalvikModule dm = vm.loadLibrary("security", true);
        dm.callJNI_OnLoad(emulator);

        signUtilClass = vm.resolveClass("com/example/security/SignUtil");
    }

    public String generateSign(String params, long timestamp) {
        DvmObject<?> context = vm.resolveClass("android/content/Context").newObject(null);
        DvmObject<?> result = signUtilClass.callStaticJniMethodObject(emulator,
                "generateSign(Landroid/content/Context;Ljava/lang/String;J)Ljava/lang/String;",
                context, new StringObject(vm, params), timestamp);
        return result != null ? result.getValue().toString() : null;
    }

    // ---------- JNI 回调 ----------
    @Override
    public DvmObject<?> callObjectMethod(BaseVM vm, DvmObject<?> dvmObject,
                                          String signature, VarArg varArg) {
        switch (signature) {
            case "android/content/Context->getPackageName()Ljava/lang/String;":
                return new StringObject(vm, "com.example.security");
            case "android/content/Context->getPackageManager()Landroid/content/pm/PackageManager;":
                return vm.resolveClass("android/content/pm/PackageManager").newObject(null);
            case "android/content/pm/PackageManager->getPackageInfo(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;":
                return vm.resolveClass("android/content/pm/PackageInfo").newObject(null);
        }
        return super.callObjectMethod(vm, dvmObject, signature, varArg);
    }

    @Override
    public DvmObject<?> callStaticObjectMethod(BaseVM vm, DvmClass dvmClass,
                                                String signature, VarArg varArg) {
        if (signature.equals("android/provider/Settings$Secure->getString(Landroid/content/ContentResolver;Ljava/lang/String;)Ljava/lang/String;")) {
            return new StringObject(vm, "a1b2c3d4e5f67890");
        }
        return super.callStaticObjectMethod(vm, dvmClass, signature, varArg);
    }

    @Override
    public int callIntMethod(BaseVM vm, DvmObject<?> dvmObject,
                             String signature, VarArg varArg) {
        if ("android/content/pm/Signature->hashCode()I".equals(signature))
            return 0x12345678;  // 从真机获取
        return super.callIntMethod(vm, dvmObject, signature, varArg);
    }

    @Override
    public DvmObject<?> getObjectField(BaseVM vm, DvmObject<?> dvmObject, String signature) {
        if ("android/content/pm/PackageInfo->signatures:[Landroid/content/pm/Signature;".equals(signature)) {
            return new ArrayObject(
                    vm.resolveClass("android/content/pm/Signature").newObject("308201..."));
        }
        return super.getObjectField(vm, dvmObject, signature);
    }

    // ---------- 文件系统 ----------
    @Override
    public FileResult<AndroidFileIO> resolve(Emulator<AndroidFileIO> emulator,
                                              String pathname, int oflags) {
        if ("/proc/self/status".equals(pathname))
            return FileResult.success(new ByteArrayFileIO(oflags, pathname,
                    "Name:\tcom.example.security\nTracerPid:\t0\n".getBytes()));
        return null;
    }

    // ---------- 主入口 ----------
    public static void main(String[] args) {
        SignEmulator emu = new SignEmulator();
        long ts = System.currentTimeMillis() / 1000;
        String params = "method=getUserInfo&uid=12345&token=abcdef";
        String sign = emu.generateSign(params, ts);
        System.out.println("Sign: " + sign);
        emu.emulator.close();
    }
}

迭代补环境过程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
第 1 次运行: UnsupportedOperation: Context->getPackageName()
  --> 补充 callObjectMethod 中的 getPackageName

第 2 次运行: UnsupportedOperation: Context->getPackageManager()
  --> 补充 PackageManager 相关处理

第 3 次运行: FileNotFoundException: /proc/self/status
  --> 补充 IOResolver 中的文件处理

第 4 次运行: UnsupportedOperation: Settings$Secure->getString()
  --> 补充 android_id 的返回值

第 5 次运行: 成功输出签名结果!

技巧: 每次出错后,复制错误信息中的方法签名,在对应的 callXxxMethod 中添加 case 即可。这就是**“补环境”**。


性能优化

后端引擎选择

后端引擎特点推荐场景
Unicorn默认后端,兼容性最好调试阶段
DynarmicJIT 编译,性能提升 5-10 倍批量调用、生产环境
KVM硬件虚拟化 (仅 Linux ARM 主机)ARM 服务器部署
1
2
3
4
// 使用 Dynarmic 后端
AndroidEmulator emulator = AndroidEmulatorBuilder.for32Bit()
        .addBackendFactory(new DynarmicFactory(true))
        .build();

复用与并发

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// Unidbg 不是线程安全的,需要使用对象池
public class SignServicePool {
    private final BlockingQueue<SignEmulator> pool;

    public SignServicePool(int poolSize) {
        pool = new LinkedBlockingQueue<>(poolSize);
        for (int i = 0; i < poolSize; i++) pool.offer(new SignEmulator());
    }

    public String sign(String params, long ts) throws InterruptedException {
        SignEmulator emu = pool.take();
        try { return emu.generateSign(params, ts); }
        finally { pool.offer(emu); }
    }
}

性能参考

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
+--------------------------------------------------+
| 测试: MacBook Pro M1, JDK 11, sign() 函数       |
+--------------------------------------------------+
| 后端       | 单次调用 | 1000 次 | QPS            |
|------------|----------|---------|----------------|
| Unicorn    | ~15ms    | ~12s    | ~83            |
| Dynarmic   | ~2ms     | ~1.8s   | ~555           |
+--------------------------------------------------+
| 初始化时间: ~500ms  |  内存占用: ~80MB/实例     |
+--------------------------------------------------+

如果需要更高 QPS,可以封装为 HTTP 服务 (如 Spring Boot),每个线程持有独立的 Emulator 实例。


相关内容

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