webpack逆向通用流程

2024-09-13

webpack原理

为了降低依赖,减轻负担,前端通常使用按需加载的方式。webpack将代码编号,在使用时分发器读取加载当前涉及到的编号内容,实现按需加载

以下是一段伪代码用于理解 webpack 的执行逻辑

  • 全部代码: 通过一个大对象内,每个key是不同的函数实现函数的分离。本文将 key 称为分发器编号,通常为数字或字符串
    var a = {
        923: function(e, t, n) {}
        120: (e, t, n) => {}
        6122: () => {}
    }
    
  • 已加载代码: 一个空对象用于存放当前已加载的内容。当使用到未加载编号时将代码装载到这个对象,后续再次使用时,直接从此对象获取
  • 分发器(加载器)用于加载大对象内对应编号的代码。先检查已加载代码的对象内是否含有此编号,是则直接返回,否则从全部代码中读取并填装到该对象
    // 本函数名 n
    // r 是编号,t 是已加载代码的对象, e 包含全部代码的对象
    
    // 已加载
    if (t[r]) return t[r].exports
    // 未加载
    // 初始化
    var i = t[r] = {exports: {}}
    // 加载 - 先将代码放入 i.exports,再返回 i.exports
    return e[r].call(i.exports, i, i.exports, n), i.exports
    
  • 主逻辑代码
    // 使用加载器导入对应编号的函数,实现导入功能
    var result = n(120)
    console.log(result)
    

本文以x乎的请求为例,发现加密字段在请求头中。下面分析请求头中的加密字段96 如有不当可联系本人删除!

加密函数的定位

  • 复制携带加密参数的请求路径。在 Source 中下XHR断点,然后右键清除缓存并刷新页面

  • 在调用堆栈中回溯,可查看到加密参数生成过程。tT中包含了加密参数。tT是通过函数ed生成的,因此ed为加密函数 (补充:t0,即er函数是取cookie中d_c0的值)

  • 通过函数所在文件的第一行,发现该函数在webpack中

    (self.webpackChunkheifetz = self.webpackChunkheifetz || []).push([[2636], {
        ...
    

webpack调试流程

以下内容为webpack内函数的快速调用办法。如果涉及到复杂的环境检测,需要补环境或者依旧使用扣逻辑的方式去解密目标函数

明确分发编号

  • 该js文件为webpack,所在编号为 61763 。编号可能是字符串或者此处的整数
  • 若是非webpack文件,调用了webpack文件内的函数,需要手动明确编号
  • webpack文件特征,搜索||[]).push([,类似 (window.webpackJsonp = window.webpackJsonp || []).push([。如本js中的 (self.webpackChunkheifetz = self.webpackChunkheifetz || []).push([[2636], {

Tips: push含义简介 把一个“模块描述对象”压入队列

(window.webpackJsonp = window.webpackJsonp || []).push([
  [chunkIds],        // ① chunk id 列表
  { modules },       // ② 模块定义对象
  [runtimeModules]   // ③ 可选:运行时模块
]);

分发器(加载器)定位

特征

通常在 runtime*.js 内,多用一元运算符引导的自执行函数写法

!function(e) {  // `!`引导的自执行函数 参数数量不一定
    ...
    // 明文函数名一般为 __webpack_require__
    function c(a) {  
        if (f[a])
            // 明文一般为 cached.exports
            return f[a].exports; // 特征,返回 exports  
        // 明文一般为 module = __webpack_module_cache__[moduleId]
        var d = f[a] = {  // 特征对象
            i: a,  // 字段1为索引
            l: !1,  // 字段2为!1
            exports: {} // 字段3为exports
        };
        // 特征,返回*.call(*.exports, *, *.exports, 函数本身)  
        // 明文一般为 __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
        return e[a].call(d.exports, d, d.exports, c),  
        d.l = !0,
        d.exports
    } // 此时函数c为加载器
    ...
}

定位

  • 方法一:在目标函数加断点,例如本例中的ed函数。右键刷新后单步调试。发现执行到 runtime.app.*.js 。代码有分发器特征,确定了分发器位置

    !function() {
        "use strict";
        var e, a, c, f, d, b, t, r, o, n, i, s, l, u = {}, m = {};
        function p(e) {
            var a = m[e];
            if (void 0 !== a)
                return a.exports;
            var c = m[e] = {
                id: e,
                loaded: !1,
                exports: {}
            };
            return u[e].call(c.exports, c, c.exports, p),
            c.loaded = !0,
            c.exports
        }
        p.m = u,
        p.c = m,
        p.amdD = function() {
            throw Error("define cannot be used indirect")
        }
        ,
        ...
    
  • 方法二:找到目标函数所在编号函数的声明处,在首行添加断点。由于三个参数固定为__unused_webpack_module, exports, __webpack_require__ ,第三个参数为该编号函数内代码的依赖,由分发器生成,因此第三个参数的位置就是分发器所在位置

处理加密函数

  • 方法1: 将分发器与目标函数全局化。使js文件可以webpack内目标函数的依赖构建与调用

    • 分发器全局化。方便初始化目标函数所需的依赖

      • 将分发器的整个自执行函数代码扣出来,在分发器内的函数前,添加 window.wp_require=分发器函数名 使分发器可被外部调用(取消代码格式化防止内部含有格式化检测)

      • 如果上述方法失败可以模仿编号函数,使用webpack执行全局化代码。查看编号函数处的webpack代码,类似 (window.webpackJsonp = window.webpackJsonp || []).push([

        (self.webpackChunkheifetz = self.webpackChunkheifetz || []).push([[2636], {
            54616: function(tt, te, tr) {
                "use strict";
                tr.d(te, {
                    Z: function() {
                        return ec
                    }
                });
                ...
                ...  // 该编号中的剩余完整代码 - 可移除部分代码,减少补环境的内容
        

        在分发器自执行函数结束后也使用该方式执行我们需要执行的代码,即分发器全局化代码(取消代码格式化防止内部含有格式化检测)

        (self.webpackChunkheifetz = self.webpackChunkheifetz || []).push([  // push 语句写法参考项目内的 webpack 代码
            [123456789],  // 随意传一个整形数组
            {},  // 不自定义编号函数,传空
            function(e) {  // 此处使用匿名函数,webpack会将分发器函数传入
                window.wp_require=e  // 分发器全局化代码
            }
        ])
        
        // 使用 webpack 将分发器全局化的原理
        // webpackJsonpCallback 内部逻辑
        /*
        function webpackJsonpCallback(data) {
            var chunkIds = data[0];
            var moreModules = data[1];
            var runtime = data[2];
        
            // ① 注册新模块
            for(var moduleId in moreModules) {
                __webpack_modules__[moduleId] = moreModules[moduleId];
            }
        
            // ② 标记 chunk 已加载
            for(var i=0; i<chunkIds.length; i++) {
                installedChunks[chunkIds[i]] = 0;
            }
        
            // ③ 如果有 runtime 函数,就立即执行,并把 __webpack_require__ 传进去
            if(runtime) {
                runtime(__webpack_require__);
            }
        }
        */
        // 由于第三步默认会将分发器传入,此时通过我们编写匿名函数将分发器挂载到 window ,实现分发器全局化
        
    • 目标函数全局化。使我们可直接在最外层调用目标函数。在目标函数定义完成后,新增行 window.target_fn=<目标函数名>; 使其全局化

    • 调用目标函数

      // 构建目标函数的依赖 加载目标函数
      wp_require(61763)  // 将目标函数所在的编号传入分发器,即第一步的明确编号
      target_fn(xxx, ...)  // 传入对应参数调用目标函数
      
  • 方法2: 目标编号块与分发器组合

    • 确认分发器的参数。查看分发器所在的自执行函数是否包含参数

      !function() {  // 自执行没有传入参数
      !function(c) {  // 自执行函数有1个参数
      !function(o, pls) {  // 自执行函数有2个参数
      
    • 将分发器代码扣出后,将目标代码段作为分发器自执行函数的参数传入或内部赋值进去。注意分发器的返回值,调用call的对象就是需要构造成 {编号: 代码块} 的对象

      • 没有参数的情况(通用方法,不论是否有参数)
        !function() {
            "use strict";
            var e, a, c, d, f, b, t, r, o, n, i, s, l, u = {}, m = {};
        
            // 2.通过分发器的返回值明确需要构造的变量
            u = {
                61763: function(tt, te, tr) {
                    "use strict";
                    tr.d(te, {
                        DH: function() {
                            return tq
                        },
                        ...  // 该编号中的剩余完整代码
            }
        
            function p(e) {
                var a = m[e];
                if (void 0 !== a)
                    return a.exports;
                var c = m[e] = {
                    id: e,
                    loaded: !1,
                    exports: {}
                };
                return u[e].call(c.exports, c, c.exports, p),  // 1.u为全局代码的对象
                c.loaded = !0,
                c.exports
            }
            ...  // 分发器自执行函数剩余代码
        }();
        
      • 一个参数
        !function(c) {
            "use strict";
            ...
            function f(n) {
                if (h[n])
                    return h[n].exports;
                var u = h[n] = {
                    i: n,
                    l: !1,
                    exports: {}
                };
                // 1.c为需要构造的对象,且c是自执行函数的参数
                return c[n].call(u.exports, u, u.exports, f),
                u.l = !0,
                u.exports
            }
            ...
        }(  // 此行之前是分发器的全部原始代码。
            // 2.将编码和代码块构造成对象 传入
            {
                61763: function(tt, te, tr) {
                    "use strict";
                    tr.d(te, {
                        DH: function() {
                            return tq
                        },
                        ...  // 该编号中的剩余完整代码
            }
        );
        
      • 两个或更多参数: 传入格式与特征行保持一致
        !function(o, p) {
            ...  // 分发器自执行函数的剩余完整代码
            function t(e) {
                if (n[e])
                    return n[e].exports;
                var d = n[e] = {
                    i: e,
                    l: !1,
                    exports: {}
                };
                return o[e].call(d.exports, d, d.exports, t),
                d.l = !0,
                d.exports
            }
            ...  // 分发器自执行函数的剩余完整代码
        // 特征行 self.webpackChunkheifetz = self.webpackChunkheifetz || []).push([[2636], { xxx: xxx }
        }([], [[], {
            61763: function(tt, te, tr) {
                "use strict";
                tr.d(te, {
                    DH: function() {
                        return tq
                    },
                    ...  // 该编号中的剩余完整代码
        }])
        
    • 将分发器全局化并将加密函数的参数与返回值绑定到window中,同时将自执行函数套一个函数壳改为普通的调用执行函数,并返回绑定在window中的结果。这样我们在调用时在window中绑定传入参数,加密函数才正常读取到我们传入的参数,并返回我们需要的结果

      
      var llll;  // 分发器全局化变量
      
      // 可直接在外边套一个函数保证其不会自执行。函数返回结果
      function ppp() {
      
          !function(c) {
              "use strict";
              ...  // 分发器自执行函数的剩余完整代码
              function f(n) {
                  if (h[n])
                      return h[n].exports;
                  var u = h[n] = {
                      i: n,
                      l: !1,
                      exports: {}
                  };
                  return c[n].call(u.exports, u, u.exports, f),
                  u.l = !0,
                  u.exports
              }
              ...  // 分发器自执行函数的剩余完整代码
      
              llll = f;  // 将分发器全局化
          }( 
              {
                  61763: function(tt, te, tr) {
                      "use strict";
                      ...  // 该编号中的剩余完成代码
                      function ed(tt, te, tr, ti) {  // 目标函数
      
                          // 传入参数改从window中获取                          
                          tt = window.tt
                          te = window.te
                          tr = window.tr
                          ti = window.ti
      
                          var ta = tr.zse93
                          , tu = tr.dc0
                          , tc = tr.xZst81
                          , tf = t3(tt)
                          , td = t6(te)
                          , tp = [ta, tf, tu, t8(td) && td, tc].filter(Boolean).join("+");
      
                          // 运行结果全局化
                          result = {
                              source: tp,
                              signature: (0,
                              tJ(ti).encrypt)(ty()(tp))
                          }
                          window.result = result;
      
                          return result
                      }
                      ...  // 该编号中的剩余完成代码
                  }
              }
          );
      
          llll[61763];  // 加载依赖
          return window.result  // 返回绑定在window上的结果
      }
      
      // Tips: 如果该目标函数依赖其他编号内的代码,在参数对象中对应添加,并在后方加载对应编号
      
    • 生成目标结果

      window.tt = 1
      window.te = 2
      window.tr = 3
      window.ti = 4
      res = ppp()  // 调用套壳函数,内部会自执行分发器,加载依赖,并返回结果
      console.log(res)  
      
  • 方法3:加密函数的依赖通过webpack加载

    • 将加密函数扣出
      function sign(e, t) {
          // ...
          o.doSign(u)  // o对象是编号代码段中的全局对象,查看该对象所在编号,此处以 'XXX' 为例
          // ...
      }
      
    • 通过webpack分发器加载加密函数依赖的对象,将加密依赖对象全局化。需放在加密函数前
      var sign_require_o;
      !function(e){
          ...  // 分发器源码
          function f() {  // 分发器函数
              ...
          }
          ...  // 分发器源码
      
          sign_require_o = f('XXX')  // 加载指定编号,用于加载加密函数的依赖对象
      }(
          {'XXX': function(e,t,r){ ... }}  // 依赖对象所在编号代码块,删减部分未引用的内容
      ) 
      
    • 依赖对象可能依赖其他代码块中的对象,或加密函数可能依赖多个对象,通过webpack方式加载
      var sign_require_o;
      !function(e){
          ...  // 分发器源码
          function f() {  // 分发器函数
              ...
          }
          ...  // 分发器源码
      
          sign_require_o = f('XXX')  // 加载指定编号,用于加载加密函数的依赖对象
          o_require_p = f('YYY')  // 3. 加载指定编号,用于加载加密函数的依赖对象
      }(
          {'XXX': function(e,t,r){ 
              ...
              o.foo = p.bar  // 1. o 依赖 p,p在编号 YYY 中声明
              ...    
          }}  // 依赖对象所在编号代码块,删减部分未引用的内容
          {'YYY': function(e,t,r){ ... }}  // 2. 依赖对象所在编号代码块,删减部分未引用的内容
      ) 
      
    • 加载后修改依赖对象的标识符名称
      function sign(e, t) {
          // ...
          sign_require_o.doSign(u)  // 加密函数中的依赖对象改为全局化的对象
          // ...
      }
      

解密

 

补充说明: 如果传入参数固定,每次生成内容不一致的,可能是加密过程中使用了随机数或者时间戳。可在重载对应方法(在加密函数调用前重载即可),看看生成内容是否不变。明确后在浏览器console中,也重载对应方法后调用,查看生成结果是否与解密一致

Math,random = function () {return 0.5}  // 重载随机数方法

总结

  • 目标所处代码所在编号。例如编号 0
  • 定位分发器所在的自执行函数,仅保留分发器。后期缺啥补啥
  • 根据分发器所在自执行函数的参数个数,构建传入的内容。全部代码对象先只放入目标代码
    (function(n){  // n - 全局代码的形参
        function 分发器函数(){
    
        }
    }({
        // webpack 编号代码
        0: function(e, t, n) {
            // 目标代码
            function target_code() {}
        }
    }))
    
  • 检查是否依赖其他代码块。当缺少指定编号代码块时,运行代码会出现以下报错
        return e[t].call(i.exports, i, i.exports, o)
    TypeError: Cannot read property 'call' of undefined
    
  • 将目标代码所依赖的其他编号代码放入
    (function(n){  // n - 全局代码的形参
        function 分发器函数(){
    
        }
    }({
        0: function(e, t, n) {
            // 目标代码
            e.exports = n(199)  // 目标代码导入了编号 199 的代码
            function target_code() {}
        },
        199: {}  // 添加 199 的代码
    }))
    
  • 目标内容暴露到全局
     (function(n){
        function 分发器函数(){
    
        }
    }({
        0: function(e, t, n) {
            // 目标代码
            e.exports = n(199)
            window.target_code = function(){}  // 暴露
        },
        199: {}
    }))