mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4mobile wallpaper 5mobile wallpaper 6
1543 字
4 分钟
某红薯x-s参数逆向分析
2026-05-31

小红书 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-sRequest Headers签名参数(主要分析目标)
x-s-commonRequest Headers另一个签名参数
x-tRequest Headers时间戳
Content-TypeRequest Headersapplication/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 关键函数定位#

image-20260531112202512

通过搜索关键字定位到关键代码:

// 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 环境中运行原始代码会遇到各种错误,原因是:

  1. 浏览器 API 缺失:window、navigator、document 等对象不存在
  2. DOM 方法缺失:removeChild、appendChild 等 DOM 操作无法执行
  3. 环境检测:代码会检测是否为真实浏览器环境

5.2 核心环境模拟#

创建 env.js 实现基本的浏览器环境模拟:

delete __filename
delete __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.mnsv2
window.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.mnsv2
window.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 技术要点#

  1. XHR 断点定位:通过设置 XHR 断点快速定位加密函数入口
  2. 自定义码表:很多网站使用自定义 Base64 码表,不能直接使用 atob()
  3. VM 保护:遇到 VM 保护时,可以尝试运行时替换而非完全逆向字节码
  4. 环境模拟:使用 Proxy 监控环境访问,逐步补全缺失的浏览器 API
  5. 版本号处理:遇到版本号不匹配时,可采用热补丁方式修正

9.2 常见问题#

问题解决方案
window.mnsv2 is not a function检查 vm.js 是否正确加载
removeChild 报错在 env.js 中重写 removeChild 方法
Base64 解码乱码检查是否使用了自定义码表
版本号不匹配采用运行时替换修正版本号

9.3 注意事项#

  1. 本文仅供学习交流,请勿用于非法用途
  2. 小红书的签名算法可能会更新,需要持续维护
  3. 绕过签名验证可能违反网站服务条款

本文基于 2026 年 5 月30日左右的小红书网页版分析,实际网站可能已更新。

分享

如果这篇文章对你有帮助,欢迎分享给更多人!

某红薯x-s参数逆向分析
https://fatdog.20060113.xyz/posts/xhs-xs/
作者
神秘大胖狗
发布于
2026-05-31
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

封面
Sample Song
Sample Artist
封面
Sample Song
Sample Artist
0:00 / 0:00