百趣云 百趣云的博客

unidbg 补环境实战:让 SO 在 JVM 里以为自己真在手机上

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 不是银弹,这些场景劝退:

  1. SO 重度依赖系统服务(比如 SecurityGuard 大量调 binder 服务):补环境工作量以周计,不如 Frida RPC
  2. SO 有反模拟检测(读 unicorn 特征、检测指令执行时序):对抗成本极高
  3. 只需要低频调用:Frida RPC 一台闲置手机就够,别折腾

决策树:调用频率高 + SO 相对独立(只算签名不碰系统服务)→ unidbg;其他情况 → Frida RPC。

总结

  1. unidbg = 用 JVM 模拟手机环境跑 SO,目标是脱离设备批量调用
  2. 补环境是报错驱动:跑起来、看异常、补对应 JNI 函数
  3. 参与签名的环境值必须与真机对齐,对照实验验证
  4. VM 池化解决并发,但先评估值不值——Frida RPC 经常是更优解

交流微信:run1255

By 百趣云 阅读量:4 On