初始配置
用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。
Unidbg Trace
同类型:IDA Instruction trace 和 frida Stalker
使用
一键启动:
emulator.traceCode();指定范围内,比如只想追踪某个so不想追踪库函数:
// 获取目标模块对象
Module module = dm.getModule();
// 仅追踪目标模块地址范围内的指令
emulator.traceCode(module.base, module.base + module.size); // start address -- end basetrace要提早
- 追踪 JNI_OnLoad: 将
traceCode放在dm.callJNI_OnLoad(emulator)之前。 - 追踪初始化函数 (init_array):
JNI_OnLoad之前还有初始化函数。要追踪它们,需要在加载模块的第一时间开启 Trace。最佳实践是使用ModuleListener。
memory.addModuleListener(new ModuleListener() {
@Override
public void onLoaded(Emulator<?> emulator, Module module) {
// 当我们关心的模块被加载时,立即开启trace
if("lib52pojie.so".equals(module.name)){
emulator.traceCode(module.base, module.base + module.size);
}
}
});
// 这之后再加载模块
DalvikModule dm = vm.loadLibrary(new File("xxx.so"), true);时间估算:
CodeHook:unidbg 每分钟约可追踪 40 万行执行流,自己根据结果预估一下要不要睡觉(
private long instructionCount = 0; // 用于指令计数
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);
System.out.println("总共执行ARM指令数: " + test.instructionCount);只关心一个调用后的Trace:
long callAddr = module.base + 0xE53C; // 假设这是BL指令的地址
final TraceHook[] traceHook = new TraceHook[1];
// 在调用处开启追踪
emulator.attach().addBreakPoint(callAddr, new BreakPointCallback() {
@Override
public boolean onHit(Emulator<?> emulator, long address) {
traceHook[0] = emulator.traceCode(module.base, module.base + module.size);
return true;
}
});
// 在调用返回后关闭追踪
emulator.attach().addBreakPoint(callAddr + 4, new BreakPointCallback() {
@Override
public boolean onHit(Emulator<?> emulator, long address) {
if (traceHook[0] != null) {
traceHook[0].stopTrace();
}
return true;
}
});甚至都可以只Trace一行
函数追踪:
unidbg version >= 0.9.6
debugger.traceFunctionCall
有现成的轮子:
// in file unidbg-api\src\main\java\com\github\unidbg\unwind\Unwinder.java
// add this function:
/**
* 将给定的内存地址格式化为包含详细信息、可读的字符串。
* 这个方法会尝试解析地址,并提供尽可能多的上下文信息,如模块名、函数名(符号)、偏移量等。
*
* @Param address 要格式化的绝对内存地址。
* @Return 一个包含地址详细信息的格式化字符串。
*/
public String formatAddressDetails(long address) {
// 1. 尝试根据地址查找其所属的模块(例如,一个 .so 文件)。
Module module = emulator.getMemory().findModuleByAddress(address);
// 2. 如果地址位于一个已加载的模块内:
if (module != null) {
// 2.1. 在模块中查找离该地址最近的符号(即函数或全局变量的名称)。
// `true` 参数表示也查找非导出的内部符号。
Symbol symbol = module.findClosestSymbolByAddress(address, true);
// 2.2. 如果找到了一个符号:
if (symbol != null) {
// 2.2.1. 创建一个 demangler 实例,用于将 C++ "mangled"(混淆)的符号名还原为可读的函数签名。
GccDemangler demangler = DemanglerFactory.createDemangler();
// 2.2.2. 对符号名进行 demangle 操作。
String demangledName = demangler.demangle(symbol.getName());
// 2.2.3. 计算当前地址相对于符号起始地址的偏移量。
long offset = address - symbol.getAddress();
// 2.2.4. 返回最详细的格式化字符串,例如:"[libnative.so] JNI_OnLoad + 0x10 (at 0x...)"
return String.format("[%s] %s + 0x%x (at 0x%x)", module.name, demangledName, offset, address);
} else {
// 2.3. 如果在模块内但没有找到具体的符号,则将其视为一个未命名的子程序(subroutine)。
// 计算地址相对于模块基地址的偏移量。
long offset = address - module.base;
// 2.3.1. 返回一个通用的子程序格式,例如:"[libnative.so] sub_c80 (at 0x...)"
return String.format("[%s] sub_%x (at 0x%x)", module.name, offset, address);
}
}
// 3. 如果地址不属于任何模块,则尝试查找它是否位于一个已知的内存区域中(例如,栈或堆)。
MemRegion region = emulator.getSvcMemory().findRegion(address);
if (region != null) {
// 3.1. 如果找到了,返回区域名称,例如:"[stack] (at 0x...)"
return String.format("[%s] (at 0x%x)", region.getName(), address);
}
// 4. 如果以上所有尝试都失败了,返回一个表示“未知”的通用格式。
return String.format("[unknown] (at 0x%x)", address);
}
/**
* 【模块化功能】: 附加一个函数调用追踪器 (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);
}
});
} 使用EP:
public static void main(String[] args) {
MainActivity test = new MainActivity(false);
String traceFile = "unidbg-android/src/test/resources/traceFunctions.txt";
try (PrintStream traceStream = new PrintStream(new FileOutputStream(traceFile), true)) {
// 在调用 crack 之前附加追踪器,持久化日志
test.attachFunctionCallTracer(traceStream);
// 执行会触发函数调用的模拟代码
test.crack();
} catch (IOException e) {
e.printStackTrace();
} finally {
// 确保模拟器资源被释放
test.destroy();
System.out.println("总共执行ARM指令数: " + test.instructionCount);
}
} 