小红书 X-S 签名逆向分析实战
一、前言
本文记录了一次完整的小红书 Web 端签名参数逆向分析过程,从抓包定位到环境补全,最终实现 Node.js 环境下的签名生成。整个过程涵盖了 JS 逆向中常见的技术手段,包括 XHR 断点调试、OB 混淆分析、VM 保护识别以及浏览器环境模拟。
注意,一定要细心,否则一个小问题就要排查好久!
二、抓包定位
2.1 确定目标接口
打开小红书网页版 https://www.xiaohongshu.com/explore,使用 Chrome DevTools 的 Network 面板监控网络请求。筛选 XHR/Fetch 类型请求后,发现一个关键接口:
https://edith.xiaohongshu.com/api/sns/web/v1/homefeed该接口返回首页推荐内容,请求方法为 POST,需要携带一个名为 x-s 的签名参数。
其他参数并不是必要的,校验没那么严格,你可以进行重放来尝试一下。当然,为了做到万无一失,肯定是所有参数都动态生成才行。本文我们只讲x-s。
2.2 初步分析请求参数
在 Network 面板中查看该请求的 Headers:
| 参数 | 位置 | 说明 |
|---|---|---|
| x-s | Request Headers | 签名参数(主要分析目标) |
| x-s-common | Request Headers | 另一个签名参数 |
| x-t | Request Headers | 时间戳 |
| Content-Type | Request Headers | application/json |
x-s 是我们需要逆向分析的核心参数,它的格式为 XYS_ 开头加上 Base64 编码的字符串。
三、XHR 断点定位加密入口
3.1 设置 XHR 断点
在浏览器开发者工具中设置 XHR 断点,触发条件为请求 URL 包含 homefeed:
break_on_xhr("homefeed")刷新页面触发请求,断点命中后查看调用栈(Call Stack)。
3.2 回溯调用栈
从调用栈底部向上分析,发现关键函数链:
sign() -> seccore_signv2() -> mnsv2()其中 seccore_signv2 是生成 x-s 签名的核心函数,mnsv2 是生成版本标识的函数。
四、代码分析与解混淆
4.1 混淆类型识别
分析获取到的 JavaScript 代码,发现以下混淆特征:
| 特征 | 混淆类型 |
|---|---|
_0x 前缀变量 | OB 混淆器 |
| 大型字符串数组 | 字符串加密 |
while(true) + switch | 控制流平坦化 |
| 字节码数组 | VM 保护 |
4.2 关键函数定位

通过搜索关键字定位到关键代码:
// seccore_signv2 函数function seccore_signv2(e, a) { var s = window.toString , u = e; u += JSON.stringify(a) var m = (0, Pu)([u].join("")) , w = (0, Pu)(e) , C = window.mnsv2(u, m, w) , P = { x0: '4.3.5', x1: "xhs-pc-web", x2: 'Windows', x3: C, x4: 'object' }; return "XYS_" + (0, b64Encode)((0, encodeUtf8)(JSON.stringify(P))) }这里P里的几个参数我们在浏览器可以直接拿到,是固定值。唯独C比较难处理。
然后是K.PU,这个是个md5,我们追进去可以看到它做了字符串解码,映射等一些列操作,直接调用K,PU(“1234656”)也可以发现是md5.
底下的b64Encode和 encodeUtf8在网站上也是个对象属性函数调用,在node运行报错,扣下来即可。
4.3 自定义 Base64 码表
通过分析 vendor-dynamic.js,发现网站使用了自定义 Base64 码表而非标准码表:
var u = [], m = "ZmserbBoHQtNP+wOcza/LpngG8yJq42KWYj0DSfdikx3VT16IlUAFM97hECvuRX5", w = 0, C = m.length;for (; w < C; ++w) u[w] = m[w];
function tripletToBase64(e) { return u[(e >> 18) & 63] + u[(e >> 12) & 63] + u[(e >> 6) & 63] + u[63 & e];}这是逆向过程中容易遗漏的细节,很多实现会直接使用 atob() 导致解码失败。
这个u是自定义的,码表。扣tripletToBase64函数时,报错会主动驱使你去找它。
五、环境补全
5.1 Node.js 环境差异
在 Node.js 环境中运行原始代码会遇到各种错误,原因是:
- 浏览器 API 缺失:window、navigator、document 等对象不存在
- DOM 方法缺失:removeChild、appendChild 等 DOM 操作无法执行
- 环境检测:代码会检测是否为真实浏览器环境
5.2 核心环境模拟
创建 env.js 实现基本的浏览器环境模拟:
delete __filenamedelete __dirname
// 使用 Proxy 监控环境访问function watch(obj, name) { return new Proxy(obj, { get: function (target, property) { const value = target[property]; // 记录环境访问日志 return value; }, set: (target, property, newValue, receiver) => { return Reflect.set(target, property, newValue); } });}
// 模拟 window 对象window = global;window.toString = Object.prototype.toString;
// 模拟 document 对象(只需要基础功能)document = { createElement: () => ({ style: {}, appendChild: () => {} }), createDocumentFragment: () => ({ appendChild: () => {} }), querySelector: () => null, removeChild: (child) => child};
// 模拟 navigator 对象navigator = { userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', webdriver: false, platform: 'Win32', language: 'zh-CN', plugins: []};
// 模拟 screen 对象screen = { width: 1920, height: 1080, colorDepth: 24, availWidth: 1920, availHeight: 1040};
// 模拟 location 对象location = { href: 'https://www.xiaohongshu.com/explore', hostname: 'www.xiaohongshu.com', pathname: '/explore', protocol: 'https:'};5.3 安全函数处理
网站代码中通常会检测函数的 toString 输出,需要hook:
const safeFunction = (function () { const $toString = Function.toString; const myFunction_toString_symbol = Symbol('('.concat('', ')'));
const myToString = function myToString() { return typeof this === 'function' && this[myFunction_toString_symbol] || $toString.$call(this); }
// Hook Function.prototype.toString Function.prototype.toString = function() { return myToString.$call(this); }
return { safeFunction, myToString };})();六、VM 保护分析与版本号问题
6.1 mnsv2 函数分析
mnsv2 函数被包裹在 VM 保护中,其实现是一个字节码解释器:
var glb = _0xe762c0(0x1a) == typeof window ? global : window;glb[_0xe762c0(0x73)] = function(_0x1f8d7a, _0x4ede15, _0xb2668e) { // 混淆的 mnsv2 实现逻辑 // ...}
// 执行字节码glb['_AUuXfEG27Xa3x'](__$c, [...]);6.2 版本号问题
问题现象:
- 本地 vm.js 生成的签名版本为
mns0201_xxx - 浏览器实际使用的版本为
mns0301_xxx
问题分析: 通过在浏览器中执行 window.mnsv2() 验证:
// 浏览器中的执行结果window.mnsv2('test', 'hash1', 'hash2')// 返回: "mns0301_gRaKqs/o4YzjCl/UED0idvq..."
// 本地 vm.js 的执行结果window.mnsv2('test', 'hash1', 'hash2')// 返回: "mns0201_O4I6HdkcP8MLY9l1TD7Drh..."版本号硬编码在 VM 字节码中,直接修改需要重新逆向字节码。
6.3 热补丁方案
既然字节码修改困难,我们采用运行时替换的方式修正版本号(取巧一下):
// 包装 window.mnsv2 来修正版本号 (0201 -> 0301)const originalMnsv2 = window.mnsv2window.mnsv2 = function(u, m, w) { const result = originalMnsv2.call(this, u, m, w) if (result && typeof result === 'string' && result.startsWith('mns0201_')) { return 'mns0301_' + result.substring(8) } return result}这种方案的优点是不需要深入理解 VM 字节码,缺点是签名部分可能与浏览器存在细微差异。但是接口校验不严格,依然是能过的,甚至回退的版本也可以。
七、完整签名生成代码
7.1 最终文件结构
再战小红书/├── env.js # 浏览器环境模拟├── vendor-dynamic.js # 网站动态脚本(参考)├── vm.js # VM 保护代码└── x-s.js # 签名生成入口7.2 x-s.js 完整代码
const crypto = require('crypto')require('./env.js')require('./vm.js')
// 包装 window.mnsv2 来修正版本号 (0201 -> 0301)const originalMnsv2 = window.mnsv2window.mnsv2 = function(u, m, w) { const result = originalMnsv2.call(this, u, m, w) if (result && typeof result === 'string' && result.startsWith('mns0201_')) { return 'mns0301_' + result.substring(8) } return result}
// MD5 哈希函数Pu = function Pu(e) { return crypto.createHash('md5').update(e).digest('hex');}
// 自定义 Base64 编码function tripletToBase64(e) { u = [ "Z", "m", "s", "e", "r", "b", "B", "o", "H", "Q", "t", "N", "P", "+", "w", "O", "c", "z", "a", "/", "L", "p", "n", "g", "G", "8", "y", "J", "q", "4", "2", "K", "W", "Y", "j", "0", "D", "S", "f", "d", "i", "k", "x", "3", "V", "T", "1", "6", "I", "l", "U", "A", "F", "M", "9", "7", "h", "E", "C", "v", "u", "R", "X", "5" ]; return u[e >> 18 & 63] + u[e >> 12 & 63] + u[e >> 6 & 63] + u[63 & e];}
function b64Encode(e) { // 完整的 Base64 编码实现 // ...}
// UTF-8 编码function encodeUtf8(e) { return Buffer.from(e, 'utf8');}
// 生成签名function generateXSign(query, body) { const q = query || '/api/sns/web/v1/homefeed'; const w = body || {}; const u = q + JSON.stringify(w); const m = Pu([u].join("")); const w_hash = Pu(q); const C = window.mnsv2(u, m, w_hash); const P = { x0: '4.3.5', x1: "xhs-pc-web", x2: 'Windows', x3: C, x4: 'object' }; return "XYS_" + b64Encode(encodeUtf8(JSON.stringify(P)));}
// 测试const xSign = generateXSign();console.log(xSign);八、验证与调试
8.1 运行测试
node x-s.js输出示例:
XYS_2UQhPsHCH0c1PUhMHjIj2erjwjQhyoPTqBPT49pjHjIj2eHjwjQgynEDJ74AHjIj2ePjwjQTJdPIPAZlgMkIPfpMJnuELL+Qt7kh...8.2 解码验证
使用自定义码表解码验证:
const customTable = [ "Z", "m", "s", "e", "r", "b", "B", "o", "H", "Q", "t", "N", "P", "+", "w", "O", "c", "z", "a", "/", "L", "p", "n", "g", "G", "8", "y", "J", "q", "4", "2", "K", "W", "Y", "j", "0", "D", "S", "f", "d", "i", "k", "x", "3", "V", "T", "1", "6", "I", "l", "U", "A", "F", "M", "9", "7", "h", "E", "C", "v", "u", "R", "X", "5"];
// 解码后的 JSON 结构{ "x0": "4.3.5", "x1": "xhs-pc-web", "x2": "Windows", "x3": "mns0301_ksjzko6O7FTIFe9bTsvNTywRRC2S...", "x4": "object"}九、总结
9.1 技术要点
- XHR 断点定位:通过设置 XHR 断点快速定位加密函数入口
- 自定义码表:很多网站使用自定义 Base64 码表,不能直接使用 atob()
- VM 保护:遇到 VM 保护时,可以尝试运行时替换而非完全逆向字节码
- 环境模拟:使用 Proxy 监控环境访问,逐步补全缺失的浏览器 API
- 版本号处理:遇到版本号不匹配时,可采用热补丁方式修正
9.2 常见问题
| 问题 | 解决方案 |
|---|---|
| window.mnsv2 is not a function | 检查 vm.js 是否正确加载 |
| removeChild 报错 | 在 env.js 中重写 removeChild 方法 |
| Base64 解码乱码 | 检查是否使用了自定义码表 |
| 版本号不匹配 | 采用运行时替换修正版本号 |
9.3 注意事项
- 本文仅供学习交流,请勿用于非法用途
- 小红书的签名算法可能会更新,需要持续维护
- 绕过签名验证可能违反网站服务条款
本文基于 2026 年 5 月30日左右的小红书网页版分析,实际网站可能已更新。
部分信息可能已经过时









