你好,我是DS Hunter。

因为一些保密需要,我们的课程充满了理论,可能你会认为难以落地。因此啊,我们总要有一讲,擦着泄密的边缘,稍稍讲一点实际的例子,也在课程的基础与理论实践篇快要结束的时候做个总结。

反爬虫领域里的例子就像魔术揭秘一样,一旦了解背后的秘密了,我打赌你会先惊讶一下:哦原来是这样?然后仔细想想,就会再说:哦……不过如此!

不过在后续自己做的时候,还是要咬着牙慢慢自己往下Coding才行,你会有一种“感觉看了一堆明明能直接落地的东西,但无能为力”的感觉,在自己落地试试的时候,似乎也摔得不轻。

是的,这就是反爬。相同的招式对圣斗士是无效的,我们必须自己学会创新才行。我们能做的,就是看历史、学思路。

今天,我会为你总结技术对抗与非技术对抗这两大思路,并一一展开说明。最后,也会给你讲一讲在反爬虫中加密强度最高、最复杂的大招——虚拟机。也就是,对各类反爬虫的手段以及实现方法做一个快速的总结。

技术对抗

首先我们要明确,所有技术对抗的本质目标不是加密,而是浏览器bug检测。其余的所有手段都是为了隐藏自己的这个目标。

我们提到过,假币唯一的特征就是与真币不一样。那么爬虫唯一的特征就是与浏览器不一样。哪里不一样?浏览器有bug,爬虫不一定有。你要模拟得一样,需要模拟所有的bug才行。我们都知道,抄别人代码很容易,但是抄别人bug?那可不是一般的困难。

举个例子,我们看这样一个小bug

在部分版本的IE8下面,如果一个a标签的innerText与href相同,那么当你修改href的时候,它会自动帮你修改innerText。
 
这个很傻的bug意味着什么?
 
这意味着你作为一个爬虫,如果对网站声称自己是IE8,那么,他会让创建一个a标签,然后运行相关的js操作DOM。然后,如果你能把这个bug模拟对了,他就相信你是浏览器。如果你不知道有这个bug,你妥妥的按照正常的方式去模拟。那么,你就是爬虫!

但是,请注意,如果你代码赤裸裸写着:if,你是IE8,那么,你要创建一个a标签……等等。你觉得爬虫傻吗,他们不会觉得这个有问题吗?他们看你这么弄肯定觉得奇怪,会真的装个IE8试试啊。然后调试完了,他发现,什么,原来是这样,太阴险了。下次他还会上当吗?

想让一个人上两次当,唯一的办法就是:让他不知道自己上当了。这样,你卖完拐,卖完车,改个担架还能卖给他。

那么,强加密必然是基操了。诶,是不是你这时候忽然意识到了js加密的重要性?而且是不是这时候也明白了,加密不是目标,加密是手段?浏览器bug的检测才是目标。或者说,各个浏览器的差异,才是反爬虫技术对抗的核心。

好了,还是要帮你解决一下这个问题,我们回到这个例子本身。你可能会觉得:我知道这个bug了,可是要怎么知道哪些版本的浏览器会触发这个bug呢?注意,不要一个一个检测。有些人可能会有洁癖,一定要知道IE什么版本,UA里面拆除一堆字段,然后逐个判断。注意啊,不需要!

我们可以直接用浏览器bug和UA进行mapping!也就是说,有bug的所有UA如果我都能存起来,这个问题是不是就解决了?我不需要拆解UA啊。那么如何建立这个mapping呢?实际上,你只要预先用这个规则跑一段时间,但是不拦截,直接把埋点发送回来就可以了。一般做反爬的公司,都不是什么小厂,有足够的流量来做这个操作。线上跑个一周半周就可以了。

那么,你可能会问了:那如果对方发现我检测这个,不是提前就有准备了吗!

没错!那么你再想想怎么办才好?

哦,是不是给埋点设置一个强加密就行了?这个破解了也不影响拉接口,爬虫才没心思破解这玩意呢。这时候你是不是就意识到了埋点需要强加密的原因了?

到这里,你应该明白了,反爬的战场是瞬息万变的。很多经验,都是由实战训练出来的;很多选择,等你面对的时候才知道要怎么选。如果你觉得前面有些结论似乎没什么用,不妨反过来想想:如果我反向操作会引发什么问题吗?当你想到了问题所在,自然就明白为什么要这么操作了。

下面我们看几个例子。

迭代对抗:hook与反hook

hook这个技术本来是在native世界常用的方法。但是浏览器端,因为js修改的便利性,hook的玩法更加丰富多彩。

我们看这样一段对话:

A:我的接口需要检测xx信息,并且经过了超强的加密,你信息不对我就不给你调接口!
 
B:我根本不用看你的加密逻辑。我用xx工具进行操作,hook掉你的检测接口,如果你检测,我就mock一个数据给你,让你以为自己拿到了物理数据,实际是我模拟的!
 
A:Too young。我用API检测你的软件列表,如果你安装了xx工具,我就不让你运行!
 
B:你才too young,我hook你的检测API,你问我装没装这个工具,我就说我没装!
 
A:你才too young,我用native代码实现检测,然后用框架调用,让你无法在框架级别进行hook!
 
B:你才too young,我hook你调用native的方法,然后塞给你一个我写的函数,说这就是你的native方法!
 
……

其实,这个对话还可以不断的扩展下去,这里先赶紧打住。我们可以看到,欺诈与反欺诈检测是一个无休止的问题,只要一方解决了对方出的难题,就可以立刻进行反制。

而大家经常说的:反爬虫系统最终总是能破解,只是时间问题。这句话是什么意思呢?其实这句话你可以这么理解:反爬虫系统,对于爬虫来说,相当于一个单机游戏。单机游戏,总有通关的一天。也许是明天,也许是后天,也许是下个月、半年?但是总是过得去的。甚至可以开外挂来过啊。

那么什么游戏不能通关呢?没错,网络游戏不能通关,它可以无休止地升级下去。你装备刚毕业,他改版了。就这样,一直下去,等于没有破解。就像一些模拟战争游戏抢要塞一样,刚抢完没多久,过期了,又要重抢。这就等于没怎么抢到手。

那么对应js里面,这个是什么呢?其实就是对函数的拦截、模拟、切片等等。比如上一课我们提到的eval的攻防,就是一个简单的hook与反hook。

此外,其余的函数也可以用这种互相hook检测的办法。这个和石头剪子布一样,永远是最后出的赢。我们要做的,就是不断地更新,迭代改版。

Debugger对抗:无限Debugger

反爬虫方反爬虫方针对很多函数,都会设置大量的debugger,甚至在EventLoop里不断设置。

事实上,这样的办法是拦截初级调试者的。爬虫方呢,只要在代理上进行一次拦截,然后批量替换掉所有的debugger即可。这样,当js到达浏览器的时候,debugger就完全消失了。

但是,反爬虫方也可以针对这种批量替换继续出招。例如,检测函数的toString是否包含debugger。如果不包含,就可以认定函数被篡改。事实上,判定函数的toString,是反爬虫方一个很常用的技巧。在一定程度上,这甚至可以当函数签名使用。而一旦有了这样的代码,反爬虫方就不敢随意替换了。

注意:debugger甚至不一定放在代码里,还可以放在注释里。要知道,Function的toString,是包含注释在内的。这样,不但可以检测debugger在不在,还可以检测debugger的位置,并且不影响自己调试。另外有了这个思路之后,注释内可以隐藏大量的签名。

视觉对抗:阿拉伯文与emoji

很多浏览器在指定区域语言下,多语言支持做得是比较差的。如果线上测试发现指定的浏览器版本支持多语言,并且兼容很好,那么阿拉伯语将成为一个相当恐怖的杀器。

类似这类语言的字符有个很恐怖的地方,就是它们有的时候是从右向左写的。而且对于不懂阿拉伯语的人来说,根本不理解到底为啥代码左右乱跳。此外,一些畸形的字符都可以使用,都可以在视觉上严重影响阅读。例如QQ群里常见烟斗字符等等。

此外,emoji也是一个十分影响观感的一个选项。举个例子,10个狗构成一个变量名,11个狗构成另一个变量名,我相信正常人类是很难在一群狗里面调试下去代码逻辑的。当然,这里同样只是建议轻微使用,不建议大规模铺开。

理由有两个。一方面是兼容性问题。一旦出现,非常难调试。理论上它们可以做变量名,但是实际上哪个浏览器兼容,要以线上测试为准。另一方面,对方只要再走一次minify即可,这个我们前面提到过,破解成本比较低,所以我们大规模铺开的意义不大。因此,emoji这种方法不宜做大招使用。不过,小范围恶心一下对手,也很可能成为压死骆驼的最后一根稻草。

JSFuck对抗

大名鼎鼎的JSFuck,这个发明一度震惊了全世界。js的混乱程度被用到了极致。但是要注意,这种加密方式已经有比较成熟的工具来逆向解析了。此外,代码长度急剧膨胀,简直不能忍受。因此,如果使用,只能放在核心代码上绕一下对手。而且,建议进行自定义,而不是使用标准做法,避免被工具逆向。

工具对抗:调试状态检测

判定浏览器调试状态也是一个很关键的做法。在一些浏览器里,你可以有意引发一个异常然后catch住,判定error的信息。你会发现,在Console里运行的函数与直接运行的函数,引发的error有很大差别,这个差别很明显,就不详细展开了。此外,启动Devtools的时候会引发一次size的change,这个也可以持续监听。最后,Console里运行的时候,会有一些Console特有的函数。

这些都可以做判定。具体可以查阅Chrome的帮助文档。

非技术对抗

除了正面硬刚的技术对抗,还会有一些角落中的非技术对抗。非技术对抗大部分是心理对抗。当然,在很大程度上,心理对抗都比技术对抗效果好。毕竟,反爬打的不是算法,是人。

薪资对抗

薪资对抗,或者福利对抗,都是比较常用的心理战术。例如,随着调试的不断深入,不断给对方报价。

举个例子,爬虫方破解你第一层加密的时候,提示:唉,这算啥啊,月薪三位数都能走到这。再破解一层,就提示:这也就拿个月薪四位数。甚至可以直接报价,乃至于直接留HR邮箱。这并非天方夜谭,很多爬虫工程师技术非常优秀,并且非常不想继续做爬虫了,能转前端多开心啊!专业也对口,HR业绩也完成了。

但是注意:不要在这里鄙视竞对,否则容易惹上法务风险。

关键字的作用

很多人都知道,某知名网站在反爬里经常会写一些稀奇古怪的话。这些话非常猖狂,让人破解完忍不住要写一篇博客吐槽他们。注意,你写的博客很快会被搜索引擎收录。而他们有个爬虫帮自己不断检测关键字是否被收录。嗯,没错,你被发现了,爬取时自以为的低调真的只是“自以为”了!他们反爬组当然会写爬虫啊!

因此后续记住,看到反爬代码里的冷嘲热讽,或者一些稀奇古怪的话,无视即可,千万不要当回事。只要多看一眼,你可能就输了。

重头:虚拟机

很多大型反爬一定会有一个虚拟机的。但是回到我们刚刚说到的反爬虫思路来看,虚拟机不是目的,只是手段。webassembly也是虚拟机。但是我们在第10讲提到过,webassembly不好用,大家还是会自己实现虚拟机的。而虚拟机的核心作用,就是对代码进行加密,让破解变得更难。因为只有难以破解的代码,才能更好地隐藏你的小心机。

虚拟机的实现

虚拟机的实现方式,基本上就是看你熟悉哪门语言,就实现对应的解释器就行了。一般不建议使用主流语言,而是尽可能用冷门语言。Lisp或者汇编都是不错的选择。甚至,你自创一门语言,都是可以的。只要你有办法写完解释器。

解释器本身还需要再进行加密。例如,eval,变量名混淆,都可以。甚至更疯狂地说,我们都知道大部分语言都是自举的。也就是说,你的解释器可以用另一个解释器来解释执行,套娃解释。最终,你的竞对一定会被绕晕,根本看不出这是两层解释器。

至于再增加层数,就没必要了。理由是,两层解释器的代码已经大大超出预期。再高可能会因为js过大,影响用户体验了。

这里强烈安利Lisp解释器。我不方便透露太多细节,我只能说,某站点的反爬是用Lisp解释器做的,已经跑了六七年了,直到现在,我搜各种破解博客,大家还都在纠结如何破解那个“汇编解释器”的问题。事实上,那是Lisp……他只是简单解释了一颗语法树……

虚拟机的部署

实现了虚拟机的功能,我们就要开始部署了。虚拟机的部署有站点直接部署与CDN部署两种。

首先看站点部署。最原始的部署方式一定是由站点直接生成js文件,那么文件头就会有一个虚拟机。因为虚拟机是共用的,所以有些大聪明可能会考虑放到CDN。也不是不能放CDN,但是放了CDN,你的虚拟机就定死了,不能变更了。实际上,这样反而降低了别人的破解难度。

但是不放CDN,又太慢了,因为很难使用缓存机制。怎么办呢?

实际上,如果你是两层虚拟机,那么可以把一层放在CDN,利用缓存。然后再套娃解释一个简单虚拟机。例如,CDN放汇编解释器,然后用汇编解释器解释一个Lisp解释器,Lisp解释器用站点直接下发。这样,就可以实现CDN大量加速,同时代码破解难度也不低。

在这种情况下,我们的代码复杂度会逐渐提升。那么,如果爬虫方不读代码,直接用浏览器运行呢?

谢天谢地那太好了,我们等得就是这一天。一旦爬虫不读代码了,那就意味着,无论发生什么,都是黑盒了,他失去了调试的能力。你可以随机封他IP,封他指纹,增加浏览器bug检测。放心检测就好,虽然他使用了浏览器,但他还是会不断换UA的,假装自己是很多种浏览器的,所以你可以用各种办法随机坑他。而他只会认为自己的浏览器模拟有问题,而不是去调试哪行代码有问题——毕竟他是自己放弃这个能力的。

这能怪谁呢……

好了,今天的答疑课堂就到这里了。希望我的分享可以帮助你了解反爬虫这件事上思路的重要性。而当你没有思路的时候,一定要回到最初的想法:我完成反爬虫这个动作的最终目的是什么呢?如果我反向操作会引发什么问题吗?回归初心,往往能够帮助你直达目标。