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

// 实战案例:QQ音乐音乐数据逆向实战(sign生成)

// 网址:https://y.qq.com/n/ryqq/toplist/62

概述

本次逆向分析针对QQ音乐采用Webpack打包的复杂前端架构,重点研究window._P签名函数的实现机制。与传统的全局函数不同,QQ音乐将核心逻辑封装在Webpack模块中,通过window.ddd(0)进行模块初始化,增加了逆向分析的难度。

网页分析

1760251478245-694fa433-a307-4c5e-bf1b-ef43db5bc9da.png

1760251540590-de31b386-bc82-488d-9170-60abb29f7ede.png

1760251593107-37fa26e1-aa22-40f7-96df-eafd12933154.png

1760251680509-8c2d1c99-5098-4f6c-9b8a-0a5f7a54554a.png

1760251712717-e067a202-8399-4c54-92a1-548121a4d4ff.png

1760251749126-df6d6079-71a4-493a-af29-f87f2e6507a6.png

1760251808334-f6ff526b-9f57-48c4-8941-de7735204523.png

1760251845536-837c4dee-34e0-4af2-b033-41a5657383d5.png

技术要点

2.1 核心难点

  • Webpack模块分析: 理解Webpack的模块加载机制是逆向基础
  • 模块依赖追踪: 通过调用链分析找到核心功能模块
  • 环境模拟: 在Node.js中重现浏览器环境行为
  • 算法还原: 通过多次测试验证签名算法逻辑

Webpack架构分析

3.1 Webpack模块系统特征

// 典型的Webpack加载器结构
(function(modules) {
    // Webpack运行时
    function __webpack_require__(moduleId) {
        // 模块加载逻辑
    }
    // 入口模块
    return __webpack_require__(0);
})([
    /* 0 */ function(module, exports, __webpack_require__) {
        // 模块0:初始化模块 - window.ddd对应的模块
        window.ddd = function(moduleId) {
            return __webpack_require__(moduleId);
        };
    },
    /* 1 */ function(module, exports, __webpack_require__) {
        // 模块1:签名核心模块 - _P函数实现
        exports._P = function(data) {
            // 签名算法实现
        };
    }
]);

3.2 模块调用链分析

// 实际调用流程
window.ddd(0);                    // 加载并执行模块0(初始化)
window.ddd(1);                    // 加载模块1(签名模块)
var sign = window._P(data);       // 调用签名函数

逆向分析过程

4.1 Webpack模块识别

// 在浏览器控制台中分析模块结构
// 方法1:查看webpackJsonp数组
console.log(webpackJsonp);

// 方法2:查找__webpack_require__函数
console.log(__webpack_require__);

// 方法3:分析模块依赖关系
Object.keys(__webpack_require__.m).forEach(key => {
    console.log(`模块 ${key}:`, __webpack_require__.m[key]);
});

4.2 模块导出函数追踪

// Hook Webpack的导出机制
const originalRequire = __webpack_require__;
__webpack_require__ = function(moduleId) {
    console.log(`加载模块: ${moduleId}`);
    const module = originalRequire.call(this, moduleId);
    if (moduleId === 1) { // 假设1是签名模块
        console.log('签名模块导出:', module);
    }
    return module;
};

代码分析

5.1 模块加载器结构

!function(t) {
    var n = {};  // 模块缓存对象
    
    function r(e) {  // 模块加载函数
        if (n[e]) return n[e].exports;
        var o = n[e] = {
            i: e,
            l: !1,
            exports: {}
        };
        return t[e].call(o.exports, o, o.exports, r),
        o.l = !0,
        o.exports
    }
    
    // 暴露各种工具方法
    r.m = t,        // 模块数组
    r.c = n,        // 缓存对象
    r.d = function(t, n, e) {  // 定义导出属性
        r.o(t, n) || Object.defineProperty(t, n, {
            enumerable: !0,
            get: e
        })
    },
    r.r = function(t) {  // 标记为ES模块
        "undefined" != typeof Symbol && Symbol.toStringTag && Object.defineProperty(t, Symbol.toStringTag, {
            value: "Module"
        }),
        Object.defineProperty(t, "__esModule", {
            value: !0
        })
    },
    r.t = function(t, n) {  // 模块转换
        // 处理不同模块格式的转换
    },
    r.n = function(t) {  // 获取默认导出
        var n = t && t.__esModule ? function() {
            return t.default
        } : function() {
            return t
        };
        return r.d(n, "a", n),
        n
    },
    r.o = function(t, n) {  // 检查属性是否存在
        return Object.prototype.hasOwnProperty.call(t, n)
    },
    r.p = "",  // 公共路径
}

常见问题解决

7.1 环境补全不完整

现象:报错提示某些属性未定义
解决

  1. 检查报错信息中缺失的对象/属性
  2. 在环境初始化部分添加对应的模拟代码
  3. 使用代理监控确认属性访问情况