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

相关文档:Frida基础 签名算法还原 反反爬技术汇总


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.solibc.so++),需要先加载依赖库或用 AndroidResolver 提供缺失符号。
解决: 检查 so 的 ELF 头中的 NEEDED 字段(用 readelf -d 或 IDA),按顺序加载所有依赖 so;使用 unidbg 内置的 LibraryFile 提供系统库。

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

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

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

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


参见

Frida基础
js逆向调试技巧
魔法数字速查

阅读更多

Web 安全基础

1. HTML 转义(服务端渲染必须): 2. CSP(Content Security Policy): 3. HttpOnly Cookie:防止 JS 读取会话 Cookie: 4. 前端框架防护: 攻击者在第三方网站构造一个表单,诱导已登录用户提交,浏览器会自动携带目标站的 Cookie。 触发条件: 1. 用户已登录目标网站(Cookie 有效) 2. 目标 API 仅凭 Cookie 识别用户身份 3. 请求来源未验证 1. CSRF Token(推荐): 2. SameSite Cookie: 3. 验证 Origin/Referer 头:

By yellowdog

HTTP 协议深度指南

HTTP(HyperText Transfer Protocol)是 Web 的基础传输协议,基于 TCP/IP,采用请求/响应模型。 相关文档:Web安全基础(/web-an-quan-ji-chu/) FastAPI完全指南(/fastapi-wan-quan-zhi-nan/) Nginx完全指南(/nginx-wan-quan-zhi-nan/) 幂等性:多次执行相同请求,服务器状态结果相同。PUT /users/1 多次执行结果一致;POST /users 每次创建新资源,非幂等。 浏览器直接从本地缓存读取,不向服务器发送请求。 缓存命中时,状

By yellowdog

系统设计基础

SLA 对照表: 选择建议:无状态服务(Web 层、API 层)优先水平扩展;数据库初期垂直扩展,达到瓶颈后考虑分库分表或读写分离。 缓存穿透(查询不存在的 key,每次都打到 DB): 缓存击穿(热点 key 过期,瞬间大量请求打到 DB): 缓存雪崩(大量 key 同时过期,或缓存服务宕机): 令牌桶 Python 实现: Redis 实现分布式限流(滑动窗口): URL 命名规则: Cursor 分页响应格式: 雪花算法结构(64 bit): 定义:分布式系统不能同时满足以下三个特性: 在分布式环境中 P 是必须保证的,所以实际是 CP vs AP

By yellowdog

算法思路与模板

二分查找要求序列有序,每次将搜索范围缩减一半,时间复杂度 O(log n)。 两个指针从两端向中间收缩,常用于有序数组。 滑动窗口维护一个满足条件的区间 left, right,right 不断向右扩张,条件不满足时收缩 left。 滑动窗口通用框架: 1. 确定"子问题":原问题可以分解为哪些规模更小的同类问题 2. 定义 dpi 或 dpij 的含义,要足够清晰 3. 推导状态转移方程 4. 确定初始状态(边界条件) 5. 确定计算顺序(确保依赖的子问题先计算) 每件物品最多选一次。dpj = 容量为 j 时的最大价值,逆序遍历容量防止重复选取。 每

By yellowdog