某书 X-s 签名逆向分析 - VM 保护下的 RC4 算法还原

0x01 抓包定位

首先抓包分析 /api/sns/web/v1/homefeed 接口,发现请求头中有两个关键签名参数:

  • X-s: 主签名
  • X-t: 时间戳

全局搜索 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

签名流程很清晰: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

// 发现 22 个随机命名的全局函数

for (k in window) {

    if (/^_[a-f0-9]{20,}$/.test(k)) console.log(k);

}

// Hook 其中一个,观察调用栈

hook_function("window._0c6b9e549fef9ab9b4798ad1f12ea82b", logStack=true)

最终定位到 VM 文件:

1

https://fe-static.xxxcdn.com/.../ds.js

0x04 AST 解混淆

VM 解释器代码当然是混淆的。使用 Babel AST 配合 AI 进行解混淆:

  1. 字符串数组还原 - 提取字符串表,替换索引访问
  2. 常量折叠 - 计算静态表达式
  3. 控制流还原 - 分析 switch-case 状态机
  4. 死代码移除 - 删除无用分支

解混淆后得到可读的解释器代码。

0x05 字节码提取

在代码中找到这样的字符串:

1

var bytecode = "ABt7CAAUSAAACADfSAAACAD1SAAACAAH..."  // 约 6KB

这就是 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      # 构建 135 字节输入

├── rc4_encrypt.js      # RC4 加密

├── custom_base64.js    # 自定义 Base64

└── 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-13426B    Extra          1随机 + 17固定 + 8随机

─────────────────────────────────────────────────

加密流程

1

输入构建 (135B) → RC4 加密 (预置 S-box) → 自定义 Base64 → "mns0301_" + result

预置 S-box (256 bytes)

1

SBOX = [108712002521024122811019818824368, ...]

自定义 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 元素挂载监听器                          │

│      │                                                       │

│      ├── #search-input      (搜索框,鼠标必经区域)            │

│      ├── #header-area       (头部,几乎必经)                  │

│      ├── #homefeed_recommend (首页推荐,主内容区)             │

│      └── #explore-guide-refresh (刷新按钮)                   │

│      ↓                                                       │

│  用户操作页面 → 计数器累积到全局变量                           │

│      ↓                                                       │

│  发起 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) {  // 间隔 >= 77ms 才计入

            window._xxx[13]++;  // 点击计数器 +1

        }

    });

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(13)

        self.mouseenter_count += random.randint(25)

    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)

# {'X-s': 'XYS_...', 'X-t': '1767359305641'}

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 入口和还原字节码逻辑。å

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐