1. 项目概述一次从应用层到Native层的深度逆向之旅最近在分析某手App的某个接口时遇到了一个老朋友sig3参数。这个参数对于经常做移动端逆向的朋友来说应该不陌生它往往是客户端请求签名体系中的关键一环用于验证请求的合法性与时效性。与常见的纯Java层实现的签名逻辑不同这次遇到的sig3生成逻辑其核心部分被下沉到了Android的Native层即C/C代码编译的.so动态库中。这意味着传统的基于Java层Hook如Xposed、Frida的Java API的分析方法在这里会“碰壁”你无法直接拦截到计算sig3的Java方法调用因为真正的计算发生在更底层的Native代码里。这恰恰是当前移动安全对抗的一个典型缩影。为了提升关键逻辑如加密算法、核心校验、风控策略的安全性越来越多的应用选择将这部分代码用C/C编写并编译成Native库。这样做的好处显而易见Native代码的反编译和逆向分析难度远高于Java字节码混淆和加固手段也更为有效能极大增加攻击者的分析成本。因此掌握Android Native层的Hook技术就成为了逆向工程师必须点亮的技能树。本次实战我们的目标非常明确定位并分析某手App中生成sig3参数的Native函数并成功Hook它获取其输入输出甚至动态修改其行为。围绕这个目标我将重点分享两种在实战中最高效、最实用的Native Hook姿势基于Frida的Interceptor和基于Cydia Substrate或它的现代继任者。这两种方法各有优劣适用场景也不同我会结合本次sig3的逆向过程详细拆解它们的原理、操作步骤以及我踩过的那些坑。无论你是刚刚接触Native逆向的新手还是想深化理解的老兵相信这篇从实战出发的总结都能给你带来直接的帮助。2. 逆向目标分析与环境搭建在开始Hook之前盲目动手是大忌。我们需要先对目标有一个清晰的认知并准备好称手的“兵器”。2.1 sig3参数与Native层逻辑定位sig3参数通常出现在App的网络请求头或POST数据中是一串看起来像Base64或Hex编码的长字符串。我们的第一步是确认它的生成确实涉及Native层。定位方法动态抓包与静态搜索使用抓包工具如Charles、Fiddler或mitmproxy捕获包含sig3的请求。然后将目标App的APK文件解包使用jadx-gui等工具反编译其Java代码。在Java代码中全局搜索“sig3”。你可能会发现一些线索比如一个名为NativeSignatureHelper的类其中包含public static native String generateSig3(String param)这样的方法声明。关键字native就是铁证表明该方法的实现在Native库中。分析so库根据上一步找到的类名和方法名我们可以确定需要分析的Native库。在APK的lib/目录下或解压后的lib/armeabi-v7a、lib/arm64-v8a等找到对应的.so文件。通常库名与包名或类名相关比如libsignature.so或libcore.so。确定目标函数符号Native函数在.so库中通过“符号”来标识。对于JNIJava Native Interface函数其符号名遵循特定的格式Java_{包名_类名}_{方法名}。例如如果完整类名是com.kwai.signature.NativeHelper方法名是generateSig3那么对应的JNI函数符号可能就是Java_com_kwai_signature_NativeHelper_generateSig3。这是我们后续Hook的关键标识。在本次实战中我们假设通过上述方法已经定位到目标函数位于libkwai_signature.so中函数符号为Java_com_kwai_security_SigUtil_calcSig3。2.2 核心工具链准备工欲善其事必先利其器。以下是本次实战的核心工具建议在开始前配置好环境。1. Frida动态注入的瑞士军刀Frida是一个强大的动态代码插桩框架它允许你将JavaScript代码片段注入到目标进程包括Android App中从而Hook函数、读写内存、调用函数等。对于Native Hook我们主要使用它的Interceptor模块。安装在电脑端Python环境执行pip install frida-tools。服务端需要将对应架构的frida-server推送到Android设备并运行。这是Frida与设备上目标进程通信的桥梁。优势脚本化、跨平台、支持同时Hook Java和Native非常适合快速验证和动态分析。2. Cydia Substrate / Frida-Gadget更底层的Hook方案Cydia Substrate及其Android版本是一个更老牌、更底层的代码修改框架。它通过直接修改目标进程的内存来实现Hook。虽然原版Substrate在Android高版本上支持有限但其思想和一些现代实现如某些定制ROM或工具链依然有参考价值。更常见的替代方案是使用Frida-Gadget。Gadget是一个.so库你可以将它注入到目标App中然后通过Frida的frida-gadget模式进行连接和脚本注入这在某些对抗Frida-server的场景下有用。与Frida-Interceptor的区别Substrate/Gadget的Hook发生在更早的阶段有时能绕过基于检测ptraceFrida-server使用的反调试。但配置相对复杂。3. 反汇编与调试器IDA Pro / Ghidra用于静态分析.so文件理解目标函数的逻辑、参数和返回值。这是逆向工程的基石。Ghidra是免费且强大的替代品。adb (Android Debug Bridge)与Android设备通信的必备工具用于安装应用、推送文件、端口转发等。4. 测试设备一台已经Root的Android物理手机或模拟器如Genymotion、官方模拟器。Root权限是进行深度Hook和内存操作通常所必需的。注意环境配置的坑架构匹配确保电脑端的Frida版本与手机端的frida-server版本一致并且frida-server的架构arm, arm64, x86等与你的手机CPU架构匹配。不匹配会导致连接失败。端口冲突与权限运行frida-server后使用adb shell进入设备先su获取root权限再./frida-server 运行。确保设备的防火墙或安全软件没有阻止Frida的通信端口默认27042。应用可调试对于非系统应用可能需要修改其AndroidManifest.xml中的android:debuggabletrue并重新打包签名才能使用某些调试特性。但在仅使用Frida进行Hook时Root权限通常已足够。3. 姿势一使用Frida Interceptor进行精准HookFrida的Interceptor API是进行Native Hook最直接、最常用的方法。它允许你在函数被调用时和执行后插入自己的代码从而观察和修改函数的输入输出。3.1 Frida Interceptor Hook原理与脚本结构Interceptor.attach(targetAddress, callbacks)是核心API。它的工作原理是Frida在目标函数的开头注入一段跳转代码trampoline将执行流重定向到Frida的控制代码在执行完用户定义的回调onEnter,onLeave后再跳转回原函数继续执行或直接返回。一个典型的Frida Native Hook脚本结构如下// hook_native_sig3.js Java.perform(function () { // 首先如果需要可以同时Hook Java层的相关方法作为切入点 // var SigUtil Java.use(com.kwai.security.SigUtil); // SigUtil.calcSig3.implementation function (param) { // console.log(Java层 calcSig3 被调用参数: ${param}); // var result this.calcSig3(param); // console.log(Java层 calcSig3 返回: ${result}); // return result; // }; // 重点Hook Native函数 // 方法1通过模块名和函数符号名查找地址推荐 var nativeFuncAddr Module.findExportByName(libkwai_signature.so, Java_com_kwai_security_SigUtil_calcSig3); // 方法2如果符号被剥离可能需要通过偏移量或特征码定位更复杂 // var baseAddr Module.findBaseAddress(libkwai_signature.so); // var nativeFuncAddr baseAddr.add(0x1234); // 假设偏移量0x1234 if (nativeFuncAddr) { console.log([*] 找到目标Native函数地址: ${nativeFuncAddr}); Interceptor.attach(nativeFuncAddr, { // 函数被调用时进入 onEnter: function (args) { console.log(\n[] Native calcSig3 被调用); // args[0] 是 JNIEnv* // args[1] 是 jobject this // args[2] 是第一个参数jstring对应Java层的String param var jniEnv args[0]; var thisObj args[1]; var inputStr args[2]; // 将jstring转换为JavaScript字符串 var inputCStr Java.vm.getEnv().getStringUtfChars(inputStr, null).readCString(); console.log([] 输入参数: ${inputCStr}); // 保存到上下文供onLeave使用 this.input inputCStr; // 你也可以在这里修改args[2]来改变输入需要构造新的jstring }, // 函数执行完毕即将返回时进入 onLeave: function (retval) { // retval 是返回值对于返回String的JNI函数是jstring类型 var resultJStr retval; var resultCStr Java.vm.getEnv().getStringUtfChars(resultJStr, null).readCString(); console.log([] 函数返回值 (sig3): ${resultCStr}); console.log([] 完整调用: ${this.input} - ${resultCStr}); // 同样你可以修改retval来改变返回值 // var newRetVal ... 构造新的jstring // retval.replace(newRetVal); } }); console.log([*] Hook已安装成功); } else { console.log([!] 未找到目标函数可能库未加载或符号名错误。); } });3.2 实战Hook sig3生成函数将上述脚本保存为hook_sig3.js。接下来在电脑端执行# 首先确保frida-server已在设备上运行 adb shell su ./data/local/tmp/frida-server # 退出adb shell # 在电脑端使用Frida附加到目标App进程 frida -U -f com.kwai.video -l hook_sig3.js --no-pause # -U 连接到USB设备 # -f 启动指定包名的应用 # -l 加载脚本 # --no-pause 立即启动主线程如果一切顺利当App运行到调用calcSig3生成sig3时你的终端就会打印出详细的输入和输出信息。通过观察不同请求下的输入输出对你就能逐步分析出sig3的生成规律。实操心得与避坑指南JNI类型转换是难点onEnter中的args是Native类型的指针。处理jstring,jarray等JNI类型时必须通过Java.vm.getEnv()获取JNIEnv接口来正确转换。直接args[2].readCString()会导致崩溃或乱码。上面的示例展示了标准做法。参数索引args的确定对于JNI函数前两个参数固定是JNIEnv*和jobject/jclass。真正的业务参数从args[2]开始。你需要根据目标函数的签名来确定参数个数和类型。可以通过静态分析IDA中的函数原型或参考JNI文档。so库的加载时机你的脚本必须在目标.so库被加载到进程内存之后执行。如果Hook得太早库还没加载Module.findExportByName会返回null。有几种策略使用setImmediate或Java.perform确保在App启动后执行。使用Module.load事件监听器Module.on(loaded, function(module){...})在特定库加载时自动安装Hook。先Hook一个Java方法在那个Java方法里确保Native库已加载再执行Native Hook。多线程与调用栈sig3生成可能在网络线程中被调用。使用Thread.backtrace(this.context, Backtracer.ACCURATE)可以在onEnter或onLeave中打印调用栈帮助你理解函数的调用路径这对于复杂逻辑分析至关重要。修改参数与返回值在onEnter中你可以通过修改args数组的元素来改变传入参数。在onLeave中通过retval.replace(newValue)来修改返回值。务必注意内存管理如果你创建了新的JNI对象如NewStringUTF要避免内存泄漏但通常替换返回值时Frida可能会处理一些上下文。4. 姿势二使用Cydia Substrate或Frida-Gadget进行底层注入当面对一些加强了反调试、检测Frida的App时直接使用frida-server附加进程可能会失败进程崩溃、无法附加。这时我们可以考虑更底层的注入方式。4.1 Cydia SubstrateMSHookFunction原理Cydia Substrate的核心是MSHookFunction函数。它的原理是直接对目标函数的内存进行写操作将函数开头的几条指令替换为跳转指令如ARM的B或BLX跳转到我们自定义的替换函数hook函数。在hook函数里我们执行自己的逻辑然后再选择是否调用原函数被保存下来的原始指令块。这种方式比Frida的Interceptor更底层不依赖Frida的运行时注入机制因此有时能绕过基于ptrace或/proc/pid/status检测的防护。然而原版Cydia Substrate对Android高版本的支持不佳且需要将Hook代码编译成另一个.so库通过LD_PRELOAD或修改init.rc等方式注入过程繁琐。因此在当今的实战中我们更多是借鉴其思想或者使用Frida-Gadget模式。4.2 使用Frida-Gadget实现免Server注入Frida-Gadget是一个动态库libfrida-gadget.so。你可以将它打包进目标APK或者注入到目标进程的内存中。当它被加载时会监听一个端口或Unix Socket等待来自外部的Frida控制端如frida命令行工具连接。这样就不需要在设备上运行一个独立的frida-server进程降低了被检测的风险。操作步骤获取Gadget库从Frida官网下载对应设备架构的libfrida-gadget.so。注入到APK重新打包解压APK将libfrida-gadget.so放入对应的lib/armeabi-v7a/等目录。修改AndroidManifest.xml确保android:extractNativeLibstrue如果为false需要修改为true或使用其他加载方式。然后重新打包并签名。修改so加载路径更常见的方法是不修改APK而是通过ptrace或LD_PRELOAD在进程启动时让目标进程动态加载libfrida-gadget.so。有现成的工具如objectionobjection patchapk命令可以自动化完成部分工作或者使用frida的--gadget模式配合定制脚本。编写脚本与连接Hook脚本JavaScript可以嵌入到Gadget配置中或者仍然通过外部连接后加载。连接时不再使用-UUSB而是使用-HHost指定Gadget监听的网络地址和端口。# 假设Gadget配置为监听本机TCP端口27042 frida -H 127.0.0.1:27042 -l hook_sig3.js实操心得与避坑指南复杂性高相比frida-serverGadget模式的配置和注入过程复杂得多需要对APK结构、动态链接过程有更深的理解。对于新手建议先熟练掌握frida-server模式。稳定性考量强行修改APK或注入so可能导致应用崩溃或功能异常尤其是当应用本身有完整性校验如签名校验、文件哈希校验时。对抗升级一些高级别的安全防护同样会检测非常规的so库如libfrida-gadget.so或非常规的线程/端口。这是一个持续的对抗过程。选择时机不要盲目使用Gadget。绝大多数情况下frida-server模式已经足够。只有当目标App明确检测并阻止了frida-server的附加时才考虑升级到Gadget模式或其他更隐蔽的Hook方案。5. 逆向分析与结果验证无论采用哪种Hook姿势成功Hook到目标函数只是第一步。我们最终目的是理解sig3的生成算法。5.1 通过Hook数据推断算法逻辑在成功拦截到多次calcSig3的调用后你会收集到一系列输入输出对。例如输入“/api/feedts1234567890”- 输出“aBcDeFgHiJkL...123”输入“/api/userts1234567891”- 输出“xYzAbCdEfGh...456”分析方法观察输入输出长度关系输出长度是否固定是否随输入长度线性增长这能提示是否是哈希固定长度或加密分组密码输出与输入有关。变化部分分析固定一部分输入如路径/api/feed只改变ts时间戳观察输出变化。如果ts变化一位输出完全不同且无规律很可能使用了哈希如MD5、SHA或带随机数的加密。寻找关键常量或密钥在Hook的基础上你可以进一步深入Native函数内部。使用Frida的Memory.scan搜索特定字节序列如常见的哈希初始化常量、AES的S盒等或者直接使用IDA静态分析calcSig3函数汇编代码结合动态Hook看到的输入输出推断其算法。你可能会发现它调用了OpenSSL或BoringSSL库中的HMAC_sha256或AES_encrypt等函数。参数追踪除了明文的字符串输入函数可能还依赖其他隐式参数如存储在Native层的密钥、设备唯一标识符等。这些可能需要你Hook更底层的函数如密钥读取函数或分析全局变量。5.2 算法复现与校验基于分析你可能会得出一个假设“sig3 Base64Encode( HMAC-SHA256( secret_key, input_string ) )”。复现步骤用Python或你熟悉的语言按照假设的算法实现一个生成函数。从Hook中提取一组或多组未在算法推导中使用的输入输出测试数据。用你的复现代码计算这些输入的sig3与Hook捕获的真实输出进行比对。如果完全一致恭喜你算法破解成功。如果不一致回到分析步骤检查是否遗漏了其他输入如请求体、URL参数排序、额外的盐值等或者算法本身更复杂多轮哈希、自定义混淆。验证的意义逆向工程不是“猜”而是“验证”。只有能稳定复现出与目标程序完全一致的结果你的分析才是可靠的。这个复现的代码就是后续自动化脚本或研究的基础。6. 常见问题排查与进阶技巧在实战中你绝不会一帆风顺。下面是我总结的一些典型问题及解决思路。6.1 Hook失败问题排查表问题现象可能原因排查步骤与解决方案Module.findExportByName返回null1. 库未加载。2. 函数符号名错误。3. 符号被剥离Stripped。1. 使用Process.enumerateModules()检查目标so是否在模块列表中。2. 使用Module.enumerateExports(libxxx.so)或Module.enumerateSymbols(libxxx.so)列出所有符号核对名称。3. 如果符号被剥离需通过偏移量HookModule.findBaseAddress(libxxx.so).add(offset)。偏移量需从IDA静态分析中获取。Frida脚本注入后App崩溃1. Hook函数时参数/返回值处理错误如JNI类型转换错误。2. 多线程冲突。3. 脚本内存错误。1.逐步注释法先注释掉onEnter和onLeave内所有逻辑只留console.log看是否崩溃。然后逐步恢复代码定位崩溃行。2. 检查JNI类型转换是否正确特别是字符串和数组。3. 确保对内存的读写在合法范围内使用Memory.readByteArray,Memory.protect等。4. 尝试在onEnter开头使用this.保存上下文避免访问可能无效的指针。Frida无法附加进程 (Failed to attach: access denied)1. 设备未Root或权限不足。2. 目标App有反调试/反附加保护。3.frida-server未运行或版本不匹配。1. 确认adb root可用或已获取su权限。2. 尝试在App启动前附加 (-f模式)而非启动后附加 (-n模式)。3. 检查frida-ps -U是否能列出进程。4. 考虑使用frida-gadget模式或其它隐藏Frida的技术如修改frida-server名称。Hook成功但无日志输出1. 函数未被调用。2. Hook的时机不对函数在Hook安装前已被调用完毕。3. 脚本逻辑错误如条件判断过滤了调用。1. 确认你的操作确实会触发sig3生成如进行特定网络请求。2. 将Hook代码放在Module.on(loaded, ...)监听器中确保so一加载就Hook。3. 在onEnter里打印简单的标记如console.log([] HOOKED!)确认Hook是否真的生效。性能问题或卡顿Hook函数被高频调用如加密循环中。在onEnter/onLeave中避免复杂的操作如频繁的console.log。可以设置条件只对特定参数或调用栈的触发进行打印。使用send()将数据发往PC端处理而不是在设备上处理。6.2 进阶技巧对抗与反对抗对抗符号剥离发布版的.so常会剥离符号表使findExportByName失效。解决方法特征码定位在IDA中分析函数找到一段独一无二的指令序列特征码。使用Frida的Memory.scan在so的内存范围内搜索这段特征码找到函数入口地址。偏移量定位通过IDA静态分析找到目标函数相对于.so文件基地址的偏移量RVA。在运行时用Module.findBaseAddress获取基址加上偏移量得到函数地址。Hook内部函数有时直接Hook JNI函数仍然太“高”逻辑复杂。可以尝试Hook其内部调用的更基础的库函数如libc的malloc、memcpy或加密库的SHA1_Init、AES_set_encrypt_key等。这些函数调用频繁需要精心过滤但一旦成功能获得更底层的数据流。内存断点与追踪Frida的Memory.accessMonitor可以监控对特定内存地址的读写。如果你通过静态分析找到了存储密钥或中间结果的全局变量地址可以对其设置访问监控追踪数据流向。调用栈监控在Hook函数时打印调用栈Thread.backtrace可以帮助你快速理解该函数在业务逻辑中的上下文找到上层调用者有时能发现更便捷的Hook点例如Hook调用它的Java函数可能更简单。逆向工程是一场与开发者之间的“猫鼠游戏”。你的Hook技术越熟练对系统底层理解越深就越能穿透层层保护触及核心逻辑。本次通过对某手Appsig3参数的Native层Hook实战我们系统性地演练了从目标定位、环境搭建到使用Frida Interceptor和了解底层注入方案再到算法分析与验证的完整流程。其中Frida的灵活性与强大功能使其成为当前动态分析的首选工具而理解其原理和局限并能应对各种异常情况才是从“会用”到“精通”的关键。记住每一次失败和排查的过程都是你经验值增长的宝贵机会。