Files

23 KiB
Raw Permalink Blame History

反调试与环境对抗模式

本文汇总六类反调试规则中的反调试、反检测和环境校验模式。每个章节保留检测特征、补丁代码、证据要求和风险提示。

**基本原则:**先验证,再修改;使用最小补丁;记录影响范围。关于 chrome-devtools-mcp 的能力边界,参阅 capability-boundaries.md。

验证顺序:

  1. 不加补丁,先记录原始现象。
  2. 判断问题类型:debugger/动态代码构造、控制台清理或日志抑制、计时或 Promise 计时、视口/ webdriver / UA / DevTools 检测、导航/关闭/历史记录干扰。
  3. 每次只选择一条最小规则进行验证。
  4. 比较前后差异:请求是否恢复、Hook 是否开始产生日志、调用栈是否可见、页面是否产生新的异常。

验收标准:

  • 补丁后产生可用于后续分析的新证据。
  • 能解释补丁的影响范围和残余风险。
  • 补丁不改变最终生成的 JSRPC / Flask / Burp 代码结构。

禁止行为:

  • 未验证就同时启用多条反调试规则。
  • 大范围伪造环境,只为了提高“看起来能运行”的概率。
  • 将仅用于调试的补丁写入最终生成产物的依赖。

1. Debugger 循环

来源: debugger-loop.md

检测特征

  • 目标请求发送前 DevTools 持续暂停。
  • eval、Function 或 constructor 收到包含 debugger 的源码字符串。
  • 同一调用栈反复出现,始终到不了参数变化位置。

需要记录

  • 触发问题的 API(eval、Function 或 constructor)。
  • 源码提示或调用栈帧。
  • 是否需要伪装 toString。

补丁:绕过动态 debugger

适用于 eval、new Function、Function.prototype.constructor 以及持续触发 debugger 的动态组装代码。

(() => {
  'use strict';

  const tempEval = eval;
  const tempToString = Function.prototype.toString;

  Function.prototype.toString = function () {
    if (this === eval) {
      return 'function eval() { [native code] }';
    } else if (this === Function) {
      return 'function Function() { [native code] }';
    } else if (this === Function.prototype.toString) {
      return 'function toString() { [native code] }';
    } else if (this === Function.prototype.constructor) {
      return 'function Function() { [native code] }';
    }
    return tempToString.apply(this, arguments);
  };

  window.eval = function () {
    if (typeof arguments[0] === 'string') {
      arguments[0] = arguments[0].replaceAll(/debugger/g, '');
    }
    return tempEval(...arguments);
  };

  const OriginalFunction = Function;
  Function = function () {
    for (let i = 0; i < arguments.length; i++) {
      if (typeof arguments[i] === 'string') {
        arguments[i] = arguments[i].replaceAll(/debugger/g, '');
      }
    }
    return OriginalFunction(...arguments);
  };

  Function.prototype = OriginalFunction.prototype;

  Function.prototype.constructor = function () {
    for (let i = 0; i < arguments.length; i++) {
      if (typeof arguments[i] === 'string') {
        arguments[i] = arguments[i].replaceAll(/debugger/g, '');
      }
    }
    return OriginalFunction(...arguments);
  };

  Function.prototype.constructor.prototype = Function.prototype;
})();

补丁后验证

  • 页面不再因 debugger 反复中断。
  • Function.prototype.toString 相关检查不产生新的异常。
  • 主要业务流程仍可正常执行。

风险

  • 该规则会改变全局执行行为,可能触发完整性检查。应尽量缩小影响范围并记录影响面。

2. 控制台检测

来源: console-detect.md

检测特征

  • console.log、console.table 或 console.clear 被覆盖。
  • 触发请求流程后控制台输出消失。
  • 计时检测依赖控制台渲染副作用。

需要记录

  • 受影响的控制台方法。
  • 覆盖操作的源码提示。
  • 恢复控制台后页面行为是否改变。

补丁一:保护 console.log / trace / groupCollapsed / groupEnd(Proxy 方法)

(() => {
  'use strict';

  const readonlyProps = ['log', 'trace', 'groupCollapsed', 'groupEnd'];
  const readonlyConsole = new Proxy(console, {
    set(t, k, v, r) {
      if (readonlyProps.includes(k)) {
        console.groupCollapsed(`%cBlocked overwrite: console.${String(k)}`, 'color: #ff6348;', v);
        console.trace();
        console.groupEnd();
        return true;
      }
      return Reflect.set(t, k, v, r);
    }
  });

  Object.defineProperty(window, 'console', {
    configurable: true,
    enumerable: false,
    get() {
      return readonlyConsole;
    },
    set(v) {
      console.groupCollapsed('%cBlocked overwrite: window.console', 'color: #ff6348;', v);
      console.trace();
      console.groupEnd();
    }
  });
})();

补丁二:阻止 console.clear()

(() => {
  'use strict';
  console.clear = function () {};
})();

补丁三:阻止 console.table()(用于计时或 Getter 诱导检测)

(() => {
  'use strict';
  console.table = function () {};
})();

补丁后验证

  • 仍可使用 console.log 等方法观察运行时数据。
  • 控制台输出不再被清空或干扰。
  • 页面不会因控制台完整性检查产生新的异常。

风险

  • 与执行流 Hook 相比,控制台补丁风险较低,但在加固构建中仍可能触发完整性检查。

3. 计时检测

来源: timer-check.md

检测特征

  • 只有单步调试时流程才中断。
  • 调试状态下 Promise 回调或计时器处理器进入不同分支。
  • performance.now、Date.now 或时间间隔差值决定是否发送请求。

需要记录

  • 使用的计时原语。
  • 观测到的阈值。
  • 绕过后解锁的分支或回调。

补丁:Hook Promise Resolve

适用于快速定位异步回调入口,或确认哪个 Promise resolve 产生了关键参数。

(() => {
  'use strict';

  const OriginalPromise = Promise;

  Promise = function (callback) {
    if (!callback) {
      return new OriginalPromise(callback);
    }
    const originCallback = callback;
    callback = function (resolve, reject) {
      const originResolve = resolve;
      resolve = function (result) {
        if (result && !(result instanceof Promise)) {
          try {
            console.groupCollapsed('[Promise resolve]');
            console.log(result);
            console.trace();
            console.groupEnd();
          } catch (e) {}
        }
        return originResolve.apply(this, arguments);
      };
      return originCallback(resolve, reject);
    };
    return new OriginalPromise(callback);
  };

  Promise.prototype = OriginalPromise.prototype;
  Object.defineProperties(Promise, Object.getOwnPropertyDescriptors(OriginalPromise));
})();

补丁后验证

  • Promise 链仍可正常执行。
  • 可以看到 resolve 参数和调用栈。
  • 页面没有明显性能下降。

风险

  • 时间归一化可能掩盖真实竞态,只应在跟踪期间使用,不得默认静默启用。

4. 环境检测

来源: env-detect.md

检测特征

  • 分支逻辑依赖视口尺寸、DevTools 打开状态、webdriver 标志、UA 或扩展状态。
  • 同一页面在不同浏览器配置下表现不一致。
  • 打开 DevTools 后请求链消失。

需要记录

  • 被检查的属性。
  • 原始值。
  • 替换值。
  • 受影响的源码提示。

补丁:修正窗口尺寸

适用于网站使用 innerHeight / innerWidth 或 outerHeight / outerWidth 判断 DevTools 是否打开的场景。

(() => {
  'use strict';

  const innerHeightValue = 660;
  const innerWidthValue = 1366;
  const outerHeightValue = 760;
  const outerWidthValue = 1400;

  const innerHeightDesc = Object.getOwnPropertyDescriptor(window, 'innerHeight');
  const innerWidthDesc = Object.getOwnPropertyDescriptor(window, 'innerWidth');
  const outerHeightDesc = Object.getOwnPropertyDescriptor(window, 'outerHeight');
  const outerWidthDesc = Object.getOwnPropertyDescriptor(window, 'outerWidth');

  Object.defineProperty(window, 'innerHeight', {
    get() { return innerHeightValue; },
    set() { return innerHeightDesc.set.call(window, innerHeightValue); }
  });

  Object.defineProperty(window, 'innerWidth', {
    get() { return innerWidthValue; },
    set() { return innerWidthDesc.set.call(window, innerWidthValue); }
  });

  Object.defineProperty(window, 'outerHeight', {
    get() { return outerHeightValue; },
    set() { return outerHeightDesc.set.call(window, outerHeightValue); }
  });

  Object.defineProperty(window, 'outerWidth', {
    get() { return outerWidthValue; },
    set() { return outerWidthDesc.set.call(window, outerWidthValue); }
  });
})();

补丁后验证

  • 打开 DevTools 后不再触发尺寸检测。
  • 页面布局没有不可接受的变形。

风险

  • 环境伪造可能影响对正式环境行为的判断,只能用于调查,并且必须明确记录。

5. 代理与导航保护

来源: proxy-guard.md

检测特征

  • window.close、history.back、重定向 Hook 或卸载处理器中断跟踪流程。
  • 只有配置代理后请求才失败。
  • 页面导航后扩展或注入脚本被阻止。

需要记录

  • 保护类型(close、history、redirect、proxy、extension)。
  • 源码提示。
  • 处理后请求重放是否成功。

补丁一:阻止 window.close

(() => {
  'use strict';
  window.close = function () {};
})();

补丁二:阻止 history.go / history.back

(() => {
  'use strict';
  window.history.go = function () {};
  window.history.back = function () {};
})();

补丁三:导航前断点(用于定位源码)

适用于页面即将导航,需要定位导航发生瞬间源码的场景。

(() => {
  'use strict';

  window.onbeforeunload = () => {
    debugger;
    return false;
  };
})();

补丁后验证

  • 页面不再被强制关闭或返回上一页。
  • 导航前可以稳定命中断点。
  • 调试结束后及时移除这些补丁。

风险

  • 导航保护补丁可能改变页面状态。条件允许时,应移除补丁后重新验证请求链。

6. 动态别名(密码库 Hook)

来源: dynamic-alias.md

检测特征

  • 请求参数发生变化,但看不到稳定的全局路径。
  • 多层包装函数逐层转发到真正的加密或签名函数。
  • 缺少 source map,且对象路径每次刷新都会变化。

策略

  • 稳定性较低时,优先使用解析器策略,不要硬编码对象路径。
  • 记录包装链,以及解析到可调用函数所需的最小运行时前置条件。
  • 记录生成的 JSRPC 使用的是解析器还是静态路径。

需要记录

  • 包装链。
  • 解析器触发条件。
  • 解析前需要满足的运行时依赖。

补丁一:Hook CryptoJS

适用于目标网站使用 CryptoJS,需要快速定位 AES / DES / MD5 / SHA / HMAC 参数来源的场景。

(() => {
  'use strict';

  let time = 0;

  function hasEncryptProp(obj) {
    const requiredProps = [
      'ciphertext',
      'key',
      'iv',
      'algorithm',
      'mode',
      'padding',
      'blockSize',
      'formatter'
    ];
    if (!obj || typeof obj !== 'object') return false;
    for (const prop of requiredProps) {
      if (!(prop in obj)) return false;
    }
    return true;
  }

  function hasDecryptProp(obj) {
    const requiredProps = ['sigBytes', 'words'];
    if (!obj || typeof obj !== 'object') return false;
    for (const prop of requiredProps) {
      if (!(prop in obj)) return false;
    }
    return true;
  }

  function getSigBytes(size) {
    switch (size) {
      case 8: return '64bits';
      case 16: return '128bits';
      case 24: return '192bits';
      case 32: return '256bits';
      default: return 'unknown';
    }
  }

  const tempApply = Function.prototype.apply;
  Function.prototype.apply = function () {
    // === 对称加密检测 ===
    if (
      arguments.length === 2 &&
      arguments[0] &&
      arguments[1] &&
      typeof arguments[1] === 'object' &&
      arguments[1].length === 1 &&
      hasEncryptProp(arguments[1][0])
    ) {
      if (Object.hasOwn(arguments[0], '$super') && Object.hasOwn(arguments[1], 'callee')) {
        if (
          this.toString().indexOf('function()') !== -1 ||
          /^\s*function(?:\s*\*)?\s+[A-Za-z_$][\w$]*\s*\([^)]*\)\s*\{/.test(this.toString()) ||
          /^\s*function\s*\(\s*\)\s*\{/.test(this.toString())
        ) {
          console.log(...arguments);

          const encryptText = arguments[0].$super.toString.call(arguments[1][0]);
          if (encryptText !== '[object Object]') {
            console.log('Symmetric ciphertext:', encryptText);
          } else {
            console.log('Symmetric ciphertext: toString unavailable, use the printed object above to call toString manually.');
          }

          const key = arguments[1][0].key.toString();
          if (key !== '[object Object]') {
            console.log('Symmetric Hex key:', key);
          } else {
            console.log('Symmetric Hex key: toString unavailable, use the printed object above to call toString manually.');
          }

          const iv = arguments[1][0].iv;
          if (iv) {
            if (iv.toString() !== '[object Object]') {
              console.log('Symmetric Hex iv:', iv.toString());
            } else {
              console.log('Symmetric Hex iv: toString unavailable, use the printed object above to call toString manually.');
            }
          } else {
            console.log('Symmetric encryption: no iv used');
          }

          if (arguments[1][0].padding) {
            console.log('Padding mode:', arguments[1][0].padding);
          }
          if (arguments[1][0].mode && Object.hasOwn(arguments[1][0].mode, 'Encryptor')) {
            console.log('Block mode:', arguments[1][0].mode.Encryptor.processBlock);
          }
          if (arguments[1][0].key && Object.hasOwn(arguments[1][0].key, 'sigBytes')) {
            console.log('Key length:', getSigBytes(arguments[1][0].key.sigBytes));
          }
          console.log('%c---------------------------------------------------------------------', 'color: green;');
        } else {
          console.groupCollapsed('If the above correctly output the key/iv etc., ignore this message.');
          console.log(...arguments);
          console.log('Symmetric encryption: due to some necessary factors, key/iv etc. were not output. Use the printed object above to call toString manually.');
          console.log('%c---------------------------------------------------------------------', 'color: green;');
          console.groupEnd();
        }
      }
    }
    // === 对称解密检测 ===
    else if (
      arguments.length === 2 &&
      arguments[0] &&
      arguments[1] &&
      typeof arguments[1] === 'object' &&
      arguments[1].length === 3 &&
      hasDecryptProp(arguments[1][1])
    ) {
      if (Object.hasOwn(arguments[0], '$super') && Object.hasOwn(arguments[1], 'callee')) {
        if (this.toString().indexOf('function()') === -1 && arguments[1][0] === 2) {
          console.log(...arguments);

          const key = arguments[1][1].toString();
          if (key !== '[object Object]') {
            console.log('Symmetric decrypt Hex key:', key);
          } else {
            console.log('Symmetric decrypt Hex key: toString unavailable, use the printed object above to call toString manually.');
          }

          if (Object.hasOwn(arguments[1][2], 'iv') && arguments[1][2].iv) {
            const iv2 = arguments[1][2].iv.toString();
            if (iv2 !== '[object Object]') {
              console.log('Symmetric decrypt Hex iv:', iv2);
            } else {
              console.log('Symmetric decrypt Hex iv: toString unavailable, use the printed object above to call toString manually.');
            }
          } else {
            console.log('Symmetric decryption: no iv used');
          }

          if (Object.hasOwn(arguments[1][2], 'padding') && arguments[1][2].padding) {
            console.log('Decrypt padding mode:', arguments[1][2].padding);
          }
          if (Object.hasOwn(arguments[1][2], 'mode') && arguments[1][2].mode) {
            console.log('Decrypt block mode:', arguments[1][2].mode.Encryptor.processBlock);
          }
          if (time === 0) {
            console.log('如需继续确认算法类型,请结合当前页面的实际调用参数和输出长度进行定向验证。');
            time += 1;
          }
          console.log('%c---------------------------------------------------------------------', 'color: green;');
        }
      }
    }
    // === 哈希/HMAC 检测 ===
    else if (
      arguments.length === 2 &&
      arguments[0] &&
      arguments[1] &&
      typeof arguments[0] === 'object' &&
      typeof arguments[1] === 'object'
    ) {
      if (
        arguments[0].__proto__ &&
        Object.hasOwn(arguments[0].__proto__, '$super') &&
        Object.hasOwn(arguments[0].__proto__, '_doFinalize') &&
        arguments[0].__proto__.__proto__ &&
        Object.hasOwn(arguments[0].__proto__.__proto__, 'finalize')
      ) {
        if (arguments[0].__proto__.__proto__.finalize.toString().indexOf('Hash/HMAC') === -1) {
          const tempFinalize = arguments[0].__proto__.__proto__.finalize;
          arguments[0].__proto__.__proto__.finalize = function () {
            if (!Object.hasOwn(this, 'init')) {
              const hash = tempFinalize.call(this, ...arguments);
              console.log('Hash/HMAC raw data:', ...arguments);
              console.log('Hash/HMAC ciphertext:', hash.toString());
              console.log('Hash/HMAC ciphertext length:', hash.toString().length);
              console.log('Note: If HMAC, this script cannot hook the key. Search for it manually.');
              console.log('%c---------------------------------------------------------------------', 'color: green;');
              return hash;
            }
            return tempFinalize.call(this, ...arguments);
          };
        }
      }
    }
    return tempApply.call(this, ...arguments);
  };
})();

补丁二:Hook JSEncrypt RSA

适用于目标网站使用 JSEncrypt,需要直接获取 RSA 公钥、私钥、明文和密文的场景。

(() => {
  'use strict';

  let u, c = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
  function f(t) {
    let e, i, r = '';
    for (e = 0; e + 3 <= t.length; e += 3) {
      i = parseInt(t.substring(e, e + 3), 16);
      r += c.charAt(i >> 6) + c.charAt(63 & i);
    }
    if (e + 1 == t.length) {
      i = parseInt(t.substring(e, e + 1), 16);
      r += c.charAt(i << 2);
    } else if (e + 2 == t.length) {
      i = parseInt(t.substring(e, e + 2), 16);
      r += c.charAt(i >> 2) + c.charAt((3 & i) << 4);
    }
    while ((3 & r.length) > 0) r += '=';
    return r;
  }

  function hasRSAProp(obj) {
    const requiredProps = [
      'constructor',
      'getPrivateBaseKey',
      'getPrivateBaseKeyB64',
      'getPrivateKey',
      'getPublicBaseKey',
      'getPublicBaseKeyB64',
      'getPublicKey',
      'parseKey',
      'parsePropertiesFrom'
    ];
    if (!obj || typeof obj !== 'object') return false;
    for (const prop of requiredProps) {
      if (!(prop in obj)) return false;
    }
    return true;
  }

  const tempCall = Function.prototype.call;
  Function.prototype.call = function () {
    if (
      arguments.length === 1 &&
      arguments[0] &&
      arguments[0].__proto__ &&
      typeof arguments[0].__proto__ === 'object' &&
      hasRSAProp(arguments[0].__proto__)
    ) {
      if (
        '__proto__' in arguments[0].__proto__ &&
        arguments[0].__proto__.__proto__ &&
        Object.hasOwn(arguments[0].__proto__.__proto__, 'encrypt') &&
        Object.hasOwn(arguments[0].__proto__.__proto__, 'decrypt')
      ) {
        if (arguments[0].__proto__.__proto__.encrypt.toString().indexOf('RSA encrypt') === -1) {
          const tempEncrypt = arguments[0].__proto__.__proto__.encrypt;
          arguments[0].__proto__.__proto__.encrypt = function () {
            const encryptText = tempEncrypt.bind(this, ...arguments)();
            console.log('RSA public key:\n', this.getPublicKey());
            console.log('RSA encrypt plaintext:', ...arguments);
            console.log('RSA encrypt Base64 ciphertext:', f(encryptText));
            console.log('%c---------------------------------------------------------------------', 'color: green;');
            return encryptText;
          };
        }

        if (arguments[0].__proto__.__proto__.decrypt.toString().indexOf('RSA decrypt') === -1) {
          const tempDecrypt = arguments[0].__proto__.__proto__.decrypt;
          arguments[0].__proto__.__proto__.decrypt = function () {
            const decryptText = tempDecrypt.bind(this, ...arguments)();
            console.log('RSA private key:\n', this.getPrivateKey());
            console.log('RSA decrypt Base64 input:', f(...arguments));
            console.log('RSA decrypt plaintext:', decryptText);
            console.log('%c---------------------------------------------------------------------', 'color: green;');
            return decryptText;
          };
        }
      }
    }
    return tempCall.bind(this, ...arguments)();
  };
})();

补丁后验证

  • 成功从密码学包装链输出关键参数。
  • 可以定位真实的 CryptoJS 或 JSEncrypt 调用位置。
  • 重写底层 call/apply 后页面功能没有损坏。

风险

  • 重写 call/apply 会影响全局作用域,可能触发加固构建的完整性检查。

7. 模式快速选择

模式 现象 文件
Debugger 循环 反复出现 debugger,或 eval/Function/constructor 被篡改 debugger-loop.md
控制台检测 控制台方法被覆盖、调用 console.clear/console.table、日志被抑制 console-detect.md
计时检测 计时差值检查、Promise 计时、性能探针 timer-check.md
环境检测 视口尺寸、DevTools、webdriver、UA 检查 env-detect.md
代理与导航保护 导航、关闭、历史记录、重定向 Hook 阻止请求重放 proxy-guard.md
动态别名 混淆别名、动态解析器、密码学包装、异步间接调用 dynamic-alias.md

只有在某条规则确实改变调查路径或风险面时,才引用并启用它。