Unidbg
初识Unidbg
sdk版本:8(1.8)
开源链接:zhkl0228/unidbg: Allows you to emulate an Android native library, and an experimental iOS emulation
文件结构解析: 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
29
30
31
32
33├── README.md # 项目介绍和使用指南
├── LICENSE # 开源许可证文件
├── .gitignore # Git 忽略文件配置
├── pom.xml # Maven 配置文件,定义了项目的依赖和构建配置
├── mvnw # 脚本文件,用于 Maven Wrapper (Linux/Mac)
├── mvnw.cmd # 脚本文件,用于 Maven Wrapper (Windows)
├── test.sh # 测试脚本 (Linux/Mac)
├── test.cmd # 测试脚本 (Windows)
├── .mvn/ # Maven 配置目录
│ └── wrapper/ # Maven Wrapper 相关配置
├── assets/ # 存放模拟过程中使用的资源文件
│ ├── *.dll # Windows 动态链接库文件
│ └── *.so # Linux/Android 动态链接库文件
├── backend/ # 后端逻辑实现,包含核心模拟功能
├── unidbg-api/ # 核心接口和抽象类模块
│ └── src/ # API 模块的源代码目录
├── unidbg-ios/ # iOS 应用模拟模块
│ └── src/ # iOS 模拟模块的源代码目录
├── unidbg-android/ # Android 应用模拟模块
│ ├── pom.xml # Maven 构建文件
│ ├── pull.sh # 拉取 Android 模拟所需依赖文件的脚本
│ └── src/ # unidbg-android 模块的源代码目录
│ ├── main/
│ │ ├── java/ # 核心 Java 源代码
│ │ │ └── com/github/unidbg/ # 包含核心模拟器、文件系统、虚拟机组件
│ │ │ └── net/fornwall/jelf # ELF 文件格式解析实现
│ │ └── resources/ # 资源文件,封装了 JNI 库、Android 系统库等
│ ├── test/
│ │ ├── java/ # 单元测试代码
│ │ ├── native/android/ # 测试 Android 原生库的 C/C++ 源代码
│ │ └── resources/ # 测试资源文件,包含预编译的二进制文件(log4j.properties这个是日志相关配置,可以对open,syscall这类的系统调用进行trace)
└── .mvn/ # Maven Wrapper 相关配置目录
如果unidbg-android\src\test\java\com\anjuke\mobile\sign\SignUtil.java代码可运行环境就算配好了
下面我们以52破解里面的文件lib52pojie.so为例子(zj大佬)
基本框架分析:
1 | public class SecurityUtil { |
一点一点来看:
1. 四个对象
1 | private final AndroidEmulator emulator; // 模拟器对象 |
emulator 是 unidbg 的核心,负责模拟 CPU 指令执行、内存管理等
vm代表 Android 的 Dalvik/ART 虚拟机
module 代表我们即将加载运行的那个 .so
文件,在内存里面就是一个独立的模块
SecurityUtils (DvmClass):代表包含了我们要调用的 native
方法的 Java 类
2. Emulator 初始化
1 | emulator = AndroidEmulatorBuilder |
for64bit就是创建64位设备,如果so是32位的,就换成for32bit
区别:64位的执行速度较快,浮动10%左右;Unidbg 对 ARM32 的支持和完善程度高于 ARM64
addBackendFactory(new DynarmicFactory(true)):这里是在选择底层的
CPU 模拟引擎。unidbg 支持 Unicorn 和 Dynarmic。Dynarmic
执行速度更快Unidbg 支持了数个后端,目前共五个 Backend,分别是
Unicorn、Unicorn2、Dynarmic(执行速度较快)、Hypervisor、KVM。new
DynarmicFactory(true)中的true,标志着在出现异常时是否使用默认后端unicorn。
emulator常用Api
| 方法名 | 返回类型 | 描述 |
|---|---|---|
getMemory() |
Memory |
获取内存操作接口。 |
getPid() |
int |
获取进程的 PID。 |
createDalvikVM() |
VM |
创建虚拟机。 |
createDalvikVM(File apkFile) |
VM |
创建虚拟机并指定 APK 文件路径。 |
getDalvikVM() |
VM |
获取已创建的虚拟机。 |
showRegs() |
void |
显示当前寄存器状态,可指定寄存器。 |
getBackend() |
Backend |
获取后端 CPU。 |
getProcessName() |
String |
获取进程名。 |
getContext() |
RegisterContext |
获取寄存器上下文。 |
traceRead(long begin, long end) |
void |
Trace 读内存操作。 |
traceWrite(long begin, long end) |
void |
Trace 写内存操作。 |
traceCode(long begin, long end) |
void |
Trace 汇编指令执行。 |
isRunning() |
boolean |
判断当前 Emulator 是否正在运行 |
VM dalvikVM = emulator.createDalvikVM(new File("apk file path"))-创建虚拟机并指定APK文件,加载指定APK文件,unidbg可以帮我们完成一些小操作,例如:解析
Apk 基本信息,Apk 的版本名、版本号、包名、 Apk
签名等信息,减少补环境操作;解析和管理 Apk 资源文件,加载 Apk 后可以通过
openAsset获取 APK assets目录下的文件。
3.内存与 Resolver
这一步相当于在装 Android 系统的基础库
1 | Memory memory = emulator.getMemory(); |
23 和 19 分别对应于 sdk23(Android 6.0) 和 sdk19(Android 4.4)的运行库环境,处理 64 位 SO 时只能选择 SDK23
memory常用Api
| 方法名 | 返回类型 | 描述 |
|---|---|---|
setLibraryResolver(AndroidResolver resolver) |
void |
设置 Android SDK 版本解析器,目前支持 19 和 23 两个版本。 |
getStackPoint() |
long |
获取当前栈指针的值。 |
pointer(long address) |
UnidbgPointer |
获取指针,指向指定内存地址,可通过指针操作内存。 |
getMemoryMap() |
Collection<MemoryMap> |
获取当前内存的映射情况。 |
findModule(String moduleName) |
Module |
根据模块名获取指定模块。 |
findModuleByAddress(long address) |
Module |
根据地址获取指定模块。 |
loadLibrary(File file, boolean forceLoad) |
ElfModule |
加载 SO 文件,会调用
Linker.do_dlopen() 方法完成加载。 |
allocatestack(int size) |
UnidbgPointer |
在栈上分配指定大小的内存空间。 |
writestackstring(String value) |
UnidbgPointer |
将字符串写入栈内存中。 |
writestackBytes(byte[] value) |
UnidbgPointer |
将字节数组写入栈内存中。 |
malloc(int size, boolean runtime) |
UnidbgPointer |
分配指定大小的内存空间,返回指向该内存的指针。 |
4.创建 Java 虚拟机 (Dalvik VM)
1 | vm = emulator.createDalvikVM(); |
vm常用Api
createDalvikVM(File apkFile) |
VM |
创建虚拟机,指定 APK 文件,file可为空 |
|---|---|---|
setVerbose(boolean verbose) |
void |
设置是否输出 JNI 运行日志。 |
loadLibrary(File soFile, boolean callInit) |
DalvikModule |
加载 SO 模块,参数二设置是否自动调用 init 函数。 |
setJni(Jni jni) |
void |
设置 JNI 交互接口,推荐实现 AbstractJni。 |
getJNIEnv() |
Pointer |
获取 JNIEnv 指针,可作为参数传递。 |
getJavaVM() |
Pointer |
获取 JavaVM 指针,可作为参数传递。 |
callJNI_OnLoad(Emulator<?> emulator, Module module) |
void |
调用 JNI_OnLoad 函数。 |
addGlobalObject(DvmObject<?> obj) |
int |
向 VM 添加全局对象,返回该对象的 hash 值。 |
addLocalObject(DvmObject<?> obj) |
int |
向 VM 添加局部对象,返回该对象的 hash 值。 |
getObject(int hash) |
DvmObject<?> |
根据 hash 值获取虚拟机中的对象。 |
resolveClass(String className) |
DvmClass |
解析指定类名,构建并返回一个 DvmClass 对象。 |
getPackageName() |
String |
获取 APK 包名。 |
getVersionName() |
String |
获取 APK 版本名称。 |
getVersionCode() |
String |
获取 APK 版本号。 |
openAsset(String assetName) |
InputStream |
打开 APK 中的指定资源文件。 |
getManifestXml() |
String |
获取 AndroidManifest.xml 文件的文本内容。 |
getSignatures() |
CertificateMeta[] |
获取 APK 签名信息。 |
findClass(String className) |
DvmClass |
通过类名获取已经加载的类(DvmClass 对象)。 |
getEmulator() |
Emulator<?> |
获取模拟器对象 emulator。 |
5.加载目标 SO 文件
1 | DalvikModule dm = vm.loadLibrary(new File("unidbg-android/src/test/java/com/zj/wuaipojie/util/lib52pojie.so"), false); |
loadLibrary(...):将我们需要破解或测试的
lib52pojie.so 文件加载入虚拟机的内存中。
loadLibrary有三个重载方法: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21/**
* 加载指定名称的库文件。
* @Param libname 库文件的名称,不包括前缀 "lib" 和后缀 ".so"(例如 "example" 对应 "libexample.so")。
* @param forceCallInit 是否强制调用库的初始化函数(如 JNI_OnLoad)。
* @Return 加载后的 DalvikModule 对象,封装了加载的库模块。
*/
DalvikModule loadLibrary(String libname, boolean forceCallInit);
/**
* 从原始字节数组中加载指定的库文件。
* @param libname 库文件的名称,仅用于标识该库,与文件路径无关。
* @param 传入buffer方便解析elf
*/
DalvikModule loadLibrary(String libname, byte[] raw, boolean forceCallInit);
/**
* 从指定路径的加载ELF。
* @param elfFile 表示库的 ELF 文件,必须是有效的 ELF 格式文件。例如:new File("unidbg-android/src/test/resources/example_binaries/armeabi-v7a/libnative-lib.so")
* @param forceCallInit 是否强制调用库的初始化函数(如 JNI_OnLoad)。
*/
DalvikModule loadLibrary(File elfFile, boolean forceCallInit);
callJNI_OnLoad(emulator):在 Android JNI
开发中,JNI_OnLoad 是 SO
库被加载时执行的第一个函数,很多应用会在这里进行动态注册(绑定 Java
方法和 C++
函数)或者进行一些反调试的初始化。手动调用它,是为了完美模拟真实的 App
运行流程。
JNI_OnLoad方法调用: 
基本类型直接传递,int、long、boolean、double 等。
下面几种对象类型unidbg也帮我们封装好了
String
byte 数组
short 数组
int 数组
float 数组
double 数组
Enum 枚举类型
对于其他数据类型需要借助resolveClass构造,例如Context
1
DvmObject<?> context = vm.resolveClass("android/content/Context").newObject(null);
getModule():获取加载成功后的模块句柄,方便后续我们通过基址计算来寻找具体的函数地址。
6.建立连接与运行入口
1 | SecurityUtils = vm.resolveClass("com/zj/wuaipojie/util/SecurityUtil"); |
让虚拟机知道我们要调用的是哪个 Java 类里的 native 方法。
符号调用与偏移调用
符号调用
1 | //第一个模拟器实例,第二个jnienv,第三个jclass,后面为可变参数 |
或者: 1
2
3
4
5
6
7
8
9
10
11 Symbol symbol = module.findSymbolByName("Java_com_zj_wuaipojie_util_SecurityUtil_diamondNum");
if (symbol != null){
//第一个模拟器实例,第二个jnienv,第三个jclass,第四个可变参数
Number numbers = symbol.call(emulator, vm.getJNIEnv(), vm.addLocalObject(SecurityUtils));
int result = numbers.intValue();
System.out.println(result);
//如果返回值是string,可以通过vm.getObject(retval)获取
// System.out.println(vm.getObject(result).getValue());
}else {
System.out.println("符号未找到");
}
两者一样的,不过在调用非JNI函数的时候:
例如有些关键的加密逻辑写在了普通的 C/C++ 内部函数里(比如
int encrypt(char* data)),它压根不是给 Java 调用的,没有
JNIEnv,也无法通过 Java 类去调用。这时候只能用
findSymbolByName 找到它并直接 call。
但是findSymbolByName
只能找到在导出表里可见的符号(通常是静态注册的函数)。如果开发者的函数名被抹除,或者是动态注册的隐蔽函数,用名字是搜不到的,只能通过基址偏移算物理地址来调用。
偏移调用
1 | //第一个模拟器实例,第二个偏移地址(thumb记得+1),第三个jnienv,第四个jclass,第五个可变参数 |
unidbg之hook
| 类别 | 描述 | 优点 | 缺点 |
|---|---|---|---|
| 内置的第三方 Hook 框架 | 包括 Dobby(前身为 HookZz)、Whale 和 xHook 等。HookZz对于32位支持较好,Dobby64位,XHook不能Hook Sub_xxx 子函数。 | - 功能丰富:支持多种 Hook
方式,如 Inline Hook 和 PLT Hook。 - 易于使用:提供简洁的 API 接口,便于快速上手。 |
-
可能被检测:某些应用可能检测到这些 Hook
框架的存在。 - 局限性:Inline Hook 在短函数或相邻地址的函数中可能出现问题;PLT Hook 无法 Hook 非导出函数。 |
| 基于 Unicorn 引擎的原生 Hook 功能 | 利用 Unicorn 引擎实现的指令级Hook,块级Hook,内存读写Hook,异常Hook等功能,Unidbg 在此基础上还封装了 Console Debugger。 | - 隐蔽性强:原生 Hook
方式更难被应用检测到。 - 灵活性高:可对任意代码位置进行 Hook,无特定限制。 |
- 使用复杂度较高:需要深入理解底层机制,配置相对复杂。 |
HookZz
1 | // 获取 HookZz 实例,用于后续的 Hook 操作 |
1.准备工具
1 | IHookZz hookZz = HookZz.getInstance(emulator); |
wrap函数有两个重载,一个基于符号寻址,一个基于地址寻址,本质没区别,符号寻址的最终也是会调用symbol.getAddress()
参数里的WrapCallback的泛型接口有三个RegisterContext(函数 Hook)、HookZzArm32RegisterContext(针对ARM32位)和HookZzArm64RegisterContext(针对ARM64位)因为可以访问某个寄存器的值,所以适用于inline
hook
2.preCall
1 |
|
3. postCall
1 | public void postCall(Emulator<?> emulator, RegisterContext ctx, HookEntryInfo info) { |
函数
1 | // 使用 HookZz 的 instrument 方法对特定地址的指令进行 Inline Hook, thumb记得+1 |
Dobby
replace 替换函数
替换整数: 1
2
3
4
5
6
7
8
9Dobby dobby = Dobby.getInstance(emulator);
dobby.replace(module.findSymbolByName("Java_com_zj_wuaipojie_util_SecurityUtil_diamondNum"), new ReplaceCallback() { // 使用Dobby inline hook导出函数
public HookStatus onCall(Emulator<?> emulator, HookContext context, long originFunction) {
return HookStatus.LR(emulator,12);
}
});
int retval = SecurityUtils.callStaticJniMethodInt(emulator, "diamondNum()I"); // 执行Jni方法
System.out.println("retval:" + retval);
1 | Dobby dobby = Dobby.getInstance(emulator); |
Unicorn_Hook
必须是unicorn引擎才能使用
1 | emulator.getBackend().hook_add_new(new CodeHook() { |
Console Debugger
1 | Debugger attach = emulator.attach(); |
| 命令 | 功能说明 | |
|---|---|---|
c |
继续执行程序 | |
n |
跨过当前指令 | |
bt |
回溯堆栈 | |
st hex |
搜索堆栈 | |
shw hex |
搜索可写堆 | |
shr hex |
搜索可读堆 | |
shx-hex |
搜索可执行堆 | |
nb |
在下一个区块中断 | |
s |
步入当前指令 | |
s [decimal] |
执行指定数量的指令 | |
s (blx) |
执行直到 blx
助记符(性能较低) |
|
m (op) [size] |
显示内存,默认大小为
0x70,大小可为十六进制或十进制 |
|
mr0-mr7, mfp, mip, msp [size] |
显示指定寄存器的内存 | |
m (address) [size] |
显示指定地址的内存,地址需以
0x 开头 |
|
wr0-wr7, wfp, wip, wsp <value> |
写入指定寄存器 | |
wb(address), ws(address), wi(address) <value> |
写入指定地址的(字节、短、整数)内存,地址需以
0x 开头 |
|
wx (address) <hex> |
将字节写入指定地址的内存,地址需以
0x 开头 |
|
b (address) |
添加临时断点,地址需以 0x
开头,可为模块偏移量 |
|
b |
添加寄存器 PC 的断点 |
|
r |
删除寄存器 PC 的断点 |
|
blr |
添加寄存器 LR 的临时断点 |
|
p (assembly) |
在 PC 地址修补汇编指令 | |
where |
显示 Java 堆栈跟踪 | |
trace [begin-end] |
设置指令跟踪 | |
traceRead [begin-end] |
设置内存读取跟踪 | |
traceWrite [begin-end] |
设置内存写入跟踪 | |
vm |
查看已加载的模块 | |
vbs |
查看断点 | |
d |
显示反汇编代码 | |
d (0x) |
在指定地址显示反汇编代码 | |
stop |
停止模拟 | |
run [arg] |
运行测试 | |
gc |
运行 System.gc() |
|
threads |
显示线程列表 | |
cc size |
将地址范围的汇编代码转为 C 函数 |
和Unicorn Hook基本一致,也可以执行hook操作
1 | // hook函数并替换值 |
unidbg之patch
patch有两种应用形式:
- patch 二进制文件
- 在内存里 patch
1 | // 定位函数位置,并确定指令集类型是32位还是64位 |
Unidbg Trace
1.基础用法
在调用目标函数前,只需简单地调用 emulator.traceCode()
即可开启对后续所有指令的追踪。
1 | // 在调用目标函数前开启指令追踪 |
2. 约束追踪范围
一般我们只需要看目标 SO 文件的执行流。traceCode
的重载方法 traceCode(long begin, long end)
允许我们精确设定追踪的内存地址范围。
1 | // 在调用目标函数前开启指令追踪 |
3. 控制追踪时机
- 追踪 JNI_OnLoad: 将
traceCode放在dm.callJNI_OnLoad(emulator)之前。 - 追踪初始化函数 (init_array):
JNI_OnLoad之前还有初始化函数。要追踪它们,需要在加载模块的第一时间开启 Trace。最佳实践是使用ModuleListener。(DalvikModule dm = vm.loadLibrary的参数要改成true)
1 | // 在构造方法里面 |
4. 预估追踪耗时
如果追踪Onload/init函数,可以在ModuleListener()里面操作,其他的,在crack函数里面调用就好了,但是注意仅仅是调用,不要再crack里面开启teackcode函数
1 | // // 设置指令计数钩子 |
5. 特定函数Trace
结合断点,可以实现对单个函数内部的精确追踪。
在函数调用指令处下断点开启 Trace,在调用指令的下一条指令处下断点关闭 Trace
再构造函数的最后调用即可
1 | attachTraceAndInspectX0(module.base + 0x15F8); |
具体实现:
1 | private void attachTraceAndInspectX0(long callAddress) { |
6.函数调用追踪
(debugger.traceFunctionCall)
1 | /** |



