unidbg 的定位:SO 的"虚拟机"
unidbg 用 Unicorn 引擎模拟 ARM 指令执行,让 Android 的 SO 文件脱离手机在 JVM 里跑起来。它的价值场景很明确:签名算法在 SO 里,你想批量调用但不想养一屋子手机。
但 SO 不是独立运行的——它会调 JNI 函数(拿 Context、读系统属性、访问文件),这些调用在 unidbg 里全要"补环境":用 Java 代码模拟 Android 系统的行为,让 SO 以为自己真在手机上。
最小可运行骨架
public class SignEmulator {
private final AndroidEmulator emulator;
private final VM vm;
private final DvmClass signUtils;
public SignEmulator() {
// 1. 创建模拟器(ARM64)
emulator = AndroidEmulatorBuilder.for64Bit()
.setProcessName("com.target.app")
.build();
// 2. 创建虚拟内存和 VM
Memory memory = emulator.getMemory();
memory.setLibraryResolver(new AndroidResolver(23)); // SDK 23
vm = emulator.createDalvikVM(new File("target.apk")); // 加载 APK(拿资源/类信息)
vm.setVerbose(false);
// 3. 加载目标 SO
DalvikModule dm = vm.loadLibrary("target", true); // libtarget.so
dm.callJNI_OnLoad(emulator); // 触发 JNI_OnLoad(动态注册在这里发生)
// 4. 拿到 DvmClass,准备调用
signUtils = vm.resolveClass("com/target/app/SignUtils");
}
public String getSign(String input) {
// 调用静态 native 方法
return signUtils.callStaticJniMethodObject(emulator,
"getSign(Ljava/lang/String;)Ljava/lang/String;",
new StringObject(vm, input));
}
}补环境:报错驱动开发
跑起来必然报错——SO 每调一个未模拟的 JNI 函数,unidbg 就抛异常。补环境的标准节奏是报错一个补一个:
// 典型报错:java.lang.UnsupportedOperationException: xxx
// 解决:继承 AbstractJni 或直接用 vm.addLocalObject 模拟
// 例1:SO 调 getPackageManager 拿签名信息
vm.resolveClass("android/content/pm/PackageManager")
.newObject(null); // 返回假对象
// 例2:SO 读系统属性 ro.product.model
// 在 __system_property_get 的 hook 里返回 "Mi 13"
// 例3:SO 读文件 /proc/version
// 用 emulator.getFileSystem() 映射一个假文件unidbg 的 AbstractJni 类已经实现了大部分常见 JNI 函数的默认行为,继承它然后只重写关键的(比如设备信息相关)是高效做法。
精髓一:哪些环境必须补真的
补环境不是全补假的就行——参与签名的环境值必须和真机一致:
- SO 如果把
ro.product.model拼进签名输入,你补个假型号,签名结果就和真机不同,服务端验不过 - 判断方法:对照实验。真机 Frida hook 同一个 native 函数,输入相同参数,比对 unidbg 的输出。不一致就说明某个环境值参与了计算且你补错了
排错技巧:把 SO 读取的所有环境值(属性、文件、系统服务返回)在真机上用 Frida 全部记录一遍,照着清单逐个对齐 unidbg 的模拟值。
精髓二:性能与并发
unidbg 是指令级模拟,性能比真机低 1-2 个数量级:
- 单次签名调用:真机 1ms,unidbg 可能 50-200ms
- VM 实例不能多线程共享:DvmObject 有线程亲和性,并发调用要搞 VM 池(每个线程一个 VM 实例)
- 批量场景:起 N 个 VM 实例做池化,用阻塞队列分发任务
// VM 池骨架
BlockingQueue<SignEmulator> pool = new LinkedBlockingQueue<>();
for (int i = 0; i < 8; i++) pool.put(new SignEmulator());
// 用的时候 take,用完 put 回去精髓三:什么时候别用 unidbg
unidbg 不是银弹,这些场景劝退:
- SO 重度依赖系统服务(比如 SecurityGuard 大量调 binder 服务):补环境工作量以周计,不如 Frida RPC
- SO 有反模拟检测(读 unicorn 特征、检测指令执行时序):对抗成本极高
- 只需要低频调用:Frida RPC 一台闲置手机就够,别折腾
决策树:调用频率高 + SO 相对独立(只算签名不碰系统服务)→ unidbg;其他情况 → Frida RPC。
总结
- unidbg = 用 JVM 模拟手机环境跑 SO,目标是脱离设备批量调用
- 补环境是报错驱动:跑起来、看异常、补对应 JNI 函数
- 参与签名的环境值必须与真机对齐,对照实验验证
- VM 池化解决并发,但先评估值不值——Frida RPC 经常是更优解
交流微信:run1255