小红薯X-S签名逆向解析(上篇):突破自定义Base64与算法轮廓

小红薯X-S签名逆向解析(上篇):突破自定义Base64与算法轮廓

在完成《JS逆向实战:某看视频hk_sign参数逆向实战全记录》后,我们继续深入JS逆向领域,今天的目标是小红薯X-S签名逆向分析。由于小红薯加密算法的加密逻辑相对复杂,我们将分两篇文章来详细讲解,本文主要聚焦于X-S参数的外层加密逻辑解析。

以小红薯的 /api/sns/web/v1/comment/like 接口为例,这是给评论点赞的一个接口,其中请求头中就包含 X-S 参数,这就是我们此次的逆向目标。

本文目录

  1. 抓包分析:定位X-S参数位置
  2. 搜索技巧:在众多结果中精准定位
  3. 代码分析:识别加密函数入口
  4. 断点调试:验证加密函数功能
  5. 算法解析:深入加密函数内部
  6. 参数分析:解密f对象结构
  7. 代码扣取:实现本地加密
  8. 运行结果
  9. 技术总结
  10. 下期预告

1. 抓包分析:定位X-S参数位置

打开小红薯网站,随意进入一个博主的评论区,随机找条评论点赞。在抓包工具中观察,会发现一个 /api/sns/web/v1/comment/like 的接口请求,在请求头中可以看到 x-s 参数,这就是我们此次的逆向目标。

小红薯评论点赞接口抓包
图1: 小红薯评论点赞接口抓包结果

2. 搜索技巧:在众多结果中精准定位

按照常规思路,首先在源代码中全局搜索 x-s 参数。但搜索结果出现大量内容,且大部分都是缩略显示,没有明显的赋值代码。

个人踩坑心得:像 x-s 这样的通用词汇,搜索结果会很多且大多缩略显示。如果不仔细排查,很容易错过关键代码,转而使用XHR断点等更复杂的方法。

实际上,只要细心查找,在 vendor-dynamic.9653eb95.js 文件中就能找到关键代码:

var _ = "X-s"

在已知参数名的情况下,可以搜索 x-s"X-S"X-s(注意大小写),这样搜索结果就只剩下 vendor-dynamic.9653eb95.js 文件的这一条结果了。

精准搜索定位加密文件
图2: 精准搜索定位加密文件

3. 代码分析:识别加密函数入口

点击进入 vendor-dynamic.9653eb95.js 文件,可以看到清晰的加密逻辑:

 try {
        var _ = "X-s"
        , b = "X-t"
        , x = getRealUrl(r, c, d)
        , v = seccore_signv2;
        v && (a.headers[_] = v(x, s),
         a.headers[b] = +new Date + "")
 } 

这段代码显示:

  • X-s 字符串被赋值给变量 _
  • a.headers[_] = v(x, s) 这行代码中,请求头的 X-s 参数通过 v(x, s) 函数生成
  • v 函数实际上就是 seccore_signv2

因此,我们需要在 v(x, s) 这里下断点进行分析。

4. 断点调试:验证加密函数功能

v(x, s) 处设置断点,然后随便找条评论点赞。当代码在断点处暂停时,在控制台执行 v(x, s) 查看是否可以生成X-S的值。

控制台验证加密函数
图3: 控制台验证加密函数功能

很明显能够正常生成X-S值,同时也可以在控制台打印传递的参数 xs 的值进行观察。

5. 算法解析:深入加密函数内部

v(x, s) 断点处单步执行,跟进函数内部。通过seccore_signv2分析,我们看到其函数原型:

function seccore_signv2(e, a) {
            var r = window.toString
              , c = e;
            "[object Object]" === r.call(a) || "[object Array]" === r.call(a) || (void 0 === a ? "undefined" : (0,
            h._)(a)) === "object" && null !== a ? c += JSON.stringify(a) : "string" == typeof a && (c += a);
            var d = (0,
            p.Pu)([c].join(""))
              , s = window.mnsv2(c, d)
              , f = {
                x0: u.i8,
                x1: "xhs-pc-web",
                x2: window[u.mj] || "PC",
                x3: s,
                x4: a ? void 0 === a ? "undefined" : (0,
                h._)(a) : ""
            };
            return "XYS_" + (0,p.xE)((0,p.lz)(JSON.stringify(f)))
        }

并且return 语句中的 "XYS_" 字符串拼接很引人注目,这正是X-S参数的开头。我们在return处下断点进行进一步分析。

通过控制台执行核心代码:

"XYS_" + (0, p.xE)((0, p.lz)(JSON.stringify(f)))

确认确实生成了X-S参数的值。分析这段代码发现:

  • (0, p.xE)(0, p.lz) 使用了逗号操作符的代码压缩写法
  • p.xE – 函数名为 b64Encode(平台自定义的Base64编码,非标准btoa)
  • p.lz – 函数名为 encodeUtf8(平台自定义的UTF-8编码,非标准TextEncoder)
  • JSON.stringify – 浏览器原生方法,用于将JavaScript对象转换为JSON字符串

6. 参数分析:解密f对象结构

X-S参数的值在return处生成,需要传递参数f。在控制台打印f,得到如下结构:

{
    "x0": "4.2.6",
    "x1": "xhs-pc-web",
    "x2": "Windows",
    "x3": "mns0301_fZaKqr1GTXi35nL7nR/vWRHbH6",
    "x4": "object"
}

这个f对象包含了版本信息、平台标识、设备信息和核心的加密数据x3

7. 代码扣取:实现本地加密

现在需要将平台自定义的p.xE(b64Encode)和p.lz(encodeUtf8)函数代码扣取出来。鼠标悬停进入这两个函数,将相关代码扣取到本地。

创建test.js文件,初步代码如下:

function encodeUtf8(e) {
        for (var a = encodeURIComponent(e), r = [], c = 0; c < a.length; c++) {
            var d = a.charAt(c);
            if ("%" === d) {
                var s = parseInt(a.charAt(c + 1) + a.charAt(c + 2), 16);
                r.push(s),
                c += 2
            } else
                r.push(d.charCodeAt(0))
        }
        return r
    }
function b64Encode(e) {
        for (var a, r = e.length, d = r % 3, s = [], f = 16383, u = 0, l = r - d; u < l; u += f)
            s.push(encodeChunk(e, u, u + f > l ? l : u + f));
        return 1 === d ? (a = e[r - 1],
        s.push(c[a >> 2] + c[a << 4 & 63] + "==")) : 2 === d && (a = (e[r - 2] << 8) + e[r - 1],
        s.push(c[a >> 10] + c[a >> 4 & 63] + c[a << 2 & 63] + "=")),
        s.join("")
    }

var f = { "x0": "4.2.6", "x1": "xhs-pc-web", "x2": "Windows", "x3": "mns0301_fZaKqb6fO7JIu1wmnR/vWb6mfa5MR85VUKjxYjAI2j597I4d7MsNUxW9U13QypUaJPChm0FQ5mnXRDv9TuzmSym/5y2G+NJ0OH2IVPSpoq2qVKm49m7A0JHKXS7dV8Sv5TMCnpsRzRJPUaT3lBnKSndVZlciE0JRIk0OHF==", "x4": "object" } 

var sign = "XYS_" + b64Encode(encodeUtf8(JSON.stringify(f))) 
console.log('生成的x-s值:', sign)

运行后发现缺少encodeChunktripletToBase64函数和c参数。进入encodeUtf8所在的文件,搜索并扣取缺少的函数。

tripletToBase64函数内设置断点,暂停时在控制台输入c获取其值:

断点处鼠标悬浮查看c参数
图4: 断点处鼠标悬浮查看c参数

最终的test.js文件完整代码:

var c = ['Z', 'm', 's', 'e', 'r', 'b', 'B', 'o', 'H', 'Q', 't', 'N', 'P', '+', 'w', 'O', 'c', 'z', 'a', '/', 'L', 'p', 'n', 'g', 'G', '8', 'y', 'J', 'q', '4', '2', 'K', 'W', 'Y', 'j', '0', 'D', 'S', 'f', 'd', 'i', 'k', 'x', '3', 'V', 'T', '1', '6', 'I', 'l', 'U', 'A', 'F', 'M', '9', '7', 'h', 'E', 'C', 'v', 'u', 'R', 'X', '5']

function tripletToBase64(e) {
        return c[e >> 18 & 63] + c[e >> 12 & 63] + c[e >> 6 & 63] + c[63 & e]
    }
function encodeChunk(e, a, r) {
        for (var c, d = [], s = a; s < r; s += 3)
            c = (e[s] << 16 & 0xff0000) + (e[s + 1] << 8 & 65280) + (255 & e[s + 2]),
            d.push(tripletToBase64(c));
        return d.join("")
    }

function encodeUtf8(e) {
        for (var a = encodeURIComponent(e), r = [], c = 0; c < a.length; c++) {
            var d = a.charAt(c);
            if ("%" === d) {
                var s = parseInt(a.charAt(c + 1) + a.charAt(c + 2), 16);
                r.push(s),
                c += 2
            } else
                r.push(d.charCodeAt(0))
        }
        return r
    }
function b64Encode(e) {
        for (var a, r = e.length, d = r % 3, s = [], f = 16383, u = 0, l = r - d; u < l; u += f)
            s.push(encodeChunk(e, u, u + f > l ? l : u + f));
        return 1 === d ? (a = e[r - 1],
        s.push(c[a >> 2] + c[a << 4 & 63] + "==")) : 2 === d && (a = (e[r - 2] << 8) + e[r - 1],
        s.push(c[a >> 10] + c[a >> 4 & 63] + c[a << 2 & 63] + "=")),
        s.join("")
    }
var f = {
    "x0": "4.2.6",
    "x1": "xhs-pc-web",
    "x2": "Windows",
    "x3": "mns0301_fZaKqqlloXfy4fA7nR/vWb6mfa5MR85VU/jxYjAI2jnq7I4dIxqs70asIoVQypUaJPChm0FQ5mnXRDv9TuzmSym/5y2G+NJ0OH2IVPSpoq2qVKm49m7A0JHKXS7dV8Sv5TMCnpsRzRJPUaT3lDnKSndVZlciE0JRIk0OHF==",
    "x4": ""
}
var sign = "XYS_" + b64Encode(encodeUtf8(JSON.stringify(f)))
console.log('生成的x-s值:',sign)

8. 运行结果

在本地控制台运行:node test.js,得到的结果与浏览器控制台生成的X-S值完全一致。

X-S签名逆向控制台运行test生成x-s

9. 技术总结

在本篇中,我们成功解析了小红薯X-S签名逆向外层加密流程。核心发现是,X-S的值由固定前缀"XYS_"拼接一段自定义Base64编码而成,而该编码的原始数据是一个结构固定的JSON对象f

整个逆向过程的关键在于:

  • 精准搜索:在众多的代码中,通过特定关键词快速定位加密入口文件。
  • 断点调试:利用断点确认加密函数seccore_signv2分析并跟进。
  • 代码扣取:将关键的自定义编码函数(encodeUtf8b64Encode)及其依赖完整还原至本地环境。

至此,我们已经能够完全复现给定f对象时的X-S签名生成。然而,这仅仅是整个加密体系的第一层。

10. 下期预告

细心的读者应该已经发现,我们至今尚未解答一个核心问题:生成f对象中那个看似随机且冗长的x3参数,其背后的算法究竟是什么?最初传递给加密函数seccore_signv2的参数xs,它们在其中扮演了怎样的角色?

这就好比我们战胜了一个守卫,拿到了宝箱(生成了X-S),却发现宝箱的钥匙(x3的生成逻辑)还藏在更深的迷宫里。x3才是整个签名机制动态变化的核心,是小红薯安全体系真正的挑战。

革命还未完成,同志仍需努力。在下一篇《小红薯X-S签名逆向解析(下篇):深入`x3`核心参数与完整算法还原》中,我们将潜入算法暗流,直面最复杂的加密核心,彻底攻克x3参数的生成逻辑,完成对整个签名机制的完整逆向。敬请期待!

本文由林石工作室提供技术支持,转载请注明出处。

Comments

No comments yet. Why don’t you start the discussion?

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注