Unidbg 实现原理剖析

Unidbg 实现原理剖析

Unidbg 是一个强大的 Android 原生库 (.so) 模拟执行框架。理解其内部工作原理——CPU 模拟、内存管理、ELF 加载、JNI 桥接和系统调用仿真——可以帮助我们更高效地解决复杂的加密算法逆向和协议分析问题。

架构概览

核心思想

Unidbg 的核心思想是:用纯 Java 在 PC 上构建一个虚拟的 Android 用户态 (User-Mode) 环境。它不是完整的操作系统模拟器,而是专注于模拟一个 Android 进程 所需的一切,让 .so 文件"感觉"自己正运行在真实的 Android 设备里。

整体架构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
+================================================================+
|                    用户代码 (Java)                               |
|    MyEmulator extends AbstractJni                               |
+================================================================+
          |                    ^                    |
          | 调用函数           | JNI 回调           | IO 请求
          v                    |                    v
+================================================================+
|                    Unidbg 核心层                                |
|  +------------------+  +-----------------+  +-----------------+ |
|  | DalvikVM / ART   |  | SyscallHandler  |  | IOResolver      | |
|  | (JNI 模拟层)     |  | (系统调用处理)  |  | (文件系统模拟)  | |
|  +------------------+  +-----------------+  +-----------------+ |
|  +------------------+  +-----------------+  +-----------------+ |
|  | ELF Loader       |  | Memory Manager  |  | Hook Engine     | |
|  | (动态链接器)     |  | (内存管理)      |  | (HookZz/xHook)  | |
|  +------------------+  +-----------------+  +-----------------+ |
+================================================================+
          |                    ^
          | 指令执行           | 中断/回调
          v                    |
+================================================================+
|                    CPU 后端 (Backend)                            |
|  +------------------+  +-----------------+  +-----------------+ |
|  | Unicorn Engine   |  | Dynarmic        |  | KVM             | |
|  | (解释执行)       |  | (JIT 编译)      |  | (硬件虚拟化)    | |
|  +------------------+  +-----------------+  +-----------------+ |
+================================================================+

CPU 模拟后端

Unidbg 采用后端抽象层设计,将 CPU 模拟能力与上层逻辑解耦。

Unicorn Engine

默认后端,基于 QEMU 的 TCG (Tiny Code Generator) 模块:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
ARM 二进制指令
     |
     v
+-------------------+
| Unicorn Frontend  |  解码 ARM/ARM64 指令
+-------------------+
     |
     v
+-------------------+
| TCG (中间表示)    |  转换为平台无关的中间码 (IR)
+-------------------+
     |
     v
+-------------------+
| Host Code Gen     |  转换为宿主机 (x86/ARM64) 本地指令
+-------------------+
     |
     v
     执行
特性说明
多架构支持ARM, ARM64, MIPS, x86, x86_64, SPARC, M68K
执行粒度逐基本块 (Basic Block) 翻译执行
回调机制支持代码执行、内存访问、中断等事件回调
线程安全单个 uc_engine 实例不是线程安全的

Dynarmic

高性能 ARM JIT 编译器,原为 Nintendo 3DS/Switch 模拟器开发。直接生成宿主机原生代码,无解释开销。

操作UnicornDynarmic加速比
AES-128 加密 1KB~0.8ms~0.1ms~8x
典型 sign() 调用~15ms~2ms~7.5x
初始化加载~400ms~500ms0.8x

Backend 接口设计

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public interface Backend {
    Number reg_read(int regId);
    void reg_write(int regId, Number value);
    void mem_map(long address, long size, int perms);
    void mem_write(long address, byte[] bytes);
    byte[] mem_read(long address, long size);
    void emu_start(long begin, long until, long timeout, long count);
    void emu_stop();
    void hook_add_new(CodeHook callback, long begin, long end, Object user);
    void hook_add_new(InterruptHook callback, Object user);
}

切换后端只需修改一行代码:

1
2
3
4
AndroidEmulator emulator = AndroidEmulatorBuilder.for32Bit()
        .addBackendFactory(new DynarmicFactory(true))   // Dynarmic
        // .addBackendFactory(new Unicorn2Factory(true)) // Unicorn
        .build();

内存模型

虚拟地址空间布局 (32 位)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
0xFFFFFFFF  +---------------------------+
            |   Kernel Space (不可用)   |
0xC0000000  +---------------------------+
            |       Stack               |  <-- 向下增长 (默认 128KB)
            |           |               |
            |           v               |
0xBFF00000  +---------------------------+
            |       (未映射区域)        |
            +---------------------------+
            |       mmap 区域           |  <-- 动态库加载区
            |   (libc.so, libm.so等)    |
0x40000000  +---------------------------+
            |       目标 SO 加载区域    |  <-- libnative.so
            +---------------------------+
            |       Heap                |  <-- 向上增长 (malloc)
0x08000000  +---------------------------+
            |       低地址保留区域      |  <-- JNI 函数表、特殊映射
0x00000000  +---------------------------+

mmap 实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 简化的 mmap 流程
public long mmap2(long addr, int length, int prot, int flags, int fd, int offset) {
    // 1. 匿名映射:从 mmap 区域分配空闲地址
    if (addr == 0) addr = allocateMapAddress(length);

    // 2. 对齐到页边界 (4KB)
    long alignedAddr = addr & ~0xFFF;
    int alignedLength = (int)(((addr + length + 0xFFF) & ~0xFFF) - alignedAddr);

    // 3. 在 Backend 引擎中创建映射
    backend.mem_map(alignedAddr, alignedLength, prot);

    // 4. 文件映射:读取文件内容到映射区域
    if (fd > 0 && (flags & MAP_ANONYMOUS) == 0) {
        byte[] data = readFromFileDescriptor(fd, offset, length);
        backend.mem_write(addr, data);
    }

    // 5. 记录映射信息
    memoryMap.put(alignedAddr, new MemoryBlock(alignedAddr, alignedLength, prot));
    return addr;
}

栈与调用约定

ARM32 调用约定中,前 4 个参数通过 r0-r3 传递,多余参数通过栈传递。Unidbg 在调用函数时需要正确设置这些:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 函数调用的简化流程
public Number[] callFunction(long address, Object... args) {
    // 1. 保存 CPU 状态
    long savedSP = backend.reg_read(UC_ARM_REG_SP).longValue();

    // 2. 设置参数
    for (int i = 0; i < args.length; i++) {
        long value = resolveArg(args[i]);
        if (i < 4) {
            backend.reg_write(UC_ARM_REG_R0 + i, value);  // r0-r3
        } else {
            long sp = backend.reg_read(UC_ARM_REG_SP).longValue() - 4;
            backend.mem_write(sp, toBytes(value));          // 压栈
            backend.reg_write(UC_ARM_REG_SP, sp);
        }
    }

    // 3. 设置 LR 为哨兵地址,开始执行
    backend.reg_write(UC_ARM_REG_LR, LR_SENTINEL);
    backend.emu_start(address, LR_SENTINEL, 0, 0);

    // 4. 从 r0 读取返回值,恢复栈
    Number result = backend.reg_read(UC_ARM_REG_R0);
    backend.reg_write(UC_ARM_REG_SP, savedSP);
    return new Number[] { result };
}

堆管理

Unidbg 内置简单的堆分配器处理 malloc / free

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
+---------------------------+  堆起始地址
| Block Header (8B)         |
|   size=64, used=true      |
+---------------------------+
| 用户数据 (64B)            |  <-- malloc(64) 返回
+---------------------------+
| Block Header (8B)         |
|   size=128, used=false    |
+---------------------------+
| 空闲区域 (128B)           |  <-- 已 free
+---------------------------+
| Block Header (8B)         |
|   size=256, used=true     |
+---------------------------+
| 用户数据 (256B)           |  <-- malloc(256) 返回
+---------------------------+

ELF 加载器

ELF 文件结构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
+---------------------------+
| ELF Header                |  魔数、架构、入口点
+---------------------------+
| Program Headers           |
|   PT_LOAD (代码段)        |  .text, .rodata
|   PT_LOAD (数据段)        |  .data, .bss
|   PT_DYNAMIC              |  动态链接信息
+---------------------------+
| Section Headers           |
|   .dynsym                 |  动态符号表
|   .dynstr                 |  动态字符串表
|   .rel.plt / .rela.plt    |  PLT 重定位条目
|   .rel.dyn / .rela.dyn    |  数据重定位条目
|   .init_array             |  构造函数指针数组
+---------------------------+

加载流程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
vm.loadLibrary("libnative.so")
         |
         v
  1. 解析 ELF 头,验证架构匹配 (ARM/ARM64)
         |
         v
  2. 遍历 PT_LOAD 段,计算需要的虚拟地址范围
         |
         v
  3. 在 mmap 区域预留连续空间 (基址通常从 0x40000000 起)
         |
         v
  4. 将 PT_LOAD 段内容写入虚拟内存,设置权限 (R/W/X)
         |
         v
  5. 解析 PT_DYNAMIC 段,提取 DT_NEEDED 依赖库列表
         |
         v
  6. 递归加载依赖库 (Unidbg 自带常用系统库)
         |
         v
  7. 符号解析与重定位 (填充 GOT 表)
         |
         v
  8. 执行 .init 段和 .init_array 中的构造函数
         |
         v
  9. 返回 Module 对象 (包含基地址、符号表等)

PLT/GOT 重定位

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
调用外部函数 (如 strlen) 的过程:

.text :
  BL strlen@PLT          ; 跳转到 PLT 桩代码

.plt :
  strlen@PLT:
    LDR PC, [PC, #offset] ; 从 GOT 表读取地址并跳转

.got :
  &strlen = 0x????????    ; 重定位前: 指向延迟绑定桩
         = 0x400023A0    ; 重定位后: 指向 strlen 真实地址

Unidbg 的重定位处理核心逻辑:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
private void processRelocation(ElfRelocation rel, Module module) {
    int type = rel.type();
    ElfSymbol symbol = rel.symbol();
    long targetAddr = module.base + rel.offset();

    switch (type) {
        case R_ARM_JUMP_SLOT:    // PLT 重定位
        case R_ARM_GLOB_DAT:     // 全局数据重定位
            long symbolAddr = resolveSymbol(symbol.getName());
            if (symbolAddr != 0) {
                backend.mem_write(targetAddr, toBytes((int) symbolAddr));
            }
            break;

        case R_ARM_RELATIVE:     // 相对重定位:加上基地址
            int origValue = readInt(targetAddr);
            backend.mem_write(targetAddr, toBytes(origValue + (int) module.base));
            break;
    }
}

自带的系统库

库名用途实现方式
libc.soC 标准库部分函数用 Java 实现
libdl.so动态链接Java 实现 dlopen/dlsym
libm.so数学库直接使用 Java Math
libstdc++.soC++ 标准库部分实现
liblog.soAndroid 日志输出到 Java 控制台
libz.sozlib 压缩使用 Java 自带的 zlib

对于未内置的库 (如 libcrypto.so),需要从真机 /system/lib/ 提取并手动加载。


JNI 桥接层

JNI 函数表机制

在真实 Android 中,JNIEnv* 指向一个函数指针表。Unidbg 需要在模拟环境中实现等效机制:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
真实 Android:
  JNIEnv* --> | GetVersion       | --> 原生函数指针
              | FindClass        | --> 原生函数指针
              | GetMethodID      | --> 原生函数指针
              | CallObjectMethod | --> 原生函数指针
              | ...              |

Unidbg 模拟:
  JNIEnv* --> | GetVersion       | --> 跳板代码地址 (触发中断)
              | FindClass        | --> 跳板代码地址 (触发中断)
              | GetMethodID      | --> 跳板代码地址 (触发中断)
              | CallObjectMethod | --> 跳板代码地址 (触发中断)
              | ...              |

跳板机制 (Trampoline)

Unidbg 使用跳板代码将 SO 对 JNI 函数的调用重定向到 Java 实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
SO 代码: (*env)->FindClass(env, "java/lang/String")
     |
     v
1. 从 JNIEnv 函数表取出 FindClass 的地址
     |
     v
2. 跳转到跳板代码,执行 SVC #0 (软中断)
     |
     v  (中断被 Unidbg 拦截)
3. InterruptHook 识别出是 FindClass 调用
     |
     v
4. 从寄存器读取参数:
   r0=env, r1=className("java/lang/String")
     |
     v
5. 执行 Java 实现: DalvikVM.findClass()
   返回 DvmClass 对象
     |
     v
6. 将结果的 hash 值写入 r0 (作为 jclass)
     |
     v
7. 恢复模拟器继续执行后续指令

DvmObject 体系

1
2
3
4
5
6
7
8
9
DvmObject<T>                  (所有 Java 对象的基类)
    |
    +-- StringObject          (java.lang.String)
    +-- ByteArray             (byte[])
    +-- IntArray              (int[])
    +-- ArrayObject           (Object[])
    +-- DvmInteger            (java.lang.Integer)
    +-- DvmLong               (java.lang.Long)
    +-- DvmBoolean            (java.lang.Boolean)

每个 DvmObject 有唯一的 hash 值,在 SO 代码中作为 jobject 指针:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public abstract class DvmObject<T> {
    private final int hashCode;        // 作为 jobject 传给 SO
    private final DvmClass objectType; // 对象的类型信息
    private T value;                   // 实际的 Java 值
}

// VM 内部维护对象映射表
// Map<Integer, DvmObject<?>> objectMap
//   hash=0x1001 -> StringObject("hello")
//   hash=0x1002 -> DvmClass(android/content/Context)

CallObjectMethod 完整生命周期

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
SO: jstring result = (*env)->CallObjectMethod(env, obj, methodId, arg1);
     |
     v
1. 从函数表取地址,跳转到跳板,触发 SVC 中断
     |
     v
2. InterruptHook 捕获,识别为 CallObjectMethod
     |
     v
3. 从寄存器读取参数:
   r1=obj -> 通过 hash 查表得到 DvmObject
   r2=methodId -> 查表得到方法签名
     |
     v
4. 构造签名: "android/content/Context->getPackageName()Ljava/lang/String;"
     |
     v
5. 调用 AbstractJni.callObjectMethod(),用户代码匹配并返回结果
     |
     v
6. 返回值注册到对象表,hash 写入 r0,继续执行

RegisterNatives 动态注册

许多加固后的 SO 在 JNI_OnLoad 中使用 RegisterNatives 动态注册:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
SO 中的 JNI_OnLoad:
  env->RegisterNatives(clazz, methods, count)
       |
       v
  Unidbg 拦截,读取 JNINativeMethod 数组:
    name="sign",      sig="(Ljava/lang/String;)Ljava/lang/String;",  fn=0x40001A3C
    name="verify",    sig="(Ljava/lang/String;Z)Z",                  fn=0x400023F0
       |
       v
  保存到 DvmClass 的 nativeMethods 映射:
    签名 -> 函数地址
       |
       v
  后续调用 callJniMethodObject 时,
  根据签名查找函数地址,设置参数并跳转执行

系统调用仿真

ARM 系统调用机制

ARM 架构通过 SVC #0 指令发起系统调用,调用号在 r7 中,参数在 r0-r5 中:

1
2
3
4
5
MOV  r7, #5          ; syscall number: open = 5
MOV  r0, pathname    ; arg1: 文件路径
MOV  r1, #0          ; arg2: O_RDONLY
SVC  #0              ; 触发系统调用
; 返回值在 r0 

处理流程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
Unicorn 执行 SVC #0
     |
     v
InterruptHook 回调
     |
     v
读取 r7 (syscall number) + r0-r5 (参数)
     |
     v
SyscallHandler.handle()
     |
     +-- 已实现 --> 执行 Java 实现,写返回值到 r0
     +-- 未实现 --> 抛异常或返回 -ENOSYS

已实现的关键系统调用

编号名称用途实现方式
3read读取文件Java FileInputStream
4write写入文件Java FileOutputStream
5open打开文件IOResolver + 虚拟文件系统
6close关闭文件关闭 Java 流
26ptrace调试相关返回 -EPERM (反调试免疫)
45brk扩展堆调整堆结束地址
125mprotect修改内存权限更新 Backend 内存权限
192mmap2内存映射分配虚拟地址+Backend映射
197fstat64文件状态Java File 属性
240futex互斥锁简单实现 (单线程)
263clock_gettime获取时钟Java System.nanoTime
322openat打开文件(相对路径)与 open 类似

Android Property 系统

__system_property_get 不是系统调用,而是通过共享内存实现。Unidbg 通过 Hook libc.so 中的该函数来处理:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public int __system_property_get(Emulator<?> emulator) {
    Pointer namePtr = UnidbgPointer.register(emulator, UC_ARM_REG_R0);
    Pointer valuePtr = UnidbgPointer.register(emulator, UC_ARM_REG_R1);
    String name = namePtr.getString(0);

    String value;
    switch (name) {
        case "ro.build.version.sdk":  value = "23";       break;
        case "ro.product.model":      value = "Pixel 2";  break;
        case "ro.debuggable":         value = "0";         break;
        default:                      value = "";          break;
    }
    valuePtr.setString(0, value);
    return value.length();
}

文件描述符管理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
虚拟文件描述符表:
+------+---------------+-----------------------------------+
|  FD  | 类型          | 实际映射                          |
+------+---------------+-----------------------------------+
|  0   | STDIN         | Java System.in                    |
|  1   | STDOUT        | Java System.out                   |
|  2   | STDERR        | Java System.err                   |
|  3   | Regular File  | /proc/self/maps -> 虚拟内容       |
|  4   | Regular File  | /proc/self/status -> 虚拟内容     |
|  5   | Regular File  | /data/app/xxx/base.apk -> 真实文件|
+------+---------------+-----------------------------------+

与 Unicorn/Qiling 对比

三者定位

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
  高层  +-- Unidbg ---+   专注 Android SO 模拟
        |             |   内置 JNI/DVM/Android 文件系统
        +-------------+

  中层  +-- Qiling ---+   通用 OS 模拟框架
        |             |   支持 Linux/Windows/macOS
        +-------------+

  底层  +-- Unicorn --+   纯 CPU 模拟引擎
        |             |   仅提供指令执行和内存管理
        +-------------+

详细对比

维度UnidbgUnicornQiling
开发语言JavaC (多语言绑定)Python
定位Android SO 模拟通用 CPU 模拟引擎OS 级模拟框架
JNI 支持内置完整实现
DVM/ART内置
Android 系统库自带精简版部分支持
Linux syscall针对 Android 优化通用 Linux 实现
Windows 支持不支持仅 CPU 层面完整支持
ELF/PE 加载ELF 内置需自行实现两者内置
Hook 框架HookZz, xHook需手动实现内置
多线程有限支持不支持有限支持
性能较高 (Dynarmic)中等较低 (Python 开销)

代码风格对比

同样的功能——加载 SO 并调用函数:

Unicorn (Python) — 需要手动处理一切:

1
2
3
4
5
6
7
8
mu = Uc(UC_ARCH_ARM, UC_MODE_THUMB)
mu.mem_map(0x10000, 0x10000)
mu.mem_write(0x10000, so_code_bytes)
mu.reg_write(UC_ARM_REG_SP, 0x20000)
mu.reg_write(UC_ARM_REG_R0, arg0)
mu.hook_add(UC_HOOK_INTR, hook_intr)
mu.emu_start(0x10000 + offset, 0x10000 + end)
# 需要自己实现 ELF 解析、重定位、JNI、syscall...

Qiling (Python) — 有 OS 抽象,但没有 JNI:

1
2
3
4
ql = Qiling(["rootfs/lib/libnative.so"], rootfs="rootfs",
            ostype="linux", archtype="arm")
ql.run()
# 没有 JNI/DVM 支持,Android SO 需要大量额外工作

Unidbg (Java) — Android 开箱即用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
AndroidEmulator emulator = AndroidEmulatorBuilder.for32Bit().build();
emulator.getMemory().setLibraryResolver(new AndroidResolver(23));
VM vm = emulator.createDalvikVM(new File("app.apk"));
vm.setJni(this);
DalvikModule dm = vm.loadLibrary("native", true);
dm.callJNI_OnLoad(emulator);
// JNI 调用一步到位
DvmObject<?> result = dvmClass.callStaticJniMethodObject(emulator,
    "sign(Ljava/lang/String;)Ljava/lang/String;",
    new StringObject(vm, input));

选择建议

目标场景推荐工具
Android SO 加密算法分析Unidbg
Windows PE 算法分析Qiling
构建自定义模拟/分析工具Unicorn
IoT 嵌入式固件模拟Qiling
高性能批量调用Unidbg + Dynarmic

优势与局限

优势

优势说明
摆脱环境限制无需真机或模拟器,无 root 要求
高可控性完全控制执行流程,任意修改内存和寄存器
自动化集成易于与 Java/Python 项目集成,支持大规模自动化
反反调试没有 ptrace 进程,绕过大多数反调试检测
可重复性相同输入始终产生相同输出

局限

局限说明
环境不完整非 100% Android 环境,强依赖系统/硬件特性的 SO 可能失败
性能开销逐条指令模拟,性能低于原生 (Dynarmic 有显著改善)
覆盖度有限未实现的 syscall/JNI 需要手动补充
多线程局限对 SO 内部创建线程的支持有限
时间敏感模拟速度不一致,依赖精确时间的代码可能产生不同结果

适用性判断

1
2
3
4
5
6
7
8
9
+-----------------------------------+-----------------------------------+
|         适合使用 Unidbg           |        不适合使用 Unidbg          |
+-----------------------------------+-----------------------------------+
| 纯算法分析 (sign/encrypt/hash)   | 涉及 UI 渲染的功能               |
| 需要绕过强反调试                  | 重度依赖 Binder IPC              |
| 批量调用生成签名                  | 需要实时网络通信                  |
| 对抗混淆后的算法还原              | 多线程密集型 SO                   |
| 无真机环境的离线分析              | 依赖硬件特性 (GPU/传感器)        |
+-----------------------------------+-----------------------------------+

相关内容

如果这篇文章对你有帮助,请我喝杯咖啡吧~
+5 支付宝支付宝
+5 微信微信