
在客户端逆向里抓包拿到接口请求之后最头疼的就是那些拼在请求头里的签名参数。最近跟小红书某个业务接口打交道被x-mini和x-s这两个字段卡了很久。请求体是结构化的JSON明文摆在眼前但只要你手动改一个字节后端就返回“签名错误”。微信、抖音、小红书这类App基本都有一套自己的签名体系小红书这边广为人知的就是x-mini签名。这个字段的设计初衷是防重放、防篡改、防批量请求但对于做安全研究、接口自动化、反爬策略分析的人来说它也是绕不开的一道门槛。这篇文章就从一个实战角度把“x-mini签名到底在哪生成、怎么通过Frida动态定位、又如何把签名过程封装成RPC调用”这条路完整走一遍。我不打算手把手把某个函数的具体字节码贴出来因为签名算法本身是动态更新的今天贴出来的代码下个月就可能失效。更重要的是把定位思路、Hook方法、脚本设计、遇到问题的排查手段讲清楚。无论你是刚接触安卓逆向还是已经能独立分析so文件这套流程都能直接复用。1. 项目背景与整体思路拆解1.1 为什么要盯住x-mini签名小红书App的接口请求头里除了常规的x-t、x-s之外x-mini是一个比较特殊的字段。它通常出现在业务API的Header中不同版本的App生成规则还不一样。从功能上看它承担的是“请求完整性校验”和“设备/会话指纹”两个职责。也就是说服务端拿到请求后会重新计算手里的参数、时间戳、设备信息再跟x-mini比对一旦不一致就直接拒绝响应。这就带来一个很实际的问题当你想基于已有接口做一个自动化工具或者分析某个推荐策略抓包得到的请求头是死的但你自己的请求是活的。每次构造新的Query或Body后端都会认为签名不匹配。所以必须找到一个办法能够自由生成合法签名。解决手段无非两种一是静态还原签名算法用纯代码复现二是在App运行时动态调用原签名函数拿到结果直接用。静态还原的优点是脱离App环境速度快、适合大规模并发但缺点是签名算法往往经过混淆和native层加固逆向成本极高。动态调用则简单粗暴App自己就是一台“签名机”只需要通过Frida这类工具把内部函数暴露出来即可。我这次选择的路径是先通过Frida Hook定位到x-mini的生成入口验证输入输出再基于Frida的RPC机制将签名函数封装成可供Python调用的接口。整个过程不需要完全读懂算法内部逻辑适合大多数“只想先跑通链路”的逆向场景。1.2 方案选型为什么不是纯静态逆向而是FridaRPC很多人拿到App第一反应就是Jadx打开搜字符串“x-mini”然后顺藤摸瓜找到算法代码。但实际搜索会发现在Java层很难直接看到明文拼接逻辑。要么签名逻辑被放到了Native层要么经过了好几层方法调用的包装。你用Jadx翻半天最终可能定位到的是一个native方法声明真正实现全在so文件里。这时你有两条路一条是拿IDA/Ghidra打开so耐着性子看反汇编识别出哈希函数、加解密常量把算法逐步还原出来。另一条是用Frida在运行时直接Hook那个native方法或它的上层Java包装方法动态观察参数和返回值。如果需求只是想绕过签名校验后者的效率要高一个数量级。尤其当算法里包含时间戳、随机因子、设备状态这些实时变量时动态调用天然就能拿到正确值而静态还原还要处理这些变量的上下文。RPC的价值则在于复用。Hook到签名函数后如果你只在Frida的控制台里手动调用没法满足业务需求。通过Frida的rpc.exports可以把App进程里的签名函数映射成Python可调用的远端方法这样自动化的脚本也能像调用本地库一样生成签名。我甚至会在最后把RPC服务封装成一个HTTP接口让多个脚本共用同一个App实例签名。1.3 整体分析流程五步走整个分析过程可以拆成五步后面每一节都会对应其中一块抓包确认x-mini的字段位置和变化特征搞清楚它是跟body相关还是跟整个请求相关。用Jadx/Frida-trace静态加动态结合定位签名生成函数。编写Frida脚本Hook关键方法打印调用栈和参数确认它就是我们要找的目标。将签名函数通过rpc.exports暴露用Python调用并拼回请求头验证。处理超时、并发、进程崩溃等稳定性问题。这里面最容易卡住的是第2步和第5步。第2步卡的往往是找不到函数入口第5步卡的往往是RPC调用超时或App进程重启。我后面会把这两块的坑单独拉出来讲。2. 环境准备与工具链搭建2.1 设备和基础环境怎么搭做这种App分析我建议优先选Android环境。模拟器可以用但要注意架构。小红书这类App的so库通常只放arm64-v8a和armeabi-v7a个别旧版本可能有x86但新版本基本都不支持x86模拟器。如果你在夜神、雷电的x86模拟器上跑很可能会直接闪退或者Frida attach之后调不到native层函数。所以我个人更推荐用真机Pixel或小米都行系统版本Android 9-13都可以。如果你只有模拟器可以试试Genymotion配合ARM Translation但稳定性不能保证。电脑端需要准备Python 3.8用来装frida-tools和后续写RPC脚本。Jadx或GDA用来静态浏览Java层代码。Frida和对应的frida-server。抓包工具Charles或Burp Suite都行用来观察请求头变化。adb工具用于设备连接和文件推送。这几个工具本身没什么难度但版本匹配是第一个容易踩坑的地方。尤其是Frida电脑端frida-tools和手机端frida-server的版本必须保持一致否则连接时会报“unable to connect to remote frida-server”或者直接卡住。2.2 Frida与frida-server的安装细节先装电脑端pip install frida-tools装完后执行frida --version记下版本号。然后去Frida官方GitHub Release页面下载对应版本的frida-server。注意看手机架构可以通过adb shell getprop ro.product.cpu.abi查看一般是arm64-v8a。下载完解压后adb push frida-server /data/local/tmp/ adb shell chmod x /data/local/tmp/frida-server adb shell /data/local/tmp/frida-server 执行完以后回到电脑端用frida-ps -U检查设备是否连通。如果能列出一堆进程列表说明Frida服务已经正常启动了。这里有个细节frida-server需要root权限才能attach到所有进程。如果你的测试机没有root可以考虑用frida-server复制到可调试进程或者换一台已root的设备。对于小红书这种完全没有调试标志的App没有root真的很难搞。注意frida-server启动后不要关掉那个shell窗口用让它后台跑也可能被系统回收。我习惯用nohup或者直接开两个终端窗口一个跑frida-server一个做其他操作。2.3 抓包环境搭建证书校验是第一个拦路虎抓包需要让App的HTTPS流量被工具解密。Android 7以上系统默认不信任用户安装的CA证书而且小红书这类App会做SSL Pinning导致即使装了证书抓包看到的也只是加密乱码。解决思路有两个一个是把抓包工具的证书植入系统证书目录另一个是绕过SSL Pinning。证书植入系统目录在root设备上很简单。用OpenSSL把Charles的.pem证书转换为Android可识别的hash.0格式然后挂载/system分区为可写复制到/system/etc/security/cacerts/目录下重启App即可。这个方法能解决大部分不校验固定证书的App。SSL Pinning的绕过可以用Frida的ssl_pinning_bypass脚本也可以在Hooking了TrustManagerImpl的verifyChain方法后直接返回true。很多现成脚本都能做这里不展开。我只提醒一点绕过SSL Pinning之后虽然抓包能看到明文请求但不要急着分析参数因为很多App在Hook之前已经生成了签名你看到的x-mini是上一次请求的值得继续往下走找到签名生成的时机。3. 动态定位x-mini签名生成位置3.1 先看抓包数据建立参数模型抓包成功后随便触发一个帖子列表接口请求头里大概长这样GET /api/sns/web/v1/homefeed?categoryrecommend HTTP/1.1 Host: edith.xiaohongshu.com x-mini: 9f12a1b3c4d5e6f7a8b9c0d1e2f3a4b5 x-s: 128abcd... x-t: 1699999999.123 ...第一眼会觉得x-mini是一串十六进制摘要长度可能是32或64看起来像MD5或SHA1。想验证它的输入范围可以手动改一下URL里的某个参数然后刷新请求看看x-mini是否变化。如果变化说明它跟URL有关如果不变可能是跟body、设备或时间戳有关。这里有个小技巧把body里的某个数字改掉再抓一次如果x-mini变了基本可以确定它是基于原始body计算出来的。小红书大多数接口的x-mini都会覆盖完整请求内容。有了这个判断下一步就是去代码里找“这段摘要到底在哪里被算出来”。在Java层搜索“x-mini”字符串通常搜不到因为签名生成之后才会被放到Header里而Header的key字符串往往在OkHttp拦截器或网络层配置里。可以先Jadx搜x-mini如果搜到就看看它被谁引用如果搜不到就搜addHeader、Interceptor这些网络关键字。3.2 静态搜索的两种思路我一般先用Jadx的全局搜索功能搜索x-mini。如果没搜到就换搜索X-Mini大小写不敏感还搜不到就搜索Mini和sign的组合。在较新版本的小红书里这个字段会被拼接成x-mini但生成函数命名往往带随机后缀比如a.b.c这类混淆命名。另一种更高效的方式是Frida自带的frida-trace直接跟踪java.net.URL或okhttp3.Request的构造方法。在抓包工具里我们看到请求是从OkHttp发出去的但直接Hook OkHttp会看到非常多的调用。我推荐先Hookokhttp3.internal.http.CallServerInterceptor.intercept这个函数它会在真正发请求前被调用打印一下chain.request().headers()就能看到包含x-mini的完整请求头然后再往上一层追。在这个阶段静态分析的意义不是直接读出算法而是缩小范围。我看代码的习惯是先找到发起请求的页面或API封装类看它构造Request时用什么方法生成Header。有了目标类之后再用Frida去动态Hook效率会高很多。3.3 Frida Hook的通用写法假设经过静态分析发现某个名为com.example.xmini.XMiniParser的类有一个实例方法getSign(String input, long timestamp)返回值是String。用Frida Hook的写法大概是Java.perform(function () { var clazz Java.use(com.example.xmini.XMiniParser); clazz.getSign.implementation function (input, timestamp) { var result this.getSign(input, timestamp); console.log(input: input); console.log(timestamp: timestamp); console.log(result: result); return result; }; });如果这个App有多个进程还要用frida -U -F附着到前端进程或者用frida -U -n com.xingin.xhs指定进程。小红书是单进程架构直接用包名就够。Hook之后触发一次下拉刷新控制台如果打印出了input和result并且result跟抓包工具里的x-mini一致那你就已经站在签名函数门口了。接下来要看的是input怎么构造。如果input是一长串字符里面包含URL、body、时间戳那这条链路就清晰了客户端在发送请求前把请求明文序列化然后传给签名函数计算摘要。你要做的只是把这个序列化过程复现出来或者更直接地——每次需要签名时调用App内这个函数即可。3.4 如果入口在Native层怎么办这是更常见的情况。签名函数可能是一个Java native方法比如public static native String sign(byte[] data, int len);在Jadx里看到的是native没有方法体。这时候Frida可以直接Hook这个native函数但需要知道它的导出符号。如果so文件里有符号用Module.findExportByName找到函数地址然后用Interceptor.attachvar nativeSign Module.findExportByName(libxminilib.so, Java_com_example_xmini_XMiniParser_sign); Interceptor.attach(nativeSign, { onEnter: function (args) { // args[1]是JNIEnv, args[2]是jclass/jobject, args[3]是byte数组 console.log(native sign called); }, onLeave: function (retval) { // retval 是jstring console.log(native sign ret: Java.vm.tryGetEnv().getStringUtfChars(retval, null)); } });但如果so函数被混淆成了动态注册没有导出符号就得用Frida的EnumerateModules遍历so文件再结合其中包含的JNI_OnLoad动态注册表硬找函数指针。这个方法比较费时间还有一种捷径在上层Java native方法声明打断点通过打印参数里jbyteArray的内容再结合Process.findModuleByAddress定位so文件路径用Module.enumerateImports看它调用了哪些系统函数帮助判断算法类型。我不建议在完全不知道native函数地址时盲目硬刚。更高效的路径是先在Java层的native方法声明处Hook拿到传给native的明文数据然后用Frida直接调用这个native方法。也就是说根本不需要还原native内部逻辑只要入口函数参数是清晰的直接把它当成一个黑盒调用它生成签名。4. 从动态Hook到RPC调用封装4.1 RPC到底解决什么问题Frida的RPC机制说白了就是让运行在App进程里的JavaScript脚本把函数绑定到rpc.exports对象上然后Python端通过script.exports.xxx()去调用这个绑定的函数。这样App内部复杂的Java/native调用链对外部Python脚本来说就是一个普通函数。在我们这个场景里用RPC的最大好处是签名算法的更新对调用方透明。App每次升级你只需要重新分析一次Hook点修改脚本里的类名或方法名Python端完全不用改。而且RPC天然是在App环境内执行的所有依赖的全局状态、设备指纹、加密随机数都能自动保持正确。相比重新实现算法RPC的抗失效能力强得多。4.2 暴露签名函数给Python在Hook脚本的最后加上rpc.exportsrpc.exports { getXminiSignature: function (rawData) { var result null; Java.perform(function () { var clazz Java.use(com.example.xmini.XMiniParser); result clazz.getSign(rawData, Date.now()); }); return result; } };这里有一个很容易忽略的坑rpc.exports导出的函数是在Frida的JavaScript线程中执行的而Java操作必须在Java.perform包裹的代码块里执行。如果直接在外面调用Java.use就会报“Cannot access Java VM”之类的错误。把Java调用包裹在回调里等回调执行完再赋值给外部变量然后返回这样才能保证线程安全。如果签名函数是普通的Java静态方法写法很简单。但如果它需要先创建一个对象并依赖App初始化完成的单例那就要在Java.perform里先拿到那个单例再调用。例如var instance clazz.getInstance(); result instance.sign(...);4.3 Python端调用RPC并验证签名Python端代码同样简洁import frida import sys device frida.get_usb_device() session device.attach(com.xingin.xhs) with open(hook_xmini.js, r, encodingutf-8) as f: script session.create_script(f.read()) script.load() # 等待脚本运行并注册exports raw_data methodGETpath/api/sns/web/v1/homefeedtimestamp1699999999 signature script.exports_sync.get_xmini_signature(raw_data) print(signature:, signature)需要注意的是script.exports_sync是同步调用接口如果App内某个操作需要较长时间可能导致Python端阻塞。这里建议给session.create_script开启调试模式或者在JS端设置合理的超时避免因为等待时间过长引发二次RPC请求被锁死。验证签名是否正确的标准做法是把拿到的signature拼到请求头里发送一条真实请求看返回是否200。我推荐在Python里直接用requests构造请求header里加上x-mini、x-s、x-t等字段。如果返回成功说明RPC链路已经通了。4.4 RPC调用的稳定性与性能优化第一次跑通RPC之后你会遇到一些平时不容易注意的问题。比如长时间挂在后台App进入休眠状态再调用RPC会报“Script crashed: process terminated”。这是因为目标App被系统回收了。解决办法是在Python端增加自动重连逻辑捕获frida.ScriptDetachedException或TransportError重新attach并重新加载脚本。另外Frida的RPC调用是串行的。如果你在Python里开了多线程并发调用同一个App的签名函数底层最终还是会排队。更好的做法是在Python端维护一个全局锁或者使用线程池但限制并发为1。不要试图同时让多个Frida会话附着到同一个App进程这会导致免杀或检测机制触发导致App闪退。还有一点小经验签名函数如果接收的是原始字符串建议在Python端把请求体先序列化成JSON字符串再传给RPC。不要传Python字典对象Frida序列化字典时不会区分字节和字符串容易出错。统一用字符串作为RPC参数能避免很多莫名其妙的类型转换问题。5. 常见问题与排查技巧实录我在做这个项目的过程中踩了不少坑整理成一份速查表方便后来的人直接对号入座。现象可能原因解决方案frida-ps -U提示找不到设备frida-server未启动或USB连接异常确认手机打开了USB调试执行adb devices在手机端运行/data/local/tmp/frida-serverattach报错“unable to find process by name”包名写错或App运行在多进程用frida-ps -UHook时类找不到类名混淆或存在于动态加载的ClassLoader先用Java.enumerateLoadedClasses搜索包含关键词的类再HookHook时方法找不到方法被混淆掉了搜索getSign、sign、mini等关键词以及对方法名做子串枚举Java.perform回调中返回值总是null目标方法可能是异步操作改用Java.use的overload明确参数类型并检查是否需要在主线程执行调用RPC超时JS线程阻塞或Java操作卡顿在JS中先打印日志定位卡住点避免在Java.perform中做耗时网络操作检查是否发生死锁signature拼回请求后返回“签名错误”签名时使用的原始数据与请求不完全一致检查URL中是否有动态参数如spm、gid并确保timestamp与请求头x-t一致App检测到Frida并闪退存在反调试机制使用更隐蔽的Frida模式如gadget注入或使用Objection结合多个脚本绕过检测模拟器so加载不了arm64 not found换真机或使用arm镜像的模拟器不要在x86模拟器上分析native层在实际操作中最花时间的反而不是写脚本而是确认签名时的原始数据格式。举个例子同样一个URL客户端在签名时可能带了?categoryrecommend但header里存的是path没有query。如果你把query拼进去了签出的值肯定不一致。我这里的建议是第一优先在Java层打印签名函数的入参把它完整保存下来第二与抓包工具的请求行做对比多试几次之后基本能找出规律。有个小技巧我一直用在Frida脚本里把每次签名入参和结果都通过console.log输出然后在Python端用script.on(message, ...)异步监听这些日志按时间戳记录。这样即使签名结果不对也能回放当时的输入定位差异点。6. 一点关于工具定位的思考写到这里核心流程已经完整了。最后说点题外话。Frida是非常强大的动态分析工具RPC调用更是打通“App内黑盒函数”和“外部自动化”之间快速通道的好方法但能力越大越要注意使用边界。我平时做这类分析要么是为了自研App的安全防护测试要么是为了评估某个接口是否存在越权或滥用风险。总而言之技术本身是中性的关键看用在什么地方。如果你只是因为业务需要分析某个反爬策略建议把请求频率控制在一个对服务端无感的范围内。不要用这套东西去抓取用户私密数据也不要去攻击、干扰目标服务。逆向分析最快乐的部分是“知其所以然”而不是“怼天怼地”。结尾最后分享一个实用的小经验当你成功跑通Frida RPC之后不要急着拆掉代码。可以再花十分钟把Python端的RPC调用封装成一个简单的HTTP服务用Flask或FastAPI暴露一个/sign接口这样其他团队的人或你自己的多语言脚本都能随时调用。App只需要在一个设备上运行整个签名能力就变成一个本地服务。以后App升级导致脚本失效你只需要更新维护Hook脚本外部调用方式完全不用动。我自己在做这个x-mini逆向时最大的体会是碰到加密算法不要头铁非要还原到底先用Frida把函数当作黑盒用起来等业务跑通了再决定要不要深入算法细节。动态分析的价值往往不在“逆清楚”而在“用起来”。希望这份实战记录能让你在下次面对类似签名协议时少踩几个坑。