初始配置
用idea打开github源码即可,初始配置会由maven自动配齐,下面是项目的部分结构解析。
├── 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 支持了数个后端,目前共五个 Backend,分别是 Unicorn、Unicorn2、Dynarmic(执行速度较快)、Hypervisor、KVM。new DynarmicFactory(true)中的true,标志着在出现异常时是否使用默认后端unicorn。
.setRootDir() //设置虚拟机根目录,可以实现io重定向,例如:app读取/data/data/1.txt,通过这个设置成D:/test,那么app读取的就会是D:/test/data/data/1.txt。
API
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 是否正在运行。 |
memory 常用api
unidbg自己配置了两个版本安卓sdk(路径为 unidbg-android/src/main/resouce/android ),一个是sdk19(Android 4.4)、一个是sdk23(Android 6.0),64位只能用sdk23 。但是unidbg对Arm32支持度和完善程度都高于64位。
| 方法名 | 返回类型 | 描述 |
|---|---|---|
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 | 分配指定大小的内存空间,返回指向该内存的指针。 |
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。 |
VM dalvikVM = emulator.createDalvikVM(new File("apk file path"))-创建虚拟机并指定APK文件,加载指定APK文件,unidbg可以帮我们完成一些小操作,例如:解析 Apk 基本信息,Apk 的版本名、版本号、包名、 Apk 签名等信息,减少补环境操作;解析和管理 Apk 资源文件,加载 Apk 后可以通过 openAsset获取 APK assets目录下的文件。
/**
* 加载指定名称的库文件。
* @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);
so层函数调用
因为unidbg的一些实现的原因:
public final UnidbgPointer findNativeFunction(Emulator<?> emulator, String method) { // method这里是JNI抽象过的,现在这里解开然后拼名字。 UnidbgPointer fnPtr = nativesMap.get(method); int index = method.indexOf('('); if (fnPtr == null && index == -1) { index = method.length(); } StringBuilder builder = new StringBuilder(); builder.append("Java_"); mangleForJni(builder, getClassName()); // <-- 就是这个b getClassName builder.append("_"); mangleForJni(builder, method.substring(0, index)); String symbolName = builder.toString(); if (fnPtr == null) { for (Module module : emulator.getMemory().getLoadedModules()) { Symbol symbol = module.findSymbolByName(symbolName, false); // <-- 还有这个 if (symbol != null) { fnPtr = (UnidbgPointer) symbol.createPointer(emulator); break; } } } if (fnPtr == null) { throw new IllegalArgumentException("find method failed: " + method); } ... return fnPtr; }这两兄弟配合,强烈建议是在对应Java层的调用位置去写对应的unidbg代码,比如:
package com.kanxue.test2; public class MainActivity extends AppCompatActivity { public native boolean jnitest(String str); public native String stringFromJNI(); static { System.loadLibrary("native-lib"); } ... }这里就在项目结构这里写同名类:
├── unidbg-android/ │ └── src/ # unidbg-android 模块的源代码目录 │ ├── main/ │ │ ├── java/ # 核心 Java 源代码 │ │ │ └── com/kanxue/test2/ # <-- 这里创建同名类来写 └── resources/ # 资源文件,封装了 JNI 库、Android 系统库等不然的话会出现同一个代码不同路径下会跑不通的情况。
Java层Jni函数调用
unidbg其实封装好了几个:
obj.callJniMethod(Emulator<?> emulator, String method, Object...args) // 后面4条最后也是调用这个函数
obj.callJniMethodBoolean(Emulator<?> emulator, String method, Object...args)
obj.callJniMethodInt(Emulator<?> emulator, String method, Object...args)
obj.callJniMethodLong(Emulator<?> emulator, String method, Object...args)
obj.callJniMethodObject(Emulator<?> emulator, String method, Object...args)
obj.callStaticJniMethod(Emulator<?> emulator, String method, Object...args) //后略下面
protected static Number callJniMethod(Emulator<?> emulator, VM vm, DvmClass objectType, DvmObject<?> thisObj, String method, Object...args) {
// 拿Native函数指针
UnidbgPointer fnPtr = objectType.findNativeFunction(emulator, method);
// 将当前对象加入到引入表,防止被回收
vm.addLocalObject(thisObj);
List<Object> list = new ArrayList<>(10);
// so层JNIEnv和this指针
list.add(vm.getJNIEnv());
list.add(thisObj.hashCode());
// 处理后续参数,进行封装
if (args != null) {
for (Object arg : args) {
if (arg instanceof Boolean) {
// 布尔类型直接封装添加
list.add((Boolean) arg ? VM.JNI_TRUE : VM.JNI_FALSE);
continue;
} else if(arg instanceof Hashable) {
list.add(arg.hashCode()); // dvm object
if(arg instanceof DvmObject) {
// 将 DvmObject 对象添加到本地引用表
vm.addLocalObject((DvmObject<?>) arg);
}
continue;
} else if (arg instanceof DvmAwareObject ||
arg instanceof String ||
arg instanceof byte[] ||
arg instanceof short[] ||
arg instanceof int[] ||
arg instanceof float[] ||
arg instanceof double[] ||
arg instanceof Enum) {
// 创建一个代理 DvmObject 对象
DvmObject<?> obj = ProxyDvmObject.createObject(vm, arg);
list.add(obj.hashCode());
vm.addLocalObject(obj);
continue;
}
// 基本类型直接传递了
list.add(arg);
}
}
// 调用函数
return Module.emulateFunction(emulator, fnPtr.peer, list.toArray());
}Unidbg将基本类型直接传递,基本的对象类型自己进行了封装。对于其他类型的数据,则需要借助resolveClass构造,例如Context
DvmObject<?> context = vm.resolveClass("android/content/Context").newObject(null);符号调用和偏移调用
DalvikModule dm = vm.loadLibrary(new File("..../libttEncrypt.so"), false); // 加载libttEncrypt.so到unicorn虚拟内存,加载成功以后会默认调用init_array等函数
dm.callJNI_OnLoad(emulator); // 手动执行JNI_OnLoad函数
module = dm.getModule(); // 加载好的libttEncrypt.so对应为一个模块
Symbol symbol = module.findSymbolByName("导出符号");
if (symbol != null){
//第一个模拟器实例,第二个jnienv,第三个jclass,第四个可变参数
Number numbers = symbol.call(emulator, vm.getJNIEnv(), vm.addLocalObject(实例类), 可变参数);
int result = numbers.intValue();
System.out.println(result);
//如果返回值是string,可以通过vm.getObject(retval)获取
System.out.println(vm.getObject(result).getValue());
}else {
System.out.println("符号未找到");
}//第一个模拟器实例,第二个偏移地址(thumb指令集记得+1),第三个jnienv,第四个jclass (就是this),第五个可变参数
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());Hook
有内置和原生的hook(基于Unicorn)
HookZz&Dobby
内置三方hook
在给的样例里面(com.bytedance.frameworks.core.encrypt/TTEncrypt)
基础的Hook:
IHookZz hookZz = HookZz.getInstance(emulator); // 加载HookZz,支持inline hook,文档看https://github.com/jmpews/HookZz
hookZz.wrap(module.findSymbolByName("ss_encrypt"), new WrapCallback<RegisterContext>() { // inline wrap导出函数
@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.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));
}
});参数里的WrapCallback的泛型接口有三个RegisterContext(函数 Hook)、HookZzArm32RegisterContext(针对ARM32位)和HookZzArm64RegisterContext(针对ARM64位)因为可以访问某个寄存器的值,所以适用于inline hook
Inline Hook:
// Thumb记得+1
hookZz.instrument(module.base + 0x00000F5C + 1, new InstrumentCallback<Arm32RegisterContext>() {
@Override
public void dbiCall(Emulator<?> emulator, Arm32RegisterContext ctx, HookEntryInfo info) { // 通过base+offset inline wrap内部函数,在IDA看到为sub_xxx那些
System.out.println("R3=" + ctx.getLongArg(3) + ", R10=0x" + Long.toHexString(ctx.getR10Long()));
}
});替换函数:
用的是dobby
// 使用 dobby 的 replace 方法替换 "Java_com_zj_wuaipojie_util_SecurityUtil_diamondNum" 函数的实现
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,888888);
//字符串
return HookStatus.LR(emulator, vm.addLocalObject(new StringObject(vm, "超级")));
}
});
Unicorn Hook
能实现函数级别、指令级别、文件级别、异常级别等hook
emulator.getBackend().hook_add_new(new CodeHook() {
@Override
public void hook(Backend backend, long address, int size, Object user) {
Arm64RegisterContext context = emulator.getContext();
if (address == module.base + 0x11330) {
int x0=emulator.getBackend().reg_read(Arm64Const.UC_ARM64_REG_X0).intValue();
System.out.println("x0:"+vm.getObject(x0));
emulator.getBackend().reg_write(Arm64Const.UC_ARM64_REG_X0,vm.addLocalObject(new StringObject(vm, "超级")));
}
}
@Override
public void onAttach(UnHook unHook) {
}
@Override
public void detach() {
}
}, module.base + 0x11240,module.base + 0x11340,null); // hook、起始地址、结束地址、user_data
StringObject Result = SecurityUtils.callStaticJniMethodObject(emulator, "vipLevel(Ljava/lang/String;)Ljava/lang/String;", "123");
System.out.println("Result: " + Result.getValue());Console Debugger
Unidbg提供的工具,允许用户在模拟执行过程中设置断点、单步调试、查看和修改内存及寄存器等操作,从而深入分析目标程序的行为。
下断点:
Debugger attach = emulator.attach();
attach.addBreakPoint(module.base + 0x11070); //下断地址断住之后会开一个交互窗口,交互指令:
| 命令 | 功能说明 |
|---|---|
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 函数 |
干坏事:
emulator.attach().addBreakPoint(module.findSymbolByName("verifyApkSign").getAddress(), new BreakPointCallback() {
@Override
public boolean onHit(Emulator<?> emulator, long address) {
System.out.println("替换函数 verifyApkSign");
RegisterContext registerContext = emulator.getContext();
emulator.getBackend().reg_write(ArmConst.UC_ARM_REG_PC, registerContext.getLRPointer().peer);
emulator.getBackend().reg_write(ArmConst.UC_ARM_REG_R0, 0);
return true;
}
});
Patch
Unidbg的内存Patch借助Keystone汇编引擎
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, #0x99"; //具体要修改的汇编指令
byte[] machineCode = keystone.assemble(s).getMachineCode(); //转换为机器码
pointer.write(machineCode); //写入内存神必小知识:
认arm和thumb可以看调用:BX addr+0x1,表示跳转至Thumb;BX addr,表示跳转至arm。
