xiaohongshu-xs-reverse

⚠️ 免责声明:本文仅用于安全研究与技术交流,请勿将相关技术用于非法用途。恶意使用相关技术造成的一切后果由使用者自行承担。

目标站点:https://www.xiaohongshu.com/explore

概述

小红书 Web 端在多数 API 请求中都携带 X-S 请求头进行签名校验。简单来说,服务端会验证请求头中的 X-S 是否合法,如果不合法就直接拒绝请求。本文会完整记录这个签名机制的分析过程,从环境模拟到算法还原,再到用 Python 调用生成签名并发起请求,一步步带你实现自动生成 X-S

逆向入口定位

想要找到签名逻辑,第一步就是定位加密函数在代码中的位置。打开目标站点,按以下步骤操作:

  1. 进入小红书探索页,打开浏览器开发者工具(F12),切换到 Network 面板。
  2. 筛选 XHR/Fetch 请求,随便滚动页面触发一些加载,找到带有 X-S 请求头的接口,例如首页 feed 接口 /api/sns/web/v1/homefeed
  3. Sources 面板中,全局搜索 X-sX-S 字符串,一般会在一些请求拦截器中找到赋值的地方。
  4. 更直接的方法是使用 XHR/fetch Breakpoints:在 Sources 面板右侧的 XHR/fetch Breakpoints 中添加一个过滤条件,比如 /api/sns/web/v1/homefeed,这样在请求发出前就会自动断下。然后通过调用栈一路向上回溯,就能找到签名生成的逻辑。

💡 小技巧:如果搜索 X-S 字符串太多,可以尝试搜索 XYS_ 前缀,这是最终签名的固定开头,极大概率直接定位到核心加密函数。

经过跟踪分析,会发现签名逻辑大致隐藏在一个巨大的 Webpack 打包文件里,并且函数名通常被混淆。我们需要做的就是将关键的加密逻辑剥离出来,在 Node.js 环境中复现。

核心签名流程分析

1. 签名生成步骤

通过断点调试和代码反混淆,梳理出 X-S 的完整生成流程:

  1. 拼接字符串:将 API 路径(如 /api/sns/web/v1/homefeed)与请求参数的 JSON 字符串拼接在一起。
  2. 计算 MD5:对上一步得到的字符串取 MD5 摘要。
  3. 调用 mnsv2 算法:将原始拼接字符串和 MD5 值传入一个名为 mnsv2 的自定义函数,生成核心签名(signature)。
  4. 组装签名对象:把版本号、平台、操作系统、上一步得到的签名等组合成一个固定结构的对象。
  5. 编码输出:将签名对象转为 JSON 字符串,经过 UTF-8 编码和自定义 Base64 编码,最后加上 XYS_ 前缀,得到最终的 X-S 值。

2. 关键数据结构

最终参与编码的对象结构大致如下:

const signObj = {
    x0: "4.2.1",        // API 版本号,需与当前页面对应,可能会更新
    x1: "xhs-pc-web",   // 平台固定标识
    x2: "Windows",      // 操作系统信息,也可以是 macOS 等
    x3: signature,      // mnsv2 算法生成的核心签名
    x4: 'object'        // 固定字段
};

整个签名过程用一句话概括就是:“参数拼起来算 MD5,再丢给 mnsv2 生成核心签名,最后用自定义 Base64 包装一下。”

关键代码实现

1. 浏览器环境模拟

小红书的前端代码会对代码运行环境进行校验(比如检查 navigator 的属性),我们要在 Node.js 中实现纯算法,就必须提前补全这些全局对象。通过 Proxy 和类继承等手段,可以“骗过”原代码,让它在 Node 环境下依然正常运行。

// 全局对象代理与模拟
window = global;
self = window;
globalThis = self;

// 以下浏览器核心类需要根据被调用的属性逐步补全
Navigator = function() {};
Navigator.prototype = { userAgent: "Mozilla/5.0 ...", platform: "Win32" };
navigator = new Navigator();

Location = function() {};
Location.prototype = { href: "https://www.xiaohongshu.com" };
location = new Location();

// 其他对象如 screen、document 等也可以如法炮制
screen = { width: 1920, height: 1080, colorDepth: 24 };

注意mnsv2 函数里具体用了哪些环境属性,需要根据你的逆向结果去补,并不是固定不变的。上面的代码只是一个示例框架。

2. 核心签名函数

有了补好的环境,就可以运行剥离出来的加密代码了。下面是一个简化的主逻辑示例,实际使用中你需要把逆向出的 mnsv2 函数完整搬过来。

const CryptoJS = require('crypto-js');

function generateXSign(apiPath, params) {
    // 1. 拼接 API 路径与参数 JSON
    const paramStr = apiPath + JSON.stringify(params);

    // 2. 计算拼接字符串的 MD5
    const md5Hash = CryptoJS.MD5(paramStr).toString();

    // 3. 调用 mnsv2 算法生成核心签名(需要你逆向得到的 mnsv2 实现)
    const signature = window.mnsv2(paramStr, md5Hash);

    // 4. 组装最终对象并编码
    const signObj = {
        x0: "4.2.1",
        x1: "xhs-pc-web",
        x2: "Windows",
        x3: signature,
        x4: 'object'
    };

    const signJson = JSON.stringify(signObj);
    const utf8Bytes = encodeUtf8(signJson);
    const base64Str = b64Encode(utf8Bytes);

    return "XYS_" + base64Str;
}

3. 辅助编码函数

小红书使用的 Base64 是自定义字典,不是标准 Base64,需要在逆向时提取对应字符表。下面是一个根据常见模式编写的示例,注意把 keyStr 替换成你逆向到的具体字符串。

// UTF-8 编码
function encodeUtf8(str) {
    return unescape(encodeURIComponent(str));
}

// 自定义 Base64 编码(注意替换正确字典)
function b64Encode(input) {
    // 此字典仅供参考,实际值需要从逆向代码中扣出来
    const keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
    let output = "";
    let i = 0;
    while (i < input.length) {
        const char1 = input.charCodeAt(i++);
        const char2 = input.charCodeAt(i++);
        const char3 = input.charCodeAt(i++);
        const enc1 = char1 >> 2;
        const enc2 = ((char1 & 3) << 4) | (char2 >> 4);
        const enc3 = ((char2 & 15) << 2) | (char3 >> 6);
        const enc4 = char3 & 63;

        if (isNaN(char2)) {
            enc3 = enc4 = 64;
        } else if (isNaN(char3)) {
            enc4 = 64;
        }
        output += keyStr.charAt(enc1) + keyStr.charAt(enc2) + keyStr.charAt(enc3) + keyStr.charAt(enc4);
    }
    return output;
}

端到端调用示例

为了方便实际使用,我们可以把 Node.js 脚本封装成一个签名服务,然后用 Python 调起脚本获取签名,再携带签名去请求接口。

1. Node.js 签名脚本(xiaohongshu_sign.js)

将上面的环境补全代码、mnsv2 函数以及生成函数整合到一个文件中,结尾输出 JSON 格式的 X-s

// ... 省略环境模拟和 mnsv2 函数引入 ...

const apiPath = '/api/sns/web/v1/homefeed';
const params = {
    cursor_score: "",
    num: 31,
    refresh_type: 1,
    note_index: 10,
    unread_begin_note_id: "",
    unread_end_note_id: "",
    unread_note_count: 0,
    category: "homefeed.fashion_v3",
    search_key: "",
    need_num: 6,
    image_formats: ["jpg", "webp", "avif"],
    need_filter_image: false,
};

const x_s = generateXSign(apiPath, params);
console.log(JSON.stringify({ "X-s": x_s }));

2. Python 调用脚本

通过 subprocess 调用 Node.js,获取返回值后发起请求。同时别忘记填入自己的 Cookie。

import requests
import subprocess
import json
import time

# 替换成你自己的 Cookie,注意定期更新
cookies = {
    'a1': '你的a1值',
    'web_session': '你的web_session值',
}

# 调用 Node.js 获取签名
result = subprocess.run(
    ['node', 'xiaohongshu_sign.js'],
    capture_output=True,
    text=True,
    encoding='utf-8'
)
output = result.stdout
sign_data = json.loads(output)
x_s_value = sign_data["X-s"]
print(f"生成的 X-S: {x_s_value}")

# 构造请求头
headers = {
    'accept': 'application/json, text/plain, */*',
    'content-type': 'application/json;charset=UTF-8',
    'origin': 'https://www.xiaohongshu.com',
    'referer': 'https://www.xiaohongshu.com/',
    'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
    'x-s': x_s_value,
    'x-s-common': '你的x-s-common值',
    'x-t': str(int(time.time() * 1000)),
}

# 请求体
json_data = {
    'cursor_score': '',
    'num': 31,
    'refresh_type': 1,
    'note_index': 10,
    'unread_begin_note_id': '',
    'unread_end_note_id': '',
    'unread_note_count': 0,
    'category': 'homefeed.fashion_v3',
    'search_key': '',
    'need_num': 6,
    'image_formats': ['jpg', 'webp', 'avif'],
    'need_filter_image': False,
}

# 发送请求
resp = requests.post(
    'https://edith.xiaohongshu.com/api/sns/web/v1/homefeed',
    cookies=cookies,
    headers=headers,
    json=json_data
)

data = resp.json()
if data.get('success'):
    for item in data['data']['items']:
        print(item)
else:
    print(f"请求失败: {data}")

关键注意事项

  1. 环境模拟要精确mnsv2 算法内部一般会访问 navigator.userAgentscreen.width 等属性,漏掉任何一个都会导致签名计算错误。
  2. JSON 序列化顺序JSON.stringify 对不同顺序的键值对输出结果可能不一样,务必保证与浏览器端完全一致。通常逆向时可以 Hook JSON.stringify 来观察原始参数。
  3. Base64 自定义字典:不要使用 Node.js 原生的 Buffer.toString('base64'),必须使用逆向出的自定义字典实现。
  4. 版本号与算法更新x0 版本、mnsv2 函数可能随着站点的升级而变化,需要做好长期维护的准备。

总结

小红书 Web 端的 X-S 签名通过“环境校验 + 多层编码 + 自定义算法”的组合来提高构造请求的门槛。还原的关键点在于:

  • 通过浏览器断点定位加密入口;
  • 补全 Node.js 所需的浏览器环境;
  • 抠出并改写 mnsv2 核心函数;
  • 正确处理参数拼接顺序和自定义 Base64 编码。

一旦完成上述步骤,就可以在本地稳定生成合法签名,并结合 Python 等语言实现自动化数据采集。希望这篇笔记能为大家在类似逆向分析中提供思路。