QQ音乐sign值逆向实战 - Webpack打包分析

实战目标:抓包QQ音乐TOP榜单(https://y.qq.com/n/ryqq/toplist/62)的请求,还原核心`sign`参数的生成流程 适用场景:前端Webpack打包架构的通用逆向分析方法

1. 初步分析

1.1 网络请求定位

打开Chrome DevTools的「Network」面板,刷新榜单页面。筛选请求后可以看到,核心数据通过/cgi-bin/musics.fcg接口返回,关键请求参数如下:

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

1.2 全局变量搜索初探索

在DevTools的「Console」中直接搜索sign生成逻辑,先试传统方法:

// 1. 搜索全局赋值含sign的语句(Chrome搜索框前加 = )
// = sign

// 2. 直接访问可能的全局变量名
console.log(window._P); // 返回undefined?这说明不在直接的全局作用域
console.log(window.ddd); // 返回了一个函数!可能是模块入口

1.3 实战前置提示

本次案例的核心难点是QQ音乐用了Webpack模块化打包,把所有逻辑都封装在内部模块数组里,只暴露了极少的全局入口。不过只要掌握Webpack的通用逆向技巧,就能快速定位核心。


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

2.1 Webpack特征快速判断

现代前端几乎都用Webpack/Vite等打包工具,我们可以通过这几个标志性特征快速识别:

  1. 查看页面加载的大体积JS文件(100KB+),搜索__webpack_require__webpackJsonp(旧版)、window.模块名(0)(自定义暴露入口)
  2. 控制台打印webpackJsonp或类似自定义变量,返回数组/函数的话基本确定

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,
        
        // 返回最终的模块导出
        o.exports
    }
    
    // QQ音乐的关键操作:把加载器暴露到window全局
    window.ddd = r;
}([
    /* 模块数组:这里存放着压缩后的所有业务+工具模块 */
    /* 0 */ function(module, exports, r) { /* 初始化相关 */ },
    /* 1 */ function(module, exports, r) { /* 可能是工具库 */ },
    /* 2 */ function(module, exports, r) { /* 这里可能就是_P签名函数 */ }
])

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

3.1 找到并启动模块加载

先确认window.ddd的使用方式,根据控制台或Network请求触发前的代码断点(比如在/cgi-bin/musics.fcg的「XHR/fetch Breakpoints」中打断点),可以看到请求前会执行:

// 核心加载流程
window.ddd(0); // 必须先加载模块0,它会初始化内部环境/暴露一些必要的中间变量
// 然后才能使用其他模块,或者通过模块0间接导出_P

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

为了快速找到导出_P的模块ID,我们可以在浏览器控制台Hook自定义的模块加载器

// 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)等已知流程
window.ddd(0);

4. 常见问题解决

4.1 环境补全不完整

如果后续要在Node.js里复现_P函数,可能会遇到「xxx is not defined」的报错,解决步骤:

  1. 仔细看报错信息,找到缺失的对象/属性/方法
  2. 在Node.js脚本开头,添加对应的模拟代码(比如模拟windownavigator等浏览器基础对象)
  3. 可以用Proxy代理模拟对象,监控具体访问了哪些属性,避免补全冗余内容

5. 总结

本次实战的核心是Webpack打包架构的通用逆向流程

  1. 定位核心接口,找到加密参数
  2. 全局搜索暴露的自定义模块加载器
  3. Hook加载器,追踪包含目标函数的模块
  4. 提取模块代码,补全环境,在非浏览器端复现

这个方法不仅适用于QQ音乐,大部分用Webpack打包、暴露自定义入口的前端项目都可以参考。