初识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

类别 描述 优点 缺点
内置的第三方 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 获取 HookZz 实例,用于后续的 Hook 操作
IHookZz hookZz = HookZz.getInstance(emulator);
// 使用 HookZz 的 wrap 方法对导出函数 "ss_encrypt" 进行 Inline Hook
hookZz.wrap(module.findSymbolByName("ss_encrypt"), new WrapCallback<RegisterContext>() {
@Override
public void preCall(Emulator<?> emulator, RegisterContext ctx, HookEntryInfo info) {
// 在函数调用前执行的操作
// 获取第三个参数的指针
Pointer pointer = ctx.getPointerArg(2);
// 获取第四个参数的整数值
int length = ctx.getIntArg(3);
// 读取指针指向的内存内容,获取密钥数据
byte[] key = pointer.getByteArray(0, length);
// 使用 Inspector 工具输出密钥内容,便于调试和分析
Inspector.inspect(key, "ss_encrypt key");
}

@Override
public void postCall(Emulator<?> emulator, RegisterContext ctx, HookEntryInfo info) {
// 在函数调用后执行的操作
// 输出函数返回值的值
System.out.println("ss_encrypt.postCall R0=" + ctx.getLongArg(0));
}
});

1.准备工具

1
2
IHookZz hookZz = HookZz.getInstance(emulator);
hookZz.wrap(module.findSymbolByName("ss_encrypt"), new WrapCallback<RegisterContext>() { ... });

wrap函数有两个重载,一个基于符号寻址,一个基于地址寻址,本质没区别,符号寻址的最终也是会调用symbol.getAddress()

image-20260311205547805参数里的WrapCallback的泛型接口有三个RegisterContext(函数 Hook)HookZzArm32RegisterContext(针对ARM32位)HookZzArm64RegisterContext(针对ARM64位)因为可以访问某个寄存器的值,所以适用于inline hook

image-20260311210013905

2.preCall

1
2
3
4
5
6
7
8
9
10
11
12
@Override
public void preCall(Emulator<?> emulator, RegisterContext ctx, HookEntryInfo info) {
// 在函数调用前执行的操作
// 获取第三个参数的指针
Pointer pointer = ctx.getPointerArg(2);
// 获取第四个参数的整数值
int length = ctx.getIntArg(3);
// 读取指针指向的内存内容,获取密钥数据
byte[] key = pointer.getByteArray(0, length);
// 使用 Inspector 工具输出密钥内容,便于调试和分析
Inspector.inspect(key, "ss_encrypt key");
}

3. postCall

1
2
3
4
5
public void postCall(Emulator<?> emulator, RegisterContext ctx, HookEntryInfo info) {
// 在函数调用后执行的操作
// 输出函数返回值的值
System.out.println("ss_encrypt.postCall R0=" + ctx.getLongArg(0));
}

函数

1
2
3
4
5
6
7
8
9
// 使用 HookZz 的 instrument 方法对特定地址的指令进行 Inline Hook, thumb记得+1
hookZz.instrument(module.base + 0x11470, new InstrumentCallback<Arm64RegisterContext>() {
@Override
public void dbiCall(Emulator<?> emulator, Arm64RegisterContext ctx, HookEntryInfo info) {
// 在指定指令执行时触发的操作
// 输出 W0 寄存器的值
System.out.println("W0=" + ctx.getXInt(0));
}
});

Dobby

replace 替换函数

替换整数:

1
2
3
4
5
6
7
8
9
Dobby dobby = Dobby.getInstance(emulator);
dobby.replace(module.findSymbolByName("Java_com_zj_wuaipojie_util_SecurityUtil_diamondNum"), new ReplaceCallback() { // 使用Dobby inline hook导出函数
@Override
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
2
3
4
5
6
7
8
Dobby dobby = Dobby.getInstance(emulator);
dobby.replace(module.findSymbolByName("Java_com_zj_wuaipojie_util_SecurityUtil_vipLevel"), new ReplaceCallback() { // 使用Dobby inline hook导出函数
@Override
public HookStatus onCall(Emulator<?> emulator, HookContext context, long originFunction) {
emulator.getUnwinder().unwind(); // 打印当前的函数调用栈(Stack Backtrace / 堆栈回溯)
return HookStatus.LR(emulator, vm.addLocalObject(new StringObject(vm, "超级")));
}
});

Unicorn_Hook

必须是unicorn引擎才能使用

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
35
36
37
38
39
40
41
42
43
        emulator.getBackend().hook_add_new(new CodeHook() {
/**
* 这个 hook 方法是核心:
* 只要 CPU 执行到了你指定的地址范围内的【每一条】指令,都会先暂停,跑进这个方法里汇报一下。
* @param address 当前准备执行的指令内存地址
* @param size 当前指令的字节长度 (ARM64 通常是 4 字节)
*/
@Override
public void hook(Backend backend, long address, int size, Object user) {
// 获取ARM64寄存器上下文
Arm64RegisterContext context = emulator.getContext();
// 因为 Hook 范围内每一行指令都会触发这个方法,所以必须用 if 过滤!
// .text:000000000001132C 070 E0 03 13 AA MOV X0, X19
// .text:0000000000011330 070 FD 7B 46 A9 LDP X29, X30, [SP,#0x60+var_s0]
// .text:0000000000011334 070 F4 4F 45 A9 LDP X20, X19, [SP,#0x60+var_10]
// .text:0000000000011338 070 F6 57 44 A9 LDP X22, X21, [SP,#0x60+var_20]
// .text:000000000001133C 070 FF C3 01 91 ADD SP, SP, #0x70 ; 'p'
// .text:0000000000011340 000 C0 03 5F D6 RET
if(address == module.base + 0x11330) {
// 读取 X0 寄存器里的值。
int x0 = emulator.getBackend().reg_read(Arm64Const.UC_ARM64_REG_X0).intValue();
// 打印原始数据
System.out.println("x0:" + vm.getObject(x0));

// 掉包:我们新建一个叫 "超级" 的字符串对象,注册到虚拟机里拿到新的句柄,并强行塞回X0里面
emulator.getBackend().reg_write(Arm64Const.UC_ARM64_REG_X0,vm.addLocalObject(new StringObject(vm,"超级")));
}
}
// Hook 挂载时的回调 (通常不用管)
@Override
public void onAttach(UnHook unHook) {

}
// Hook 卸载时的回调 (通常不用管)
@Override
public void detach() {

}
// 监视范围
}, module.base + 0x11240, module.base + 0x11340, null);

StringObject result = SecurityUtils.callStaticJniMethodObject(emulator, "vipLevel(Ljava/lang/String;)Ljava/lang/String;", "123456");
System.out.println("result:" + result.getValue());

Console Debugger

1
2
3
4
5
Debugger attach = emulator.attach();
// 下断点
attach.addBreakPoint(module.base + 0x11070);
Number number = module.callFunction(emulator, 0x11014, vm.getJNIEnv(), vm.addLocalObject(SecurityUtils), vm.addLocalObject(new StringObject(vm, "123456")));
System.out.println("result:" + number.intValue());
命令 功能说明
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
        // hook函数并替换值
attach.addBreakPoint(module.base + 0x11330, new BreakPointCallback() {
@Override
public boolean onHit(Emulator<?> emulator, long address) {
// UnidbgPointer pointer = UnidbgPointer.register(emulator,Arm64Const.UC_ARM64_REG_X0);
// System.out.println("str:"+vm.getObject(pointer.toIntPeer()));
int x0 = emulator.getBackend().reg_read(Arm64Const.UC_ARM64_REG_X0).intValue();
System.out.println("str:"+ vm.getObject(x0));
emulator.getBackend().reg_write(Arm64Const.UC_ARM64_REG_X0,vm.addLocalObject(new StringObject(vm, "超级")));
return true;
}
});

StringObject Result = SecurityUtils.callStaticJniMethodObject(emulator, "vipLevel(Ljava/lang/String;)Ljava/lang/String;", "572782"); // 执行Jni方法
System.out.println("Result: " + Result.getValue());

unidbg之patch

patch有两种应用形式:

  • patch 二进制文件
  • 在内存里 patch
1
2
3
4
5
6
7
8
// 定位函数位置,并确定指令集类型是32位还是64位
UnidbgPointer pointer = UnidbgPointer.pointer(emulator,module.base + 0x1146C);
//在进行 Patch 操作前,需确保已正确定位目标函数的地址和指令集类型(如 ARM 或 Thumb)
//如果是32位的,代码如下:Keystone keystone = new Keystone(KeystoneArchitecture.Arm, KeystoneMode.ArmThumb);
Keystone keystone = new Keystone(KeystoneArchitecture.Arm64, KeystoneMode.LittleEndian);
String s = "MOV W0, #0x88"; //具体要修改的汇编指令
byte[] machineCode = keystone.assemble(s).getMachineCode(); //转换为机器码
pointer.write(machineCode); //写入内存

Unidbg Trace

1.基础用法

在调用目标函数前,只需简单地调用 emulator.traceCode() 即可开启对后续所有指令的追踪。

1
2
3
4
5
6
// 在调用目标函数前开启指令追踪
private void crack(){
emulator.traceCode();
boolean result = security.callStaticJniMethodBoolean(emulator, "check", "1234567");
System.out.println("函数调用结束,返回结果: " + result);
}

2. 约束追踪范围

一般我们只需要看目标 SO 文件的执行流。traceCode 的重载方法 traceCode(long begin, long end) 允许我们精确设定追踪的内存地址范围。

1
2
3
4
5
6
// 在调用目标函数前开启指令追踪
private void crack(){
emulator.traceCode(module.base, module.base+module.size);
boolean result = security.callStaticJniMethodBoolean(emulator, "check", "1234567");
System.out.println("函数调用结束,返回结果: " + result);
}

3. 控制追踪时机

  • 追踪 JNI_OnLoad: 将 traceCode 放在 dm.callJNI_OnLoad(emulator) 之前。
  • 追踪初始化函数 (init_array): JNI_OnLoad 之前还有初始化函数。要追踪它们,需要在加载模块的第一时间开启 Trace。最佳实践是使用 ModuleListener。(DalvikModule dm = vm.loadLibrary的参数要改成true)
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
// 在构造方法里面
Memory memory = emulator.getMemory();
memory.setLibraryResolver(new AndroidResolver(23));

memory.addModuleListener(new ModuleListener() {
@Override
public void onLoaded(Emulator<?> emulator, Module module) {
if ("libndkdemo.so".equals(module.name)) {
System.out.println("[+] 检测到 " + module.name + " 已加载,立即开启 Trace 以追踪 init_array");
emulator.traceCode(module.base, module.base + module.size);
}
}
});
vm = emulator.createDalvikVM();
vm.setVerbose(logging);

DalvikModule dm = vm.loadLibrary(new File("unidbg-android/src/test/java/com/example/ndkdemo/libndkdemo.so"), false);
// 【说明】:如果只想追踪 JNI_OnLoad 而不想追踪 init_array,可以注释掉上面的 ModuleListener,然后取消注释下面这行:
// emulator.traceCode(dm.getModule().base, dm.getModule().base + dm.getModule().size);

// 手动调用 JNI_OnLoad,此时 Trace 处于开启状态,会被完整记录
dm.callJNI_OnLoad(emulator);
module = dm.getModule();

security = vm.resolveClass("com/example/ndkdemo/MainActivity");

// 因为已经在前面开启了针对整个模块的 Trace,crack里面的调用也会被 Trace 到

4. 预估追踪耗时

如果追踪Onload/init函数,可以在ModuleListener()里面操作,其他的,在crack函数里面调用就好了,但是注意仅仅是调用,不要再crack里面开启teackcode函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//        // 设置指令计数钩子
emulator.getBackend().hook_add_new(new CodeHook() {
@Override
public void hook(Backend backend, long address, int size, Object user) {
instructionCount++;
}

@Override
public void onAttach(UnHook unHook) {

}

@Override
public void detach() {

}
}, module.base, module.base+module.size, null);

5. 特定函数Trace

结合断点,可以实现对单个函数内部的精确追踪。

在函数调用指令处下断点开启 Trace,在调用指令的下一条指令处下断点关闭 Trace

再构造函数的最后调用即可

1
attachTraceAndInspectX0(module.base + 0x15F8);

具体实现:

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
private void attachTraceAndInspectX0(long callAddress) {
final TraceHook[] traceHook = new TraceHook[1];

emulator.attach().addBreakPoint(callAddress, (emu, address) -> {
traceHook[0] = emu.traceCode(module.base, module.base + module.size);
return true;
});

long returnAddress = callAddress + 4;
emulator.attach().addBreakPoint(returnAddress, new BreakPointCallback() {
@Override
public boolean onHit(Emulator<?> emu, long address) {
if (traceHook[0] != null) {
traceHook[0].stopTrace();
System.out.println();
}

Arm64RegisterContext ctx = emu.getContext();
long x0 = ctx.getXLong(0);
System.out.printf("[+] 检查地址: 0x%x, x0寄存器值: 0x%x\n", address, x0);

UnidbgPointer pointer = UnidbgPointer.pointer(emu, x0);
if (pointer == null) {
System.out.println("[-] x0的值不是一个有效的指针或指向未映射的内存");
return true;
}

String cstring = safeReadCString(pointer);
if (cstring != null && isPrintable(cstring)) {
System.out.println("[+] x0指向的字符串: " + cstring);
} else {
System.out.println("[-] x0指向的内容不是一个可打印的字符串");
}

int dumpSize = 256;
byte[] data = pointer.getByteArray(0, dumpSize);
System.out.println("[+] x0指向内存的HexDump (前" + dumpSize + "字节):");
System.out.println(prettyHexDump(data, x0));
System.out.println("--- x0寄存器检查结束 ---\n");

return true;
}
});
}

private static String safeReadCString(UnidbgPointer p) {
try {
return p.getString(0);
} catch (Exception e) {
return null;
}
}

private static boolean isPrintable(String s) {
if (s == null || s.isEmpty()) {
return false;
}
int printableChars = 0;
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if ((c >= 32 && c <= 126) || Character.isWhitespace(c)) {
printableChars++;
}
}
return s.length() >= 2 && (double) printableChars / s.length() > 0.8;
}

private static String prettyHexDump(byte[] data, long baseAddr) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < data.length; i += 16) {
sb.append(String.format("%016x: ", baseAddr + i));
StringBuilder hexPart = new StringBuilder();
StringBuilder asciiPart = new StringBuilder();
for (int j = 0; j < 16; j++) {
if (i + j < data.length) {
byte b = data[i + j];
hexPart.append(String.format("%02x ", b));
char c = (b >= 32 && b <= 126) ? (char) b : '.';
asciiPart.append(c);
} else {
hexPart.append(" ");
}
if (j == 7) {
hexPart.append(" ");
}
}
sb.append(hexPart).append(" |").append(asciiPart).append("|\n");
}
return sb.toString();
}

6.函数调用追踪 (debugger.traceFunctionCall)

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
/**
* 【模块化功能】: 附加一个函数调用追踪器 (traceFunctionCall)。
* 此功能会监听所有的函数调用,并以树状结构打印出调用关系。
* @param traceStream 日志输出流
*/
public void attachFunctionCallTracer(final PrintStream traceStream) {
Debugger debugger = emulator.attach();
System.out.println("函数调用关系追踪器已附加,结果将输出到日志文件。");

debugger.traceFunctionCall(null, new FunctionCallListener() {
private int depth = 0;

private String getPrefix(int currentDepth) {
if (currentDepth <= 0) {
return "";
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < currentDepth - 1; i++) {
sb.append("│ ");
}
sb.append("├─ ");
return sb.toString();
}

@Override
public void onCall(Emulator<?> emulator, long callerAddress, long functionAddress) {
String prefix = getPrefix(depth + 1);
String details = emulator.getUnwinder().formatAddressDetails(functionAddress);
traceStream.printf("%sCALL -> %s%n", prefix, details);
depth++;
}

@Override
public void postCall(Emulator<?> emulator, long callerAddress, long functionAddress, Number[] args) {
depth--;
String prefix = getPrefix(depth + 1);
String details = emulator.getUnwinder().formatAddressDetails(functionAddress);

Backend backend = emulator.getBackend();
Number retVal = emulator.is64Bit() ? backend.reg_read(Arm64Const.UC_ARM64_REG_X0) : backend.reg_read(ArmConst.UC_ARM_REG_R0);
long retValLong = retVal.longValue();

// 尝试将返回值作为指针解析
String retValFormatted = String.format("0x%x", retValLong);
UnidbgPointer pointer = UnidbgPointer.pointer(emulator, retValLong);
if (pointer != null) {
String cstring = safeReadCString(pointer);
// 如果是一个可打印的字符串,则附加到日志中
if (isPrintable(cstring)) {
retValFormatted += String.format(" -> \"%s\"", cstring);
}
}

traceStream.printf("%sRET <- %s, ret=%s%n", prefix, details, retValFormatted);
}
});
}

Unidbg补环境