爬虫漫游指南:瑞数的反调试陷阱

爬虫漫游指南

瑞数的反调试陷阱

遇上有反爬的网站,第一反应肯定是要先打开开发者工具调试一波,于是,反爬工程师们就在此处设下了第一道防线。初级一点的,例如监听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种方式,setTimeoutsetInterval,简单介绍一下两者的区别。
第一是在计时方式上有区别。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了吗,计划通。

当然,瑞数还会做很多别的二次检查,我上面讲的方法只是一种思路而已,实际怼瑞数的时候未必好使。举一反三懂吧。

发布了24 篇原创文章 · 获赞 39 · 访问量 12万+

猜你喜欢

转载自blog.csdn.net/shayuchaor/article/details/103629294