本帖最后由 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
涉及原理
Function.prototype.toString
以字符串形式打印函数代码
new Function([...args] , functionBody)
或Function.prototype.constructor([...args] , functionBody)
以字符串构造函数
- 对象方法劫持
4.// @run-at document-start
油猴metadata元信息:网页初始化时便运行油猴脚本
(能够在网页js代码运行前提前注入油猴代码,以达到劫持网页代码的目的)
示例网页调试
仅为示例网页先期调试,证明思路无误
初始原木20秒1个
刷新游戏,代码定位后可知闭包中M对象中的Log对象中原木初始化时间为20秒
断点加快原木时间10倍
解除断点后原木时间变为2秒,确实加快10倍,证明此处代码有效
本文目标
以一个较为简单、较为通用的方法劫持webpack4打包代码,
类似于Devtools 的覆盖override,比override操作复杂,但可将其写为油猴脚本分享给他人使用、或游戏js更新后无需修改代码亦可达到劫持效果
具体体现为通过劫持webpack4代码达到资源网格(Resource Grid)游戏内修改资源加速10倍并暴露闭包内对象M
研究过程
示例页面较简单,一共就2个js文件,可以快速阅读源码。
首先我们观察到M对象是闭包41中的一个变量,而闭包41是通过this.webpackJsonptest.push()
方法新增的。
此处this指向window,且webpackJsonptest = webpackJsonptest || []
说明webpackJsonptest
是一个全局数组,简单在控制台打印一下,确实印证了这一点。
简单猜测一下后续网页初始化是通过webpackJsonptest[1][1][41]()
调用函数的。
为了印证这一点我们可以编写脚本劫持一下Array.prototype.push
劫持前先打印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)
}
我们通过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
嗯?不对劲,属实是不对劲。你见过普通数组打印出来会有push:f t(t)
属性吗?
这是阴沟里翻船了啊,平日都是我们劫持别人,这次被webpack反劫持了,这是webpackJsonptest.push
而不是Array.prototype.push
。
怪不得我们之前的劫持代码不会生效……这波属实是大意了。
调整一下心态,我们右键push:f t(t)
追一下函数定义
可以看到之前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;")
这样我们就可以动态调整加速倍率了。
将此原理尝试在另一个更复杂的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页面通用。
完结撒花~