☰
小红书x-s签名逆向分析:从抓包到算法还原的完整指南
2026/10/8 4:45:44 网站建设 项目流程

简介:小红书客户端x-s参数逆向分析资料包聚焦x-s参数生成机制,面向具有一定编程与逆向基础的安全研究者和爬虫开发者,旨在通过还原补环境源码,剖析应用与服务器之间的加密通信和签名流程。资源包共1511个文件,压缩包约3.34MB,以JavaScript、TypeScript源码为主体,同时包含大量map映射、JSON配置、Markdown说明文档以及eslintrc、yml等工程配置文件,覆盖从核心逻辑到构建工具链的完整代码形态,便于对照分析调用关系与数据结构。已有404人学习下载,适合用于学习逆向工程中动态参数定位、补环境构建以及协议还原的实际案例。通过阅读源码结构,读者可理解x-s参数在身份验证与防篡改中的作用,掌握从抓包、反编译、断点调试到关键函数定位的完整思路,同时结合文档资源梳理加密算法和签名过程,为开发合规的数据采集工具或开展移动应用安全评估提供具体参考。

1. 小红书 x-s 参数逆向分析:从抓包到算法还原的完整路线

当你打开小红书 APP 的请求调试面板,除了 Cookie 和 Token,请求头里还藏着一个 32 位左右的十六进制字符串 x-s。用 requests 直接带上这个值访问,接口会返回“请求失败”或“触发风控”。x-s 不是登录态,而是每次请求发出前客户端生成的一次性签名,用来让服务端校验请求是否来自真实 APP。很多小红书爬虫和数据分析项目卡就卡在 x-s 上:算法藏在 native 层,每次版本更新都可能变化。这篇文章按一线工程师的动手思路,把抓包、hook、算法还原到服务端重放这一路讲透,适合想做小红书数据采集、图片批量下载或反爬策略研究的人参考。

2. 抓包定位 x-s 参数:环境、工具与请求头特征

2.1 HTTPS 抓包环境搭建:mitmproxy 与 Charles 的取舍

x-s 参数本身是 HTTPS 请求的一部分,想看到明文就必须先能解密 HTTPS。常见做法是挂代理加安装 CA 证书。我一般用 mitmproxy 做命令行验证,用 Charles 做可视化分析。

# 安装 mitmproxy pip install mitmproxy # 启动 web 界面,监听 8888 端口 mitmweb --listen-port 8888 # 手机连上同一局域网,设置代理为电脑 IP:8888 # 手机浏览器打开 http://mitm.it 下载并安装 mitmproxy 的 CA 证书

逻辑说明:mitmproxy 是中间人代理,手机上的请求先经过它,再由它转发到小红书服务器。装了它的 CA 证书后,代理就能解包 HTTPS 流量。注意 Android 7 及以上系统默认不信任用户安装的 CA,所以需要把证书挪到系统证书目录,或者改包把网络安全配置里的user信任打开。charles 在 macOS 上的安装类似,但图形化展示请求头更直观。如果你是直连 PC 流量调试,Fiddler 也可以,但在 Linux 服务器上还是 mitmproxy 更方便。

参数说明:--listen-port指定代理端口,默认 8080。如果电脑有几个网卡,还要确认手机代理指向的是正确的内网 IP。证书安装完毕后在mitm.it页面能看绿色提示,说明 HTTPS 解密生效。如果页面提示你要在系统设置里信任证书描述文件,iOS 端记得去“通用—关于本机—证书信任设置”里手动打开开关。

在抓包工具的选择上,不要盲目从众。日常 debug 我推荐 Charles,因为它的 Filter 和断点功能好用,能看到完整的请求头层级;批量抓取和自动化时用 mitmproxy 的 Python API 更顺手。Fiddler 在 Windows 上也可以,但处理 iOS 的 HTTPS 时证书信任步骤略繁琐。

2.2 找到携带 x-s 的接口:首页 feed 与笔记详情

拿到明文流量后,刷新小红书首页,找到/api/sns/web/v1/homefeed这个接口。它通常在edith.xiaohongshu.com域名下。展开请求头,你会看到这么一组关键参数:

参数名样例值作用
x-s33aa2e6d1b1e...请求签名,32位hex
x-t1712345678901Unix 毫秒时间戳
x-common-params%7B%22app_version%22%3A...URL编码的设备/版本信息
x-signA2...另一个签名值,新版才有
x-b3-traceid...全链路追踪ID

小红书爬虫项目里,x-s 是绕不过去的核心。截取同一接口的几次刷新数据,你会发现 x-t 每秒都在变,x-s 也跟着变,说明 x-s 至少依赖了时间戳。接着你手动改一下请求体里的page_size,而不改动 x-s,直接提交,会得到“签名错误”。这说明 x-s 也覆盖了请求体的内容。通过这种方式,你能粗略判断哪些请求字段被签名保护了,为后续拼接逻辑做准备。

2.3 观察 x-s 的变化规律:时间戳、体参、设备指纹

在抓到几十组请求样本后,可以做一个戏剧性实验:固定同一个 x-s,只改 x-t 里的毫秒数超出一定范围,比如改大 1 秒,再发送请求。如果依然返回正常数据,说明服务端对时间戳有容忍窗口;如果直接拒绝,说明它要求严格一致。这个窗口值对你服务端的重放脚本非常重要。

另一个观察点:把手机恢复出厂设置或登录不同账号,重新抓包,对比相同接口、相同操作下的 x-s。你会发现即使 path 和 timestamp 相同,x-s 也不一样。这是因为 x-common-params 里的 device_id、device_fingerprint 参与了签名。所以当你后期在服务端生成 x-s 时,必须保证设备指纹参数稳定,不能每次随机生成。最稳妥的方式是把抓包得到的 x-common-params 原样保存,作为生成器的输入之一。

3. 逆向还原 x-s 生成算法:JNI 到底做了什么

3.1 静态分析 so:从导出符号到 RegisterNatives

x-s 的生成逻辑几乎不可能写在 Java 层,因为太容易用 jadx 直接看穿。常见做法是把核心算法放进 so 文件,Java 层只留一个 native 方法壳。你需要从安装包中提取对应的 so 文件。

# 用 apktool 解包 apktool d xiaohongshu.apk -o xhs # 进入原生库目录,arm64 架构 cd xhs/lib/arm64-v8a/ # 搜索 sign 相关字符串 strings libsecurity_guard.so | grep -i sign

逻辑说明:apktool d解包后,大部分逻辑会保留在 smali 目录里,但 so 文件会被原样释放。strings命令能快速看到 so 内可打印字符串,比如sign、md5、sha256这样的单词。如果你的搜索结果显示了很多混淆过的符号,可能 so 做了字符串加密。这时候需要用 IDA 或 Ghidra 打开 so,看导出函数表中是否有Java_开头的方法。

如果导出函数表里没有Java_com_xingin_xxx_sign这类名字,说明 native 层用的是动态注册:系统会在加载 so 时调用JNI_OnLoad,然后通过RegisterNatives将 Java 方法名和 native 函数指针绑定。这时候要从JNI_OnLoad的 disassembly 入手,找到RegisterNatives的调用参数,再回溯出实际处理函数。这个过程比较费时间,但对小红书这种级别的 App 来说,是必经之路。

3.2 动态 hook:用 Frida 抓取 native 入参与返回值

静态分析能告诉你函数地址,但不知道它的输入输出是不是你想要的。动态 hook 是最快的验证方式。

import frida import sys # 从抓包定位到 Java 层签名方法路径 script_code = """ Java.perform(function() { // 这个方法名以真实逆向结果为准 var signClass = Java.use('com.xingin.opensdk.sign.SignManager'); signClass.sign.implementation = function(data) { var result = this.sign(data); console.log('[x-s] input: ' + data); console.log('[x-s] output: ' + result); return result; }; }); """ def on_message(message, data): if message['type'] == 'send': print(message['payload']) else: print(message) device = frida.get_usb_device() pid = device.spawn(['com.xingin.xiaohongshu']) session = device.attach(pid) script = session.create_script(script_code) script.on('message', on_message) script.load() device.resume(pid) input()

逻辑说明:这个脚本做的是在 Java 层 hook 签名方法。当你操作 App 触发首页请求时,控制台会打印传入的data字符串和返回的 x-s 值。通过对比几十组数据,你会发现data是一个由 URL path、query 参数字典、请求体、时间戳、设备指纹拼接起来的文本。这个方法非常直观,适合新手入门。

参数说明:device.spawn用于冷启动 App,挂在早期阶段,避免错过插件初始化。如果你已经打开 App,可以直接用device.attach('com.xingin.xiaohongshu')。implement是 Frida 的替换实现写法,执行原函数后再打印返回值。遇到 hook 不到的情况,检查进程是否选择正确、类是否被延迟加载,或者 so 内部有没有反调试逻辑。

3.3 还原拼接逻辑:一个可推导的伪代码框架

这里不写完整代码,因为你会看到网上很多开源仓库里的算法突然“失效”,小红书这两年会定期更换 salt 和 seed。但 x-s 生成的骨架通常是这样的:

import hashlib import time def build_sign_data(path, query, body, timestamp, device_params, salt): # 拼接顺序必须从 hook 到的 data 反推 raw = f"{path}&{query}&{body}&{timestamp}&{device_params}&{salt}" # 第一轮提取摘要 digest1 = hashlib.md5(raw.encode('utf-8')).hexdigest() # 很多版本在摘要后再补一段 key,再做一次 md5 digest2 = hashlib.md5((digest1 + "custom_key").encode('utf-8')).hexdigest() # 也可能是 base64 后取前 32 位,取决于 hook 结果 return digest2

逻辑说明:这个伪代码的作用是引导你梳理出拼接顺序。真正的算法可能是 MD5、SHA256、MurmurHash 的组合,也可能在中间插入设备唯一 ID。你需要把自己的 hook 结果套进这个模板,不断调整顺序和额外 key,直到输出与抓包里的 x-s 完全一致。

参数说明:body不能直接用 dict 转 str,因为 dict 的键排序和 URL 编码方式会影响摘要结果。建议用urllib.parse.urlencode去规范化 query。salt一般藏在 so 的.rodata段,可以用 Ghidra 搜索 hex 字符串或直接用 Frida 读内存。如果你发现同一时间戳同一 path 的 x-s 每次都不同,说明算法里加了随机因子,服务端要么能验随机因子,要么对随机因子有容忍度,处理方式会更复杂。

4. 把 x-s 放进请求服务:收获小红书图片和笔记内容

4.1 封装签名函数与请求库

假设你已经从上一章的反推中拿到了可用的签名算法,下一步是把它封装成一个独立的签名服务。我一般用 Python 写一个类,里面包含get_xs(timestamp, path, query, body)和一个request()包装方法:

import time import hashlib import requests class XiaoHongShuSigner: def __init__(self, device_params, salt): self.device_params = device_params self.salt = salt def _sign(self, path, query, body, ts): raw = f"{path}&{query}&{body}&{ts}&{self.device_params}&{self.salt}" sign = hashlib.md5(raw.encode()).hexdigest() return sign def request(self, method, path, query="", body=""): ts = int(time.time() * 1000) xs = self._sign(path, query, body, ts) headers = { "x-s": xs, "x-t": str(ts), "x-common-params": self.device_params, "user-agent": "discovery/8.67.0 (iPhone; iOS 16.0; Scale/3.00)" } url = "https://edith.xiaohongshu.com" + path if method.upper() == "GET": resp = requests.get(url, params=query, headers=headers) else: resp = requests.post(url, data=body, headers=headers) return resp.json()

逻辑说明:这里把签名需要的几个要素作为成员变量固化下来。每次请求前先取毫秒时间戳,再对标准化的 path、query、body 做签名。整个请求封装成一个方法,后续只需关心业务参数。注意query尽量传字典或原始字符串,不要传 Python 的 dict 再让 requests 去编码,因为编码顺序不同会导致签名不一致。

参数说明:device_params是 X-Common-Params 的 URL 编码值,不要改动。user-agent必须与设备类型匹配,iPhone 的 UA 和 Android 的 UA 不能混用。edith.xiaohongshu.com是小红书 web API 的常见 host,如果你抓包抓到的是其他 host,就改成自己的。

4.2 常见接口的签名调用示例:笔记详情与图片列表

以“小红书图片提取”为例,这是新手最容易上手的方向。请求笔记详情接口:

signer = XiaoHongShuSigner( device_params="%7B%22app_version%22%3A%228.67.0%22%2C%22device_id%22%3A%22...%22%7D", salt="your_salt_here" ) # 笔记详情接口,note_id 从分享链接中解析 note_id = "from_shortlink" path = f"/api/sns/web/v1/feed" query = f"source_note_id={note_id}&source=web" data = signer.request("GET", path, query=query) if data.get("code") == 0: note_info = data["data"]["items"][0]["note_card"] image_list = note_info["image_list"] for img in image_list: print(img["url_default"]) else: print("请求失败:", data)

逻辑说明:source_note_id是笔记的唯一 ID。拿到返回的image_list后,每个元素里有url_default,这是原图地址的 CDN 链接,通常带签名参数,可以直接下载。注意接口返回的image_list可能嵌套在images字段下,不同 App 版本的字段名略有差异。

参数说明:source参数值要看接口定义,有的接口是web,有的是discovery。这个值也参与签名,不能随便改。如果你的note_id被小红书风控识别为“非正常来源”,接口可能返回code = -1,这是签名以外的 IP 或设备问题。

4.3 分享链接解析、url scheme 与批量下载场景

很多用户想下载自己收藏的笔记图片,但 App 没提供一键导出。常见做法是复制分享链接,链接会先重定向到xhslink.cn短链,最终落到https://www.xiaohongshu.com/discovery/item/{note_id}这样一个长链接。你需要用requests.head跟随重定向拿 final URL,再提取 note_id。这套流程在“小红书分享链接解析 id网站”这类工具里很常见。

对于“小红书爬虫”和自动化任务,还可以用 x-s 生成器去调用评论接口、用户主页接口、搜索接口。我通常会再写一个download_images()函数,把上面拿到的url_default并发下载到本地,用hashlib.md5重命名文件,避免重复。批量下载时控制并发数在 5 以内,因为 CDN 也有风控,过快会给你返回 403。注意,无论是图片下载还是数据采集,请只处理自己有权访问的内容,不要无限抓取大量非授权数据。

5. 避坑指南:小红书 x-s 逆向中高频翻车的五个问题

5.1 Hook 不到函数:进程选错或时机太早

现象:Frida 脚本执行后没有红字报错,但 App 刷新请求时,控制台完全没输出。

原因:小红书有主进程、子进程、push 进程等多个进程,你 attach 的可能不是处理网络请求的进程。另一个常见原因是类加载时机晚于 Hook 时机,Java.use在静态 hook 时类还不存在。

解决:先用frida-ps -U列出所有进程,筛选名称里带com.xingin.xiaohongshu的进程,挑 PID 最大的那个试试。在脚本里把Java.perform包在setTimeout里延迟 500ms 再执行;如果还不行,就 HookClassLoader.loadClass,拦截目标类的加载后立刻执行替换。

5.2 签名一致但被拒:x-t、x-common-params 缺一不可

现象:你用自己的签名函数算出 x-s,放在请求头里,服务器返回类似STATUS_PARAM_ERROR或sign error。

原因:小红书案校验签名时,不是只拿 x-s 和请求参数比对,还会校验 x-t 的时间戳是否在容差范围,校验 x-common-params 里的 device_id 是否在黑名单。或者你漏了 x-sign 这个新参数。

解决:把抓包里所有x-开头的请求头都复制到本地请求里,一步一步删减,找出哪些是必须的。大多数情况下,x-t、x-common-params、x-sign 和 x-s 必须同时存在,缺一个都不行。参数与原请求保持一致,服务器才认账。

5.3 Android 上抓包全是密文:证书与 Pinning 的处理

现象:手机设置了代理并装了证书,但 mitmproxy 看到的请求和响应都是二进制乱码,或只是连接重置。

原因:APP 没有把用户 CA 当作信任根,或者它在 native 层做了 SSL Pinning。

解决:把 CA 证书装进系统证书目录而不是用户目录。在 root 的 Android 手机上,用 Magisk 模块或者手动 remount 系统分区,将 PEM 证书cp到/system/etc/security/cacerts/。如果 Pinning 仍生效,就 Frida Hook 掉验证书函数,比如 HookSSL_CTX_set_custom_verify让它返回0。这在 iOS 上对应的是 ATS 和证书双向校验,需要用SSLKillSwitch这类工具处理。不要指望一台不越狱的 iOS 设备能轻松搞定这个问题。

5.4 iOS 与 Android 签名算法不共用

现象:你在 Android 上还原出 x-s 生成规则,放到 iOS 的设备参数和请求头里,接口返回签名错误。

原因:小红书 iOS 和 Android 使用两套 so 逻辑,虽然 key 名称相同,但内部拼接 salt、迭代次数可能不同。而且 iOS 的x-common-params字段大小写和安卓也不同。

解决:要么在 iOS 环境单独还原一套,要么统一用 Android 端生成签名,然后放在任何要访问的请求头里。后端不会校验来自哪个客户端,它只看签名是否正确。所以我实际做爬虫时会重点保 Android 这条链路,iOS 只用于抓包参考。

5.5 请求频率太规律,被风控盯上

现象:每秒固定请求一次,连续跑了 50 条之后开始出现滑块验证或接口无数据返回。

原因:签名本身可能没错,但行为特征异常。服务器不仅校验 x-s,还会统计同一个 device_id、同 IP 的请求间隔和时间分布。固定间隔的请求跟真实用户滑动行为完全不一样。

解决:在请求循环里加随机区间,比如 1.5~3.5 秒之间浮动;准备多个 device_id 轮流使用;请求时自带不做重试策略,失败就停下来等待足够长的时间。对个人项目而言,把并发降到 2 以下远比堆并发更有效。

6. 回归验证与工程化:让 x-s 生成器成为稳定依赖

当你写完签名函数,先不要急着跑数据。我习惯拿一组抓包样本做回归验证:把样本里的 path、query、body、x-t 原样输入签名函数,看输出是否等于样本里的 x-s。如果全部匹配,恭喜你,算法正确率至少是 90%。接下来做时间戳变化验证:把样本里的 x-t 改为当前时间,重新生成 x-s 并进行一次真实请求。如果返回正常数据,说明服务端允许秒级时间窗口;如果返回“签名过期”,需要把 x-t 与生成 x-s 时的时间严格绑定。

工程化再往下走一步,我是建议把签名函数优化成一个独立模块,并且把盐值和拼接规则放在配置文件里,不允许业务代码直接改。这样当小红书在某天更新 so,你的热更新机制能快速切换新算法。我自己的教训是曾在业务线程里直接调用签名函数,并加了一个随机退化逻辑,导致并发一高就有大量“签名过期”的日志。后来改成只在一个 worker 线程中生成签名,其他请求复用结果并把时间戳控制在 2 秒内,问题就消失了。x-s 逆向不是一条路走到黑,它更接近持续维护的过程——抓包、hook、反推、验证,再循环。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询