unidbg 基础
最后更新: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/ 目
官方文档:https://github.com/zhkl0228/unidbg
适用场景:在 JVM 中模拟运行 Android Native(.so)加密函数,无需真实设备
最后更新:2026-03-31
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/系统调用 | 相对简单 |
| 适合 | 最终还原量产 | 分析阶段 |
准备工作
# 克隆 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 函数
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 模式
emulator = AndroidEmulatorBuilder
.for32Bit() // ARM32
.setProcessName("com.example.app")
.build();
4. 补环境(JNI 回调)
so 中调用 Java 层方法(JNI 回调)时,需要手动实现。
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. 内存操作与调试
读写内存
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);
打印调用栈
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);
在指定地址设置断点
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
// 先加载依赖,再加载目标
vm.loadLibrary(new File("libc++_shared.so"), false);
vm.loadLibrary(new File("libssl.so"), false);
DalvikModule dm = vm.loadLibrary(new File("libencrypt.so"), false);
补虚拟文件系统
// so 尝试读取 /proc/cpuinfo 等文件时
emulator.getSyscallHandler().setVerbose(true); // 开启 syscall 日志,找到哪个文件
// 挂载虚拟文件
emulator.getMemory().addHook(new MemoryReadHook() { ... });
开启详细日志排查问题
vm.setVerbose(true); // JNI 调用日志
emulator.getBackend().setVerbose(true); // 底层指令日志
8. 实用技巧
使用 Frida 配合定位函数偏移
# 在真机上用 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 服务
// 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 调用
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 中返回固定字符串。