文章目录
前言
常见反爬策略整理
专栏全面整理了常见的反爬策略,包括headers反爬、js加密、CSS加密、浏览器反调试、数据传输格式、IP封禁等,还介绍了如HTTP2、TLS指纹、WebSocket、请求规律、蜜罐陷阱等其他反爬手段及相应的解决方案。这篇文章是专栏的系列之一,主要讲解浏览器反调试的一些特征及解决方案,看汇总版的可以移步 常见反爬策略整理
一、禁用控制台
1.1 浏览器控制打开方式
一般打开浏览器控制台有以下几种方式
- 鼠标右键 -> 检查
- 键盘快捷键
F12
command + option + i/j/c(Mac 系统)
ctrl + shift + i(Win 系统)
- 浏览器右上角
三个点
->更多工具
->开发者工具
1.2 绕过禁用的打开控制台
- 在进入目标网页前,打开新的标签页,先打开控制台,再进入目标网址
- 浏览器右上角
三个点
->更多工具
->开发者工具
二、检测调试行为
2.1 检测控制台是否打开
常见的是通过 js 检测窗口大小来判断是否有打开控制台.
-
解决方案
- 控制台采用
undock into seperate window
模式展示可以绕过
- 调试 js 定位检测位置,删掉检测代码
- 有些是通过定时任务去定时检测,可以
hook
掉定时任务或clearInterval / clearTimeout
清除定时器// hook 定时任务的创建逻辑 window.setTimeout = function setTimeout(code, delay) {}; window.setInterval = function setInterval(code, delay) {}; // 清除已有定时任务 ( function() { function clearAllInterval(){ let max_interval_id = setInterval(() => {}, 1000); for (let i = 0; i <= max_interval_id; i++){ clearInterval(i); } } setInterval(clearAllInterval, 3000); } )
- 控制台采用
2.2 检测代码是否阻塞
在调试模式下,某些操作(如断点调试、步进执行)会导致代码执行时间异常延长,可以利用这些特性检测调试行为。
// 示例:使用 `setInterval` 检测代码是否被阻塞
let start = Date.now();
setInterval(() => {
const duration = Date.now() - start;
if (duration > 100) {
console.log('Debugger detected!');
// 执行反调试操作
}
start = Date.now();
}, 50);
- 解决方案
- 调试
js
定位检测位置,删掉检测代码 - 提前
hook
定时任务(参考上面) clearInterval / clearTimeout
清除定时器(参考上面)
- 调试
2.3 利用 console
通过覆盖 console
的方法,导致调用 console
时会进入开发者自定义的反爬逻辑中,误导调试效果,进入到错误逻辑中,甚至引起内存崩溃无法调试。
利用浏览器正常模式和开发者模式下 console.log
的执行区别实现
- 开发者工具模式下,
console.log
为了展示更多信息,会尝试调用对象的toString
方法,即使变量是字符串或简单对象。因此在打开开发者模式时,不管变量是什么类型,一律会调用它的toStirng
方法。 - 浏览器正常运行情况下,
cnosole.log
会根据值的类型选择最合适的输出方式。对于字符串直接输出原始值,对于其他类型的对象则可能会试图调用toString
方法来生成字符串表示,或输出对象的原始结构。
- 解决方案
- 提前保存原始的
console
对象为myconsole
,禁用原来的console
,后续使用myconsole
来打印信息
- 结合
override
调试js
,定位到覆盖console
位置,删除代码
- 提前保存原始的
三、debugger 地狱
在代码调试过程中,频繁中断在 debugger
语句,陷入一个频繁暂停的“地狱”
状态,难以继续正常调试和执行代码。
3.1 构建 debugger 的方式
3.1.1 普通 debugger
在 html
中的 script
标签只要有 debugger
,程序都会在该语句暂停,属于最直接的 debugger
干扰。
- 静态构建 debugger
在 html 中可直接看到 debugger 关键字,或者被混淆的debugger
。
- 解决方案
- 借助浏览器工具,在 debugger 暂停行,
鼠标右键
=>Never pause here
,即可绕过
- override 源文件,删掉 debugger 代码
override
使用步骤,首先将目标文件覆写到本地,使其可编辑
打开Enable Local Overrides
,我们覆写的文件有个紫色小点标记,意味着我们可以直接编辑文件删掉里面所有的debugger
,后续请求该URL
时,就会执行我们编辑后的这个html
。
- 借助浏览器工具,在 debugger 暂停行,
- 解决方案
- 动态构建 debugger
区别于上述直白的debugger
形式,有时会通过appendChild
在代码执行过程中对 html 的文档结构进行修改,动态增加嵌入了dubugger 关键字的节点,并且隐藏关键字和修改文档的位置。
- 解决方案
- 调试 js 定位到修改
html
结构的位置,删掉代码 - 提前
hook appendChild
解决// 解决 appendChild debugger _appendChild = Node.prototype.appendChild; Node.prototype.appendChild = function () { if (arguments[0].innerHTML && arguments[0].innerHTML.indexOf('debugger') != -1) { arguments[0].innerHTML = ''; }; return _appendChild.apply(this, arguments); }
- 调试 js 定位到修改
- 解决方案
3.1.2 Eval 构建 debugger
在 JavaScript 中,eval()
函数是一个内置的全局函数,函数将传入的字符串作为 JavaScript 代码解析并在虚拟机中执行,允许动态执行代码。
- 解决方案
提前 hook 掉 eval 函数,替换执行代码字符串中的debugger
内容// Hook eval 函数(示例代码): eval_ = eval; eval = function () { if (arguments[0].indexOf('debugger') == -1) { return eval_.apply(this, arguments); } }
3.1.3 Function 构建 debugger
在 JavaScript 中,Function
是一个内置的构造器对象,用于创建新的函数。Function
构造器允许动态地创建函数并传递字符串形式的代码。它的行为类似于 eval()
,但比 eval()
更加安全和受限。
Function 构建 debugger 的形式有以下两种:
- 直接通过 Function 创建
a = Function('debugger; console.log("i am in the anonymous;")'); a();
- 解决方案
提前 hook 掉 Function,替换掉 debugger 内容。MyFunction = Function; Function = function () { if (arguments[0].indexOf('debugger') != -1) { arguments[0] = arguments[0].replace('debugger', ''); } return MyFunction.apply(this, arguments); }
- 解决方案
- 调用函数的
constructor
构建 debuggerfunction test() { console.log("i am in the test"); } test["constructor"]('debugger; console.log("i am in the anonymous;")')['call']();
- 解决方案
提前 hook 掉Function
的constructor
,替换掉 debugger 内容。_Function = Function; Function.prototype.constructor = function () { if (arguments[0].indexOf('debugger') != -1) { arguments[0] = arguments[0].replace('debugger', ''); } return _Function.apply(this, arguments); };
- 解决方案
3.2 无限 debugger 反爬的实现方式
3.2.1 定时任务
定时任务结合上述构建 debugger 的各种方式来达到贯穿全程的 debugger 效果。
- 解决方案
override
文件,删掉创建定时器的代码- 测试下定时器中是否有关业务逻辑,无影响则删除所有定时器
3.2.2 递归
通过函数的递归调用实现无限 debugger,如图所示:
- 解决方案
- 分析嵌套函数中
debugger
的构建方式,按照上述debugger
的解决方案hook
代码 - 分析递归函数代码,若递归函数中无业务逻辑,可将该递归函数置空
- 分析递归的流程,可能存在环境检测未通过,进入了不该走的逻辑
- 分析嵌套函数中
四、内存爆破
当程序检测到是爬虫行为时,会触发一些操作,这些操作通过不断消耗浏览器内存资源,使其崩溃或响应缓慢,从而达到阻碍爬虫获取数据的目的。
4.1 实现方式
通过定时任务或者递归操作,不断的进行一些消耗内存的行为,例如:
-
死循环
- 利用 while-true 构造死循环。
- 函数无限递归,或者 for 循环不断往数组中写入数据,耗尽内存。
- 需要注意的是,对于函数无限递归的情况,最外层如果有 try-catch,则并不影响程序后续运行。这是因为同步栈溢出(如递归导致的
RangeError
)可以被try-catch
捕获。内存耗尽(如大量数据分配导致的内存溢出)通常无法被try-catch
捕获,因为这是引擎层级的问题,会直接终止进程。
-
大量DOM操作
不断创建、删除或修改 DOM 元素,触发浏览器频繁的 DOM 重绘和重排,消耗大量内存。 -
重写本地数据
频繁重写或追加 cookie、history 等本地数据,耗尽浏览器资源。
4.2 触发条件
整理了一些常见的内存爆破检测条件,检测未通过则会导致js
代码进入内存爆破的逻辑中。最终导致内存崩溃。
4.2.1 代码格式化检测
伴有关键字 new RegExp
、test
、toString
,利用 RegExp 对象的 test 对代码进行是否格式化的检测,如:换行,空格之类的变化,检测到代码被格式化,则进入死循环中。
- 解决方案
- 尽量直接扣取不做任何格式化的代码,可以很大程度避免
- hook 被检测对象的
toString
方法,输出符合检测规则的内容
4.2.2 浏览器指纹检测
一般会对浏览器和 Node
常见的 API 进行检测,如:document
、location
、navigator
、global
、require
、toString
、sessionStorage
、localStorage
等进行不同程度的检测,检测不通过则进入反爬逻辑中。
- 解决方案
解决方案就是补
,分享几个补环境的小技巧,尽可能提高补环境效率。- 扣取代码之前,将基本的浏览器对象简单补充,如
window
、location
、navigator
这些 - 代码混淆严重的情况,可以先 AST 简单解混淆,得到更多的明文,便于调试
- 出现了明文的情况下,则可以先搜索一些
node
环境检测的关键字,如:global
、require
等,提前处理掉这些检测
- 测试定时任务是否影响主业务,不影响可以直接在本地程序提前清理掉定时器
- 仔细认真的调试,然后分阶段补环境
- 扣取代码之前,将基本的浏览器对象简单补充,如
五、总结
文章结合自己经验,列举了一些常见的浏览器反调试的策略,也给出了一些解决方案,大家有更好的方案可以在评论区里提供,我看到后会及时更新到文章,为大家提供更多的解题思路,如有错误和不足之处还请指正。常见反爬策略整理
的汇总篇可以移步 常见反爬策略整理。
6、交流群
不会经常刷博客,有需要者可以加本人,搜索 LOVE_SELF_AD_LIFE,进逆向群聊,一起探讨技术