There are no secrets in client-side code.
Never trust the frontend.
Client-side code is public.
由于Web前端代码固有的公开性质,基于安全考虑我们必须避免在前端代码中硬编码敏感信息如密码或API秘密密钥,而应使用环境变量或其他安全配置机制来保护信息。如果客户端代码包含敏感内容(专有算法等),一般来说我们会使用非对称加密,并将其混淆到无法使用的程度。 更进一步安全策略可以参考:
- 将敏感逻辑迁移至服务端,通过API提供服务。
- 对剩余的前端代码进行混淆处理。
- 在服务端进行关键数据的验证和处理
- 前端仅保留必要的交互和展示逻辑。
那么除了这些基本的安全规则之外,如果还有什么事是前端必须要做的,那一定就是添加通过反调试技术手段来提高攻击成本,作为前端信息安全的辅助手段,也就是我们今天要讲的前端代码反调试的道与魔。
作为防御方(防止调试)
- 使用 debugger 断点卡住前端
通过定时器不断触发 debugger 断点,当开发者工具打开时,程序断点导致无法正常执行。一般会使用一行代码,同时配合代码混淆使用。
setInterval(() => { debugger; }, 50);
这个方法可以有变种:例如在匿名函数构造器里面放个debugger
- 检测开发者工具打开
例如监听窗口大小变化,实际比例和可见区域差异明显,判断非法操作,然后可跳转到空白页或执行其他操作:
// 窗口检测
function detectDevTools() {
const threshold = { width: 100, height: 130 };
const widthDiff = window.outerWidth - window.innerWidth;
const heightDiff = window.outerHeight - window.innerHeight;
return widthDiff > threshold.width || heightDiff > threshold.height;
}
setInterval(() => {
if detectDevTools() {
window.location.href = "about:blank";
}
}, 1000);
- 拦截F12、Ctrl+Shift+I 等快捷键以及右键菜单 可以防止开发者便捷的打开开发者面板或一些其他操作,但是实际很容易绕过,毕竟去往罗马的道路有千万条。
document.addEventListener('keydown', (e) => {
if (e.key === 'F12' || (e.ctrlKey && e.shiftKey && (e.key === 'I' || e.key === 'C'))) {
e.preventDefault(); }
});
document.addEventListener('contextmenu', (e) => e.preventDefault());
- 利用 console 输出时间相对比较长来判断打开了开发者工具 基本原理很简单,console未打开无输出和实际输出的耗时会有明显差异。 用户流程按理:打开开发者工具->debugger卡住->判断上下文时间间隔增加->直接跳转到空白页
setInterval(function () {
var startTime = performance.now();
// 设置断点
debugger;
var endTime = performance.now();
// 设置一个阈值,例如100毫秒
if (endTime - startTime > 100) {
window.location.href = 'about:blank';
}
}, 100);
那么有没有更简单易用的方法呢? 答案是肯定的,直接用现成的轮子 -- disable-devtool:
- 项目仓库:github.com/theajack/di…
- npm 包:www.npmjs.com/package/dis…
- 文档:github.com/theajack/di…
- 最简使用:
<script disable-devtool-auto src='<https://cdn.jsdelivr.net/npm/disable-devtool>'></script>
攻击方(突破限制)
了解了原理,突破限制简直就可以手到擒来了。
- 禁用断点:
点击“Deactivate Breakpoints”按钮, 按 Ctrl+Shift+F8 也可以。
- 替换网页内容
注释掉禁用调试的代码。 ①Firefox 的“覆盖网络资源”或 Chrome 的“编辑并重发” ②使用 Fiddler 等抓包工具,拦截并替换网页请求
- 绕过窗口大小检测
将开发者工具设置为独立窗口--“取消停靠”
- 进阶工具高级版-通过油猴或者篡改猴插件修改网页脚本:
例如应对console 输出时间判断: 把浏览器自带的 console 函数全部替换成空函数即可
<script>
const consoleKeys = Object.keys(window.console)
for (const key of Object.keys(window.console)) {
if (typeof window.console[key] === 'function') {
window.console[key] = function () {}
}
}
</script>
解决定时器断点
(function() { 'use strict'; setInterval = () => {} })();
解决构造函数断点
Function.prototype._constructor = Function.prototype.constructor Function.prototype.constructor = () => {
if ( arguments && typeof arguments[0] === 'string') {
if ('debugger' === arguments[0]) { return }
}
return Function.prototype._constructor.apply(this, arguments)
}
检测到disable-devtool直接href跳转,核心绕过原理是hook console.table:
@run-at document-start
(function() { 'use strict'; console.table = () => {} })();
结尾小结
攻防现状与实际价值
通过前面的技术分析可以看出,前端反调试本质上是一场不对等的攻防战。防御方需要在用户的环境中执行代码,而攻击方拥有完全的环境控制权。正如安全领域的经典原则:"防御者需要保护所有入口,而攻击者只需要找到一个突破点"。 作为开发者的我们应当始终遵循"前端没有秘密"的设计原则,将关键业务逻辑和敏感数据处理放在服务端。反调试只是辅助手段,不应成为安全防护的主要依赖。 如果单纯的从前端反调试技术考虑,未来可能需要继续关注下WebAssembly和SSR,也算是留个坑位,也许未来我会续写一篇文章:
- WebAssembly:提供更好的代码保护能力
- SSR服务端渲染复兴:减少客户端敏感逻辑暴露