You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

162 lines
15 KiB
Markdown

2 years ago
# 10 | 反爬虫概述无收益的前端是怎么进行key处理的呢
你好我是DS Hunter。
上一讲,我们提到了高收益的后端为了保护自己,进行了大量的反爬支持。但是反爬的主战场,依然是前端。
众所周知,做反爬,对于前端来说是没什么收益的,因此动力会差很多。如何解决动力问题,我们会在进阶篇深入探讨。我们目前亟待明确的,是前端在帮助后端进行反爬的时候,具体能够做些什么。
在反爬虫工作里前端主要的作用是key加密。除此之外还有一些杂活比如收集信息、埋点统计等等。最后我们会把这一切聚集到规则引擎中统一收口。今天我们就先来探讨**前端反爬虫的主力部分——key的加密**。至于其它的辅助以及收尾工作,我会在下节课跟你一起探讨。
在09讲中我们已经明确过了服务端的key是加密后下发的。那么客户端必然需要解密方法。不过解密方法的基础框架是什么呢除此之外基础框架内有什么可以用到的代码保护方式呢我们先从第一个问题开始分析。
## 放置方式:成对加解密
这里特意发明了一个新词,叫成对加解密,和“对称加解密”这种加密方式不是一件事。我们所熟知的对称加解密是一个加解密的方式,或者说过程,而**成对加解密是一个存储方式**。
加密这件事在服务端,也就是后端已经直接执行掉了,而解密操作是发到客户端让客户端去做的。通常来说,解密操作是如何进行的呢?
举个例子服务端生成的key是10。那么如果我们进行如下一些简单的加密措施
![](https://static001.geekbang.org/resource/image/c0/19/c0146ef355be1cd66f3340ce65f51219.jpg?wh=1142x67)
那么对应的解密方法自然就是所有操作的逆运算:
![](https://static001.geekbang.org/resource/image/b2/e2/b2aae4c1d76852c080a5ba90bc8b21e2.jpg?wh=1142x67)
这样就可以得到原始的key了是10。当然这里的四则运算只是demo我想聪明的你不至于学完了就真的在生产用加减乘除吧……不过不论你用的是什么方法**在整个的运算中,我们的主要要求都是可逆和不丢精度这两点。**
因此我们在服务端生成key的时候就需要同时确定加密解密的链路顺序这样才能保证客户端按照顺序解密后得到正确的结果。
如图:
![](https://static001.geekbang.org/resource/image/c5/0c/c5140158e80a1b5ed8135168bc68010c.jpg?wh=1920x286)
那么对应的解密流程就是:
![](https://static001.geekbang.org/resource/image/5f/c0/5f7cd642487031065101f0445d8a6bc0.jpg?wh=1920x242)
**为了方便客户端的解密,我们可以在服务端把加密方法和解密方法成对存储,**“加密1”对应“解密1”“加密2”对应“解密2”以此类推。这就是成对加解密的核心了。而前端的解密方法也是由服务端这样推送下来的。
解密方法从服务端推送下来之后就意味着所有的题目也已经出完了。我们假设现在你的服务端一共存储了100对加解密方法。你可以随机取出n对并通过洗牌算法确定一个随机的顺序通过这个随机顺序生成的加密方法来对key进行加密。这样每个爬虫在破解的时候就需要逆序阅读对应的解密方法相当于面对了不同的代码。整个过程类似于题库抽题的原理爬虫方无论破解多久也不敢保证自己爆破了题库。所以就算爬虫方的代码上线了他的心里也还是没底的。
是的,这很像开盲盒对不对?那么盲盒套路我们都知道,是存在限量款的。所以,你完全可以给个隐藏款,爆率极低,调试的时候很难撞到,但是竞对的生产在爬取的时候,一旦量变大了,就会撞上对不对?
盲盒的事情就先说到这里我们回到客户端的解密上。服务端roll到了一堆的加密方法并且按顺序加密了那么客户端就应该倒叙进行解密。是的这是一个栈。在12讲我们会单独讲如何生成这段js代码。而客户端执行了对应的js就可以把解密后的key拿到了。
当然,这样的破解难度对于竞对来说是很低的。这里虽然有一定的随机性,但是还不够。成对加解密是隐藏一切的基石。因此,我们后面会增加一些浏览器端的代码保护方式,提升破解难度。
## 代码保护方式变量名混淆、eval和虚拟机
这里我为你提供了三种代码保护方式,按照复杂度排序,越往后越复杂。但是在使用的时候,我们全部都会用到。
### 变量名混淆
变量名混淆是最低一层的加密,也是必备的加密。
我们通常会使用一些js的minifier工具来进行变量名加密但是实际上这个并不算加密因为从名字来看minifier是minify加er的变形也就是说它本质上是在做minify——缩短变量名长度。诚然缩短变量名也会导致调试体验差但是这对于爬虫来说是远远不够的。
想象一下我们工作中最可怕的是变量名短吗其实并不是很多人编码习惯不好变量名都是abcd之类的我们日常生活中不也调试得不亦乐乎吗这对爬虫根本也不是事。
那么我们最怕的是什么呢?其实最怕的反倒是视觉上容易混淆,比如变量名过长,又很近似。举个例子,下面有两个变量名:
![](https://static001.geekbang.org/resource/image/27/9c/27c473c625268ae7a471e6f3b9a5a39c.jpg?wh=1142x89)
你能一眼就看出来这不是一个变量名吗? 这个难度就比单纯的短变量名难度大多了。
**因此,我们的变量名要进行混淆,降低可读性。**
在早年这个操作只能通过模版替换的方式来做例如将js写到模版中我们在12讲中说到engine混淆的时候会详细介绍。随着AST技术的发展这个操作慢慢变得容易了而且定制化也更强了感兴趣的话也可以查一下webpack的AST技术。当然模版还是不可替代的因为模版无需解析语法树因此效率非常高。
最后,你一定会问:如果我是爬虫方,面对变量名加密我怎么办?
答案其实很简单我们刚刚是不是提到了js的minifer工具缩短的变量名反倒可读性高了一些是的你可以在拉取到js之后自己minify一次虽然这样比源码可读性降低了很多但是比起混淆后的变量名可读性反倒高了不少。这也算是稍稍扳回一城了。
### eval
eval是js最臭名昭著的功能所以对于反爬虫来说几乎是必备的。即使有些使用了虚拟机也会使用eval来运行虚拟机。
eval的问题在于它会将字符串进行处理然后送入eval执行。那么无论是阅读还是调试体验都很糟糕。所以我们在engine里面会大量使用eval。那么针对这两点爬虫的对策是什么怎么才能反制爬虫的对策呢
第一阅读。eval难以阅读主要是因为不能找到实际执行的代码。但是如果爬虫方换个思路这个问题就好解决了。我们看下面的例子
```javascript
eval = console.log
eval('1+1');
```
你可以找个console运行一下试试。是不是输出了1+1这样代码就可读了。不过又出现了代码本身不会运行的问题。这个问题对爬虫方来说并不是一件难事备份eval调用console.log然后运行备份的eval就可以了js基本操作。
这样,所有的方法都会运行前先输出,就可以拿到可阅读的代码了。
之后就到了反爬虫方再次反针对的环节了。反爬虫方可以判定eval的toString或者重写console.log调用一个无害的eval触发无限递归而爬虫方则相应的需要重写eval的toString以及安全处理递归结束条件。再接下来反爬方可以再重写Function的toString方法来进行检测或者直接备份eval的symbol等等。
这里的攻防方式就不一一细说了比拼的就是谁js能力更强。
第二,调试体验。
eval一个很大的黑点就是不方便调试因为他是一行不是多行没法打断点进去。这一点爬虫方可能怎么处理呢
其实还是一样的办法。我们都知道Chrome的调试工具碰到debugger会自动断下来。我们刚刚注入了eval实现了先console再eval。那么其实爬虫方还可以再进一步在拿到字符串之后在字符串的前面拼一个debugger进去再送进eval。这样eval的代码会首先被中断。爬虫方也就实现了对eval做调试。这个就是爬虫方针对eval做调试的一个小技巧。
那么,我们作为反爬虫方是不是束手无策了呢?毕竟看起来这个是无解的,没办法阻止他注入进去啊。是的,在没有办法阻止的情况下,我们的选择就是:让他注入进去,然后在后面坑他。
最普遍的一个做法就是让eval里面的代码大量检测运行时间。单步调试的代码与直接运行的代码最大的不同就是运行时间不同。做了这个检测之后爬虫方就会发现很多代码可能越调试越不对劲。因为反爬虫方可能已经把运行时间这个条件放到key解密里面了。
举个例子key的某一位可以用一个固定的整数除以运行时间并取整这样相当于判定运行时间的阈值然后用数学方法展开一下不要直接取时间而是把一堆时间作为参数传入进去最终的数学化简形式是卡时间阈值即可。我们在中学学过很多多项式化简的办法这里作为反爬方你只要反过来操作变成“化繁”即可。
### 虚拟机
虚拟机和eval的思路是一样的。与eval不同的是虚拟机技术能够有效避免代码被直接截取并且可以自定义指令集而指令集本身的名称又是可以被混淆的。
所谓的虚拟机指的就是使用js实现解释器的功能来解释服务端下发的代码。所以有些地方也会叫解释器在反爬虫领域这指的是一个东西。
常用的解释器有两种一种是类Lisp解释器一种是类汇编解释器。当然两者各有优缺点你可以根据情况任意选择。
Lisp解释器的优点是服务端编译的时候速度很快因为Lisp代码直接可以转成语法树。而缺点是编写很难对方首次阅读可能会迷惑而一旦理解后续的破解难度就大大降低了。自己的编写时间和对方的破解时间相比耗费太多了。
至于类汇编解释器编写很简单你可以使用类C语言来进行编写然后服务端进行一次编译即可。缺点是编译本身耗时比较严重并且还要写一个复杂的编译器。优点就是可读性大大降低对爬虫方的困扰最大。
也许你会问我不可以直接上WebAssembly这种简单又快速的方式吗
答案是可以但是很难。原因之一是WebAssembly过于标准已经开始有一些逆向工具了。原因之二是WebAssembly的兼容性还不是很好。所以也许未来会用的上但截止到现在还不是一个最优选择。
## 实现方式自定义Node.js的engine
说实话,为了迷惑爬虫方,上面所有的操作都很混乱。
虽然我们说,混乱对于反爬虫不是件坏事。但是,**过度混乱也会导致调试体验的缺失**。因此我们需要服务端针对爬虫和规则使用两套不同的阅读方式服务端看到的应该是清晰明了的代码而客户端看到的应该是混淆后的代码。这就是自定义engine要做的事情。
但是或许你意识到了一点:似乎这种事情后端做不了或者代价太大,前端因为暴露给客户又不适合做。那么这部分灰色地带怎么办呢。
是的一切没人做的事情都会沦落倒让BFF去处理在12讲我们讲BFF的时候会着重讲Node.js的engine的具体实现方式。这里我们就先提一下这么做的必要性。
## 小结
这一讲我们主要说的是前端反爬虫动作中的key加密部分。
首先,放置方式上,推荐你使用成对加解密的方式,这会提升系统随机性,为爬虫方解密增加一定的难度。
而代码保护的部分我给你提供了三种方式分别是变量名混淆、eval和虚拟机。
变量名混淆主要是让你变成一个脸盲也就是降低了代码可读性而eval和虚拟机则是用于把代码搅浑非常像炉石里的卡牌“尤格-萨隆”在调试上让人获得困惑而又意想不到的体验。当然这部分的保护有的时候也会让你觉得混乱。那么为了避免在迷惑爬虫方的时候把自己搞晕了最后我也给你提到了自定义Node.js的engine这个实现方式的必要性。之后我们也会在12讲中进行详细的讲解。
这里我要强调的是key加密本身是为了增加调试复杂度而不是为了让对方找不到key不要小瞧自己的对手以为对方连找都找不到。另一方面你也不用过于高估对手他们也只是普通的人类并非无所不能的超人千万不要妄自菲薄。
至此反爬虫最大的核心key加密相关就告一段落了。但是为了让系统更加完善稳定我们依然需要一些非关键模块来辅助。在下一讲里我们会详细介绍这些看起来对拦截爬虫没有太大贡献但是又必不可少的部分。
此外虽然成对加解密提供了一个不错的框架但是还需要实际的内容来增强加密效果一些隐藏的检测套路也是必要的。针对key加密的这些具体实现套路我们还会在下一讲后面的加餐中详细解读你也可以期待一下。
## 思考题
好了,又到了愉快的思考题时间。还是三选一的老规矩,你可以任选一个问题在留言区和我一起讨论。
1. 所有的加解密都是执行js那么如果对方使用浏览器直接运行js是否意味着所有的加解密都形同虚设 如果不是,那原因是什么?
2. 成对加解密本身就存在拦截概率了,因为对方可能匹配到熟悉的题目,也可能匹配不到。那么,我们如果有意放过爬虫,要计算条件概率吗? 如何计算呢?
3. 成对加解密roll出来之后要做一次洗牌如果不做会有什么问题吗
期待你在评论区的分享,我会及时回复你。反爬无定式,我们一起探索。
![](https://static001.geekbang.org/resource/image/70/45/7015830f7175168b1deab276d086d845.jpg?wh=1500x1615)