平时对一些网站开启F12大法,会遇到无限Debugger阻断,导致网页无法正常执行。
比较典型的就是樱花动漫(真真假假难辨,但是大部分都有反爬),就像这样:
这个问题也算是网站爬虫遇到的经典问题了,解决这个问题有两个方法:
- 修改网页的代码,去掉所有的Debugger断点。这个方法比较麻烦,每次打开网站都需要修改JS。
- 修改浏览器,无视Debugger断点。这个算是一劳永逸的方法了,让网站的Debugger断点反爬直接失效,这也是这次我打算走的路线。
定好了方法,接下来就开干吧!我打算拿Firefox进行开刀,因为Firefox是完全开源的浏览器,也比较贴合自由软件相关的理念。
拉取Firefox源代码
Firefox的源代码是公开的,可以在github.com/mozilla-firefox/firefox找到源代码。源代码会比较大,后期编译这类工作做完之后会占用到16GB,所以拉取代码的时候建议找个空间比较大的地方放置代码。
拉取代码后,可以切换分支进行修改,我个人建议是使用ESR的版本进行修改,使用git切换一下到ESR分支即可,我是在esr140分支进行修改的。
修改代码,无视Debugger
对于修改代码的工作,我是不可能上手修改的,直接丢给AI改了,提示词如下:
这是firefox 140 esr 的源代码,我需要你完成一个任务:让浏览器在开启开发者控制台的时候,无视 debugger 断点继续执行实现 反反爬 功能。
修改完成后,请进行编译验证。如果系统自带的python 环境因为权限无法更改的话,请使用conda创建合适的环境进行操作。
验收的标准是,当页面存在 debugger ,打开浏览器控制台,页面继续执行不会被打断。
我用的是华为云的CodeArts CLI,模型是GLM-5.2。不得不说,现在AI确实厉害,不一会就找到了相关的代码位置:
js/src/debugger/DebugAPI-inl.hjs/src/jit/VMFunctions.cpp
修改的代码量实际上并不算大,修改几行代码即可完成任务。
DebugAPI-inl.h 修改情况
函数: DebugAPI::onDebuggerStatement(第 154 行)
这是 debugger 语句的统一入口点,解释器和基线解释器均经此处理。
改动前:检查是否被调试器跟踪,是则触发钩子暂停执行
bool DebugAPI::onDebuggerStatement(JSContext* cx, AbstractFramePtr frame) {
if (MOZ_UNLIKELY(cx->realm()->isDebuggee())) {
return slowPathOnDebuggerStatement(cx, frame); // 触发 onDebuggerStatement 钩子 → 暂停
}
return true;
}改动后:直接返回,不触发任何钩子
bool DebugAPI::onDebuggerStatement(JSContext* cx, AbstractFramePtr frame) {
return true;
}作用: 切断 debugger 语句到 DevTools 调试器的通知链。原代码中,
当 realm 被 DevTools 跟踪时(isDebuggee() 为 true),会调用slowPathOnDebuggerStatement → fireDebuggerStatement,触发已注册的onDebuggerStatement 钩子,使执行暂停。改动后直接返回,钩子永不触发。
VMFunctions.cpp 修改情况
函数: GlobalHasLiveOnDebuggerStatement(第 1288 行)
这是 Warp JIT 代码中 debugger 语句的 bailout 判定函数。
改动前:有调试器钩子时返回 true,触发 bailout 回退到解释器
bool GlobalHasLiveOnDebuggerStatement(JSContext* cx) {
AutoUnsafeCallWithABI unsafe;
return cx->realm()->isDebuggee() &&
DebugAPI::hasDebuggerStatementHook(cx->global());
}改动后:始终返回 false,JIT 代码中不 bailout
bool GlobalHasLiveOnDebuggerStatement(JSContext* cx) {
AutoUnsafeCallWithABI unsafe;
return false;
}作用: JIT 代码执行到 debugger 语句时(CodeGenerator::visitDebugger),
原逻辑调用此函数判断是否需要 bailout 到解释器去触发钩子。改动后返回 false,debugger 语句在 JIT 代码中变为 no-op,避免不必要的 bailout,兼顾正确性与性能。
配置Firefox编译环境
完整编译Firefox的时间比较久,我这边用E5-1680v4编译耗时大约34分钟。
系统依赖环境准备
编译对工具链的要求如下:
| 工具 | 最低版本 |
|---|---|
| Clang | 15+ |
| Rust | 1.82.0 |
| Python | 3.12 |
| NASM | 2.14+ |
| Node.js | 18+ |
| cbindgen | 0.27+ |
可以用下面的脚本批量检查一下pkg-config的依赖:
for lib in gtk+-3.0 nss nspr dbus-1 x11 xrender xrandr \
glib-2.0 pango cairo freetype2 fontconfig \
libpng hunspell vpx icu-uc icu-i18n; do
pkg-config --exists $lib && echo "$lib: ✓" || echo "$lib: ✗ 缺失"
done如果缺依赖,可以用下面的命令安装:
pacman -S --needed \
gtk3 gtk4 \
nss nspr \
dbus dbus-glib \
libpulse alsa-lib \
icu hunspell libvpx \
libwebp libffi \
freetype2 fontconfig \
expat zlib xz \
sdl2 libevent \
nasm yasm \
clang rustup \
nodejs npm \
python base-devel构建环境配置
先进行环境初始化(如果是首次配置):
进入项目文件夹,如果 .build-env/ 尚未存在,按以下步骤初始化:
mkdir -p .build-env/rustup .build-env/cargo .build-env/conda/pkgs配置环境设置脚本.build-env/setup.sh,内容如下:
#!/bin/bash
PROJECT_ROOT="/run/media/chocola/3T-DATA02/Projects/firefox"
export RUSTUP_HOME="$PROJECT_ROOT/.build-env/rustup"
export CARGO_HOME="$PROJECT_ROOT/.build-env/cargo"
export PATH="$PROJECT_ROOT/.build-env/cargo/bin:$PATH"
export MOZBUILD_STATE_PATH="$PROJECT_ROOT/.build-env/mozbuild-state"
export MACH_USE_SYSTEM_PYTHON=1
export PYTHON3="$PROJECT_ROOT/.build-env/conda/py312/bin/python3"
mkdir -p "$MOZBUILD_STATE_PATH"请注意PROJECT_ROOT换成你实际项目所在的地址。
然后继续配置:
source .build-env/setup.sh
rustup default stable
cargo install cbindgen
export CONDA_PKGS_DIRS=$(pwd)/.build-env/conda/pkgs
conda create -y -p $(pwd)/.build-env/conda/py312 python=3.12后续进行编译,每次新终端会话开始时,先 source 环境脚本:
source .build-env/setup.sh后续所有 mach 命令使用 conda Python 执行:
./.build-env/conda/py312/bin/python3 ./mach <command>编译Firefox
Firefox 编译需要设置配置文件再进行编译,和Linux内核有点像。
mozconfig 配置
完整浏览器构建的mozconfig内容:
# Full Firefox browser build (anti-anti-scraping)
mk_add_options MOZ_OBJDIR=@TOPSRCDIR@/obj-full-build
ac_add_options --enable-application=browser
ac_add_options --enable-optimize
ac_add_options --disable-debug-symbols
ac_add_options --disable-tests
ac_add_options --without-wasm-sandboxed-libraries
ac_add_options --host=x86_64-unknown-linux-gnu
ac_add_options --target=x86_64-unknown-linux-gnu各选项说明:
| 选项 | 说明 |
|---|---|
MOZ_OBJDIR | 构建输出目录(与 SpiderMonkey 验证构建分开) |
--enable-application=browser | 构建完整 Firefox 浏览器 |
--enable-optimize | 启用优化(release 模式) |
--disable-debug-symbols | 不生成调试符号(减小体积) |
--disable-tests | 不编译测试代码(加快编译) |
--without-wasm-sandboxed-libraries | 禁用 RLBox WASM 沙箱(避免 wasi headers 依赖) |
--host/--target | 修正 triplet(系统编译器报告 pc vendor,rustc 不认识) |
完整浏览器编译
source .build-env/setup.sh
# 配置(首次或修改 mozconfig 后)
./.build-env/conda/py312/bin/python3 ./mach configure
# 编译
./.build-env/conda/py312/bin/python3 ./mach build
# 打包生成 .tar.xz
./.build-env/conda/py312/bin/python3 ./mach package产物位置:
| 产物 | 路径 |
|---|---|
| 可运行的 Firefox | obj-full-build/dist/firefox/firefox |
| 安装包 | obj-full-build/dist/firefox-140.17.0.en-US.linux-x86_64.tar.xz |
测试修改效果
可以使用下面的HTML代码保存为一个HTML文件测试无视Debugger的效果:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>反反爬测试 - debugger 语句验证</title>
<style>
body { font-family: monospace; padding: 20px; max-width: 800px; }
.pass { color: green; font-weight: bold; }
.fail { color: red; font-weight: bold; }
.box { border: 1px solid #ccc; padding: 15px; margin: 10px 0; border-radius: 5px; }
h1 { color: #333; }
code { background: #f4f4f4; padding: 2px 6px; border-radius: 3px; }
</style>
</head>
<body>
<h1>Firefox 140 ESR 反反爬功能测试</h1>
<div class="box">
<h3>测试说明</h3>
<p>此页面包含多个 <code>debugger</code> 语句。在未修改的 Firefox 中,打开开发者工具(F12)后,页面执行会被 <code>debugger</code> 语句暂停。在已修改的 Firefox 中,<code>debugger</code> 语句被忽略,页面正常执行。</p>
<p><strong>测试方法:</strong> 按 F12 打开开发者工具,切换到"调试器"面板,然后点击下方"开始测试"按钮。</p>
</div>
<div class="box">
<h3>测试结果</h3>
<div id="results">
<p>点击"开始测试"按钮开始...</p>
</div>
</div>
<button id="startBtn" onclick="runTest()" style="font-size:16px;padding:10px 20px;">开始测试</button>
<script>
function runTest() {
var results = document.getElementById('results');
var output = [];
var startTime = Date.now();
// 测试1: 单个 debugger 语句
debugger;
output.push('<p class="pass">✓ 测试1通过: debugger 语句未暂停执行</p>');
// 测试2: 循环中的多个 debugger 语句
var sum = 0;
for (var i = 0; i < 5; i++) {
debugger;
sum += i;
}
if (sum === 10) {
output.push('<p class="pass">✓ 测试2通过: 循环中5个debugger语句全部被忽略 (sum=' + sum + ')</p>');
} else {
output.push('<p class="fail">✗ 测试2失败: sum=' + sum + ' (期望10)</p>');
}
// 测试3: 嵌套函数中的 debugger
function inner() {
debugger;
return "inner-ok";
}
var r = inner();
if (r === "inner-ok") {
output.push('<p class="pass">✓ 测试3通过: 嵌套函数中debugger语句被忽略</p>');
} else {
output.push('<p class="fail">✗ 测试3失败: r=' + r + '</p>');
}
// 测试4: try-catch 中的 debugger
try {
debugger;
output.push('<p class="pass">✓ 测试4通过: try块中debugger语句被忽略</p>');
} catch(e) {
output.push('<p class="fail">✗ 测试4失败: 异常 - ' + e + '</p>');
}
// 测试5: 定时器中的 debugger
setTimeout(function() {
debugger;
var elapsed = Date.now() - startTime;
document.getElementById('results').innerHTML += '<p class="pass">✓ 测试5通过: setTimeout中debugger语句被忽略 (耗时' + elapsed + 'ms)</p>';
}, 100);
var elapsed = Date.now() - startTime;
output.push('<p>总耗时: ' + elapsed + 'ms</p>');
output.push('<p class="pass"><strong>结论: 所有debugger语句均被忽略,页面正常执行。反反爬功能生效!</strong></p>');
results.innerHTML = output.join('');
}
</script>
</body>
</html>浏览器打开这个文件,然后打开开发者工具,点击开始测试,如果过程没有被打断说明无视Debugger成功。
再次测试一下樱花动漫的情况:
樱花动漫内心OS:
他说他改过了浏览器的源代码,啊。看来是——有bear来。这个,浏览器,不讲武德。来,骗!来,偷袭,我网站JS的,Debugger。这好吗?这不好。我劝!这个,浏览器,耗子尾汁,好好反思。以后不要再犯这样的聪明,小聪明啊。呃,网站,要以和为贵,要讲武德,不要搞,爬虫工具。谢谢朋友们!
当然,真要看番也不应该去樱花动漫,去找点种子站(例如 bangumi.moe,打不开是你的问题)把番剧下回来看比这玩意好多了。
我这边有编译好的 Firefox 140 ESR,想要的可以来拿:
firefox-140.17.0.en-US.linux-x86_64.tar.xz
另外,你可以去查看我Fork进行修改的GitHub仓库:github.com/Chocola-X/firefox
什么?你要Windows的?我现在电脑只用GNU/Linux,我编译Windows的版本干嘛?