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

webpack4打包代码劫持方法探究

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

    [LV.6]常住居民II

    9

    主题

    220

    帖子

    537

    积分

    荣誉开发者

    Rank: 10Rank: 10Rank: 10

    积分
    537

    新人进步奖荣誉开发者喜迎中秋

    发表于 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页面通用。


    完结撒花~

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

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

  • TA的每日心情
    开心
    前天 23:59
  • 签到天数: 88 天

    [LV.6]常住居民II

    386

    主题

    3405

    帖子

    3388

    积分

    管理员

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

    Rank: 10Rank: 10Rank: 10

    积分
    3388

    喜迎中秋国庆纪念章荣誉开发者家财万贯管理员

    发表于 2022-8-25 23:08:25 | 显示全部楼层
    写的很详细
    给哥哥个赞!
    混的人。
    ------------------------------------------
    進撃!永遠の帝国の破壊虎---李恒道
    个人宣言:この世界で私に胜てる人とコードはまだ生まれていません。死ぬのが怖くなければ来てください。
    回复

    使用道具 举报

  • TA的每日心情
    开心
    前天 23:59
  • 签到天数: 88 天

    [LV.6]常住居民II

    386

    主题

    3405

    帖子

    3388

    积分

    管理员

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

    Rank: 10Rank: 10Rank: 10

    积分
    3388

    喜迎中秋国庆纪念章荣誉开发者家财万贯管理员

    发表于 2022-8-25 23:08:28 | 显示全部楼层
    写的很详细
    给哥哥个赞!
    混的人。
    ------------------------------------------
    進撃!永遠の帝国の破壊虎---李恒道
    个人宣言:この世界で私に胜てる人とコードはまだ生まれていません。死ぬのが怖くなければ来てください。
    回复

    使用道具 举报

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

    [LV.1]初来乍到

    15

    主题

    479

    帖子

    836

    积分

    荣誉开发者

    Rank: 10Rank: 10Rank: 10

    积分
    836

    卓越贡献活跃会员热心会员突出贡献三好学生荣誉开发者喜迎中秋

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

    使用道具 举报

  • TA的每日心情
    开心
    昨天 13:55
  • 签到天数: 100 天

    [LV.6]常住居民II

    171

    主题

    2253

    帖子

    2299

    积分

    管理员

    Rank: 10Rank: 10Rank: 10

    积分
    2299

    荣誉开发者喜迎中秋热心会员活跃会员突出贡献三好学生管理员家财万贯

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

    使用道具 举报

  • TA的每日心情
    开心
    昨天 13:55
  • 签到天数: 100 天

    [LV.6]常住居民II

    171

    主题

    2253

    帖子

    2299

    积分

    管理员

    Rank: 10Rank: 10Rank: 10

    积分
    2299

    荣誉开发者喜迎中秋热心会员活跃会员突出贡献三好学生管理员家财万贯

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

    后面我优化优化一下
    上不慕古,下不肖俗。为疏为懒,不敢为狂。为拙为愚,不敢为恶。/ 微信公众号:一之哥哥
    回复

    使用道具 举报

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

    [LV.6]常住居民II

    9

    主题

    220

    帖子

    537

    积分

    荣誉开发者

    Rank: 10Rank: 10Rank: 10

    积分
    537

    新人进步奖荣誉开发者喜迎中秋

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

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

    使用道具 举报

  • TA的每日心情
    无聊
    2022-8-28 11:19
  • 签到天数: 41 天

    [LV.5]常住居民I

    12

    主题

    89

    帖子

    78

    积分

    初级工程师

    Rank: 4

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

    使用道具 举报

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

    [LV.6]常住居民II

    9

    主题

    220

    帖子

    537

    积分

    荣誉开发者

    Rank: 10Rank: 10Rank: 10

    积分
    537

    新人进步奖荣誉开发者喜迎中秋

    发表于 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]初来乍到

    15

    主题

    479

    帖子

    836

    积分

    荣誉开发者

    Rank: 10Rank: 10Rank: 10

    积分
    836

    卓越贡献活跃会员热心会员突出贡献三好学生荣誉开发者喜迎中秋

    发表于 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;
    复制代码
    回复

    使用道具 举报

    发表回复

    本版积分规则

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