初识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
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
34
public class SecurityUtil {
private final AndroidEmulator emulator;
private final VM vm;
private final Module module;
private final DvmClass SecurityUtils;

private SecurityUtil() {
emulator = AndroidEmulatorBuilder
.for64Bit()
.addBackendFactory(new DynarmicFactory(true))
.build();
Memory memory = emulator.getMemory();
memory.setLibraryResolver(new AndroidResolver(23));

vm = emulator.createDalvikVM();
vm.setVerbose(false);
DalvikModule dm = vm.loadLibrary(new File("unidbg-android/src/test/java/com/zj/wuaipojie/util/lib52pojie.so"), false);
dm.callJNI_OnLoad(emulator);

module = dm.getModule();
SecurityUtils = vm.resolveClass("com/zj/wuaipojie/util/SecurityUtil");

}

private void crack() {

}

public static void main(String[] args) {
SecurityUtil securityUtil = new SecurityUtil();
securityUtil.crack();
}

}

一点一点来看:

1. 四个对象

1
2
3
4
private final AndroidEmulator emulator; // 模拟器对象
private final VM vm; // 虚拟机对象
private final Module module; // 模块对象(加载的SO库)
private final DvmClass SecurityUtils; // 目标类对象

emulator 是 unidbg 的核心,负责模拟 CPU 指令执行、内存管理等

vm代表 Android 的 Dalvik/ART 虚拟机

module 代表我们即将加载运行的那个 .so 文件,在内存里面就是一个独立的模块

SecurityUtils (DvmClass):代表包含了我们要调用的 native 方法的 Java 类

2. Emulator 初始化

1
2
3
4
emulator = AndroidEmulatorBuilder
.for64Bit()
.addBackendFactory(new DynarmicFactory(true))
.build();

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
2
Memory memory = emulator.getMemory();
memory.setLibraryResolver(new AndroidResolver(23));

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
2
vm = emulator.createDalvikVM();
vm.setVerbose(false);

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
2
3
DalvikModule dm = vm.loadLibrary(new File("unidbg-android/src/test/java/com/zj/wuaipojie/util/lib52pojie.so"), false);
dm.callJNI_OnLoad(emulator);
module = dm.getModule();

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方法调用: image-20260310234453224

image-20260310235302620

基本类型直接传递,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
2
3
//第一个模拟器实例,第二个jnienv,第三个jclass,后面为可变参数
int retval = SecurityUtils.callStaticJniMethodInt(emulator,"diamondNum()I");
System.out.println("retval:" + retval);

或者:

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
2
3
4
//第一个模拟器实例,第二个偏移地址(thumb记得+1),第三个jnienv,第四个jclass,第五个可变参数
Number number = module.callFunction(emulator, 0x11240, vm.getJNIEnv(),vm.addLocalObject(SecurityUtils),vm.addLocalObject(new StringObject(vm, "超级")));
DvmObject<?> object = vm.getObject(number.intValue());
System.out.println("result:" + object.getValue());

unidbg之hook

unidbg之patch

Unidbg补环境