Web开发限制调试模式的攻防战

103 阅读4分钟

devtool.png

There are no secrets in client-side code.

Never trust the frontend.

Client-side code is public.

由于Web前端代码固有的公开性质,基于安全考虑我们必须避免在前端代码中硬编码敏感信息如密码或API秘密密钥,而应使用环境变量或其他安全配置机制来保护信息。如果客户端代码包含敏感内容(专有算法等),一般来说我们会使用非对称加密,并将其混淆到无法使用的程度。 更进一步安全策略可以参考:

  1. 将敏感逻辑迁移至服务端,通过API提供服务。
  2. 对剩余的前端代码进行混淆处理。
  3. 在服务端进行关键数据的验证和处理
  4. 前端仅保留必要的交互和展示逻辑。

那么除了这些基本的安全规则之外,如果还有什么事是前端必须要做的,那一定就是添加通过反调试技术手段来提高攻击成本,作为前端信息安全的辅助手段,也就是我们今天要讲的前端代码反调试的道与魔。

作为防御方(防止调试)

  • 使用 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:

<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服务端渲染复兴:减少客户端敏感逻辑暴露