> ## Content Index
> Fetch the complete content index at: https://blog.vercanti.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# unidbg 基础
- URL: https://blog.vercanti.com/unidbg-ji-chu/
- Published: 2026-08-28T14:35:10.000Z
- Updated: 2026-08-28T14:58:13.000Z
- Description: 最后更新：2026-03-31 相关文档：Frida基础(/frida-ji-chu-zhi-nan/) 签名算法还原(/qian-ming-suan-fa-huan-yuan/) 反反爬技术汇总(/fan-fan-pa-ji-zhu-hui-zong/) unidbg 是一个基于 Unicorn 引擎的 Android Native 模拟执行框架，可以在 JVM 上模拟运行 ARM/ARM64 的 .so 动态库，无需真机或模拟器，直接调用 native 函数。 so 中调用 Java 层方法（JNI 回调）时，需要手动实现。 APK 的 lib/ 目
- Author: yellowdog
- Tags: js逆向, 技巧步骤

> 官方文档：<https://github.com/zhkl0228/unidbg>  
> 适用场景：在 JVM 中模拟运行 Android Native（.so）加密函数，无需真实设备

最后更新：2026-03-31

相关文档：[Frida基础](https://blog.vercanti.com/frida-ji-chu-zhi-nan/) [签名算法还原](https://blog.vercanti.com/qian-ming-suan-fa-huan-yuan/) [反反爬技术汇总](https://blog.vercanti.com/fan-fan-pa-ji-zhu-hui-zong/)

---

## 1\. 基础概念

### unidbg 是什么

unidbg 是一个基于 Unicorn 引擎的 Android Native 模拟执行框架，可以在 JVM 上模拟运行 ARM/ARM64 的 `.so` 动态库，无需真机或模拟器，直接调用 native 函数。

| 特性         | 说明                             |
| ---------- | ------------------------------ |
| 基于 Unicorn | 底层 CPU 指令模拟，支持 ARM32/ARM64     |
| JVM 运行     | 用 Java/Kotlin 编写调用代码           |
| 无需设备       | 无需 Android 真机或模拟器              |
| 速度快        | 比 Frida Hook 分析更快，适合批量调用       |
| 适用场景       | 逆向 so 层加密算法、还原 sign/token 生成逻辑 |

### 与 Frida 的对比

| 维度   | unidbg         | Frida              |
| ---- | -------------- | ------------------ |
| 运行环境 | JVM（无需设备）      | Android/iOS 设备或模拟器 |
| 用途   | 批量调用 native 函数 | 动态插桩、Hook、内存读写     |
| 难度   | 需要补 JNI/系统调用   | 相对简单               |
| 适合   | 最终还原量产         | 分析阶段               |

### 准备工作

```bash
# 克隆 unidbg
git clone https://github.com/zhkl0228/unidbg.git
cd unidbg

# Maven 构建（需 JDK 11+）
mvn install -Dmaven.test.skip=true

# 推荐使用 IntelliJ IDEA 打开项目

```

---

## 2\. 项目结构

```
unidbg/
├── unidbg-android/     ← Android 环境模拟（JNI、Android API）
├── unidbg-ios/         ← iOS 环境模拟
├── unidbg-api/         ← 核心 API
├── unidbg-backend-unicorn/  ← Unicorn 引擎后端
└── samples/            ← 示例代码

```

---

## 3\. 基础使用（Java）

### 最简示例：调用 so 中的 native 函数

```java
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.VM;
import com.github.unidbg.memory.Memory;

import java.io.File;

public class SignDemo extends AbstractJni {

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

    public SignDemo() {
        // 创建 ARM64 模拟器
        emulator = AndroidEmulatorBuilder
            .for64Bit()                               // 64 位（ARM64）
            .setProcessName("com.example.app")        // 包名
            .addBackendFactory(new Unicorn2Factory())
            .build();

        Memory memory = emulator.getMemory();
        // 设置 Android SDK 版本
        memory.setLibraryResolver(new AndroidResolver(23));

        // 创建 Dalvik VM
        vm = emulator.createDalvikVM(new File("src/test/resources/app.apk"));
        vm.setJni(this);
        vm.setVerbose(false);

        // 加载 so 文件
        DalvikModule dm = vm.loadLibrary(new File("src/test/resources/libencrypt.so"), false);
        dm.callJNI_OnLoad(emulator);

        // 获取目标类
        targetClass = vm.resolveClass("com/example/app/util/SignUtils");
    }

    /**
     * 调用 native sign 方法
     */
    public String getSign(String data) {
        // 构造参数，调用方法
        Object result = targetClass.callStaticJniMethodObject(
            emulator,
            "sign(Ljava/lang/String;)Ljava/lang/String;",  // 方法签名
            data
        );
        return (String) result;
    }

    public static void main(String[] args) {
        SignDemo demo = new SignDemo();
        System.out.println(demo.getSign("hello world"));
        demo.emulator.getBackend().destroy();
    }
}

```

### 32 位 ARM 模式

```java
emulator = AndroidEmulatorBuilder
    .for32Bit()   // ARM32
    .setProcessName("com.example.app")
    .build();

```

---

## 4\. 补环境（JNI 回调）

so 中调用 Java 层方法（JNI 回调）时，需要手动实现。

```java
import com.github.unidbg.linux.android.dvm.*;

public class MyJni extends AbstractJni {

    // 实现 so 中调用的 Java 静态方法
    @Override
    public DvmObject<?> callStaticObjectMethod(BaseVM vm, DvmClass dvmClass,
                                               String signature, VarArg varArg) {
        switch (signature) {
            case "com/example/app/util/DeviceUtils->getDeviceId()Ljava/lang/String;":
                return new StringObject(vm, "fake-device-id-12345678");

            case "com/example/app/util/TimeUtils->getTimestamp()J":
                return new LongObject(vm, System.currentTimeMillis() / 1000);

            default:
                return super.callStaticObjectMethod(vm, dvmClass, signature, varArg);
        }
    }

    // 实现 so 中调用的 Java 实例方法
    @Override
    public DvmObject<?> callObjectMethod(BaseVM vm, DvmObject<?> dvmObject,
                                         String signature, VarArg varArg) {
        if ("java/lang/String->getBytes(Ljava/lang/String;)[B".equals(signature)) {
            String str = (String) dvmObject.getValue();
            String charset = varArg.getObjectArg(0).toString();
            try {
                return new ByteArray(vm, str.getBytes(charset));
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
        return super.callObjectMethod(vm, dvmObject, signature, varArg);
    }

    // 获取静态字段
    @Override
    public DvmObject<?> getStaticObjectField(BaseVM vm, DvmClass dvmClass, String signature) {
        if ("com/example/app/BuildConfig->VERSION_NAME:Ljava/lang/String;".equals(signature)) {
            return new StringObject(vm, "3.2.1");
        }
        return super.getStaticObjectField(vm, dvmClass, signature);
    }
}

```

---

## 5\. 内存操作与调试

### 读写内存

```java
import com.github.unidbg.pointer.UnidbgPointer;

// 在内存中分配并写入数据
UnidbgPointer ptr = emulator.getMemory().malloc(64).getPointer();
ptr.write(0, "hello".getBytes(), 0, 5);

// 读取内存
byte[] data = ptr.getByteArray(0, 64);

```

### 打印调用栈

```java
emulator.getBackend().hook_add_new(new EventMemHook() {
    @Override
    public boolean hook(Backend backend, long address, int size, long value, Object user) {
        System.out.println("mem write: 0x" + Long.toHexString(address));
        return true;
    }
}, HookType.UC_HOOK_MEM_WRITE, null);

```

### 在指定地址设置断点

```java
import com.github.unidbg.debugger.Debugger;

// 启动调试模式
Debugger debugger = emulator.attach();

// 在相对地址 0x1234 设置断点（需加基址）
long moduleBase = dm.base;
debugger.addBreakPoint(moduleBase + 0x1234);

// 运行时自动暂停并打印寄存器

```

---

## 6\. 工作流程

### 逆向 so 签名的完整流程

```
1. 获取目标 APK
   adb pull /data/app/com.example.app/base.apk .

2. 解压 APK，获取 so 文件
   unzip base.apk lib/arm64-v8a/libencrypt.so

3. 用 IDA/Ghidra 分析 so
   - 找到 JNI_OnLoad 函数（初始化）
   - 搜索目标函数名（如 Java_com_example_sign）
   - 分析函数参数和返回值类型

4. 编写 unidbg 调用代码
   - 创建模拟器，加载 so
   - 根据 IDA 分析结果补 JNI 回调
   - 调用目标函数，验证输出

5. 解决环境依赖
   - so 调用其他 so（加载依赖库）
   - 读取 /proc/cpuinfo 等系统文件（补虚拟文件）
   - 访问网络/文件系统（补系统调用）

6. 封装成服务
   - 用 Spring Boot 包装成 HTTP 接口
   - Python 调用接口获取 sign

```

---

## 7\. 常见问题处理

### so 依赖其他 so

```java
// 先加载依赖，再加载目标
vm.loadLibrary(new File("libc++_shared.so"), false);
vm.loadLibrary(new File("libssl.so"), false);
DalvikModule dm = vm.loadLibrary(new File("libencrypt.so"), false);

```

### 补虚拟文件系统

```java
// so 尝试读取 /proc/cpuinfo 等文件时
emulator.getSyscallHandler().setVerbose(true);  // 开启 syscall 日志，找到哪个文件

// 挂载虚拟文件
emulator.getMemory().addHook(new MemoryReadHook() { ... });

```

### 开启详细日志排查问题

```java
vm.setVerbose(true);   // JNI 调用日志
emulator.getBackend().setVerbose(true);  // 底层指令日志

```

---

## 8\. 实用技巧

### 使用 Frida 配合定位函数偏移

```bash
# 在真机上用 Frida 找到目标函数基址
frida -U -n com.example.app -e "
  var base = Module.findBaseAddress('libencrypt.so');
  var sign = Module.findExportByName('libencrypt.so', 'Java_com_example_SignUtils_sign');
  console.log('base:', base, 'sign offset:', sign.sub(base));
"

```

### 封装成 Python 可调用的 HTTP 服务

```java
// SignServer.java (Spring Boot)
@RestController
public class SignController {
    private static final SignDemo signDemo = new SignDemo();

    @GetMapping("/sign")
    public Map<String, String> sign(@RequestParam String data) {
        return Map.of("sign", signDemo.getSign(data));
    }
}

```

```python
# Python 调用
import httpx

def get_sign(data: str) -> str:
    resp = httpx.get("http://localhost:8888/sign", params={"data": data})
    return resp.json()["sign"]

```

---

## 9\. 踩坑与注意事项

### ARM32 vs ARM64

APK 的 `lib/` 目录下可能有 `armeabi-v7a`（32位）和 `arm64-v8a`（64位）两个版本。大多数现代 App 优先用 64 位，选错架构会导致加载失败。

### JNI 函数名转义

JNI 函数名中，包名的 `.` 转为 `_`，`_` 转为 `_1`，`$` 转为 `_00024`：

```
com.example.app.util.SignUtils.sign
→ Java_com_example_app_util_SignUtils_sign

```

### 多线程问题

unidbg 模拟器不是线程安全的，多线程调用需为每个线程创建独立的模拟器实例。

### 反调试检测

部分 so 有反 unidbg 检测（检测 Unicorn 特征）。解决方案：

- patch 掉检测代码（需 IDA 分析）
- 用 Hook 方式跳过检测函数

---

## 最佳实践

**先用 Frida 在真机上确认目标函数**：在真机上用 Frida Hook 目标 so 的函数，确认函数地址、参数和返回值格式后，再用 unidbg 复现，避免在 unidbg 中盲目调试未确认的函数。

**`AbstractJni` 只实现被调用的 JNI 方法**：unidbg 执行中会调用各种 JNI 方法，不需要全部实现，只需要实现 so 实际调用的那几个（通过 Logcat 或 Frida 确认）；其余方法保持默认行为（返回 null 或 0）。

**用 `Debugger` 接口在关键指令处设断点**：unidbg 支持地址断点，在 IDA 分析确认的关键指令地址设断点，查看寄存器状态（`registerContext.getLongArg(0)` 读 x0），类似 GDB 调试。

**模拟器实例保持长期复用**：unidbg 初始化（加载 so、解析符号）耗时较长，在 HTTP 服务中复用同一个模拟器实例处理多个请求（注意线程安全）；不要每次请求都重新初始化。

**用 `MallocHandler` 追踪内存分配**：复杂加密函数可能动态分配内存存储中间结果，注册自定义 `MallocHandler` 打印分配地址和大小，帮助追踪关键数据的内存位置。

---

## 常见陷阱

### 陷阱：so 依赖其他 so 导致加载失败

**现象：** `vm.loadLibrary("libtarget.so")` 报 `cannot load library` 或符号找不到。  
**原因：** 目标 so 依赖其他系统库（如 `libssl.so`、`libc.so++`），需要先加载依赖库或用 `AndroidResolver` 提供缺失符号。  
**解决：** 检查 so 的 ELF 头中的 `NEEDED` 字段（用 `readelf -d` 或 IDA），按顺序加载所有依赖 so；使用 unidbg 内置的 `LibraryFile` 提供系统库。

### 陷阱：JNI 函数返回 null 导致 NPE 崩溃

**现象：** unidbg 运行到某一步崩溃，错误信息含 `NullPointerException`。  
**原因：** so 调用了 JNI 方法（如 `GetStringUTFChars`、`CallObjectMethod`）获取 Java 对象，unidbg 中的 `AbstractJni` 默认返回 null，so 解引用 null 崩溃。  
**解决：** 在 `AbstractJni` 实现中重写对应方法，返回合理的 mock 对象（如 `vm.addLocalObject(new StringObject(vm, "mock_value"))`）。

### 陷阱：同一参数每次调用结果不同

**现象：** 相同输入多次调用目标函数，输出不同。  
**原因：** 函数依赖时间戳（`gettimeofday`）、随机数（`/dev/urandom`）或设备 ID（从 JNI 获取），unidbg 中这些值每次不同。  
**解决：** Hook `gettimeofday` 和 `open("/dev/urandom")` 系统调用，返回固定值；设备 ID 在 `AbstractJni` 中返回固定字符串。

---

## 参见

[Frida基础](https://blog.vercanti.com/frida-ji-chu-zhi-nan/)  
[js逆向调试技巧](https://blog.vercanti.com/js-ni-xiang-diao-shi-ji-qiao/)  
[魔法数字速查](https://blog.vercanti.com/jia-mi-suan-fa-mo-fa-shu-zi-su-cha/)