一、unidbg介绍 1.1 unidbg unidbg 是一个基于 unicorn 的逆向工具,可以黑盒调用安卓和 iOS 中的 so 文件。这使得逆向人员可以在无需了解 so 内部算法原理的情况下,主动调用 so 中的函数,让其中的算法“为我所用”,只需要传入所需的参数、补全运行所需的环境,即可运行出所需要的结果。及由此衍生的辅助分析、算法还原、SO 调试与逆向等等功能。
对于Android逆向来说,unidbg的特点有以下几种:
模拟 JNI 调用的 API,因此可以调用JNI_OnLoad
函数。
支持JavaVM
和JNIEnv
。
支持模拟系统调用指令。
支持ARM32
和ARM64
。
支持基于Dobby
的inline hook。
支持基于xHook
的GOT hook。
unicorn
后端支持简单的控制台调试器,gdb stub,指令追踪和内存读写追踪。
支持dynarmic
快速的后端。
1.2 unicorn Unicorn 是一个轻量级, 多平台, 多架构的 CPU 模拟器框架,可支持的架构包括 x86、ARM、MIPS 和 PowerPC 等,是一个强大的逆向工程和动态分析工具。
二、环境搭建 unidbg 项目地址:https://github.com/zhkl0228/unidbg
三、unidbg入门用例 3.1 基本框架 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 TTEncrypt { private final AndroidEmulator emulator; private final VM vm; private final Module module ; private final DvmClass TTEncryptUtils; TTEncrypt(){ emulator = AndroidEmulatorBuilder.for32Bit() .addBackendFactory(new DynarmicFactory (true )) .setProcessName("com.qidian.dldl.official" ) .build(); Memory memory = emulator.getMemory(); memory.setLibraryResolver(new AndroidResolver (23 )); vm = emulator.createDalvikVM(new File ("sssss.apk" )); vm.setVerbose(true ); 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" ); } }
3.2 unidbg中的函数 3.2.1 AndroidEmulator相关函数 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 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 dalvikVM1 = emulator.createDalvikVM(new File ("path of apkfile" )); VM dalvikVM2 = emulator.getDalvikVM(); emulator.showRegs(); Backend backend = emulator.getBackend(); String processName = emulator.getProcessName(); RegisterContext context = emulator.getContext(); emulator.traceRead(long begin, long end [, TraceReadListener listener]); emulator.traceWrite(long begin, long end [, TraceWriteListener listener]); emulator.traceCode(long begin, long end [, TraceCodeListener listener]);boolean running = emulator.isRunning();
3.2.2 Memory相关函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 Memory memory = emulator.getMemory(); memory.setLibraryResolver(new AndroidResolver (23 )); UnidbgPointer pointer = memory.pointer(0x11111111 ); Collection<MemoryMap> memoryMap = memory.getMemoryMap(); Module sss = memory.findModule("module name" ); Module moduleByAddress = memory.findModuleByAddress(0x111111 );
3.2.3 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 27 VM vm = emulator.createDalvikVM(new File ("path of apk" )); vm.setVerbose(true ); DalvikModule dalvikModule = vm.loadLibrary(new File ("path of so file" ), true ); vm.setJni(this ); Pointer jniEnv = vm.getJNIEnv(); Pointer javaVM = vm.getJavaVM(); dalvikModule.callJNI_OnLoad(emulator); vm.callJNI_OnLoad(emulator,dalvikModule.getModule());int dvmObjHash = vm.addGlobalObject(dvmObj); DvmObject<?> object = vm.getObject(dvmObjHash);
3.2.4 符号调用 1 2 3 4 5 6 DvmObject<?> obj = vm.resolveClass("com/example/demo01/MainActivity" ).newObject(null );String signSting = "123456" ;DvmObject dvmObject = obj.callJniMethodObject(emulator, "jniMd52([B)Ljava/lang/String;" , signSting.getBytes(StandardCharsets.UTF_8));String result = (String) dvmObject.getValue(); System.out.println("[symble] Call the so md5 function result is ==> " + result);
3.2.5 地址调用 1 2 3 4 5 6 7 8 9 10 11 ArrayList<Object> args = new ArrayList <>();Pointer jniEnv = vm.getJNIEnv();DvmObject object1 = ProxyDvmObject.createObject(vm, this ); args.add(jniEnv); args.add(null ); args.add(vm.addLocalObject(new StringObject (vm, "123456" )));Number number = module .callFunction(emulator, 0x11AE8 + 1 , args.toArray()); System.out.println("[addr] number is ==> " + number.intValue()); DvmObject<?> object = vm.getObject(number.intValue()); System.out.println("[addr] Call the so md5 function result is ==> " + object.getValue());
四、unidbg中的Hook Unidbg 支持的Hook可以分为两大类:
Unidbg 内置的第三方Hook框架,包括xHook/Whale/HookZz/Dobby
Unicorn Hook 以及 Unidbg基于它封装的Console Debugger
第一类是Unidbg支持并内置的第三方Hook框架,有Dobby(前身HookZz)/Whale这样的Inline Hook框架,也有xHook这样的PLT Hook 框架。
第二类是当Unidbg的底层引擎选择为Unicorn时(默认引擎),Unicorn自带的Hook功能。Unicorn提供了各种级别和粒度的Hook,内存Hook/指令/基本块 Hook/异常Hook 等等,十分强大和好用,而且Unidbg基于它封装了更便于使用的Console Debugger。
4.1 HookZz HookZz现在叫Dobby。Unidbg中是HookZz和Dobby是两个独立的Hook库,因为作者认为HookZz在arm32上支持较好,Dobby在arm64上支持较好。HookZz是inline hook方案,因此可以Hook Sub_xxx,缺点是短函数可能出bug,受限于inline Hook 原理。
method hook
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 IHookZz hookZz = HookZz.getInstance(emulator); 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.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 )); } },true ); hookZz.replace(module .findSymbolByName("Java_com_example_demo_MainActivity_stringFromJNI" ), new ReplaceCallback () { @Override public HookStatus onCall (Emulator<?> emulator, long originFunction) { System.out.println("stringFromJNI call pre" ); return super .onCall(emulator, originFunction); } @Override public void postCall (Emulator<?> emulator, HookContext context) { System.out.println("stringFromJNI call after" ); super .postCall(emulator, context); } }, true );
其中replace
函数的第三个参数enablePostCall
是一个Boolean
类型,其值表示是否使ReplaceCallback
的postCall
函数生效。
inline hook
1 2 3 4 5 6 7 8 9 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())); } });
如果需要修改入参或者返回结果,需要将 context 转成 EditableArm64RegisterContext 或 EditableArm32RegisterContext。例如:
1 2 3 4 5 6 7 8 9 Symbol lrand48 = module .findSymbolByName("lrand48" ); hookZz.replace(lrand48, new ReplaceCallback () { @Override public void postCall (Emulator<?> emulator, HookContext context) { EditableArm32RegisterContext ctx = emulator.getContext(); log.info("lrand48 return:" + ctx.getIntArg(0 )); ctx.setR0(1 ); } }, true );
4.2 Dobby Dobby的前身是HookZz,具备HookZz的功能。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 Dobby dobby = Dobby.getInstance(emulator);Symbol stringFromJNI = module .findSymbolByName("Java_com_example_demo_MainActivity_stringFromJNI" ); dobby.replace(stringFromJNI, new ReplaceCallback () { @Override public HookStatus onCall (Emulator<?> emulator, long originFunction) { System.out.println("Doddy replace hooked" ); return super .onCall(emulator, originFunction); } @Override public void postCall (Emulator<?> emulator, HookContext context) { System.out.println("stringFromJNI finished" ); super .postCall(emulator, context); } },true ); dobby.instrument(...)
4.3 XHook XHook 是一个针对 Android 平台 ELF 的 PLT (Procedure Linkage Table) hook 库,即它只能用于 hook 导入函数 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 IxHook xHook = XHookImpl.getInstance(emulator); xHook.register("libttEncrypt.so" , "strlen" , new ReplaceCallback () { @Override public HookStatus onCall (Emulator<?> emulator, HookContext context, long originFunction) { Pointer pointer = context.getPointerArg(0 ); String str = pointer.getString(0 ); System.out.println("strlen=" + str); context.push(str); return HookStatus.RET(emulator, originFunction); } @Override public void postCall (Emulator<?> emulator, HookContext context) { System.out.println("strlen=" + context.pop() + ", ret=" + context.getIntArg(0 )); } }, true ); xHook.refresh();
4.4 Whale Whale 支持 inline hooking 和 method hooking。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 IWhale whale = Whale.getInstance(emulator);Symbol free = memory.findModule("libc.so" ).findSymbolByName("free" ); whale.inlineHookFunction(free, new ReplaceCallback () { @Override public HookStatus onCall (Emulator<?> emulator, long originFunction) { System.out.println("WInlineHookFunction free = " + emulator.getContext().getPointerArg(0 )); long addr = emulator.getContext().getPointerArg(0 ).peer; if (addr == 0x12175000 || addr == 0x12176000 ){ return HookStatus.LR(emulator, 0 ); } else { return HookStatus.RET(emulator,originFunction); } } }); whale.importHookFunction(...); whale.replace(...);
4.5 UnicornHook Unidbg 是基于 Unicorn 的项目,因此也具备 UnicornHook,Unicorn原生的Hook功能强大,而且不容易被检测。使用UnicornHook进行inline hook时也不需要考虑是否 + 1,会自动转换。
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 emulator.getBackend().hook_add_new(new CodeHook () { @Override public void hook (Backend backend, long address, int size, Object user) { RegisterContext context = emulator.getContext(); if (address == module .base + 0x1FF4 ){ Pointer md5Ctx = context.getPointerArg(0 ); Inspector.inspect(md5Ctx.getByteArray(0 , 32 ), "md5Ctx" ); Pointer plainText = context.getPointerArg(1 ); int length = context.getIntArg(2 ); Inspector.inspect(plainText.getByteArray(0 , length), "plainText" ); }else if (address == module .base + 0x2004 ){ Pointer cipherText = context.getPointerArg(1 ); Inspector.inspect(cipherText.getByteArray(0 , 16 ), "cipherText" ); } } @Override public void onAttach (UnHook unHook) { System.out.println("attach" ); } @Override public void detach () { System.out.println("detach" ); } }, module .base + 0x1FE8 , module .base + 0x2004 , emulator)
4.6 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);
五、打印函数调用栈 hook目标函数,添加如下代码:
1 emulator.getUnwinder().unwind();
输出示例:
1 2 3 4 5 [0x040000000][0x040001ff4][libxiaojianbang.so][0x01ff4]Java_com_xiaojianbang_ndk_NativeHelper_md5 + 0xc8 [0x040000000][0x040003b24][Libxiaojianbang.so][0x03b24]MD5Final(MD5_CTX*,unsigned char*) + 0xac [0x040000000][0x040002000][Libxiaojianbang.so][0x02000]Java_com_xiaojianbang_ndk_NativeHelper_md5 + 0xd4 [0x040000000][0x040003b34][Libxiaojianbang.so][0x03b34]MD5Final(MD5_CTX*,unsigned char*) + 0xbc [0x040000000][0x040002000][Libxiaojianbang.so][0x02000]Java_com_xiaojianbang_ndk_NativeHelper_md5 + 0xd4
六、Console Debugger 1 2 Debugger attach = emulator.attach(); attach.addBreakPoint(module .base + 0xC365 );
同样也是基于unicorn的console debugger,因此断点位置不需要考虑是否 + 1,会自动转换。
console模式下支持如下指令:
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 c : 继续 n: 跨过 s|si:步入 bt:回溯 st hex:搜索堆栈 shw hex:搜索可写堆 shr hex:搜索可读堆 shx-hex:搜索可执行堆 m(op)[size]:显示内存,默认大小为0x70 ,大小可以是十六进制或十进制 mr0 -mr7 ,mfp,mip,msp[size]:显示指定寄存器的内存 m(address)[size]:显示指定地址的内存,地址必须以0 x 开头 wr0 -wr7 ,wfp,wip,wsp<value>:写入指定寄存器 wb(address), ws(address), wi(address)<value>:写入指定地址的(字节、短、整数)内存,地址必须以0 x 开头 wx(address)<hex>:将字节写入指定地址的内存,地址必须以0 x 开头 b(address):添加临时断点,地址必须以0 x 开头,可以是模块偏移量 b: 添加寄存器PC的断点 r: 删除寄存器PC的断点 blr:添加寄存器LR的临时断点 p (assembly):位于PC地址的修补程序集where: 显示java堆栈跟踪 trace[begin -end ]:设置跟踪指令 traceRead[begin -end ]:设置跟踪内存读取 traceWrite〔begin -end 〕:设置跟踪内存写入 vm:查看加载的模块 vbs:查看断点 d|dis:显示反汇编 d(0 x ):在指定地址显示反汇编stop: 停止模拟 run[arg]:运行测试gc :运行System.gc ()threads: 显示线程列表cc size:将asm 从PC-PC+size字节转换为c 函数
七、监控内存读写 1 2 3 4 5 6 7 8 9 10 String traceFile = "myMonitorFile" ;PrintStream traceStream = null ;try { traceStream = new PrintStream (new FileOutputStream (traceFile), true ); } catch (FileNotFoundException e) { e.printStackTrace(); } emulator.traceRead(module .base, module .base + module .size).setRedirect(traceStream); emulator.traceWrite(module .base, module .base + module .size).setRedirect(traceStream);
输出的信息格式:
1 [16 :39 :35 404 ] Memory READ at 0x12033d94 , data size = 4 , data value = 0x12151ec0 , PC=RX@0x1203116c [libszstone. so]0x3116c , LR=RX@0x1212f617 [libc. so]0x58617
主要是读的位置、读了几个字节、读了什么值、发生读操作的地址、读操作完成后的返回地址。
八、trace 8.1 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 ; } }); }
8.2 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)); } });
两个时机都可以访问 callerAddress 和 functionAddress,postCall 时机还可以访问 args,即参数。
打印结果如下
1 2 3 onCallFinish caller=RX@0x4000d954[libmx.so]0xd954, function=RX@0x4000dab4[libmx.so]0xdab4 onCallFinish caller=RX@0x4000db30[libmx.so]0xdb30, function=RX@0x4000d620[libmx.so]0xd620 onCallFinish caller=RX@0x4000d954[libmx.so]0xd954, function=RX@0x4000dab4[libmx.so]0xdab4
九、日志系统 9.1 日志系统 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); }
比如上面的代码,就是对 ARM32 下的系统调用类、系统调用父类做日志输出。
如果希望输出所有模块的日志,可以修改unidbg-android/src/test/resources/log4j.properties
文件,将 unidbg 的日志等级从 INFO 改为 DEBUG。
1 2 3 4 # 修改前 log4j.logger.com.github.unidbg=INFO # 修改后 log4j.logger.com.github.unidbg=DEBUG
那么不管是系统调用、指针管理、多线程,还是多线程处理器等等,所有组件的日志都会打印出来。
但一般不会这么做,过多过杂的日志输出对使用者并无益处,还会拖累 Unidbg 执行的速度。
9.2 虚拟机日志 除了常规日志,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 大全 - SeeFlowerX
https://whitebird0.github.io/post/unidbg%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0.html
[原创]unidbg入门笔记-Android安全-看雪-安全社区|安全招聘|kanxue.com
[Unidbg 的日志系统](https://www.yuque.com/lilac-2hqvv/xdwlsg/yaxqgh?# 《Unidbg 的日志系统》)