理解爬虫HOOK技术

3,019 阅读11分钟

理解爬虫HOOK技术

前言

  工作中为了帮助其他部门同事更好的完成负担比较重的操作,研究过一些网站反爬的加密措施,但是工作中的例子不太适合展示。正巧之前在爬虫挑战题目当中,遇到了一个加密问题,题目地址 ,虽然不是第一次遇到。但是这次让我思考到了一件事情,在逆向网站的时候,我们能不能有一些比较方便的办法来更高效的解决这类问题呢?从百度上简单搜索了一下,发现了HOOK技术,比较符合我的想法,也让我开启了一个新世界的大门,原来还可以这样?综上正好把自己顺带的一些小经验跟大家唠唠。

开胃菜

  先思考这么一个问题?当我们在浏览页面的时候,有些网站(0几年的时候网页吧)老喜欢 alert('你看我烦不烦?')你会不会感受到崩溃?假设这类的弹窗一直存在,会导致我们的用户体验极差,那么现在请你思考一下,运用你所掌握的技术,如何搞掉这个烦人的东西?

​ -----这里是5分钟的思考时间------

  如果你没有什么思路,那么这篇文章将对你有很大帮助。

  我们先将问题拆分一下,如果是一个我们自己的网页,要取消掉 alert 功能,我们可以这么做:

alert = function(message){
   console.log(message)
}

image-20200911153926607

  此时你会发现,这段代码执行完以后,页面上的 alert 功能已经失效了,转变成了 console.log 更加友好的打印出来了消息。

问题引申

经过前面 “开胃菜” 篇章,我们可以了解到,其实改写一个系统内置函数是相当方便的。那么我们是不是可以把之前的例子在写的好一些呢?答案是肯定的,考虑这段代码:

let myalert = alert;//备份alert函数
alert = function (message){//改写alert
   console.log('拦截到的alert函数消息:',message)
}

有同学会有疑问:

  • 为什么要备份alert函数?
  • 只是 console 提示信息改了下而已?

先回答第一个问题:在我们修改任何的东西,养成备份是个好习惯,这仅仅是习惯问题。

第二个问题:其实是从这个 consolelog 的改写中,有没有想到一个问题?我们此刻改写了 alert 其实相当于是把它拦截掉了!没错,这就是我们要说的 HOOK 终于带入到我们的主题正式开始了!

何为HOOK

先来引用一段比较官话的定义:

Hook 技术又叫做钩子函数,在系统没有调用该函数之前,钩子程序就先捕获该消息,钩子函数先得到控制权,这时钩子函数既可以加工处理(改变)该函数的执行行为,还可以强制结束消息的传递。简单来说,就是把系统的程序拉出来变成我们自己执行代码片段。

官话一般都不讲人话,简单来说,我们举个例子。

你点了一个外卖:

  • 正常流程:下单-外卖小哥配送-收到送来食物
  • HOOK技术能做的事情:(hook做点事,比如让你别下单)-下单(可以下单前打个电话问朋友)-(hook做点事,比如取消订单)-外卖小哥配送(可以跟外卖小哥说已经取消了订单)-(hook做点事,比如拒绝这笔配送)-收到送来食物

根据这个例子,可以发现 Hook 技术就是在某个正常流程的中间某个时机,可以改变原本我们固定的一些操作,他就像个捣蛋鬼,可以给你的事情中间加点料。

第一个HOOK

我们回到刚刚最早说的 alert 想想,是不是就是做了我们说的这么一件事情? alert 本身是用来弹窗的,这是我们正常的流程。

我们在弹窗前可以做个是否弹窗的确认按钮,然后接下来再确认要不要弹窗,这就是 hook,我们来实现完善一下:

let myalert = alert;//备份alert函数
alert = function (message){//改写alert
    console.log('开始弹窗,这里可以做一些事情');
    console.log('拦截到的alert函数消息:',message)
    if(confirm('是否要进行弹窗?')){
    myalert(message)
}else{}
   console.log('弹窗取消,拦截消息成功!')
}

在控制台跑一跑这个代码试试看,是不是能看到我们成功的 hook 住了这个 alert 功能?

控制台操作

发现有本电子书写的很好,可以移步Chrome DevTools 使用技巧的介绍

里面有个没讲到的api:debug/undebug 可以自己研究下

如何hook别人网页?

大家都知道,自己的东西搞起来这种简单,无非就是加加代码搞搞即可,别人家的代码打开页面都执行完了,都没得玩了!其实不然,我们仔细想想,能否借助其他工具来实现呢?

  • 谷歌插件或者油猴注入,油猴可以监听文档加载的几种不同状态,并在特定时刻执行js代码。

  • 代理注入,修改应答数据,在标签内的第一个位置插入

  • 使用chrome-devtools-protocol, 通过Page.addScriptToEvaluateOnNewDocument注入。

这几种办法都可以让代码提前注入到页面前面,就可以实现我们说的这种情况啦~

但是重点来了!之前介绍的这几种都是需要另外安装工具,我这里要推荐一种0安装的方法,浏览器就可以改写。你需要先准备一个 chrome 浏览器(我记得大概 85 版本以上)随后打开控制台面板:

  • 可以看到左侧有个 page 面板,里面包含了所有的此次页面加载资源
  • 在往旁边找,可以找到 overrides 面板。

image-20200914124016060

image-20200914124037752

我们主要就看着这两个面板即可,首先(首次打开需要指定一个本地文件夹目录)打开 overrides 点击 enable 启用 local overrides 功能。

再回到 page 面板里 对着需要改写的资源右键 save for overrides 选项,可以看到这样点击过后,会有一个小圆点在图标下方,这样说明我们成功改写了,控制台自带的编辑器就可以让我们改写内容了,我们将 head 标签里面加入我们需要放的 hook 函数,改完以后 ctrl +s 保存,刷新页面即可生效(注意不要关闭控制台)。

这种方式更加方便,发散下思维 network 里面的 get/post 请求是不是也可以通过这种方式拦截改写 response 呢?答案是肯定的!只不过你得转码一下请求路径里的特殊字符,例如: 模拟请求接口,如果接口携带参数 ? 用%3f来替换问号当做文件名。

详细的编码参考: www.w3school.com.cn/tags/html_r…

方法网站文件路径
POSTcoolaf.com:1010/tool/ajaxgp文件目录\coolaf.com%3a1010\tool\ajaxgp 其中 ajaxgp 是个文件
GETwww.baidu.com/s?wd=2131文件目录 \www.baidu.com\s%3fwd=2131 其中 s%3fwd=2131 是个文件

文件里面写自己想写的东西即可,不必在意后缀名(重要)。这样我们就可以实现在控制台改写内容的操作了谁也阻挡不了我们想要调试的想法了

常见的几种hook函数

首先我们来看第一种,先思考下,如何让控制台自动报告页面创建了哪些元素?

-----这里是5分钟的思考时间------

好了,思考结束我们来看看,如何实现这样的一个 hook 函数:

let create_element = document.createElement.bind(document);
document.createElement = function (_element) {
  console.log("创建DOM标签:", _element);
  return create_element(_element);
}

这里要注意 document.createElement 这个方法,如果不使用 bind 指向 document 会产生非法调用的报错:

所以我们需要在引用方法的时候,bind 一下。

是否和我想的一样?不一样也没关系,可以在评论下给出自己的方式,条条大路通罗马。

那么如果再让你实现一个 eval 函数,是不是也类似更简单了?

let my_eval = eval;
eval = function (message) {
  console.log("eval:", message);
  my_eval(message);
}

基本上使用这个思路,都能搞定hook函数。

检测页面变量

我们再深入思考一个问题,如何 hook 到页面上声明了哪些变量呢?

-----这里是5分钟的思考时间------

我的方法是分好几步(你有更好的可以留言):

  • 第一步,先拿到一个空页面(注意浏览器不要装插件干扰) window 对象底下的全部初始化对象,拿到 key 数组。因为window是能记录下来所有的全局变量的,拿它作为突破口是必然的,缺点:只能拿到全局的,闭包内的就没办法了(老一辈常说:话不能说太满,有其他的方法欢迎留言)。
  • 第二步,拿这些去对比现在的页面上,看到底哪些变量是多出来的。
  • 第三步,对这些变量进行 hook 检测它们的动态。

好了,思路大概是这样,那么接下来改如何实现呢?我们一步一步来:

在确保所有插件和干扰元素都排除的情况下,我们用浏览器新开一个页面,然后记录下来这个页面目前的一些默认变量信息,秀操作:

var originKey = [];//数组保存window下默认变量
for(key in window){
    originKey.push(key)
}

可以看到,这些变量被我们收集好了。如果一个目标网页,去掉这些变量,剩下来的就是我们额外声明的变量。

用同样的方法去收集目标网页的变量,两个数组一对比,就可以知道了。空白页面是固定的,因此这个数组我们第一次跑完,第二次就可以接着用了。于是我们的代码长这样:

(()=>{
var originKey = ["parent"...省略很多系统变量"dispatchEvent"];
var moreKey = [];
 for (key in window) {
        if (window.hasOwnProperty(key) && !originKey.includes(key))
            moreKey.push(key)
    }
    console.log(moreKey)
})()

首先立即执行函数必须执行,如果暴露在了外面,会导致变量挂载到了window底下,就乱了。

那么接下来,我们来看如何检测一个变量从声明到被设置?

先了解两个 api 直接去看mdn上的介绍吧,写的非常好了:

非常简单的一段代码:

var temp = '';
Object.defineProperty(window, 'mytest', {
set: function (value) {
    console.log('发现变量此刻正在赋值!');
    temp = value;
},
get: function () {
    console.log('发现变量此刻正在被人读取!');
    return temp;
}
})

我们可以先试试看,是不是可以hook到它的行为?

可以看到,完全没问题。大家可能会想,为什么不用proxy,不是一个更前沿的 api 么?proxy无法代理一个未定义的变量,并且必须是对象。因此我们只能使用 Object.defineProperty 当然你也知道它的坏处,就是无法深入监听,有没有这种的办法呢?当然有!

看看我的例子,hook ‘axios’,‘Vue’:

(function () {
    var values = {};
    function hooks(varName, values) {
        Object.defineProperty(window, varName, {
            set: function (value) {
                console.log("变量[" + varName + "]:普通 正在被赋值:", value);
                values[varName] = value;
                if (Object.prototype.toString.call(value).indexOf('object')) {
                    values[varName] = new Proxy(values[varName], {
                        set: function (obj, prop, value) {
                            console.log("变量[" + varName + "]:对象 proxy设置属性", prop, "值:", value)
                            obj[prop] = value
                            return value;
                        },
                        get: function (obj, prop) {
                            console.log("变量[" + varName + "]:对象 proxy读取属性", prop, "值:", obj[prop])
                            return obj[prop];
                        }
                    })
                }
            },
            get: function () {
                console.log("变量[" + varName + "]:普通 正在直接读取")
                return values[varName]
            }
        })
    }
    hooks("axios", values);
    hooks("Vue", values);
})();

其实更加常见的 hook ,是我们在进行一些特殊的 api 调试,比如说 cookie 操作等,那就 hook cookie 。

监控某些特殊闭包内的变量

这个其实目前我的办法只能曲线救国,使用之前提到 hook 别人网页的操作,在它们的代码里面,使用 object.defineProperty 或者 proxy 即可!就当自己的代码改了就行!

为什么自己的浏览器可以,node或者其他语言脚本跑不行?

这里最主要的一个点在于,很多网站使用了浏览器检测。例如:检测当前环境 canvas 是否支持?检测常规的浏览器特性,可以参考文章!因此我们在做hook的时候,也要注意一下如果脚本要放到其他语言里面去跑,要特别注意一下这个网站有没有检测过浏览器环境,不能一股脑的代码拷贝过来就用。