QQ音乐 sign 值逆向实战:破解 Webpack 打包的签名算法

🎯 实战目标:抓取 QQ音乐 PC 端 TOP榜单页面(https://y.qq.com/n/ryqq/toplist/62)中,接口请求携带的 sign 参数,并还原其生成流程。
🧩 适用场景:使用 Webpack 等模块化打包工具的前端应用,隐藏核心逻辑的通用逆向分析方法。

1. 初步分析:寻找加密入口

1.1 网络请求定位

打开 Chrome DevTools 的 Network 面板,刷新榜单页面。在过滤框中输入 musics.fcg 快速筛选请求,可以看到这个接口返回了榜单数据。点开请求详情,查看 Payload 标签,会发现几个关键参数:

参数名作用
sign本次逆向的核心加密值
data业务参数的 JSON 字符串
g_tk_new与登录状态相关的标识

显然,sign 是一个动态生成的校验值,我们的目标就是找到它的生成函数并模拟出来。

1.2 全局变量搜索初探索

传统逆向通常会直接在 Sources 面板全局搜索 sign,或尝试在 Console 中打印可能的全局变量。先试一把:

// 方法1:Chrome DevTools 搜索栏输入 = sign (搜索全局赋值语句)
// 可能返回大量干扰结果

// 方法2:尝试猜测变量名
console.log(window._P); // undefined,说明 _P 没有直接挂在 window 上
console.log(window.ddd); // 返回了一个函数!疑似模块入口

window.ddd 存在,且是一个函数,但 _P 却找不到。这暗示着 QQ音乐使用了模块化打包,核心函数被封装在内部了。

💡 实战提示:现代前端框架的逆向难点往往不在于算法本身,而在于如何从 Webpack 打包的“黑盒”中提取出目标函数。

2. Webpack 架构的通用识别与分析

2.1 Webpack 特征快速判断

QQ音乐采用的正是极其常见的 Webpack 模块化打包。可以通过这几个特征快速识别:

  1. 打开页面加载的 JS 文件(通常体积很大,几百 KB 到几 MB),搜索 __webpack_require__ 或旧版的关键字 webpackJsonp
  2. 在 Console 中输入 window.webpackJsonp,如果返回数组/函数,基本可以确定是 Webpack 打包;
  3. 如果以上都没搜到,注意观察是否有类似 window.ddd(0) 这种自定义暴露的加载入口。

2.2 自定义暴露入口的 Webpack 结构

QQ音乐没有使用默认的 webpackJsonp 全局数组,而是暴露了 window.ddd 作为加载器。经过还原,它的核心结构大致如下(这是逆向时提炼的简化版,不是原始压缩代码):

!function(t) {
    // 模块缓存对象,防止重复加载
    var n = {};

    // 核心 Webpack 模块加载器(对应 QQ音乐 的 window.ddd 函数)
    function r(e) {
        // 如果模块已加载,直接返回导出
        if (n[e]) return n[e].exports;

        // 创建模块对象并存入缓存
        var o = n[e] = {
            i: e,         // 模块 ID
            l: !1,        // 是否已加载标记
            exports: {}   // 模块导出内容
        };

        // 执行模块自身的代码,把导出挂到 o.exports 上
        t[e].call(o.exports, o, o.exports, r);
        o.l = !0;

        // 返回模块最终导出
        return o.exports;
    }

    // 关键一步:将加载器暴露到全局,方便外部调用
    window.ddd = r;
}([
    /* 模块数组:包含了所有压缩后的业务和工具模块 */
    /* 0 */ function(module, exports, r) { /* 初始化环境 */ },
    /* 1 */ function(module, exports, r) { /* 工具函数 */ },
    /* 2 */ function(module, exports, r) { /* 这里可能就包含了 _P 签名函数 */ }
]);

看懂这个结构后,我们就知道:只要 Hook 这个加载器,就能在模块被导出时“偷看”里面都有什么。

3. 逆向定位 _P 签名函数的步骤

3.1 找到并触发模块加载

先确认 window.ddd 的实际用法。在 Network 面板中对 musics.fcg 发起之前的位置打一个 XHR 断点(Sources > XHR/fetch Breakpoints 添加 /cgi-bin/musics.fcg),页面刷新后会停在请求发出前。观察调用栈,可以发现类似这样的调用:

window.ddd(0); // 必须先加载模块 0,它会初始化内部环境,并可能间接导出 _P

也就是说,整个模块系统需要先加载 ID 为 0 的模块进行初始化,之后才能使用其他模块。

3.2 Hook 加载器,追踪导出的模块

现在,我们在 Console 中写一段简易的 Hook 脚本,让 window.ddd 每次加载模块时都打印日志,并检查导出中是否含有我们心心念念的 _P

// 1. 先保存原始的加载器
const originalDdd = window.ddd;

// 2. 重写加载器,加上我们的“监控”
window.ddd = function(moduleId) {
    console.log(`正在加载模块 ID: ${moduleId}`);
    const moduleExports = originalDdd.call(this, moduleId);
    // 检查导出对象里是否有 _P
    if (moduleExports && moduleExports._P) {
        console.warn(`✅ 找到 _P 签名函数所在模块!模块 ID: ${moduleId}`);
        console.log('模块导出内容:', moduleExports);
        debugger; // 自动断点,方便进一步分析
    }
    return moduleExports;
};

// 3. 手动触发模块初始化,让加载器跑起来
window.ddd(0);

运行后,如果控制台出现 ✅ 找到 _P 签名函数所在模块! 的警告,就说明定位成功了。此时可以直接查看该模块的导出对象,里面就包含了 _P 方法。接下来只需要把相关代码抠出来,分析算法逻辑即可。

4. 常见问题与解决思路

4.1 环境补全不完整

当你试图把 _P 函数拿到 Node.js 中模拟调用时,大概率会遇到 xxx is not defined 的报错。这是因为模块里用到了浏览器环境下的全局对象(如 windownavigatordocument 等)。

解决办法分两步:

  1. 定位缺失项:根据报错信息,逐一找出缺少的浏览器对象或方法;
  2. 构造模拟对象:在脚本顶部补上对应的模拟代码。例如:
// 简单的 window 模拟
const window = {
    location: {
        hostname: "y.qq.com",
        protocol: "https:"
    },
    navigator: {
        userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) ..."
    },
    // 如果用到了 document,可以这样简陋模拟
    document: {
        createElement: () => ({})
    }
};

💡 进阶技巧:可以用 Proxy 代理一个空对象,拦截对未知属性的访问,这样就能快速知道代码到底读了什么属性,避免写一堆无用补全。

5. 总结

本次逆向实战的核心并不是某个加密算法有多复杂,而是演示了一套针对 Webpack 打包架构的通用逆向流程

  1. 通过 Network 面板定位到核心接口和加密参数;
  2. 全局搜索找到自定义的模块加载器(这里是 window.ddd);
  3. Hook 加载器,在模块导出时检查是否包含目标函数(_P);
  4. 提取模块代码,补全必要的浏览器环境,最终在任意 JS 环境下复现签名。

只要掌握了这个思路,无论是 QQ音乐 还是其他同样采用 Webpack 打包、只暴露出自定义加载入口的前端应用,你都可以从容应对。祝你在逆向的路上越走越顺!