首先抓包分析 /api/sns/web/v1/homefeed 接口,发现请求头中有两个关键签名参数:
全局搜索 X-s,定位到签名生成位置:
跟进 seccore_signv2:
签名流程很清晰:url + body → MD5 → mnsv2 加密 → JSON 包装 → 自定义 Base64
核心就是 window.mnsv2。
这是最难的一步。window.mnsv2 只是个入口,实际执行的解释器藏在别处。
VM 的典型特征:
我使用自研的逆向 MCP 工具,通过 Hook + 调用栈追踪:
最终定位到 VM 文件:
VM 解释器代码当然是混淆的。使用 Babel AST 配合 AI 进行解混淆:
解混淆后得到可读的解释器代码。
在代码中找到这样的字符串:
这就是 VM 的字节码,通过自定义的 switch 基于栈来执行。
基于栈的 VM 只是一种实现方式,还有基于寄存器的 VM,原理类似。
分析解释器的 switch-case,构建操作码映射:
这一步用 AI 辅助分析非常高效。
有了操作码表,就可以对字节码进行反编译:
这里需要多次动态调试来验证反编译结果的正确性,同样交给 AI 完成。
反编译产出一个大文件,需要分割成独立函数:
有些厂商会在这层再加控制流平坦化,这时需要在浏览器中 trace 执行流程。
最后,结合静态分析和动态调试,还原完整算法。
Base64 表:
JSON 结构:
关键点:mns0301 使用 RC4 + 预置 S-box 加密,服务器持有相同的 S-box,可以完全解密还原原始数据。
签名不只是"防篡改",更是"行为数据上报通道"。
回顾 135 字节输入结构,关键字段都是风控相关:
服务器解密签名后,可以进行以下判断:
核心机制:行为数据全局累积,每次请求时打包进签名
为什么选这些元素? 都是用户正常浏览必然会经过的区域。如果一个"用户"发了 100 个请求,但从未触碰过这些核心元素 → 爬虫。
服务器通过对比多次请求中的计数器变化来判断:
从反汇编还原的逻辑:
人类点击的物理极限约 50-100ms,77ms 是一个合理的阈值:
对于不使用浏览器、直接发 HTTP 请求的纯协议爬虫,这套风控机制带来了显著挑战:
问题:计数器不能恒定
纯协议必须维护会话状态
这套风控的设计意图
结论:纯协议爬虫必须维护会话状态,模拟计数器的单调递增趋势,这大大增加了爬虫的开发成本。
mns0301 使用 RC4 流密码 + 预置 S-box,整体难度适中,核心在于定位 VM 入口和还原字节码逻辑。
try {
var _ = "X-s",
b = "X-t",
x = getRealUrl(a, c, d),
p = seccore_signv2;
p && (r.headers[_] = p(x, s),
r.headers[b] = +new Date + "")
}
try {
var _ = "X-s",
b = "X-t",
x = getRealUrl(a, c, d),
p = seccore_signv2;
p && (r.headers[_] = p(x, s),
r.headers[b] = +new Date + "")
}
function seccore_signv2(e, r) {
var a = window.toString,
c = e;
"[object Object]" === a.call(r) || "[object Array]" === a.call(r) ||
(void 0 === r ? "undefined" : (0, m._)(r)) === "object" && null !== r
? c += JSON.stringify(r)
: "string" == typeof r && (c += r);
var d = (0, h.Pu)([c].join("")),
s = (0, h.Pu)(e),
f = window.mnsv2(c, d, s),
l = {
x0: u.i8,
x1: "xxx-pc-web",
x2: window[u.mj] || "PC",
x3: f,
x4: r ? void 0 === r ? "undefined" : (0, m._)(r) : ""
};
return "XYS_" + (0, h.xE)((0, h.lz)(JSON.stringify(l)))
}
function seccore_signv2(e, r) {
var a = window.toString,
c = e;
"[object Object]" === a.call(r) || "[object Array]" === a.call(r) ||
(void 0 === r ? "undefined" : (0, m._)(r)) === "object" && null !== r
? c += JSON.stringify(r)
: "string" == typeof r && (c += r);
var d = (0, h.Pu)([c].join("")),
s = (0, h.Pu)(e),
f = window.mnsv2(c, d, s),
l = {
x0: u.i8,
x1: "xxx-pc-web",
x2: window[u.mj] || "PC",
x3: f,
x4: r ? void 0 === r ? "undefined" : (0, m._)(r) : ""
};
return "XYS_" + (0, h.xE)((0, h.lz)(JSON.stringify(l)))
}
for (k in window) {
if (/^_[a-f0-9]{20,}$/.test(k)) console.log(k);
}
hook_function("window._0c6b9e549fef9ab9b4798ad1f12ea82b", logStack=true)
for (k in window) {
if (/^_[a-f0-9]{20,}$/.test(k)) console.log(k);
}
hook_function("window._0c6b9e549fef9ab9b4798ad1f12ea82b", logStack=true)
https://fe-static.xxxcdn.com/.../ds.js
https://fe-static.xxxcdn.com/.../ds.js
var bytecode = "ABt7CAAUSAAACADfSAAACAD1SAAACAAH..."
var bytecode = "ABt7CAAUSAAACADfSAAACAD1SAAACAAH..."
| OpCode |
助记符 |
操作 |
| 0x00 |
PUSH |
压栈 |
| 0x01 |
POP |
出栈 |
| 0x02 |
ADD |
加法 |
| 0x03 |
CALL |
调用函数 |
| ... |
... |
... |
output/mns0301/functions/
├── build_input.js
├── rc4_encrypt.js
├── custom_base64.js
└── main.js
output/mns0301/functions/
├── build_input.js
├── rc4_encrypt.js
├── custom_base64.js
└── main.js
偏移 大小 字段 说明
─────────────────────────────────────────────────
[0-3] 4B Header "xh`)" 魔数
[4-7] 4B Random 随机数 (little-endian)
[8-15] 8B Timestamp1 当前时间戳 ms
[16-23] 8B Timestamp2 页面加载时间戳 ms
[24-27] 4B Field3 固定值 (15-17)
[28-31] 4B Field4 计数器
[32-35] 4B Field5 计数器
[36-43] 8B Double 随机浮点数
[44-96] 53B a1 "4" + a1_cookie (53字节)
[97] 1B 分隔符 '\n'
[98-107] 10B xsecappid "xxx-pc-web"
[108] 1B 分隔符 '\x01'
[109-134] 26B Extra 1随机 + 17固定 + 8随机
─────────────────────────────────────────────────
偏移 大小 字段 说明
─────────────────────────────────────────────────
[0-3] 4B Header "xh`)" 魔数
[4-7] 4B Random 随机数 (little-endian)
[8-15] 8B Timestamp1 当前时间戳 ms
[16-23] 8B Timestamp2 页面加载时间戳 ms
[24-27] 4B Field3 固定值 (15-17)
[28-31] 4B Field4 计数器
[32-35] 4B Field5 计数器
[36-43] 8B Double 随机浮点数
[44-96] 53B a1 "4" + a1_cookie (53字节)
[97] 1B 分隔符 '\n'
[98-107] 10B xsecappid "xxx-pc-web"
[108] 1B 分隔符 '\x01'
[109-134] 26B Extra 1随机 + 17固定 + 8随机
─────────────────────────────────────────────────
输入构建 (135B) → RC4 加密 (预置 S-box) → 自定义 Base64 → "mns0301_" + result
输入构建 (135B) → RC4 加密 (预置 S-box) → 自定义 Base64 → "mns0301_" + result
SBOX = [108, 71, 200, 252, 102, 41, 228, 110, 198, 188, 243, 68, ...]
SBOX = [108, 71, 200, 252, 102, 41, 228, 110, 198, 188, 243, 68, ...]
MfgqrsbcyzPQRStuvC7mn501HIJBo2DEFTKdeNOwxWXYZap89+/A4UVLhijkl63G
MfgqrsbcyzPQRStuvC7mn501HIJBo2DEFTKdeNOwxWXYZap89+/A4UVLhijkl63G
ZmserbBoHQtNP+wOcza/LpngG8yJq42KWYj0DSfdikx3VT16IlUAFM97hECvuRX5
ZmserbBoHQtNP+wOcza/LpngG8yJq42KWYj0DSfdikx3VT16IlUAFM97hECvuRX5
┌─────────────────────────────────────────────────────────────┐
│ 风控数据流 │
├─────────────────────────────────────────────────────────────┤
│ 浏览器端 服务器端 │
│ ──────── ──────── │
│ 采集行为数据 │
│ ↓ │
│ 写入 135B 输入结构 │
│ ↓ │
│ RC4 加密 (预置 S-box) │
│ ↓ │
│ Base64 编码 → X-s 签名 ──────→ Base64 解码 │
│ ↓ │
│ RC4 解密 (相同 S-box) │
│ ↓ │
│ 还原 135B 原始数据 │
│ ↓ │
│ 分析行为特征 → 风控判定 │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 风控数据流 │
├─────────────────────────────────────────────────────────────┤
│ 浏览器端 服务器端 │
│ ──────── ──────── │
│ 采集行为数据 │
│ ↓ │
│ 写入 135B 输入结构 │
│ ↓ │
│ RC4 加密 (预置 S-box) │
│ ↓ │
│ Base64 编码 → X-s 签名 ──────→ Base64 解码 │
│ ↓ │
│ RC4 解密 (相同 S-box) │
│ ↓ │
│ 还原 135B 原始数据 │
│ ↓ │
│ 分析行为特征 → 风控判定 │
└─────────────────────────────────────────────────────────────┘
偏移 字段 风控用途
─────────────────────────────────────────────────
[4-7] Random 请求唯一标识,防重放
[8-15] Timestamp1 当前时间戳,检测时间异常
[16-23] Timestamp2 页面加载时间,计算停留时长
[24-27] Field3 行为标记位
[28-31] Field4 ★ 点击计数器
[32-35] Field5 ★ mouseenter 计数器
[36-43] Double 随机数,增加熵值
─────────────────────────────────────────────────
偏移 字段 风控用途
─────────────────────────────────────────────────
[4-7] Random 请求唯一标识,防重放
[8-15] Timestamp1 当前时间戳,检测时间异常
[16-23] Timestamp2 页面加载时间,计算停留时长
[24-27] Field3 行为标记位
[28-31] Field4 ★ 点击计数器
[32-35] Field5 ★ mouseenter 计数器
[36-43] Double 随机数,增加熵值
─────────────────────────────────────────────────
| 检测项 |
正常用户 |
爬虫/脚本 |
| 页面停留时长 |
> 几秒 |
0 或极短 |
| 点击计数 |
有累积 |
始终为 0 |
| mouseenter 计数 |
有累积 |
始终为 0 |
| 点击/mouseenter 比例 |
接近 1:N |
异常比例 |
| 请求间隔 |
> 100ms |
< 77ms 或完全一致 |
| 时间戳连续性 |
递增 |
跳跃或回退 |
┌──────────────────────────────────────────────────────────────┐
│ 页面生命周期 │
├──────────────────────────────────────────────────────────────┤
│ 页面加载 │
│ ↓ │
│ VM 初始化,对关键 DOM 元素挂载监听器 │
│ │ │
│ ├──
│ ├──
│ ├──
│ └──
│ ↓ │
│ 用户操作页面 → 计数器累积到全局变量 │
│ ↓ │
│ 发起 API 请求时,签名函数读取计数器写入 X-s │
└──────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ 页面生命周期 │
├──────────────────────────────────────────────────────────────┤
│ 页面加载 │
│ ↓ │
│ VM 初始化,对关键 DOM 元素挂载监听器 │
│ │ │
│ ├──
│ ├──
│ ├──
│ └──
│ ↓ │
[培训]Windows内核深度攻防:从Hook技术到Rootkit实战!