steven026 发表于 2022-8-25 22:28:21

webpack4打包代码劫持方法探究

本帖最后由 steven026 于 2022-8-26 18:37 编辑

## 前文
第一次写相关文章,仅供交流参考,受作者水平所限本文可能有诸多不合理之处,尽请谅解。
本文依托油猴脚本,通过原生JS逆向方式,逐步研究webpack4代码,从而达到劫持代码目的。
以一款web放置游戏:资源网格(Resource Grid)为例,
(纯本地运算、页面比较简单、代码混淆较少便于理解。
针对这一页面可能有更好的劫持方法,但本文仅演示webpack4通用劫持方法。)
游戏地址:https://g8hh.github.io/resource-grid
游戏介绍:https://www.gityx.com/g8hh/yihanhua/222.html

## 涉及原理
1. ```Function.prototype.toString```以字符串形式打印函数代码
!(data/attachment/forum/202208/26/182835hkmassvpqa5aaqzq.png)
2. ```new Function([...args] , functionBody)```或```Function.prototype.constructor([...args] , functionBody)```
以字符串构造函数
!(data/attachment/forum/202208/25/192628jnbb1jvpfn29dpjq.png)
3. 对象方法劫持
!(data/attachment/forum/202208/25/193628eku9fzu9m89uek19.png)
4.```// @run-at document-start```油猴metadata元信息:网页初始化时便运行油猴脚本
(能够在网页js代码运行前提前注入油猴代码,以达到劫持网页代码的目的)

## 示例网页调试
仅为示例网页先期调试,证明思路无误
![](data/attachment/forum/202208/25/195754xqpqqt486b1qm60g.png)
初始原木20秒1个
![](data/attachment/forum/202208/25/195805fiiixvef8pufnnfi.png)
刷新游戏,代码定位后可知闭包中M对象中的Log对象中原木初始化时间为20秒
![](data/attachment/forum/202208/25/195813rt68z668ay8cgz6w.png)
断点加快原木时间10倍
!(data/attachment/forum/202208/26/183524yueoyq7oztwfs7wp.png)
解除断点后原木时间变为2秒,确实加快10倍,证明此处代码有效

## 本文目标
以一个较为简单、较为通用的方法劫持webpack4打包代码,
类似于Devtools 的覆盖override,比override操作复杂,但可将其写为油猴脚本分享给他人使用、或游戏js更新后无需修改代码亦可达到劫持效果
具体体现为通过劫持webpack4代码达到资源网格(Resource Grid)游戏内修改资源加速10倍并暴露闭包内对象M

## 研究过程
!(data/attachment/forum/202208/25/201109r1eq97eeq3qr1qge.png)
示例页面较简单,一共就2个js文件,可以快速阅读源码。
首先我们观察到M对象是闭包41中的一个变量,而闭包41是通过```this.webpackJsonptest.push()```方法新增的。
此处this指向window,且```webpackJsonptest = webpackJsonptest || []```说明```webpackJsonptest```是一个全局数组,简单在控制台打印一下,确实印证了这一点。
***
简单猜测一下后续网页初始化是通过```webpackJsonptest()```调用函数的。
为了印证这一点我们可以编写脚本劫持一下```Array.prototype.push```
![劫持1.png](data/attachment/forum/202208/25/202744naemff4m15off51m.png)
劫持前先打印```window.webpackJsonptest.toSrting()```观察一下方便后续写劫持脚本
```
// ==UserScript==
// @name         资源网格(Resource Grid) webpack4劫持脚本
// @version      0.1
// @descriptionHook webpack4 Demo
// @author       DreamNya
// @match      https://g8hh.github.io/resource-grid/
// @grant      none
// @run-at       document-start
// ==/UserScript==
const realPush = Array.prototype.push;
Array.prototype.push = function (...args) {
    if (typeof args?.?. == "function") {
      console.log(this, ...args)
      let fucText = args.toString()

      //replace去头+slice去尾
      fucText = fucText.replace("function (e, n, t) {", "").slice(0, -1)
      //利用正则批量替换craftTime并加速10倍
      fucText = fucText.replace(/craftTime: (\d+),/g, (m, a) => { return `craftTime: ${a / 10},` })

      args = new Function("e, n, t", fucText)
      console.log(args)
      Array.prototype.push = realPush //劫持后还原push 防止后续冲突+提高效率
    }
    return realPush.call(this, ...args)
}
```
![劫持2.png](data/attachment/forum/202208/25/203723ug6nz8z86gvg6iaz.png)
![](data/attachment/forum/202208/25/195754xqpqqt486b1qm60g.png)
我们通过```Array.prototype.push```劫持原始函数,并通过```Function.prototype.toString().replace()```替换原始函数中需要劫持的部分,保存脚本刷新游戏,可以看到控制台打印出来的41闭包已经成功替换为我们劫持的函数了,但实际游戏中原木时间并没有提高。

***

一定是哪里出现了问题,重新观察一下代码和控制台,
首先根据控制台打印结果,我们确实成功替换了代码,且替换的代码经过【示例网页调试】验证,确实有效,但没有正常起到作用。
说明网页函数调用在劫持之前。为了验证这一看法
我们通过```debugger```进行调试,
将脚本中```fucText = fucText.replace("function (e, n, t) {", "").slice(0, -1)```
替换为```fucText = fucText.replace("function (e, n, t) {", "debugger;").slice(0, -1)```
保存,打开控制台,刷新游戏,竟然没有断点……
这说明我们的代码虽然替换成功了,但是没有被执行,
再根据游戏正常运行,且原木时间为20秒,我们猜测原始代码成功被调用了,
于是在M对象初始化后的地方打个断点刷新游戏,发现游戏进入断点了。
这说明我们的代码替换在原始代码调用之后。

***

根据我们之前的观察闭包41是直接通过```this.webpackJsonptest.push()```新增的,没有经过其他任何地方的变量存储,这就奇怪了,难不成webpack还会变戏法?
继续在```push()```处打一个断点,控制台打印一下```this.webpackJsonptest```
![劫持3.png](data/attachment/forum/202208/25/210050odd2i1dfd161fdzp.png)
嗯?不对劲,属实是不对劲。你见过普通数组打印出来会有```push:f t(t)```属性吗?
这是阴沟里翻船了啊,平日都是我们劫持别人,这次被webpack反劫持了,这是```webpackJsonptest.push```而不是```Array.prototype.push```。
怪不得我们之前的劫持代码不会生效……这波属实是大意了。

***
调整一下心态,我们右键```push:f t(t)```追一下函数定义
![劫持4.png](data/attachment/forum/202208/25/211132nyswxiatsx87iini.png)
可以看到之前```push()```的闭包函数就是t函数的传入变量```t```而我们的41闭包函数就是```t```而只有```c=t```这条代码读取了```t```,因此我们只要看```c```变量就行了,
而又只有```for (n in c)```这一处for循环和```c```有关,之后就是return了,根据之前的验证return之后不会再执行我们劫持替换的代码,所以只需要集中观察for循环。
又发现```e=c```只有这一处代码传出了```c```变量,因此不用再观察后续代码了,只有```e```这一处是我们要找的关键代码。

***
我们又观察到在```e=c```之前有一个```Object.prototype.hasOwnproperty.call(c, n)```,因此我们可以直接劫持```Function.prototype.call```,重新修改脚本
```
// ==UserScript==
// @name         资源网格(Resource Grid) webpack4劫持脚本
// @version      0.2
// @descriptionHook webpack4 Demo
// @author       DreamNya
// @match      https://g8hh.github.io/resource-grid/
// @grant      none
// @run-at       document-start
// ==/UserScript==
const realCall=Function.prototype.call;
Function.prototype.call=function (...args){
    if (typeof args?. == "function") {
      console.log(this, ...args)
      let fucText = args.toString()

      //replace去头+slice去尾
      fucText = fucText.replace("function (e, n, t) {", "").slice(0, -1)
      //利用正则批量替换craftTime并加速10倍
      fucText = fucText.replace(/craftTime: (\d+),/g, (m, a) => { return `craftTime: ${a / 10},` })

      args = new Function("e, n, t", fucText)
      console.log(args)
      Function.prototype.call = realCall //劫持后还原call 防止后续冲突+提高效率
    }
    return realCall.apply(this, args)
}
```
![](data/attachment/forum/202208/25/195819ig1cgaach3yqdgrr.png)
成功劫持了,至此我们完成了第一个目标加速10倍
为了完成第二个目标,暴露闭包变量,我们在fucText下面再加一行,将M变量暴露到全局
```fucText = fucText.replace("x = new Array(81).fill(null);","x = new Array(81).fill(null);window.M=M;")```
![劫持5.png](data/attachment/forum/202208/25/214530pazittea4aeihdxx.png)
这样我们就可以动态调整加速倍率了。
将此原理尝试在另一个更复杂的webpack4放置游戏 (https://gltyx.github.io/progressive-mine-sweep) 中进行测试,同样成功劫持(代码省略)
至此我们完成了所有目标。

## 另一种劫持方法
上面我们最终通过```Function.prototype.call```的方法劫持了webpack4的代码,
那么还有没有其他较为简单、较为通用的劫持方法呢?答案当然是有的。
还记得我们第一次尝试劫持```Array.prototype.push```吗?
虽然被阴了一手,但大致思路是没有问题的,我们还是准确找到了切入点。
因此我们还是可以通过劫持push方法,只不过是```webpackJsonptest.push```。
由于```webpackJsonptest```是window下的全局变量,我们可以通过```Object.defineProperty```观察window对象中的```webpackJsonptest```当其被赋值时直接劫持push方法(本方法受@李恒道 哥哥指点得以实现)
```
// ==UserScript==
// @name         资源网格(Resource Grid) webpack4劫持脚本
// @version      0.3
// @descriptionHook webpack4 Demo
// @author       DreamNya
// @match      https://g8hh.github.io/resource-grid/
// @grant      none
// @run-at       document-start
// ==/UserScript==
let value;
let hooked=false;
Object.defineProperty(window,"webpackJsonptest",{
    get(){
      return value
    },
    set(newValue){
      value=newValue
      if(!hooked && window.webpackJsonptest.push && window.webpackJsonptest.push!=window.Array.prototype.push){
            window.webpackJsonptest.realPush=window.webpackJsonptest.push
            window.webpackJsonptest.push=function(...args){
                if(typeof args?.?.=="function"){
                  console.log(this, ...args)
                  let fucText = args.toString()

                  //replace去头+slice去尾
                  fucText = fucText.replace("function (e, n, t) {", "").slice(0, -1)
                  //利用正则批量替换craftTime并加速10倍
                  fucText = fucText.replace(/craftTime: (\d+),/g, (m, a) => { return `craftTime: ${a / 10},` })
                  //暴露闭包对象到全局
                  fucText=fucText.replace("x = new Array(81).fill(null);","x = new Array(81).fill(null);window.M=M;")

                  args = new Function("e, n, t", fucText)

                  this.push=this.realPush
                }
                this.realPush(...args)
            }
            hooked=true
      }
    }
})
```

该方法经测试也在其他webpack4页面通用。

***

### 完结撒花~

李恒道 发表于 2022-8-25 23:08:25

写的很详细
给哥哥个赞!

李恒道 发表于 2022-8-25 23:08:28

写的很详细
给哥哥个赞!

cxxjackie 发表于 2022-8-25 23:35:54

这个页面是React的,好像直接取React属性更简单一点。webpack劫持的话,我之前写过一个类似的,不一定适用于这个页面,仅供参考:https://bbs.tampermonkey.net.cn/forum.php?mod=viewthread&tid=1537&page=1#pid11691

王一之 发表于 2022-8-26 09:37:26

现在好像都webpack5了

王一之 发表于 2022-8-26 09:41:27

哥哥图片挂了一张

后面我优化优化一下

steven026 发表于 2022-8-26 09:56:44

王一之 发表于 2022-8-26 09:37
现在好像都webpack5了

暂时没遇到webpack5的页面,碰到好几个都还是4的
图片我晚上回去补吧 好像是昨天删错了一张,有缓存没发现

BlenderB 发表于 2022-8-26 10:55:40

都是神仙{:4_88:}小白暂时看不懂

steven026 发表于 2022-8-26 11:17:05

cxxjackie 发表于 2022-8-25 23:35
这个页面是React的,好像直接取React属性更简单一点。webpack劫持的话,我之前写过一个类似的,不一定适用 ...

不会react……react之前逆向了一会里面通过switch case绕来绕去的实在难读代码
我看了下哥哥写的劫持webpack代码,是通过Function.prototype.call劫持到exports,然后通过exports.Serializer()方法获取实际需要的方法


感觉哥哥劫持的应该是这段的call (这段代码里e是我想要劫持的函数 哥哥没有劫持而是直接获取了exports)
exports(暴露到全局我随便设了个变量名abc)里面很简单就是__esModule和Symbol,并没有.Serializer()

由于淘宝的页面已经失效了,没法在淘宝复现哥哥的代码,有点疑问想直接问问哥哥,
不知道哥哥劫持的网页用的是webpack4还是5或者是淘宝自己魔改的?,我在4里面用这个方法无效,获取到的exports也没用



哥哥的思路是直接劫持的exports不劫持原始代码,原始代码正常运行了一遍后,把exports暴露出来,然后对这个exports进行操作
(我的思路是原始代码被调用前直接劫持原始代码,把想要的操作全部写进原始代码里面去,然后让webpack正常调用被劫持的代码)
可能exports.Serializer()可以重复调用?

cxxjackie 发表于 2022-8-26 20:55:31

steven026 发表于 2022-8-26 11:17
不会react……react之前逆向了一会里面通过switch case绕来绕去的实在难读代码
我看了下哥哥写的劫持webp ...

不是,这个exports是webpack导出的模块,相当于函数的执行结果,因为每个模块不一样,导出来的是什么对象都有可能,Serializer只是淘宝那个特定模块里的方法,不具有广泛性。一般来说,webpack劫持更关注模块而非执行过程(比如代码里常见的 单字母(数字),这个就是exports),这个模块只是正好没有将你需要的对象导出(我估计是直接绑到了React上,也可能在其他模块里导出了,没有细究)。换句话说,你的劫持是暴露函数的中间变量,我是暴露函数的执行结果。由于这个劫持涉及了函数的执行,稍加改造一下也是可以取得中间变量的:
Function.prototype.call = function() {
    if (arguments.length === 4 && arguments && arguments.exports && arguments === arguments) {
      for (let key in arguments) {
            if (key !== 'exports' && arguments === 41) {
                let fucText = this.toString();
                fucText = fucText.replace("function (e, n, t) {", "").slice(0, -1);
                fucText = fucText.replace(/craftTime: (\d+),/g, (m, a) => { return `craftTime: ${a / 10},` });
                fucText=fucText.replace("x = new Array(81).fill(null);","x = new Array(81).fill(null);window.M=M;");
                Function.prototype.call = _call;
                return _call.apply(new Function("e, n, t", fucText), arguments);
            }
      }
    }
    return _call.apply(this, arguments);
};
React的话跟Vue一样会在元素上绑属性(准确的说是Vue借鉴了React),你可以试一下用我那个脚本$searchKey('craftTime'),大部分数据都能在一个元素上找到,我整理了一下代码如下:
const node = document.querySelector('.App > div > div:nth-child(2)');
const prop = Object.keys(node).find(p => p.startsWith('__reactProps'));
const obj = {};
for (const child of node.children) {
    obj = child.props.Resource;
}
obj.Log.craftTime /= 10;
页: [1] 2
查看完整版本: webpack4打包代码劫持方法探究