欢迎光临
我们一直在努力

小红书 X-s 签名逆向分析(含风控点)-有了 AI人人都能还原VMP

某书 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 进行解混淆:

  • 字符串数组还原 – 提取字符串表,替换索引访问
  • 常量折叠 – 计算静态表达式
  • 控制流还原 – 分析 switch-case 状态机
  • 死代码移除 – 删除无用分支
  • 解混淆后得到可读的解释器代码。

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

    │      │                                                       │

    │      ├── #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(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)

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

    赞(0)
    未经允许不得转载:171主机测评 » 小红书 X-s 签名逆向分析(含风控点)-有了 AI人人都能还原VMP
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址