上一主题 下一主题
ScriptCat,新一代的脚本管理器脚本站,与全世界分享你的用户脚本油猴脚本开发指南教程目录
12下一页
返回列表 发新帖

webpack4打包代码劫持方法探究

[复制链接]
  • TA的每日心情
    慵懒
    13 小时前
  • 签到天数: 811 天

    [LV.10]以坛为家III

    31

    主题

    552

    回帖

    1555

    积分

    荣誉开发者

    积分
    1555

    荣誉开发者新人进步奖油中2周年生态建设者新人报道挑战者 lv2油中3周年喜迎中秋

    发表于 2022-8-25 22:28:21 | 显示全部楼层 | 阅读模式

    本帖最后由 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以字符串形式打印函数代码
      toString.png
    2. new Function([...args] , functionBody)Function.prototype.constructor([...args] , functionBody)
      以字符串构造函数
      new Function.png
    3. 对象方法劫持
      hookPush.png
      4.// @run-at document-start油猴metadata元信息:网页初始化时便运行油猴脚本
      (能够在网页js代码运行前提前注入油猴代码,以达到劫持网页代码的目的)

    示例网页调试

    仅为示例网页先期调试,证明思路无误

    初始原木20秒1个

    刷新游戏,代码定位后可知闭包中M对象中的Log对象中原木初始化时间为20秒

    断点加快原木时间10倍
    hookPush.png
    解除断点后原木时间变为2秒,确实加快10倍,证明此处代码有效

    本文目标

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

    研究过程

    push.png
    示例页面较简单,一共就2个js文件,可以快速阅读源码。
    首先我们观察到M对象是闭包41中的一个变量,而闭包41是通过this.webpackJsonptest.push()方法新增的。
    此处this指向window,且webpackJsonptest = webpackJsonptest || []说明webpackJsonptest是一个全局数组,简单在控制台打印一下,确实印证了这一点。


    简单猜测一下后续网页初始化是通过webpackJsonptest[1][1][41]()调用函数的。
    为了印证这一点我们可以编写脚本劫持一下Array.prototype.push
    劫持1.png
    劫持前先打印window.webpackJsonptest[1][1][41].toSrting()观察一下方便后续写劫持脚本

    // ==UserScript==
    // @name         资源网格(Resource Grid) webpack4劫持脚本
    // @version      0.1
    // @description  Hook 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[0]?.[1]?.[41] == "function") {
            console.log(this, ...args)
            let fucText = args[0][1][41].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[0][1][41] = new Function("e, n, t", fucText)
            console.log(args[0])
            Array.prototype.push = realPush //劫持后还原push 防止后续冲突+提高效率
        }
        return realPush.call(this, ...args)
    }

    劫持2.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
    嗯?不对劲,属实是不对劲。你见过普通数组打印出来会有push:f t(t)属性吗?
    这是阴沟里翻船了啊,平日都是我们劫持别人,这次被webpack反劫持了,这是webpackJsonptest.push而不是Array.prototype.push
    怪不得我们之前的劫持代码不会生效……这波属实是大意了。


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


    我们又观察到在e[n]=c[n]之前有一个Object.prototype.hasOwnproperty.call(c, n),因此我们可以直接劫持Function.prototype.call,重新修改脚本

    // ==UserScript==
    // @name         资源网格(Resource Grid) webpack4劫持脚本
    // @version      0.2
    // @description  Hook 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[0]?.[41] == "function") {
            console.log(this, ...args)
            let fucText = args[0][41].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[0][41] = new Function("e, n, t", fucText)
            console.log(args[0])
            Function.prototype.call = realCall //劫持后还原call 防止后续冲突+提高效率
        }
        return realCall.apply(this, args)
    }


    成功劫持了,至此我们完成了第一个目标加速10倍
    为了完成第二个目标,暴露闭包变量,我们在fucText下面再加一行,将M变量暴露到全局
    fucText = fucText.replace("x = new Array(81).fill(null);","x = new Array(81).fill(null);window.M=M;")
    劫持5.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
    // @description  Hook 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[0]?.[1]?.[41]=="function"){
                        console.log(this, ...args)
                        let fucText = args[0][1][41].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[0][1][41] = new Function("e, n, t", fucText)
    
                        this.push=this.realPush
                    }
                    this.realPush(...args)
                }
                hooked=true
            }
        }
    })

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


    完结撒花~

    已有2人评分好评 油猫币 贡献 理由
    wwwwwllllk + 1 + 7 很给力!
    李恒道 + 1 + 2 + 1 ggnb!

    查看全部评分 总评分:好评 +2  油猫币 +9  贡献 +1 

  • TA的每日心情
    慵懒
    2024-10-28 07:07
  • 签到天数: 193 天

    [LV.7]常住居民III

    712

    主题

    5959

    回帖

    6758

    积分

    管理员

    非物质文化遗产社会摇传承人

    积分
    6758

    荣誉开发者喜迎中秋油中2周年生态建设者

    发表于 2022-8-25 23:08:25 | 显示全部楼层
    写的很详细
    给哥哥个赞!
    混的人。
    ------------------------------------------
    進撃!永遠の帝国の破壊虎---李恒道

    入驻了爱发电https://afdian.net/a/lihengdao666
    个人宣言:この世界で私に胜てる人とコードはまだ生まれていません。死ぬのが怖くなければ来てください。
    回复

    使用道具 举报

  • TA的每日心情
    慵懒
    2024-10-28 07:07
  • 签到天数: 193 天

    [LV.7]常住居民III

    712

    主题

    5959

    回帖

    6758

    积分

    管理员

    非物质文化遗产社会摇传承人

    积分
    6758

    荣誉开发者喜迎中秋油中2周年生态建设者

    发表于 2022-8-25 23:08:28 | 显示全部楼层
    写的很详细
    给哥哥个赞!
    混的人。
    ------------------------------------------
    進撃!永遠の帝国の破壊虎---李恒道

    入驻了爱发电https://afdian.net/a/lihengdao666
    个人宣言:この世界で私に胜てる人とコードはまだ生まれていません。死ぬのが怖くなければ来てください。
    回复

    使用道具 举报

  • TA的每日心情
    慵懒
    2022-3-8 11:41
  • 签到天数: 2 天

    [LV.1]初来乍到

    22

    主题

    881

    回帖

    1379

    积分

    荣誉开发者

    积分
    1379

    荣誉开发者卓越贡献油中2周年生态建设者油中3周年挑战者 lv2

    发表于 2022-8-25 23:35:54 | 显示全部楼层
    这个页面是React的,好像直接取React属性更简单一点。webpack劫持的话,我之前写过一个类似的,不一定适用于这个页面,仅供参考:https://bbs.tampermonkey.net.cn/ ... amp;page=1#pid11691
    回复

    使用道具 举报

  • TA的每日心情
    开心
    4 小时前
  • 签到天数: 213 天

    [LV.7]常住居民III

    305

    主题

    4189

    回帖

    4056

    积分

    管理员

    积分
    4056

    管理员荣誉开发者油中2周年生态建设者喜迎中秋油中3周年挑战者 lv2

    发表于 2022-8-26 09:37:26 | 显示全部楼层
    现在好像都webpack5了
    上不慕古,下不肖俗。为疏为懒,不敢为狂。为拙为愚,不敢为恶。
    回复

    使用道具 举报

  • TA的每日心情
    开心
    4 小时前
  • 签到天数: 213 天

    [LV.7]常住居民III

    305

    主题

    4189

    回帖

    4056

    积分

    管理员

    积分
    4056

    管理员荣誉开发者油中2周年生态建设者喜迎中秋油中3周年挑战者 lv2

    发表于 2022-8-26 09:41:27 | 显示全部楼层
    哥哥图片挂了一张

    后面我优化优化一下
    上不慕古,下不肖俗。为疏为懒,不敢为狂。为拙为愚,不敢为恶。
    回复

    使用道具 举报

  • TA的每日心情
    慵懒
    13 小时前
  • 签到天数: 811 天

    [LV.10]以坛为家III

    31

    主题

    552

    回帖

    1555

    积分

    荣誉开发者

    积分
    1555

    荣誉开发者新人进步奖油中2周年生态建设者新人报道挑战者 lv2油中3周年喜迎中秋

    发表于 2022-8-26 09:56:44 | 显示全部楼层
    王一之 发表于 2022-8-26 09:37
    现在好像都webpack5了

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

    使用道具 举报

  • TA的每日心情
    擦汗
    2024-1-28 11:21
  • 签到天数: 59 天

    [LV.5]常住居民I

    19

    主题

    102

    回帖

    124

    积分

    中级工程师

    积分
    124
    发表于 2022-8-26 10:55:40 | 显示全部楼层
    都是神仙  小白暂时看不懂
    回复

    使用道具 举报

  • TA的每日心情
    慵懒
    13 小时前
  • 签到天数: 811 天

    [LV.10]以坛为家III

    31

    主题

    552

    回帖

    1555

    积分

    荣誉开发者

    积分
    1555

    荣誉开发者新人进步奖油中2周年生态建设者新人报道挑战者 lv2油中3周年喜迎中秋

    发表于 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()方法获取实际需要的方法
    微信截图_20220826110139.png

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

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



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

    回复

    使用道具 举报

  • TA的每日心情
    慵懒
    2022-3-8 11:41
  • 签到天数: 2 天

    [LV.1]初来乍到

    22

    主题

    881

    回帖

    1379

    积分

    荣誉开发者

    积分
    1379

    荣誉开发者卓越贡献油中2周年生态建设者油中3周年挑战者 lv2

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

    不是,这个exports是webpack导出的模块,相当于函数的执行结果,因为每个模块不一样,导出来的是什么对象都有可能,Serializer只是淘宝那个特定模块里的方法,不具有广泛性。一般来说,webpack劫持更关注模块而非执行过程(比如代码里常见的 单字母(数字),这个就是exports),这个模块只是正好没有将你需要的对象导出(我估计是直接绑到了React上,也可能在其他模块里导出了,没有细究)。换句话说,你的劫持是暴露函数的中间变量,我是暴露函数的执行结果。由于这个劫持涉及了函数的执行,稍加改造一下也是可以取得中间变量的:
    1. Function.prototype.call = function() {
    2.     if (arguments.length === 4 && arguments[1] && arguments[1].exports && arguments[0] === arguments[2]) {
    3.         for (let key in arguments[1]) {
    4.             if (key !== 'exports' && arguments[1][key] === 41) {
    5.                 let fucText = this.toString();
    6.                 fucText = fucText.replace("function (e, n, t) {", "").slice(0, -1);
    7.                 fucText = fucText.replace(/craftTime: (\d+),/g, (m, a) => { return `craftTime: ${a / 10},` });
    8.                 fucText=fucText.replace("x = new Array(81).fill(null);","x = new Array(81).fill(null);window.M=M;");
    9.                 Function.prototype.call = _call;
    10.                 return _call.apply(new Function("e, n, t", fucText), arguments);
    11.             }
    12.         }
    13.     }
    14.     return _call.apply(this, arguments);
    15. };
    复制代码

    React的话跟Vue一样会在元素上绑属性(准确的说是Vue借鉴了React),你可以试一下用我那个脚本$searchKey('craftTime'),大部分数据都能在一个元素上找到,我整理了一下代码如下:
    1. const node = document.querySelector('.App > div > div:nth-child(2)');
    2. const prop = Object.keys(node).find(p => p.startsWith('__reactProps'));
    3. const obj = {};
    4. for (const child of node[prop].children) {
    5.     obj[child.key] = child.props.Resource;
    6. }
    7. obj.Log.craftTime /= 10;
    复制代码
    回复

    使用道具 举报

    发表回复

    本版积分规则

    快速回复 返回顶部 返回列表