在完成《JS逆向实战:某看视频hk_sign参数逆向实战全记录》后,我们继续深入JS逆向领域,今天的目标是小红薯X-S签名逆向分析。由于小红薯加密算法的加密逻辑相对复杂,我们将分两篇文章来详细讲解,本文主要聚焦于X-S参数的外层加密逻辑解析。
以小红薯的 /api/sns/web/v1/comment/like 接口为例,这是给评论点赞的一个接口,其中请求头中就包含 X-S 参数,这就是我们此次的逆向目标。
本文目录
- 抓包分析:定位X-S参数位置
- 搜索技巧:在众多结果中精准定位
- 代码分析:识别加密函数入口
- 断点调试:验证加密函数功能
- 算法解析:深入加密函数内部
- 参数分析:解密f对象结构
- 代码扣取:实现本地加密
- 运行结果
- 技术总结
- 下期预告
1. 抓包分析:定位X-S参数位置
打开小红薯网站,随意进入一个博主的评论区,随机找条评论点赞。在抓包工具中观察,会发现一个 /api/sns/web/v1/comment/like 的接口请求,在请求头中可以看到 x-s 参数,这就是我们此次的逆向目标。

2. 搜索技巧:在众多结果中精准定位
按照常规思路,首先在源代码中全局搜索 x-s 参数。但搜索结果出现大量内容,且大部分都是缩略显示,没有明显的赋值代码。
个人踩坑心得:像 x-s 这样的通用词汇,搜索结果会很多且大多缩略显示。如果不仔细排查,很容易错过关键代码,转而使用XHR断点等更复杂的方法。
实际上,只要细心查找,在 vendor-dynamic.9653eb95.js 文件中就能找到关键代码:
var _ = "X-s"
在已知参数名的情况下,可以搜索 x-s"、X-S" 或 X-s(注意大小写),这样搜索结果就只剩下 vendor-dynamic.9653eb95.js 文件的这一条结果了。

3. 代码分析:识别加密函数入口
点击进入 vendor-dynamic.9653eb95.js 文件,可以看到清晰的加密逻辑:
try {
var _ = "X-s"
, b = "X-t"
, x = getRealUrl(r, c, d)
, v = seccore_signv2;
v && (a.headers[_] = v(x, s),
a.headers[b] = +new Date + "")
}
这段代码显示:
X-s字符串被赋值给变量_- 在
a.headers[_] = v(x, s)这行代码中,请求头的X-s参数通过v(x, s)函数生成 v函数实际上就是seccore_signv2
因此,我们需要在 v(x, s) 这里下断点进行分析。
4. 断点调试:验证加密函数功能
在 v(x, s) 处设置断点,然后随便找条评论点赞。当代码在断点处暂停时,在控制台执行 v(x, s) 查看是否可以生成X-S的值。

很明显能够正常生成X-S值,同时也可以在控制台打印传递的参数 x 和 s 的值进行观察。
5. 算法解析:深入加密函数内部
在 v(x, s) 断点处单步执行,跟进函数内部。通过seccore_signv2分析,我们看到其函数原型:
function seccore_signv2(e, a) {
var r = window.toString
, c = e;
"[object Object]" === r.call(a) || "[object Array]" === r.call(a) || (void 0 === a ? "undefined" : (0,
h._)(a)) === "object" && null !== a ? c += JSON.stringify(a) : "string" == typeof a && (c += a);
var d = (0,
p.Pu)([c].join(""))
, s = window.mnsv2(c, d)
, f = {
x0: u.i8,
x1: "xhs-pc-web",
x2: window[u.mj] || "PC",
x3: s,
x4: a ? void 0 === a ? "undefined" : (0,
h._)(a) : ""
};
return "XYS_" + (0,p.xE)((0,p.lz)(JSON.stringify(f)))
}
并且return 语句中的 "XYS_" 字符串拼接很引人注目,这正是X-S参数的开头。我们在return处下断点进行进一步分析。
通过控制台执行核心代码:
"XYS_" + (0, p.xE)((0, p.lz)(JSON.stringify(f)))
确认确实生成了X-S参数的值。分析这段代码发现:
(0, p.xE)和(0, p.lz)使用了逗号操作符的代码压缩写法p.xE– 函数名为b64Encode(平台自定义的Base64编码,非标准btoa)p.lz– 函数名为encodeUtf8(平台自定义的UTF-8编码,非标准TextEncoder)JSON.stringify– 浏览器原生方法,用于将JavaScript对象转换为JSON字符串
6. 参数分析:解密f对象结构
X-S参数的值在return处生成,需要传递参数f。在控制台打印f,得到如下结构:
{
"x0": "4.2.6",
"x1": "xhs-pc-web",
"x2": "Windows",
"x3": "mns0301_fZaKqr1GTXi35nL7nR/vWRHbH6",
"x4": "object"
}
这个f对象包含了版本信息、平台标识、设备信息和核心的加密数据x3。
7. 代码扣取:实现本地加密
现在需要将平台自定义的p.xE(b64Encode)和p.lz(encodeUtf8)函数代码扣取出来。鼠标悬停进入这两个函数,将相关代码扣取到本地。
创建test.js文件,初步代码如下:
function encodeUtf8(e) {
for (var a = encodeURIComponent(e), r = [], c = 0; c < a.length; c++) {
var d = a.charAt(c);
if ("%" === d) {
var s = parseInt(a.charAt(c + 1) + a.charAt(c + 2), 16);
r.push(s),
c += 2
} else
r.push(d.charCodeAt(0))
}
return r
}
function b64Encode(e) {
for (var a, r = e.length, d = r % 3, s = [], f = 16383, u = 0, l = r - d; u < l; u += f)
s.push(encodeChunk(e, u, u + f > l ? l : u + f));
return 1 === d ? (a = e[r - 1],
s.push(c[a >> 2] + c[a << 4 & 63] + "==")) : 2 === d && (a = (e[r - 2] << 8) + e[r - 1],
s.push(c[a >> 10] + c[a >> 4 & 63] + c[a << 2 & 63] + "=")),
s.join("")
}
var f = { "x0": "4.2.6", "x1": "xhs-pc-web", "x2": "Windows", "x3": "mns0301_fZaKqb6fO7JIu1wmnR/vWb6mfa5MR85VUKjxYjAI2j597I4d7MsNUxW9U13QypUaJPChm0FQ5mnXRDv9TuzmSym/5y2G+NJ0OH2IVPSpoq2qVKm49m7A0JHKXS7dV8Sv5TMCnpsRzRJPUaT3lBnKSndVZlciE0JRIk0OHF==", "x4": "object" }
var sign = "XYS_" + b64Encode(encodeUtf8(JSON.stringify(f)))
console.log('生成的x-s值:', sign)
运行后发现缺少encodeChunk、tripletToBase64函数和c参数。进入encodeUtf8所在的文件,搜索并扣取缺少的函数。
在tripletToBase64函数内设置断点,暂停时在控制台输入c获取其值:

最终的test.js文件完整代码:
var c = ['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']
function tripletToBase64(e) {
return c[e >> 18 & 63] + c[e >> 12 & 63] + c[e >> 6 & 63] + c[63 & e]
}
function encodeChunk(e, a, r) {
for (var c, d = [], s = a; s < r; s += 3)
c = (e[s] << 16 & 0xff0000) + (e[s + 1] << 8 & 65280) + (255 & e[s + 2]),
d.push(tripletToBase64(c));
return d.join("")
}
function encodeUtf8(e) {
for (var a = encodeURIComponent(e), r = [], c = 0; c < a.length; c++) {
var d = a.charAt(c);
if ("%" === d) {
var s = parseInt(a.charAt(c + 1) + a.charAt(c + 2), 16);
r.push(s),
c += 2
} else
r.push(d.charCodeAt(0))
}
return r
}
function b64Encode(e) {
for (var a, r = e.length, d = r % 3, s = [], f = 16383, u = 0, l = r - d; u < l; u += f)
s.push(encodeChunk(e, u, u + f > l ? l : u + f));
return 1 === d ? (a = e[r - 1],
s.push(c[a >> 2] + c[a << 4 & 63] + "==")) : 2 === d && (a = (e[r - 2] << 8) + e[r - 1],
s.push(c[a >> 10] + c[a >> 4 & 63] + c[a << 2 & 63] + "=")),
s.join("")
}
var f = {
"x0": "4.2.6",
"x1": "xhs-pc-web",
"x2": "Windows",
"x3": "mns0301_fZaKqqlloXfy4fA7nR/vWb6mfa5MR85VU/jxYjAI2jnq7I4dIxqs70asIoVQypUaJPChm0FQ5mnXRDv9TuzmSym/5y2G+NJ0OH2IVPSpoq2qVKm49m7A0JHKXS7dV8Sv5TMCnpsRzRJPUaT3lDnKSndVZlciE0JRIk0OHF==",
"x4": ""
}
var sign = "XYS_" + b64Encode(encodeUtf8(JSON.stringify(f)))
console.log('生成的x-s值:',sign)
8. 运行结果
在本地控制台运行:node test.js,得到的结果与浏览器控制台生成的X-S值完全一致。

9. 技术总结
在本篇中,我们成功解析了小红薯X-S签名逆向的外层加密流程。核心发现是,X-S的值由固定前缀"XYS_"拼接一段自定义Base64编码而成,而该编码的原始数据是一个结构固定的JSON对象f。
整个逆向过程的关键在于:
- 精准搜索:在众多的代码中,通过特定关键词快速定位加密入口文件。
- 断点调试:利用断点确认加密函数seccore_signv2分析并跟进。
- 代码扣取:将关键的自定义编码函数(
encodeUtf8和b64Encode)及其依赖完整还原至本地环境。
至此,我们已经能够完全复现给定f对象时的X-S签名生成。然而,这仅仅是整个加密体系的第一层。
10. 下期预告
细心的读者应该已经发现,我们至今尚未解答一个核心问题:生成f对象中那个看似随机且冗长的x3参数,其背后的算法究竟是什么?最初传递给加密函数seccore_signv2的参数x和s,它们在其中扮演了怎样的角色?
这就好比我们战胜了一个守卫,拿到了宝箱(生成了X-S),却发现宝箱的钥匙(x3的生成逻辑)还藏在更深的迷宫里。x3才是整个签名机制动态变化的核心,是小红薯安全体系真正的挑战。
革命还未完成,同志仍需努力。在下一篇《小红薯X-S签名逆向解析(下篇):深入`x3`核心参数与完整算法还原》中,我们将潜入算法暗流,直面最复杂的加密核心,彻底攻克x3参数的生成逻辑,完成对整个签名机制的完整逆向。敬请期待!
本文由林石工作室提供技术支持,转载请注明出处。

