王一之 发表于 2021-3-25 16:48:07

[油猴脚本开发指南]油猴hook - 实现b站评论小尾巴

## 本节主要内容

> 介绍js hook在油猴脚本中的使用,然后会由hook牵扯到this,原型链等.最后使用hook技术,让我们在bilibili发评论的时候自动带上一个小尾巴,会介绍两种不同的hook方法去实现它,本章中一种,还会留一种到下一章.对于很多概念的东西本人可能写得不太明白,另外本系列教程也是针对油猴脚本的,所以也会模糊带过,还请海涵阅读其它资料😣.

## hook

关于hook的技术之前在[`时间加速`](https://bbs.tampermonkey.net.cn/thread-176-1-1.html)这一篇中简单的说过一次,当时描述成 `劫持`,让实际调用的方法变成我们的方法,这时候就可以对它为所欲为了,可以选择不调用它走自己的逻辑,或者处理一下它的参数再进行调用等等,本节的目标就是hook了发评论时候的方法,处理一下参数再继续调用.熟悉web后端开发的同学可能觉得有点像中间件,嗯我也觉得,再次感叹技术真是互通的.

####

### 一个练习页

因为方法存在的位置不同,实现不同,于是有很多种不同的hook方式,下面我会写一下简单的js,放在这个网站上:(https://bbs.tampermonkey.net.cn/demo/hook.html),以供学习.

值得注意的是这个页面中的方法调用,写在了 `window.addEventListener("load")`中,这是因为油猴脚本的执行并不能完全的保证在页面的js之前执行,为了练习所以这样给大家使用.除了这个以外,还有一些匿名函数也是无法进行hook的,有一定的局限性.

### 全局函数

像我们之前时间加速中的 `setInterval`,当时我们的方法就是下面这样,很简单.在我们上述提供的demo网站中,因为globalFunc也是全局中,那么也可以直接像下面这样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呢?像下面这样,我们直接替换掉对象,然后经过我们的对象实例化后,再进行替换.相比于上面的函数,也很好理解,往里面进入了一层嘛.

```
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;
}
```

### 原型链上的方法

> 原型链是什么,请阅读此内容:[https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Inheritance_and_the_prototype_chain](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Inheritance_and_the_prototype_chain) 我就不现丑了

与上面对象中的方法不同,原型链上的方法不需要等它调用了之后再继续找对应的对象,直接修改原型链上的方法就可以了.

```
let hookPrototypeFunc=hookGlobalObject.prototype.prototypeFunc;
hookGlobalObject.prototype.prototypeFunc = function (p) {
    hookPrototypeFunc("hook"+p);
}
```

### 使用到了this的方法

如果调用的方法使用到了this,可以看demo中的 `thisFunc`,其实与hook对象中的方法一样,只是调用的时候使用apply或者call,让对象里的this指向我们期望的this,否则会影响结果,就像下图.对于this我打算在下一节中讲,会涉及到一些作用域的内容.

!(data/attachment/forum/202103/25/164737t98v9t5qgevzevdb.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "image-20210325151205647.png")

apply和call的区别在于,apply传递参数是用的数组,call传递参数使用多个参数,就像下面这样:

```
func.apply(this,);
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;
}
```

### 匿名函数

如果是匿名的函数,我们将很难处理,局限性也更大,匿名函数如果直接使用变量,因为作用域的原因,我们根本无法访问到.例如在demo中,实现了一个匿名函数,但是如果想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评论小尾巴

> 文档:[https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest](https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest)

本节来使用hook xhr这个方法来实现小尾巴,这个方法是毕竟容易想到的,因为如果你要发评论,就肯定会发请求,如果发请求又不刷新页面的话,大部分都是 `XMLHttpRequest`,当然还有 `fetch`这个标准.

我们直接hook掉 `XMLHttpRequest`,在发送的数据后面添加小尾巴.

我们先抓下包,判断这个评论的数据包是怎么样的.红框处就是调用这个请求的js代码,我们点进去就可以判断是fetch还是xhr了,如果根据调用栈继续往上找还能看到数据是怎么来的,这个我们到下一节讲.

!(data/attachment/forum/202103/25/164749u5t5skaf64t1wsmk.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "image-20210325161502330.png")

然后看一些post的数据:

!(data/attachment/forum/202103/25/164755r3tog7lt33lmo5bb.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "image-20210325161650696.png")

这时候我们就可以hook掉xhr原型链上的send方法,每次send时判断是否有message这个参数,有我们就对message的内容进行替换.当然message这个参数,万一其它post也有,我们也可以hook xhr这个对象,判断url,这里我就按简单的来了.

```
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,);
}
```

当然这种方法还是不够优雅,下一节我们继续优化.


哔哩哔哩小尾巴
https://bbs.tampermonkey.net.cn/thread-431-1-1.html
(出处: 油猴中文网)


hook demo
https://bbs.tampermonkey.net.cn/thread-430-1-1.html
(出处: 油猴中文网)

Nanako 发表于 2021-3-25 16:56:18

哥哥牛逼!

zdhmrh 发表于 2021-3-25 20:30:42

哥哥牛逼!

ChP.GNS 发表于 2021-3-25 23:05:19

哥哥牛逼!

大药科技 发表于 2021-3-26 16:19:16

牛逼!多出这样的教程

zqdlly 发表于 2021-6-27 14:12:05

哥哥牛逼!

lty123 发表于 2021-10-29 23:18:27

哥哥牛逼!学到了!!!

小胖子的梦想 发表于 2021-12-1 20:06:47

XMLHttpRequest.prototype.send=function(body)的body是指劫持的信息内容是吗

小胖子的梦想 发表于 2021-12-1 20:12:40

if(/&message=(.*?)&/.test(body)){
      //替换body内容
      body=body.replace(/&message=(.*?)&/,"&message=$1"+encodeURIComponent(tail)+"&");
    }
判断语句/&message=(.*?)&/.test(body)这个是什么意思
&message=$1和结尾的&不太理解,麻烦gg解答

王一之 发表于 2021-12-1 20:38:34

小胖子的梦想 发表于 2021-12-1 20:06
XMLHttpRequest.prototype.send=function(body)的body是指劫持的信息内容是吗

是的
页: [1] 2
查看完整版本: [油猴脚本开发指南]油猴hook - 实现b站评论小尾巴