【WEB安全- 某书 X-s 签名逆向分析(含风控点)-有了 AI人人都能还原VMP】此文章归类为:WEB安全。
某书 X-s 签名逆向分析 - VM 保护下的 RC4 算法还原
0x01 抓包定位
首先抓包分析 /api/sns/web/v1/homefeed 接口,发现请求头中有两个关键签名参数:
全局搜索 X-s,定位到签名生成位置:
1 2 3 4 5 6 7 8 | 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 + "")
}
|
0x02 签名函数分析
跟进 seccore_signv2:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 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)))
}
|
签名流程很清晰:url + body → MD5 → mnsv2 加密 → JSON 包装 → 自定义 Base64
核心就是 window.mnsv2。
0x03 定位 VM 入口
这是最难的一步。window.mnsv2 只是个入口,实际执行的解释器藏在别处。
VM 的典型特征:
- 一个超长的 Base64 串或十六进制字符串(字节码)
while(true) + switch 或多层 if-else 结构(解释器)
- 虚拟寄存器、栈操作
我使用自研的逆向 MCP 工具,通过 Hook + 调用栈追踪:
1 2 3 4 5 6 7 | for (k in window) {
if (/^_[a-f0-9]{20,}$/.test(k)) console.log(k);
}
hook_function("window._0c6b9e549fef9ab9b4798ad1f12ea82b", logStack=true)
|
最终定位到 VM 文件:
1 | https://fe-static.xxxcdn.com/.../ds.js
|
0x04 AST 解混淆
VM 解释器代码当然是混淆的。使用 Babel AST 配合 AI 进行解混淆:
- 字符串数组还原 - 提取字符串表,替换索引访问
- 常量折叠 - 计算静态表达式
- 控制流还原 - 分析 switch-case 状态机
- 死代码移除 - 删除无用分支
解混淆后得到可读的解释器代码。
0x05 字节码提取
在代码中找到这样的字符串:
1 | var bytecode = "ABt7CAAUSAAACADfSAAACAD1SAAACAAH..."
|
这就是 VM 的字节码,通过自定义的 switch 基于栈来执行。
基于栈的 VM 只是一种实现方式,还有基于寄存器的 VM,原理类似。
0x06 构建操作码表
分析解释器的 switch-case,构建操作码映射:
| OpCode |
助记符 |
操作 |
| 0x00 |
PUSH |
压栈 |
| 0x01 |
POP |
出栈 |
| 0x02 |
ADD |
加法 |
| 0x03 |
CALL |
调用函数 |
| ... |
... |
... |
这一步用 AI 辅助分析非常高效。
0x07 反编译字节码
有了操作码表,就可以对字节码进行反编译:
; mns0301 字节码反编译
0000: PUSH_CONST "xh`)" ; 魔数
0004: PUSH_LOCAL r0
0008: CALL build_header
000C: PUSH_CONST 135
0010: CALL alloc_buffer
...
这里需要多次动态调试来验证反编译结果的正确性,同样交给 AI 完成。
0x08 函数分割与分析
反编译产出一个大文件,需要分割成独立函数:
1 2 3 4 5 | output/mns0301/functions/
├── build_input.js
├── rc4_encrypt.js
├── custom_base64.js
└── main.js
|
有些厂商会在这层再加控制流平坦化,这时需要在浏览器中 trace 执行流程。
0x09 算法还原
最后,结合静态分析和动态调试,还原完整算法。
输入数据结构 (135 bytes)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 偏移 大小 字段 说明
─────────────────────────────────────────────────
[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随机
─────────────────────────────────────────────────
|
加密流程
1 | 输入构建 (135B) → RC4 加密 (预置 S-box) → 自定义 Base64 → "mns0301_" + result
|
预置 S-box (256 bytes)
1 | SBOX = [108, 71, 200, 252, 102, 41, 228, 110, 198, 188, 243, 68, ...]
|
自定义 Base64 表
1 | MfgqrsbcyzPQRStuvC7mn501HIJBo2DEFTKdeNOwxWXYZap89+/A4UVLhijkl63G
|
XYS_ 外层编码
Base64 表:
1 | ZmserbBoHQtNP+wOcza/LpngG8yJq42KWYj0DSfdikx3VT16IlUAFM97hECvuRX5
|
JSON 结构:
{
"x0": "4.3.0", // 版本号
"x1": "xxx-pc-web", // appId
"x2": "Mac OS", // 平台
"x3": "mns0301_...", // mns0301 签名
"x4": "object" // 参数类型
}
0x0A 风控检测机制
核心原理:签名即数据上报
关键点:mns0301 使用 RC4 + 预置 S-box 加密,服务器持有相同的 S-box,可以完全解密还原原始数据。
签名不只是"防篡改",更是"行为数据上报通道"。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | ┌─────────────────────────────────────────────────────────────┐
│ 风控数据流 │
├─────────────────────────────────────────────────────────────┤
│ 浏览器端 服务器端 │
│ ──────── ──────── │
│ 采集行为数据 │
│ ↓ │
│ 写入 135B 输入结构 │
│ ↓ │
│ RC4 加密 (预置 S-box) │
│ ↓ │
│ Base64 编码 → X-s 签名 ──────→ Base64 解码 │
│ ↓ │
│ RC4 解密 (相同 S-box) │
│ ↓ │
│ 还原 135B 原始数据 │
│ ↓ │
│ 分析行为特征 → 风控判定 │
└─────────────────────────────────────────────────────────────┘
|
签名中携带的风控数据
回顾 135 字节输入结构,关键字段都是风控相关:
1 2 3 4 5 6 7 8 9 10 | 偏移 字段 风控用途
─────────────────────────────────────────────────
[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 或完全一致 |
| 时间戳连续性 |
递增 |
跳跃或回退 |
行为采集与请求的关联
核心机制:行为数据全局累积,每次请求时打包进签名
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | ┌──────────────────────────────────────────────────────────────┐
│ 页面生命周期 │
├──────────────────────────────────────────────────────────────┤
│ 页面加载 │
│ ↓ │
│ VM 初始化,对关键 DOM 元素挂载监听器 │
│ │ │
│ ├──
│ ├──
│ ├──
│ └──
│ ↓ │
│ 用户操作页面 → 计数器累积到全局变量 │
│ ↓ │
│ 发起 API 请求时,签名函数读取计数器写入 X-s │
└──────────────────────────────────────────────────────────────┘
|
为什么选这些元素? 都是用户正常浏览必然会经过的区域。如果一个"用户"发了 100 个请求,但从未触碰过这些核心元素 → 爬虫。
服务器如何判定风控
服务器通过对比多次请求中的计数器变化来判断:
1 2 3 4 5 6 7 8 9 10 11 12 13 | 正常用户:
───────────────────────────────────────────────────
请求1: 解密 → 点击=0, mouseenter=2, 停留=2s (刚进入)
请求2: 解密 → 点击=3, mouseenter=8, 停留=15s (正常浏览)
请求3: 解密 → 点击=5, mouseenter=12, 停留=30s (持续交互)
↑ 计数器递增,停留时长增加 → ✅ 正常
爬虫/脚本:
───────────────────────────────────────────────────
请求1: 解密 → 点击=0, mouseenter=0, 停留=0s
请求2: 解密 → 点击=0, mouseenter=0, 停留=0s
请求3: 解密 → 点击=0, mouseenter=0, 停留=0s
↑ 全是 0,没有任何页面交互 → ❌ 异常
|
VM 中的采集代码
从反汇编还原的逻辑:
1 2 3 4 5 6 7 8 9 10 11 | document.querySelector("#search-input")
.addEventListener("mouseenter", () => {
window._xxx[6] = Date.now();
})
.addEventListener("click", () => {
let interval = Date.now() - window._xxx[6];
if (interval >= 77) {
window._xxx[13]++;
}
});
|
77ms 阈值的意义
; 反汇编关键代码
PUSH_INT8 77
LT ; if (interval < 77) 不计入有效点击
人类点击的物理极限约 50-100ms,77ms 是一个合理的阈值:
- < 77ms:判定为程序触发,不计入计数器
- ≥ 77ms:判定为人类操作,计数器 +1
纯协议爬虫的挑战
对于不使用浏览器、直接发 HTTP 请求的纯协议爬虫,这套风控机制带来了显著挑战:
问题:计数器不能恒定
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | ❌ 错误做法 1:计数器始终为 0
───────────────────────────────────────
请求1: 点击=0, mouseenter=0 → 服务器:没有任何交互,爬虫
请求2: 点击=0, mouseenter=0
请求3: 点击=0, mouseenter=0
❌ 错误做法 2:计数器固定值
───────────────────────────────────────
请求1: 点击=5, mouseenter=10 → 服务器:数值不变,爬虫
请求2: 点击=5, mouseenter=10
请求3: 点击=5, mouseenter=10
❌ 错误做法 3:计数器随机但无规律
───────────────────────────────────────
请求1: 点击=3, mouseenter=8 → 服务器:数值跳跃/回退,爬虫
请求2: 点击=7, mouseenter=2 (mouseenter 不应该减少)
请求3: 点击=1, mouseenter=15 (点击不应该减少)
|
纯协议必须维护会话状态
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | class SessionState:
def __init__(self):
self.click_count = 0
self.mouseenter_count = 0
self.page_load_time = int(time.time() * 1000)
def before_request(self):
self.click_count += random.randint(1, 3)
self.mouseenter_count += random.randint(2, 5)
def get_sign_params(self):
return {
'click': self.click_count,
'mouseenter': self.mouseenter_count,
'page_load': self.page_load_time,
'current': int(time.time() * 1000)
}
|
这套风控的设计意图
| 爬虫类型 |
难度 |
原因 |
| 简单脚本 |
❌ 无法绕过 |
计数器为 0,立即识别 |
| 固定参数 |
❌ 无法绕过 |
多次请求数值不变 |
| 随机参数 |
⚠️ 可能被识别 |
数值变化不符合真实规律 |
| 状态维护 |
✅ 可绕过 |
需要理解业务逻辑,成本高 |
| 浏览器自动化 |
✅ 可绕过 |
真实触发事件,但效率低 |
结论:纯协议爬虫必须维护会话状态,模拟计数器的单调递增趋势,这大大增加了爬虫的开发成本。
0x0B Python 实现
1 2 3 4 5 6 7 8 9 10 11 | from output.x_sign import XSign
x_sign = XSign(a1="your_a1_cookie", platform="Mac OS")
url = "/api/sns/web/v1/homefeed"
params = {"cursor_score": "", "num": 27}
headers = x_sign.get_headers(url, params)
|
0x0C 总结
逆向分析流程
1 2 3 4 5 6 7 8 9 | ┌─────────────────────────────────────────────────────────┐
│ 逆向分析流程 │
├─────────────────────────────────────────────────────────┤
│ 抓包定位 → 函数分析 → 定位 VM 入口 │
│ ↓ │
│ AST 解混淆 → 提取字节码 → 构建操作码 │
│ ↓ │
│ 反编译 → 函数分割 → 算法还原 → Python 实现 │
└─────────────────────────────────────────────────────────┘
|
架构总览
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | ┌─────────────────────────────────────────────────────────────┐
│ 某书签名系统 │
├─────────────────────────────────────────────────────────────┤
│ seccore_signv2(url, body) │
│ │ │
│ ▼ │
│ ┌─────────┐ ┌─────────────┐ ┌──────────────┐ │
│ │ MD5 │ → │ mnsv2 │ → │ customBase64 │ → X-s │
│ └─────────┘ └──────┬──────┘ └──────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────┐ │
│ │ mns0301 │ │
│ │ (RC4) │ │
│ └────┬─────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ RC4 (JS) │ │
│ │ 预置 S-box │ │
│ └──────────────┘ │
└─────────────────────────────────────────────────────────────┘
|
mns0301 使用 RC4 流密码 + 预置 S-box,整体难度适中,核心在于定位 VM 入口和还原字节码逻辑。
更多【WEB安全- 某书 X-s 签名逆向分析(含风控点)-有了 AI人人都能还原VMP】相关视频教程:www.yxfzedu.com