介绍
项目地址:zhkl0228/unidbg
UniDbg 是一个开源的基于 Unicorn 的 Android Native 调试框架,专注于仿真 Android Native 层的动态库(如 .so
文件)和调用其内部方法。UniDbg 结合了 Unicorn 的指令仿真能力,封装了一套高层次的 API
Unicorn
Unicorn 是一个强大的开源 CPU 仿真框架,它支持多种架构的指令集模拟,允许用户在多种平台上仿真不同架构的代码。它基于更底层的 QEMU,但封装了更简洁易用的接口
UniDbg 的特点
- 基于 Unicorn 的轻量仿真:
- 使用 Unicorn 引擎进行指令仿真,支持多种 CPU 架构(如 ARM、ARM64 等)。
- 只模拟 Native 层,不需要完整的 Android 系统运行环境。
- Android 系统接口模拟:
- 内置了许多 Android 系统调用的仿真(如
JNIEnv
、libc.so
、libandroid_runtime.so
)。
- 模拟常用的系统函数(如
gettimeofday
、dlopen
、mmap
等)。
- 高效的动态库加载:
- 支持直接加载
.so
文件,并调用其导出的方法。
- 自动解析 ELF 格式文件的符号表和重定位。
- 动态调试:
- 支持 hook Native 方法,拦截函数调用并查看参数和返回值。
- 支持断点设置、寄存器查看、内存访问等调试功能。
- 跨平台支持:
- UniDbg 是一个基于 Java 的框架,可以在多种平台上运行。
- 逆向工程辅助:
- 专为逆向工程设计,支持快速仿真加密函数、动态分析 Native 层逻辑。
- 扩展性强:
- 可以通过自定义 Hook、模拟环境等方式扩展其功能。
指令集+1
在 ARM 架构中,函数地址是否需要 +1 通常与 Thumb 指令集 有关。在 ARM 和 Thumb 指令集中,有以下关键点需要注意:
- ARM 与 Thumb 指令集:
- ARM 指令是固定 32 位宽度,地址必须是 4 字节对齐。
- Thumb 指令是 16 位宽度,地址可以是 2 字节对齐。
- 指令集的切换(Thumb 模式标志位):
- ARM 指令集和 Thumb 指令集的切换通过地址最低位是否设置为 1来指示:
- 最低位为 0:ARM 模式。
- 最低位为 1:Thumb 模式。
- 例如,在调用函数时,如果最低位为
1
,表示调用的是 Thumb 模式函数。
- +1 的问题:
- 在传统动态链接或手动调试时,如果要调用 Thumb 模式的函数,需要在获取的函数地址基础上手动加
1
。
- 这是因为程序通常返回的是实际的函数入口地址,而不是带有 Thumb 模式标志的地址。
unidbg入门用例
基本框架
unidbg-master\unidbg-android\src\test\java\com\bytedance\frameworks\core\encrypt
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| TTEncrypt(boolean logging) { this.logging = logging;
emulator = AndroidEmulatorBuilder.for32Bit() .setProcessName("com.qidian.dldl.official") .addBackendFactory(new Unicorn2Factory(true)) .build(); final Memory memory = emulator.getMemory(); memory.setLibraryResolver(new AndroidResolver(23));
vm = emulator.createDalvikVM(); vm.setVerbose(logging); DalvikModule dm = vm.loadLibrary(new File("unidbg-android/src/test/resources/example_binaries/libttEncrypt.so"), false); dm.callJNI_OnLoad(emulator); module = dm.getModule(); TTEncryptUtils = vm.resolveClass("com/bytedance/frameworks/core/encrypt/TTEncryptUtils"); }
|
unidbg函数
emulator
Unidbg 的 Emulator
是一个高层抽象,封装了 CPU 仿真(基于 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
| AndroidEmulator emulator = AndroidEmulatorBuilder .for32Bit() .addBackendFactory(new DynarmicFactory(true)) .setProcessName("com.github.unidbg") .setRootDir(new File("target/rootfs/default")) .build();
Memory memory = emulator.getMemory();
int pid = emulator.getPid();
VM dalvikVM = emulator.createDalvikVM();
VM dalvikVM = emulator.createDalvikVM(new File("apk file path"));
VM dalvikVM = emulator.getDalvikVM();
emulator.showRegs();
Backend backend = emulator.getBackend();
String processName = emulator.getProcessName();
RegisterContext context = emulator.getContext();
emulator.traceRead(1,0);
emulator.traceWrite(1,0);
emulator.traceCode(1,0);
boolean running = emulator.isRunning();
|
Memory操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| Memory memory = emulator.getMemory();
memory.setLibraryResolver(new AndroidResolver(23));
UnidbgPointer pointer = memory.pointer(0x4000000);
Collection<MemoryMap> memoryMap = memory.getMemoryMap();
Module module = memory.findModule("module name");
Module module = memory.findModuleByAddress(0x40000000);
|
VM操作
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
| VM vm = emulator.createDalvikVM(new File("apk file path"));
vm.setVerbose(true);
DalvikModule dalvikModule = vm.loadLibrary(new File("so file path"), true);
vm.setJni(this);
Pointer jniEnv = vm.getJNIEnv();
Pointer javaVM = vm.getJavaVM();
vm.callJNI_OnLoad(emulator,dalvikModule.getModule());
int hash = vm.addGlobalObject(dvmObj);
DvmObject<?> object = vm.getObject(hash);
|
CallMethod
执行JNI函数
- 创建一个VM对象,此对象相当于在Java层去调用native函数的类的实例对象
1
| DvmObject<?> obj = ProxyDvmObject.createObject(vm, this);
|
注意:Unidbg会根据第二个参数类的包名进行匹配JNI方法,所以此处的this对象所属类的包名应该与目标函数相匹配
1
| boolean result = obj.callJniMethodBoolean(emulator, "jnitest(Ljava/lang/String;)Z", str);
|
参数二是方法的签名,根据参数二找到对应的函数来执行。假设此时的this对象所属类的包名为:com.kanxue.test2,那此时就会调用:
1
| jboolean Java_com_kanxue_test2_jnitest(JNIEnv *env, jobject thiz, jstring str);
|
执行任意函数
当我们想执行SO中任意函数时,我们也可以通过地址来进行调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| Pointer jniEnv = vm.getJNIEnv();
DvmObject<?> thiz = vm.resolveClass("com.kanxue.test2").newObject(null);
List<Object> args = new ArrayList<>(); args.add(jniEnv); args.add(vm.addLocalObject(thiz)); args.add(vm.addLocalObject(new StringObject(vm,"XuE")));
Number[] numbers = module.callFunction(emulator, 0x9180 + 1, args.toArray()); System.out.println(numbers[0].intValue());
|
当入参为非指针和Number类型,都需要将对象先添加到VM,才能够在VM中使用这些对象 当返回值为对象时,此时的Number为该对象在VM中的hash值,需要通过vm.getObject()将对象取出进行使用,例:
1 2
| DvmObject<?> object = vm.getObject(numbers[0].intValue()); String result = (String) object.getValue();
|
执行返回值由参数指针传递的函数
如:
1
| void md5(const uint8_t *initial_msg, size_t initial_len, uint8_t *digest);
|
这种函数我们来看一下我们该如何进行主动执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| String initial = "unidbg"; int initial_length = initial.length();
MemoryBlock initial_msg = emulator.getMemory().malloc(initial_length+1, false); UnidbgPointer initial_msg_ptr=initial_msg.getPointer();
initial_msg_ptr.write(initial.getBytes());
MemoryBlock digest = emulator.getMemory().malloc(16, false); UnidbgPointer digest_ptr=digest.getPointer();
List<Object> args = new ArrayList<>(); args.add(initial_msg); args.add(initial_length); args.add(digest_ptr);
module.callFunction(emulator, 0x7A8D + 1, args.toArray());
Inspector.inspect(digest_ptr.getByteArray(0, 0x10), "digest");
|
unidbg中的Hook
Unidbg 的 Hook 功能可以分为两大类:
- 内置的第三方 Hook 框架: Dobby(HookZz)、Whale、xHook 。
- 基于 Unicorn 的 Hook 机制:Unicorn 提供的底层 Hook 功能,以及 Unidbg 在其基础上封装的 Console Debugger。
HookZz
HookZz 现在叫 Dobby,Unidbg 中是 HookZz 和 Dobby 是两个独立的 Hook 库,因为作者认为 HookZz 在 arm32 上支持较好,Dobby 在 arm64 上支持较好。HookZz 是 inline hook 方案,因此可以 Hook Sub_xxx,缺点是短函数可能出 bug,受限于 inline Hook 原理
wrap
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| HookZz hook = HookZz.getInstance(emulator); hook.wrap(module.base + 0xC09D, new WrapCallback<RegisterContext>() { @Override public void preCall(Emulator<?> emulator, RegisterContext ctx, HookEntryInfo info) { Pointer input = ctx.getPointerArg(0); System.out.println(input.getString(0)); }
@Override public void postCall(Emulator<?> emulator, RegisterContext ctx, HookEntryInfo info) { Pointer result = ctx.getPointerArg(0); System.out.println(result.getString(0)); } });
|
replace
replace
函数的第三个参数enablePostCall
是一个Boolean
类型,其值表示是否使ReplaceCallback
的postCall
函数生效。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| HookZz hook = HookZz.getInstance(emulator); hook.replacemodule.findSymbolByName(""), new ReplaceCallback() { @Override public HookStatus onCall(Emulator<?> emulator, HookContext context, long originFunction) { emulator.getBackend().reg_write(Unicorn.UC_ARM_REG_R0,1); return HookStatus.RET(emulator,context.getLR()); }
@Override public void postCall(Emulator<?> emulator, HookContext context) { System.out.println("postCall!"); super.postCall(emulator, context); } },true);
|
instrument
1 2 3 4 5 6 7 8
| HookZz hook = HookZz.getInstance(emulator); hookZz.instrument(module.base + 0x00000F5C + 1, new InstrumentCallback<Arm32RegisterContext>() { @Override public void dbiCall(Emulator<?> emulator, Arm32RegisterContext ctx, HookEntryInfo info) { System.out.println("R3=" + ctx.getLongArg(3) + ", R10=0x" + Long.toHexString(ctx.getR10Long())); } });
|
Dobby
64位模式下推荐使用Dobby,不支持wrap
replace
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| Dobby dobby = Dobby.getInstance(emulator); dobby.replace(module.base+0xAC90, new ReplaceCallback() {
@Override public HookStatus onCall(Emulator<?> emulator, HookContext context, long originFunction) { Pointer result = context.getPointerArg(0); System.out.println("input:" + result.getString(0)); return super.onCall(emulator, context, originFunction); }
@Override public void postCall(Emulator<?> emulator, HookContext context) { super.postCall(emulator, context); } },true);
|
XHook
XHook 是一个针对 Android 平台 ELF 的 PLT (Procedure Linkage Table) hook 库,即它只能用于 hook 导入函数。符号表函数
1 2 3 4 5 6 7 8 9 10
| IxHook hook = XHookImpl.getInstance(emulator); hook.register("libutility.so", "free", new ReplaceCallback() { @Override public HookStatus onCall(Emulator<?> emulator, HookContext context, long originFunction) { System.out.println("free called!"); return HookStatus.RET(emulator,context.getLR()); } });
hook.refresh();
|
Whale
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| public void HookByWhale(){ IWhale whale = Whale.getInstance(emulator); whale.inlineHookFunction(module.findSymbolByName("base64_encode"), new ReplaceCallback() { Pointer buffer; @Override public HookStatus onCall(Emulator<?> emulator, long originFunction) { RegisterContext context = emulator.getContext(); Pointer input = context.getPointerArg(0); int length = context.getIntArg(1); buffer = context.getPointerArg(2); Inspector.inspect(input.getByteArray(0, length), "base64 input"); return HookStatus.RET(emulator, originFunction); }
@Override public void postCall(Emulator<?> emulator, HookContext context) { System.out.println("base64 result:"+buffer.getString(0)); } }, true); }
whale.importHookFunction(...);
whale.replace(...);
|
Unicorn Hook
如果想对某个函数进行集中的、高强度的、同时又灵活的调试,Unicorn CodeHook 是一个好选择。比如我想查看目标函数第一条指令的 r1,第二条指令的 r2,第三条指令的 r3,类似于这种需求。
hook_add_new 第一个参数是 Hook 回调,我们这里选择 CodeHook,它是逐条指令 Hook,参数 2 是起始地址,参数 3 是结束地址,参数 4 一般填 null。这意味着从起始地址到终止地址这个执行范围内的每条指令,我们都可以在其执行前处理它。
找到目标函数的代码范围
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
| public void HookByUnicorn(){ long start = module.base+0x97C; long end = module.base+0x97C+0x17A; emulator.getBackend().hook_add_new(new CodeHook() { @Override public void hook(Backend backend, long address, int size, Object user) { RegisterContext registerContext = emulator.getContext(); if(address == module.base + 0x97C){ int r0 = registerContext.getIntByReg(ArmConst.UC_ARM_REG_R0); System.out.println("0x97C 处 r0:"+Integer.toHexString(r0)); } }
@Override public void onAttach(Unicorn.UnHook unHook) { }
@Override public void detach() {
} }, start, end, null); }
|
SystemPropertyHook
为了方便处理应用访问设备属性(例如ro.build.id),Unidbg实现了SystemPropertyHook,使用方法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| SystemPropertyHook systemPropertyHook = new SystemPropertyHook(emulator); systemPropertyHook.setPropertyProvider(new SystemPropertyProvider() { @Override public String getProperty(String key) { switch (key){ case "ro.build.id":{ return "get id"; } case "ro.build.version.sdk":{ return "get sdk"; } } return null; } });
memory.addHookListener(systemPropertyHook);
|
打印函数调用栈
1 2
| emulator.getUnwinder().unwind(); emulator.printStackTrace();
|
特性 |
emulator.printStackTrace() |
emulator.getUnwinder().unwind() |
输出格式 |
自动格式化打印调用栈 |
返回调用栈的原始数据(StackFrame 列表) |
灵活性 |
固定格式,不可定制 |
可自定义解析、过滤或存储 |
适用场景 |
快速查看调用栈 |
深入分析调用栈或定制输出 |
是否符号化 |
自动解析符号表 |
依赖模块符号表 |
监控内存读写
Unidbg 提供了一个 TraceMemory
类,可以轻松实现对指定内存范围的访问跟踪。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| String traceFile = "myMonitorFile"; PrintStream traceStream = null; try { traceStream = new PrintStream(new FileOutputStream(traceFile), true); } catch (FileNotFoundException e) { e.printStackTrace(); return; }
emulator.traceRead(module.base, module.base + module.size).setRedirect(traceStream); emulator.traceWrite(module.base, module.base + module.size).setRedirect(traceStream);
|
作用:
- 对指定范围的内存访问进行详细追踪。
- 自动输出读写行为。
trace
traceCode
1 2 3 4 5 6 7 8 9
| String traceFile = "myTraceCodeFile"; PrintStream traceStream = null; try { traceStream = new PrintStream(new FileOutputStream(traceFile), true); } catch (FileNotFoundException e) { e.printStackTrace(); }
emulator.traceCode(module.base, module.base + module.size).setRedirect(traceStream);
|
要注意 trace 的时机,如果我们想 trace 从某个函数开始的执行流,那就让 traceCode 早于它执行即可。比如想 trace 从 JNI_OnLoad 开始的目标 SO 执行流,在如下的代码位置添加 trace 即可。
1 2 3 4 5 6
| DalvikModule dm = vm.loadLibrary("mx", true); module = dm.getModule(); security = vm.resolveClass("com/mengxiang/arch/security/MXSecurity");
emulator.traceCode(module.base, module.base + module.size); dm.callJNI_OnLoad(emulator);
|
如果想到更早的时机开启追踪,即追踪 init_proc、init_array 这些初始化函数的执行情况,那就需要将 traceCode 放到 loadLibrary 之前调用,但此时还没有获取到module
对象。因此最正确的处理办法是使用模块监听器,在模块加载的第一时间开始 trace 。
1 2 3 4 5 6 7 8 9 10 11 12
| memory.addModuleListener(new ModuleListener() { @Override public void onLoaded(Emulator<?> emulator, Module module) { if(module.name.equals("libmx.so")){ emulator.traceCode(module.base, module.base+module.size); } } }); DalvikModule dm = vm.loadLibrary("mx", true); module = dm.getModule(); security = vm.resolveClass("com/mengxiang/arch/security/MXSecurity"); dm.callJNI_OnLoad(emulator);
|
如果想只 trace 某个函数内的汇编,这里分为两种情况:
- 只关注某地址处的某一函数调用,而不关注于该函数在别处的调用
1 2 3 4 5 6 7 8
| .text:000000000000E530 MOV X0, X19 .text:000000000000E534 MOV X2, X21 .text:000000000000E538 LDR X1, [X8] ; "SHA1" .text:000000000000E53C BL ._Z6digestP7_JNIEnvPKcP11_jbyteArray ; digest(_JNIEnv *,char const*,_jbyteArray *) .text:000000000000E540 LDR X8, [X19] .text:000000000000E544 MOV X1, X0 .text:000000000000E548 LDR X2, [X8,#0x538] .text:000000000000E54C B loc_E55C
|
只关注 0xE53C 处的 digest 函数调用,那么需要在 0xE53C 开始 traceCode,0xE540 处停止 trace。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public void traceDigest(){ long callAddr = module.base + 0xE53C;
emulator.attach().addBreakPoint(callAddr, new BreakPointCallback() { @Override public boolean onHit(Emulator<?> emulator, long address) { traceHook = 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) { traceHook.stopTrace(); return true; } }); }
|
关注某一函数的所有调用,那么在进入该函数前开始 traceCode,离开该函数停止 trace。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| public void traceDigest(){ long callAddr = module.base + 0xd804;
emulator.attach().addBreakPoint(callAddr, new BreakPointCallback() { @Override public boolean onHit(Emulator<?> emulator, long address) { RegisterContext registerContext = emulator.getContext(); traceHook = emulator.traceCode(module.base, module.base+module.size); emulator.attach().addBreakPoint(registerContext.getLR(), new BreakPointCallback() { @Override public boolean onHit(Emulator<?> emulator, long address) { traceHook.stopTrace(); return true; } }); return true; } }); }
|
traceFunctionCall
1 2 3 4 5 6 7 8 9 10 11
| Debugger debugger = emulator.attach(); debugger.traceFunctionCall(module, new FunctionCallListener() { @Override public void onCall(Emulator<?> emulator, long callerAddress, long functionAddress) { } @Override public void postCall(Emulator<?> emulator, long callerAddress, long functionAddress, Number[] args) { System.out.println("onCallFinish caller=" + UnidbgPointer.pointer(emulator, callerAddress) + ", function=" + UnidbgPointer.pointer(emulator, functionAddress)); } });
|
日志系统
日志系统
Unidbg 中使用 Apache 的开源项目 log4j 和 commons-logging 处理日志。Unidbg 的绝大多数代码逻辑都提供了信息展示,但只在DEBUG
日志等级下才做输出打印。Unidbg 基于模块去管理输出,想了解哪部分日志,就指定具体的类为 DEBUG 等级。
1 2 3 4 5 6 7 8 9 10
| import org.apache.log4j.Level; import org.apache.log4j.Logger;
public static void main(String[] args) { NetWork nw = new NetWork(); Logger.getLogger(ARM32SyscallHandler.class).setLevel(Level.DEBUG); Logger.getLogger(AndroidSyscallHandler.class).setLevel(Level.DEBUG); String result = nw.callSign(); System.out.println("call s result:"+result); }
|
除了常规日志,Unidbg 还有另一套日志输出,主要打印 JNI 、Syscall 调用相关的内容。它和常规日志的输出有重叠,但内容更详细一些。我们通过 vm.setVerbose 开启或关闭它。在一般情况下,我们都会开启这个日志。
虚拟机日志
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public WeiBo() { emulator = AndroidEmulatorBuilder .for32Bit() .addBackendFactory(new Unicorn2Factory(true)) .setProcessName("com.weico.international") .build(); Memory memory = emulator.getMemory(); memory.setLibraryResolver(new AndroidResolver(23)); vm = emulator.createDalvikVM(new File("unidbg-android/src/test/resources/weibo/sinaInternational.apk")); vm.setJni(this); vm.setVerbose(false); DalvikModule dm = vm.loadLibrary("utility", true); WeiboSecurityUtils = vm.resolveClass("com/sina/weibo/security/WeiboSecurityUtils"); dm.callJNI_OnLoad(emulator); }
|
龙哥Unidbg Hook 大全 - Silas