爬虫漫游指南
瑞数的反调试陷阱
遇上有反爬的网站,第一反应肯定是要先打开开发者工具调试一波,于是,反爬工程师们就在此处设下了第一道防线。初级一点的,例如监听F12,禁用鼠标右键,作为防线的一部分,这些小伎俩顶多就算个路障吧,成不了气候。真正能够搭建防御工事,形成火力网,还得靠debugger。
瑞数就是设置这种防线的典型。常见的瑞数一般会有2处反调试,下面就先来介绍一下这两道反调试的实现方法。
瑞数2种反调试的具体实现
1. 判断条件执行debugger
先贴一段代码样例,瑞数的代码是每次都会动态变化的,所以不要纠结这些变量名。
if (_$cI < 469) {
try {
_$v2 = _$dL(100);
if (_$v2) {
_$dL(249, _$zM[39], _$v2);
_$dL(787, 8);
}
} catch (_$3c) {}
} else if (_$cI < 470) {
_$kX[_$3c++] = _$dL(257, _$oX);
} else if (_$cI < 471) {
_$kl = _$dL(235, _$zM[68]);
} else {
debugger ;
}
这段代码肯定是毫无可读性的,只需要把它当成
if (没有打开控制台) {
执行正常流程
} else {
debugger ;
}
然后这段代码的外层是个while (1)死循环,你不能进入正常流程return出去的话就会永远陷在里面debugger,再怎么按F8也没用。
2. 利用eval + 定时器触发debugger
还是先贴源码
if (_$e1 < 239) {
var _$SM = _$SN[_$VR[340]](_$Az(_$VR[288]));
}
相信各位看到这段代码一定化身祖安文科状元,开始问候我的亲人了。这也配叫js?对不起,可惜瑞数就是这么恶心,善良的我来翻译一下。
首先把_$Az(_$VR[288])
丢进控制台,发现返回了一串字符串:
"(function() {var a = new Date(); debugger; return new Date() - a > 100;}())"
稍微了解js的就知道了,前面一定是个eval咯,不信的话把_$SN[_$VR[340]]
丢进去看一下,果然:
ƒ eval() { [native code] }
那就可以把那坨代码翻译成这样了
if (打开了控制台) {
(function() {
var a = new Date();
debugger;
return new Date() - a > 100;
} ())
}
这段函数的外层就不是while死循环了,而是个setInterval定时器。while (1)好歹还有个return的机会,进了这个setInterval,那你就真出不去了。
突破debugger防线
上面介绍了2种设置防线的方法,突破防线可就远远不止2种方法了。
1. 剿灭全部定时器
使用定时器造成无限debugger是一种非常常见的方式,干掉了定时器,里面的debugger自然没法无限执行了。那么首先先学习一下关于定时器的基础知识。
众所周知,浏览器中的JS引擎是单线程的,如果把定时器的任务交给JS引擎来做,不仅它忙不过来,而且在单线程的阻塞状态下,计时的准确性还会受到影响。因此,浏览器专门为定时器单独开了一个线程——定时器线程。
触发定时器线程有2种方式,setTimeout
和 setInterval
,简单介绍一下两者的区别。
第一是在计时方式上有区别。setTimeout
是先执行,后计时。setInterval
则是执行和计时各归各,互不影响。举个例子,setTimeout(func, 1000)
和setInterval(func, 1000)
,已知func函数执行需要耗时0.5秒,那么前者会在0.5秒执行完任务后,开始计时等待1秒,在(1+0.5)秒后,才会再次执行func,而后者,则会准确的在1秒后再次执行func。
第二是在周期执行的实现方式上有区别。
setTimeout(function repeat() {
// ...
setTimeout(repeat, 10);
}, 10);
setInterval(function () {
// ...
}, 10)
显然,使用setInterval
实现周期执行更为简洁。这也是为什么绝大部分无限debugger都会用它来实现。
上面之所以要介绍这么多基础知识,是因为干掉定时器的代码太简单了,只贴一行代码这部分会显得有些许单薄。
for (var i = 1; i < 99999; i++)window.clearInterval(i);
写一个for循环,把定时器线程里的Interval按照id一个个clear掉,没有定时器可用,自然就没法执行定时任务了。
当进入定时器无限debugger后,在控制台输入上面那行代码,再按个F8往下走,就能愉快的跳出无限debugger了。
2. 文本替换
文本替换就简单粗暴多了。加一道中间人代理,在浏览器得到js代码之前,通过中间人来篡改js代码,以mitmproxy为例
def response(flow: http.HTTPFlow) -> None:
print(flow.request.content.decode())
flow.response.content = flow.response.content.replace(
"debugger".encode('utf-8'),
"".encode('utf-8')
)
直接把js中的"debugger"字符替换掉,釜底抽薪。
所以做反爬的经常会在这儿防一手,比如
eval('debu'+'gger')
这样你替换debugger字符串就没用了,更有甚者让你debugger几个字母都找不着
eval('\x64\x65\x62\x75\x67\x67\x65\x72')
还能再加个后手
function f() {
debugger;
}
if (f.toString().indexOf("debugger") < 0) {
console.log("小兔崽子你敢篡改代码?")
}
检查debugger所在函数的字符串,一旦没有发现"debugger"的身影,就干他丫的。
3. 编辑断点
debugger这一行代码,说到底就是给你打了个断点,而在Chrome开发者工具中,是可以编辑断点的。
如图所示,单击行号打下断点后,右击,可以看到Edit breakpoint这一选项。编辑断点的意思,就是给这个断点加一个先验条件,只有条件为真的时候,才会进入断点。就像图上说的,Pause only when the condition is true
既然如此,就写个1===0
的先验条件,永远为假,就永远不会进入这个断点了。
4. 具体问题具体分析
经历过利剑、重剑、木剑,把所有招式融会贯通了,才能达到无剑的阶段。无招胜有招,见招拆招,才是解决问题的不二法门。每一种反爬都会有不同的清奇思路,上面几种方法,未必能通用,实战中还得具体分析js代码。
还是用瑞数的代码来举例。
第一种,反复检查了_$cI
的值,不小于471就进入debugger,在控制台检查_$cI
的值,正好等于471,会进入debugger,那就给他赋值等于个469啥的,不管别的影响,起码debugger给绕过去了。
第二种方法,_$VR[288]
是一个乱七八糟的加密字符串,_$Az
对他做了通操作,生成了用于eval的debugger代码。那么给_$VR[288]
重新赋值一个字符串,还原不出debugger代码来了,不就不能eval了吗,计划通。
当然,瑞数还会做很多别的二次检查,我上面讲的方法只是一种思路而已,实际怼瑞数的时候未必好使。举一反三懂吧。