别再纠结了!Unidbg和Frida在Android逆向中到底怎么选?一个实战案例给你讲透

张开发
2026/4/15 10:19:30 15 分钟阅读

分享文章

别再纠结了!Unidbg和Frida在Android逆向中到底怎么选?一个实战案例给你讲透
Unidbg与Frida实战抉择从算法还原到系统调用追踪的深度对比每次面对复杂的Android Native层逆向任务时工具选型总是让人纠结。Unidbg和Frida作为两大主流方案各自拥有独特的优势场景。本文将通过一个包含JNI交互、系统属性获取和管道调用的完整案例拆解两种工具在真实逆向工程中的表现差异。1. 核心能力定位与适用场景边界逆向工程师常陷入工具宗教战争的误区而忽略了一个基本事实没有万能工具只有最适合当前任务的工具选择。理解Unidbg和Frida的本质差异需要从设计哲学开始Unidbg是沙盒化模拟执行引擎通过虚拟化CPU指令集和系统环境让SO文件在隔离环境中运行。它的核心价值在于无需真机或root环境完全控制执行流程可任意修改内存、寄存器值完整的执行日志JNI调用链、系统调用序列Frida则是动态插桩框架通过注入JavaScript脚本实时干预目标进程。其优势体现在接近零环境适配成本直接附着到运行中的进程灵活的运行时Hook能力丰富的进程内存操作接口在算法还原任务中两种工具呈现出明显的场景分化评估维度Unidbg优势场景Frida优势场景环境复杂度需要完整模拟JNI/Syscall等环境只需简单Hook少数关键函数代码可见性黑盒SO需要观察完整执行流白盒分析已知算法实现交互复杂度涉及多层JNI回调或系统服务调用纯Native函数调用链调试需求需要指令级单步追踪只需输入输出监控表工具选型决策矩阵实际项目中我常采用混合策略先用Frida快速验证核心算法逻辑当遇到复杂环境依赖时再用Unidbg进行深度分析。这种渐进式方案能有效平衡效率与深度。2. 系统属性获取的三种方式与工具应对Android系统属性访问是Native层常见操作也是检验工具能力的试金石。以下通过三种典型实现方式对比工具的实际表现2.1 JNI路径的属性获取当SO通过android.os.Build类获取属性时两种工具的处理差异显著Frida方案Interceptor.attach(Module.findExportByName(libnative.so, Java_com_example_getSerial), { onEnter: function(args) { console.log(JNIEnv-, args[0]); console.log(jobject-, args[1]); }, onLeave: function(retval) { console.log(Return-, retval.readUtf8String()); } });优势直接附着到目标进程无需关心JNI环境初始化局限难以修改返回值对复杂JNI交互支持有限Unidbg方案Override public DvmObject? callStaticObjectMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) { if(signature.equals(android/os/Build-getSerial()Ljava/lang/String;)) { return new StringObject(vm, custom_serial_123); } return super.callStaticObjectMethodV(vm, dvmClass, signature, vaList); }优势可完全控制返回值支持深层次JNI调用链成本需要实现完整的JNI环境模拟提示当样本存在反调试时Unidbg的隔离环境往往能绕过常规检测而Frida可能需要额外对抗措施2.2 __system_property_get的系统调用通过libc接口获取属性时工具面临不同的挑战Frida拦截方案const system_property_get Module.findExportByName(libc.so, __system_property_get); Interceptor.replace(system_property_get, new NativeCallback((keyPtr, valuePtr) { const key keyPtr.readUtf8String(); Memory.writeUtf8String(valuePtr, key ro.build.id ? custom_build_id : ); return 0; }, int, [pointer, pointer]));Unidbg定制处理SystemPropertyHook hook new SystemPropertyHook(emulator); hook.setPropertyProvider((key) - { switch(key) { case ro.build.id: return emulated_build_123; default: throw new UnsupportedOperationException(key); } }); memory.addHookListener(hook);在性能消耗方面实测数据显示操作类型Frida平均耗时(ms)Unidbg平均耗时(ms)单次属性读取0.121.85批量属性读取(50次)6.492.7表系统属性访问性能对比基于Galaxy S20实测2.3 popen管道调用的特殊处理通过shell命令获取属性是最复杂的情况需要处理进程创建和管道通信Frida动态追踪const popen Module.findExportByName(libc.so, popen); Interceptor.attach(popen, { onEnter(args) { this.command args[0].readUtf8String(); console.log(Executing: ${this.command}); }, onLeave(retval) { if(!retval.isNull()) { console.log(hexdump(retval, { length: 32 })); } } });Unidbg系统调用重定向Override protected boolean handleUnknownSyscall(Emulator? emulator, int NR) { if(NR 190) { // vfork Arm32RegisterContext ctx (Arm32RegisterContext)emulator.getContext(); ctx.setR0(emulator.getPid() 100); // 模拟子进程PID return true; } return super.handleUnknownSyscall(emulator, NR); }管道场景下Unidbg需要实现完整的文件描述符管理private void pipe2(Emulator? emulator) { EditableArm32RegisterContext ctx (EditableArm32RegisterContext)emulator.getContext(); Pointer pipefd ctx.getPointerArg(0); int readFd fdManager.alloc(); int writeFd fdManager.alloc(); fdMap.put(readFd, new ByteArrayFileIO(0, pipe_read, custom_value\n.getBytes())); fdMap.put(writeFd, new DumpFileIO(writeFd)); pipefd.setInt(0, readFd); pipefd.setInt(4, writeFd); ctx.setR0(0); // 返回成功 }3. Hook能力与调试接口的实战对比逆向工程的核心是对运行时的控制能力两种工具提供了不同层次的Hook实现3.1 函数级Hook对比Frida的Interceptor示例const targetFunc Module.findExportByName(libcrypto.so, SHA1_Update); Interceptor.attach(targetFunc, { onEnter(args) { this.inputPtr args[1]; this.len args[2].toInt32(); }, onLeave(retval) { const input this.inputPtr.readByteArray(this.len); console.log(SHA1 input:, bytesToHex(input)); } });Unidbg的HookZz实现hookZz.wrap(module.base 0x1234, new WrapCallbackHookZzArm32RegisterContext() { Override public void preCall(Emulator? emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) { Pointer input ctx.getPointerArg(1); int len ctx.getIntArg(2); ctx.push(input.getByteArray(0, len)); // 保存输入 } Override public void postCall(Emulator? emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) { byte[] input ctx.pop(); Inspector.inspect(input, SHA1 input); } });关键差异点参数访问Frida直接读取指针内容Unidbg需要显式内存操作线程安全Unidbg在单线程模拟环境中无需考虑并发问题性能影响Frida的JavaScript引擎会引入额外开销3.2 调试器集成能力Unidbg内置的ConsoleDebugger提供了独特的优势# 启动调试器 debugger emulator.attach() debugger.addBreakpoint(module.base 0x5678) # 交互式命令 debugger.send(reg read r0-r3) debugger.send(x/10x $sp)相比之下Frida需要结合其他工具链# 使用frida-trace生成桩代码 frida-trace -U -i open -i read com.target.app # 通过frida-server远程调试 frida -H 192.168.1.100:27042 -n App Process在逆向某金融类APP的加密算法时Unidbg的指令级追踪帮助我发现了隐藏在ARMv7指令集下的花指令0x7ef00f44: e1a00000 nop ; 实际为无效指令 0x7ef00f48: e12fff1e bx lr ; 函数返回 0x7ef00f4c: e320f000 nop ; 对齐填充4. 复杂样本的联合分析策略真实场景中我推荐采用分阶段联合分析方案快速原型阶段使用Frida进行函数级Hook快速定位关键算法模块// 监控所有JNI调用 Java.perform(() { const JNIEnv Java.vm.getEnv(); Interceptor.attach(JNIEnv.GetMethodID, { onEnter(args) { console.log(GetMethodID: ${args[2].readUtf8String()}); } }); });深度分析阶段对复杂环境依赖部分移植到Unidbg进行全流程追踪// 配置Unidbg模拟环境 emulator.getSyscallHandler().addIOResolver((path, oflags) - { if(path.endsWith(/dev/urandom)) { return new RandomFileIO(oflags); // 重定向随机数源 } return null; });验证阶段对比两种工具的输出结果确保分析一致性# 交叉验证算法输出 frida_result get_frida_output() unidbg_result get_unidbg_output() assert frida_result unidbg_result, 结果不一致需重新分析在最近分析的某IoT设备通信协议中这种方案成功解决了以下难题Frida快速定位到密钥生成函数Unidbg还原了依赖特定系统属性的初始化流程联合验证发现了厂商自定义的系统调用工具的选择最终取决于具体需求。当需要快速验证假设时Frida的低门槛优势明显而面对复杂的黑盒分析Unidbg的确定性执行环境往往能揭开更深层的秘密。

更多文章