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,第五个可变参数 |




