当前位置: 首页 > news >正文

小红书 X-s 签名逆向分析实录

小红书 X-s 签名逆向分析实录

前言

前阵子接了个需求要爬小红书首页 Feed 流,打开一看请求头里躺着三个东西:X-sX-tX-S-Common。X-t 明显是时间戳,另外两个一眼动态签名。这种活儿干过不少,基本套路就那几个:找到签名函数、抠出来、补环境、跑通。

但小红书这次确实绕了点弯子,记录一下完整的分析过程,算是给自己留个笔记。


第一阶段:摸清接口长什么样

先打开首页,抓了一轮请求。目标接口很快定位到:

POST https://edith.xiaohongshu.com/api/sns/web/v1/homefeed

请求头里需要关注的有:

| 字段 | 值示例 | 初步判断 |

|------|--------|----------|

| X-t |1730196432123| Unix 毫秒时间戳 |

| X-s |XYS_xxxxxxxxxx| 签名,每次都不一样 |

| X-S-Common | 一串 base64 字符串 | 另一个签名 |

| Cookie |a1=xxxx; webId=xxxx; ...| 登录态和会话标识 |

| Origin |https://www.xiaohongshu.com| 固定 |

| Referer |https://www.xiaohongshu.com/explore| 固定 |

请求体是个 JSON,结构也不复杂:

{"num":20,"cursor_score":"","refresh_type":1,"note_index":0,"unread_begin_note_id":"","unread_end_note_id":"","unread_note_count":0,"search_page_request_source":"discover_feed"}

小意思。先试着改参数直接发请求,返回 403。那签名是非解不可了。


第二阶段:定位签名函数

把页面所有 JS 拉下来,搜X-sXYS_这些关键字。在vendor-dynamic.880089a3.js这个文件里找到了生成逻辑。

文件经过 webpack 打包和混淆,函数名都是单字母。不过 webpack 的模块结构很清楚,每个模块就是一个函数包在function(module, exports, __webpack_require__)里。

定位到模块41439,里面定义了一个K对象,包含了一堆工具函数:

  • K.Pu— 这个看起来是 MD5

  • K.kn— 疑似 CRC32

  • K.lz— UTF-8 编码

  • K.xE— 自定义 base64

  • K.tb— 另一个 CRC32 变体

然后在另一个模块59839里找到了seccore_signv2函数,这就是签名的核心逻辑。

用浏览器控制台试了一下:

// 找到一个实际请求样本,断点停在 seccore_signv2 被调用的地方// 可以看到传入的参数:// e = "/api/sns/web/v1/homefeed"// a = JSON.stringify(body) // 请求体字符串

第三阶段:一步步拆签名算法

第一步:还原签名公式

seccore_signv2(e, a)的流程如下:

  1. u = e + (typeof a === 'object' ? JSON.stringify(a) : a || '')

— 把 URL 和请求体拼成一个字符串

  1. m = MD5(u)

— 对拼接后的字符串取 MD5

  1. w = MD5(e)

— 对 URL 单独取 MD5

  1. C = window.mnsv2(u, m, w)

— 调一个叫mnsv2的函数,传三个参数下去,返回值是关键

  1. 拼一个对象P = { x0: "4.3.7", x1: "xhs-pc-web", x2: "PC", x3: C, x4: typeof a }

  2. 对 P 做 JSON.stringify 再 UTF-8 编码,然后自定义 base64

  3. 前面加上XYS_前缀

所以最终的 X-s 就是:

"XYS_" + 自定义base64(UTF8(JSON.stringify({ x0: "4.3.7", x1: "xhs-pc-web", x2: "PC", x3: mnsv2返回值, x4: "string" 或 "object" })))

第二步:搞定自定义 base64

标准 base64 的字母表是ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/

翻了模块 41439 里K.xE的实现,发现它用了一个完全不同的字母表:

ZmserbBoHQtNP+wOcza/LpngG8yJq42KWYj0DSfdikx3VT16IlUAFM97hECvuRX5

长度正好 64 个字符。这就不难了,用这个表替换标准表,写一个自定义 base64 编码函数。等号补齐的逻辑和标准 base64 一样。

第三步:确认 MD5

模块里K.Pu的实现对照了一下标准 MD5 的初始值和常数,完全一致。初始值0x674523010xefcdab890x98badcfe0x10325476,四个轮的常数也没动过。就是个标准 MD5,没有加盐没有魔改,直接用 Node 的crypto.createHash('md5')就行。

第四步:搞定 CRC32

K.tb用了多项式0xedb88320,标准的 CRC32。用来干嘛的?在 X-S-Common 里用到。看了下 X-S-Common 的生成逻辑,就是把一些请求元信息(浏览器信息、时间戳等)拼起来,CRC32 一下,再 base64 编码。

展开看 X-S-Common 的生成代码,大概是这个结构:

// 伪代码letef={"x0":"4.3.7","x1":"xhs-pc-web","x2":"PC","x3":someBrowserInfo,"x4":timestamp,"x5":crc32(某些拼接数据)};X-S-Common=自定义b64(UTF8(JSON.stringify(ef)));

第四阶段:最大的坑 — mnsv2

前面几步都很顺利,MD5、base64、CRC32 都是标准算法,一两小时就全部还原并且 Node 里能跑了。

但生成出来的 X-s 始终不对,跟浏览器里的对不上。逐字节对比发现,问题出在那个mnsv2函数的返回值上。

这个mnsv2函数不在我搜到的 JS 文件里。它在页面运行期才动态加载,藏在内存里。用window.mnsv2能访问到,但它的源码是 JSVMP(JavaScript Virtual Machine Protected)——代码被编译成了字节码,由一个解释器逐条执行。

尝试把函数 toString(),得到的是字节码解码器的代码,不是真正的算法。这就比较麻烦了。

仔细看了一下mnsv2这个函数的调用签名:

window.mnsv2(u, MD5(u), MD5(url))

三个参数:

  1. u— URL + body 拼接的字符串

  2. MD5(u)— 对拼接串取 MD5

  3. MD5(url)— 对 URL 取 MD5

返回值格式:mns0301_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

一段看起来像 hash 的字符串,前缀mns0301_似乎是版本标识。

这个函数的实现是 JSVMP 保护过的,反汇编工程量太大。但好消息是:这个函数它就是个纯函数——同样的输入永远得到同样的输出。而且它在浏览器环境中是全局可访问的。

所以我的方案是:

  1. 在浏览器控制台执行window.mnsv2(u, m, w)拿到 token

  2. 把这个 token 当作一个配置参数传给 Node 脚本

  3. 剩下的 MD5、base64、CRC32 全部在 Node 本地算

这样做的结果就是:X-s 签名中只有 mnsv2Token 这一项需要从浏览器获取,其他所有计算都可以在 Node 里独立完成。


第五阶段:拼接验证

算法全部梳理清楚之后,写了一个完整的测试脚本。流程是这样的:

functionbuildXSign(url,body,mnsv2Token){constu=url+(typeofbody==='object'?JSON.stringify(body):body||'');constm=md5(u);constw=md5(url);// mnsv2Token 从浏览器获取constC=mnsv2Token;constP={x0:"4.3.7",x1:"xhs-pc-web",x2:"PC",x3:C,x4:typeofbody};constjson=JSON.stringify(P);constutf8Bytes=encodeUtf8(json);constb64=customBase64(utf8Bytes);return"XYS_"+b64;}

拿一个真实请求的原始数据来验证——同样的 URL、同样的 body、同样的 mnsv2Token——本地算出来的 X-s 跟浏览器里抓到的一模一样,逐字节对比完全一致。然后拿这个签名去发请求,返回 200,拿到了 11 条 feed 内容。


总结

小红书的签名体系拆开来看其实不复杂:

| 算法 | 说明 | 还原难度 |

|------|------|----------|

| MD5 | 标准 MD5,没有魔改 | ★☆☆☆☆ |

| 自定义 base64 | 换了字母表,逻辑同标准 base64 | ★★☆☆☆ |

| CRC32 | 标准 CRC32 | ★☆☆☆☆ |

| mnsv2 | JSVMP 保护的纯函数,输入输出确定 | ★★★★★ |

前三个都可以在 Node 里独立跑。最后一个 mnsv2 是 JSVMP 保护的,没法完整还原,但它是一个纯函数且浏览器全局可调,所以直接从浏览器拿 token 来用,效率最高。

最终的产物就是homefeed.js,一个 Node.js 脚本。依赖就只有 Node 内置的crypto模块,外加一个从浏览器获取的 mnsv2Token。


几个教训

  1. 先验证再逆向— 抓到签名后第一件事不是读代码,而是确认签名长得什么样、长什么样、跟什么有关。我这次走了弯路,一开始以为是 AES 加密,看着长度像就去翻 S-box,浪费了大半天。

  2. 注意自定义 base64— 小红书这个站最坑的地方不是它用了什么复杂加密,而是它改了 base64 字母表。一眼看过去以为是标准 base64,结果解码出来是乱码。要是早点注意到 64 个字符的顺序不对,能省很多时间。

  3. JSVMP 的函数别硬啃— 确认它是纯函数之后,直接拿来用就行了,不用非要把它反汇编出来。能跑通接口才是最终目标,不是非要还原每一行代码。

  4. 拼接规则最重要— 签名算法本身(MD5/base64)都是现成的,难的是搞清楚它把哪些东西拼在一起、什么顺序。这个只能靠断点调试和日志来确认,没有捷径。

http://www.jsqmd.com/news/1140921/

相关文章:

  • 进销存管理系统-ssm vue
  • 从零拆解Linux设备驱动模型:platform_bus注册到probe调用,每一步都给你画清楚
  • 加工坐标系摆放方式详解(现场实用版)
  • 从单体到微服务的灰度迁移策略:分步拆解不掉线的完整方案
  • 2026年选PP吸塑加工公司,看准这三点不踩坑
  • 金蝶云星空的网络控制设置
  • ComfyUI视频合成终极指南:5分钟掌握AI视频工作流
  • 计算机毕业设计之jsp某公司网络信息安全方案的设计
  • Linux 运维核心命令与配置
  • 工业信号干扰解决方案与FOD4216光耦应用
  • QQ空间备份终极指南:一键保存你的青春回忆
  • 终极免费直播录制神器:StreamCap如何一键搞定40+平台自动化录制
  • 浏览器文档下载解决方案:kill-doc如何让您轻松获取30+平台文档资源
  • 数据中心液冷冷却液怎么选?看懂ASHRAE第五版热指南就够了
  • 3分钟掌握ROS机器人仿真:wpr_simulation零基础入门终极指南
  • AI工作流自动化的触发与回退设计:人机协作的分级策略、审核节点与状态机建模
  • 性能测试|Tomcat服务器监控与调优
  • HapTile:面向接触密集型操作的触觉-视觉-语言多模态数据基座
  • 移动端部署EmbeddingGemma-300M:5个核心技巧实现离线语义搜索
  • 键盘定制革命:3天从零掌握QMK Toolbox的智能刷写艺术
  • 5分钟上手:Switch图形化注入工具TegraRcmGUI终极指南
  • 终极微信小程序图表库指南:wx-charts完整教程
  • 不用反复切换软件 Hermes 离线数字助手零基础落地全过程
  • 隐藏神器,一键隐藏
  • 厚膜无感高压电阻在储能行业的应用
  • EhViewer如何构建移动端漫画应用的安全防护体系?
  • 边缘推理延迟瓶颈分析工具链:从模型级到算子级的逐层 Profile 方法详解
  • Vim编辑器完全指南与Shell命令补充:从零到实战
  • MySQL 系统学习 第二阶段 第三章 DQL(Data Query Language)第三节:ORDER BY + LIMIT(排序 + 分页)
  • 近期这4个技术小动态,可能悄悄影响你的企业数字化