JS `Anti-Debugging` 技术:`debugger` 语句、`console.log` 重写与时间检测

各位观众老爷,大家好!我是今天的主讲人,咱们今天聊点有意思的——JS反调试技术。听说有很多同学深受调试之苦,被各种花式反调试搞得焦头烂额,今天咱们就来扒一扒这些反调试的底裤,看看它们到底是怎么工作的,又该如何应对。

咱们今天主要聊三个方面:

  1. debugger 语句:最简单也最常见的反调试手段。
  2. console.log 重写:让你看不到想看的信息,干扰调试过程。
  3. 时间检测:通过检测调试器带来的时间差异来判断是否被调试。

准备好了吗?咱们这就开始!

一、 debugger 语句:简单粗暴的反调试

debugger 语句,顾名思义,就是用来启动调试器的。如果你在代码中插入了 debugger 语句,当浏览器执行到这一行代码时,如果调试器是打开的,那么浏览器就会自动断点到这里。

这玩意儿看起来挺方便的,程序员可以用它来调试代码,但是,坏人也可以用它来反调试。

1. debugger 语句的反调试原理

反调试者会在代码中插入大量的 debugger 语句,甚至是在循环中插入。这样,当你尝试调试这段代码时,浏览器就会频繁地断点,让你烦不胜烦,根本无法正常调试。

举个例子:

function antiDebug() {
  for (let i = 0; i < 1000; i++) {
    debugger;
    console.log("防止调试" + i);
  }
}

antiDebug();

这段代码,一旦你打开开发者工具,就会疯狂触发断点,让你根本没法干活。

2. 绕过 debugger 语句的方法

  • 禁用断点: 最简单粗暴的方法,直接在开发者工具里禁用所有断点。不同浏览器的操作方式略有不同,但一般都能找到禁用断点的选项。

  • 条件断点: 如果你只想在特定条件下跳过 debugger 语句,可以使用条件断点。比如,你可以设置一个条件,让断点只在某个变量的值为特定值时才生效。

    // 举例:只在变量 flag 为 true 时才断点
    let flag = false;
    function someFunction() {
      if (flag) {
        debugger; // 设置条件断点:flag === true
      }
      console.log("继续执行");
    }
  • Hook debugger 语句: 这是一种更高级的方法,可以通过修改 JavaScript 引擎的行为来阻止 debugger 语句的执行。

    // 重写 debugger 语句,使其无效
    (function() {
      'use strict';
      var originalDebugger = eval;
      eval = function(arg) {
        if (arg === 'debugger') {
          console.log("debugger语句被拦截!");
          return; // 阻止 debugger 语句执行
        }
        return originalDebugger(arg);
      };
    })();
    
    function test() {
      debugger; // 尝试触发 debugger
      console.log("debugger 之后的代码");
    }
    
    test(); // 调用函数,测试 debugger 是否被拦截

    这段代码的核心在于重写 eval 函数。当 eval 函数接收到的参数是字符串 'debugger' 时,我们直接返回,阻止了 debugger 语句的执行。

    注意: 这种方法可能会影响其他代码的执行,需要谨慎使用。并且现代浏览器对此类hook的检测越来越严格,效果可能不佳。

3. 更高级的 debugger 反调试

有些反调试会使用更高级的技巧,比如使用 setIntervalsetTimeout 来定期执行 debugger 语句,让你防不胜防。

// 每隔一段时间执行 debugger 语句
setInterval(function() {
  debugger;
}, 50);

对于这种反调试,我们可以使用类似的方法来 Hook setIntervalsetTimeout,阻止它们执行 debugger 语句。

// 重写 setInterval 和 setTimeout,阻止 debugger 语句的执行
(function() {
  'use strict';
  var originalSetInterval = window.setInterval;
  var originalSetTimeout = window.setTimeout;

  window.setInterval = function(callback, delay) {
    if (typeof callback === 'function') {
      let originalCallback = callback;
      callback = function() {
        try {
          return originalCallback.apply(this, arguments);
        } catch (e) {
          if (e.toString().includes("Debugger statement")) {
            console.log("setInterval中的debugger被拦截");
            return;
          } else {
            throw e;
          }
        }
      };
    }
    return originalSetInterval(callback, delay);
  };

  window.setTimeout = function(callback, delay) {
     if (typeof callback === 'function') {
      let originalCallback = callback;
      callback = function() {
        try {
          return originalCallback.apply(this, arguments);
        } catch (e) {
          if (e.toString().includes("Debugger statement")) {
            console.log("setTimeout中的debugger被拦截");
            return;
          } else {
            throw e;
          }
        }
      };
    }
    return originalSetTimeout(callback, delay);
  };
})();

// 测试代码
setInterval(function() {
  debugger;
  console.log("这个信息不应该被打印");
}, 100);

setTimeout(function() {
  debugger;
  console.log("这个信息也不应该被打印");
}, 500);

这段代码重写了 setIntervalsetTimeout 函数,在回调函数执行之前,对回调函数进行了包装。如果回调函数中包含 debugger 语句,并且执行时抛出了包含 "Debugger statement" 的错误,则会捕获该错误并阻止 debugger 语句的执行。

二、 console.log 重写:让你眼瞎的反调试

console.log 是我们调试代码时最常用的工具之一。但是,有些反调试会重写 console.log 函数,让你看不到想看的信息,或者输出一些误导性的信息。

1. console.log 重写的反调试原理

反调试者会重写 console.log 函数,使其:

  • 不输出任何信息。
  • 输出一些垃圾信息,干扰你的判断。
  • 检测调试器是否打开,并采取相应的措施。

举个例子:

// 重写 console.log 函数,使其不输出任何信息
console.log = function() {};

// 重写 console.log 函数,输出垃圾信息
console.log = function(message) {
  //生成随机字符串作为干扰
  let randomString = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
  originalConsoleLog("垃圾信息:" + randomString);
};

// 检测调试器是否打开,并采取相应的措施
console.log = function(message) {
  if (/* 检测调试器是否打开 */) {
    // 执行反调试操作
    alert("不要调试我!");
  } else {
    // 正常输出信息
    originalConsoleLog(message);
  }
};

2. 绕过 console.log 重写的方法

  • 保存原始的 console.log 函数: 在代码的开头,保存原始的 console.log 函数,以便在需要的时候使用。

    // 保存原始的 console.log 函数
    var originalConsoleLog = console.log;
    
    // 重写 console.log 函数
    console.log = function(message) {
      // ...
    };
    
    // 使用原始的 console.log 函数
    originalConsoleLog("这是一条重要的信息");
  • 使用其他调试工具: 除了 console.log,浏览器还提供了其他的调试工具,比如 console.dirconsole.table 等。这些工具可能没有被重写,可以使用它们来查看信息。

  • 使用浏览器开发者工具的 "Logpoints": Logpoints 允许你在代码的特定位置记录信息,而无需修改代码本身。这可以绕过一些简单的 console.log 重写。

  • 重写重写后的 console.log: 没错,就是这么套娃。你可以再次重写已经被重写过的 console.log 函数,恢复其原始功能。

    // 假设 console.log 已经被重写
    console.log = function(message) {
      // 一些反调试逻辑
      // ...
    };
    
    // 再次重写 console.log 函数,恢复其原始功能
    (function() {
      var iframe = document.createElement('iframe');
      iframe.style.display = 'none';
      document.body.appendChild(iframe);
      console.log = iframe.contentWindow.console.log;
    })();
    
    // 现在 console.log 应该可以正常工作了
    console.log("这是一条重要的信息");

    这段代码创建了一个隐藏的 iframe 元素,然后将 console.log 函数指向 iframeconsole.log 函数。由于 iframe 中的 console.log 函数没有被重写,因此可以恢复其原始功能。

3. 更高级的 console.log 反调试

有些反调试会使用更高级的技巧,比如检测开发者工具是否打开,并根据情况选择是否重写 console.log 函数。

// 检测开发者工具是否打开
function isDevtoolsOpen() {
  // 一些检测逻辑
  // ...
}

// 重写 console.log 函数
console.log = function(message) {
  if (isDevtoolsOpen()) {
    // 开发者工具已打开,执行反调试操作
    // ...
  } else {
    // 开发者工具未打开,正常输出信息
    originalConsoleLog(message);
  }
};

对于这种反调试,我们需要先绕过开发者工具的检测,然后再重写 console.log 函数。开发者工具的检测方法有很多种,比如检测 window.outerWidthwindow.innerWidth 的差异,或者检测 console.firebug 是否存在。针对不同的检测方法,我们需要采取不同的绕过策略。

三、 时间检测:让你慢下来的反调试

时间检测是一种比较隐蔽的反调试手段。它通过检测代码执行的时间来判断是否被调试。

1. 时间检测的反调试原理

调试器会影响代码的执行速度。当代码在调试器中运行时,执行速度会变慢。反调试者会利用这一点,通过检测代码执行的时间来判断是否被调试。

举个例子:

// 检测代码执行的时间
var startTime = new Date().getTime();

// 执行一些代码
for (let i = 0; i < 1000000; i++) {
  // ...
}

var endTime = new Date().getTime();

// 计算代码执行的时间
var elapsedTime = endTime - startTime;

// 判断是否被调试
if (elapsedTime > 1000) {
  // 代码执行的时间超过了 1000 毫秒,可能被调试
  alert("不要调试我!");
}

这段代码会计算循环执行的时间。如果在调试器中运行,由于调试器的影响,循环执行的时间会变长,超过 1000 毫秒,从而触发反调试操作。

2. 绕过时间检测的方法

  • 禁用断点: 断点会显著影响代码的执行速度,因此禁用断点可以减少时间检测的误差。

  • 提高代码执行速度: 可以通过优化代码来提高代码的执行速度,减少时间检测的误差。比如,可以减少循环的次数,或者使用更高效的算法。

  • 修改时间检测的代码: 如果能够找到时间检测的代码,可以直接修改它,使其失效。比如,可以修改时间阈值,或者直接删除时间检测的代码。

  • 使用性能分析工具: Chrome 开发者工具的 "Performance" 面板可以帮助你分析代码的性能瓶颈,找出导致代码执行速度变慢的原因。通过优化这些瓶颈,可以减少时间检测的误差。

3. 更高级的时间检测

有些反调试会使用更高级的技巧,比如:

  • 使用 performance.now() 函数: performance.now() 函数提供更高精度的时间戳,可以更准确地检测代码执行的时间。

  • 结合多种时间检测方法: 同时使用多种时间检测方法,可以提高检测的准确性。

  • 动态调整时间阈值: 根据代码的运行环境,动态调整时间阈值,可以提高反调试的适应性。

对于这些更高级的时间检测,我们需要更深入地分析代码,找出时间检测的具体实现方式,然后采取相应的绕过策略。

总结

今天我们聊了三种常见的 JS 反调试技术:debugger 语句、console.log 重写和时间检测。这些反调试技术各有特点,但都旨在阻止你调试代码。

反调试技术 原理 绕过方法
debugger 语句 强制启动调试器,频繁断点,干扰调试。 禁用断点,条件断点,Hook debugger 语句,阻止其执行。对于 setIntervalsetTimeout 触发的 debugger,可以 Hook 这两个函数,阻止其执行 debugger
console.log 重写 重写 console.log 函数,使其不输出信息、输出垃圾信息或检测调试器状态。 保存原始的 console.log 函数,使用其他调试工具(console.dirconsole.table),使用 Logpoints,重写重写后的 console.log。对于检测开发者工具状态的反调试,需要先绕过开发者工具检测,再重写 console.log
时间检测 通过检测代码执行的时间来判断是否被调试。调试器会影响代码执行速度,导致时间变长。 禁用断点,提高代码执行速度,修改时间检测的代码(修改阈值或删除代码),使用性能分析工具。对于使用 performance.now()、结合多种时间检测方法或动态调整时间阈值的反调试,需要更深入地分析代码,找出具体实现方式,再采取相应策略。

记住,反调试和反反调试是一场永无止境的猫鼠游戏。新的反调试技术层出不穷,我们需要不断学习,不断提升自己的技能,才能更好地应对这些挑战。

希望今天的讲座对大家有所帮助! 祝大家调试愉快,早日摆脱反调试的困扰!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注