hook
关于hook的技术,即:让实际调用的方法变成我们的方法,这时候就可以对它为所欲为了,可以选择不调用它走自己的逻辑,或者处理一下它的参数再进行调用等等
目标:hook发评论时候的方法,处理一下参数再继续调用
额外知识:油猴脚本的执行并不能完全的保证在页面的js之前执行,可以将页面的js写在 window.addEventListener("load")
中;
全局函数
当时我们的方法(本例是window.setInterval)定义在全局中时,是很简单的,可以直接像下面这样hook.
let hookSetInterval=window.setInterval;//将系统提供的setInterval保存
window.setInterval=function(a,b){//将系统的setInterval替换为我们自己的
return hookSetInterval(a,b/2);//经过处理后再调用系统的setInterval
}
全局对象中的方法
常用的hook setInterval
外,我们还会hook XMLHttpRequest
,这是一个全局对象,用于处理网页请求(例如广告的),获取网页数据,这节中我们就利用此思路去实现哔哩哔哩评论小尾巴.
在下面的demo中,定义了 globalObject
的全局对象,然后 let obj = new globalObject()
实例化它,使用 obj.func
调用它.那么我们怎么hook呢?像下面这样,我们直接替换掉对象,然后经过我们的对象实例化后,再进行替换.相比于上面的函数,也很好理解,往里面进入了一层嘛.
var globalObject = {
func: function(){}
};
let obj = new globalObject();
obj.func();
// ========= hook =========
let hookGlobalObject=window.globalObject;
window.globalObject=function(){
let ret=new hookGlobalObject();
let hookFunc=ret.func;
ret.func=function(p){
return hookFunc("hook"+p);
}
return ret;
}
原型链上的方法
\> 原型链是什么,请阅读此内容: 原型链
与上面对象中的方法不同,原型链上的方法不需要等它调用了之后再继续找对应的对象,直接修改原型链上的方法就可以了.
let hookPrototypeFunc=hookGlobalObject.prototype.prototypeFunc;
hookGlobalObject.prototype.prototypeFunc = function (p) {
hookPrototypeFunc("hook"+p);
}
使用到了this的方法
如果调用的方法使用到了this,其实与hook对象中的方法一样,只是调用的时候使用 apply 或者 call,让对象里的this指向我们期望的this,否则会影响结果,就像下图.对于this我打算在下一节中讲,会涉及到一些作用域的内容.
apply和call的区别在于,apply传递参数是用的数组,call传递参数使用多个参数,就像下面这样:
func.apply(this,[arg1,arg2]);
func.call(this,arg1,arg2);
hook代码:
let hookGlobalObject=window.globalObject;
window.globalObject=function(){
let ret=new hookGlobalObject();
let hookThisFunc=ret.thisFunc;
// hook对象中的方法
ret.thisFunc=function(p){
return hookThisFunc.apply(this,["hook"+p]);
}
return ret;
}
匿名函数
如果是匿名的函数,我们将很难处理,局限性也更大,匿名函数如果直接使用变量,因为作用域的原因,我们根本无法访问到.
假设实现了一个匿名函数,但是如果想hook达到我们的修改的话几乎没有办法.而且对hook的匿名函数来说,很多时候还要复制粘贴他原本的源码去修改来达到目的,如果源码一大串几乎可以放弃了.
let hookAnonymous=window.anonymous;
// hook匿名
let i = 0;
setInterval(() => {
i++;
}, 1000);
window.anonymous=function(anonymous){
return hookAnonymous(function () {
document.querySelector('#object-anonymous').innerHTML = "hook第" + i + "次";
});
}
可以看到...现在我们是将这个匿名函数直接另外起了一个计时器和复制粘贴了代码,修改达到的目的.
一些不能hook的方法
一般来说,能hook的方法,应该满足他能访问到的对象,我们也能够访问得到,我们脚本能访问到的对象应该全在 window
这个全局的变量上,所以上述例子都是以全局为基准的.另外对于匿名的方法或者离 window
对象的距离越远,那么hook的成本将越大,这时候就应该切换其它的思路了.就像上方的,我们可以hook掉 document.querySelector
,在判断参数为 '#object-anonymous'
时,innerHTML变为其它的.也就是从其它我们能hook到的对象来入手,或者使用其它方法
实战:Bilibili评论小尾巴
使用hook xhr这个方法来实现小尾巴,这个方法是毕竟容易想到的,因为如果你要发评论,就肯定会发请求,如果发请求又不刷新页面的话,大部分都是 XMLHttpRequest,当然还有 fetch这个标准.
我们直接hook掉 XMLHttpRequest,在发送的数据后面添加小尾巴.
我们就可以hook掉xhr原型链上的send方法,每次send时判断是否有message这个参数,有我们就对message的内容进行替换.当然message这个参数,万一其它post也有,我们也可以hook xhr这个对象,判断url,这里我就按简单的来了.
// ==UserScript==
// @name 哔哩哔哩小尾巴
// @namespace https://bbs.tampermonkey.net.cn/
// @version 0.1
// @description 让你的评论带上小尾巴!
// @author wyz
// @match https://www.bilibili.com/video/*
// @grant none
// ==/UserScript==
let tail="\n----臭水沟捞的奔腾机";
let hookXhrSend=XMLHttpRequest.prototype.send
XMLHttpRequest.prototype.send=function(body){
console.log(body);
if(/&message=(.*?)&/.test(body)){
//替换body内容
body=body.replace(/&message=(.*?)&/,"&message=$1"+encodeURIComponent(tail)+"&");
}
hookXhrSend.apply(this,[body]);
}