JS `Self-Defending` 代码:反调试、反篡改与检测虚拟机

各位老铁,早上好啊!今天咱不聊妹子,聊点硬核的——JS“自卫反击战”,也就是如何写出能抵抗调试、篡改,还能检测虚拟机的JS代码。这玩意儿,江湖人称“Self-Defending”代码。

开玩笑归开玩笑,这东西在实际应用中还是挺重要的,比如:

  • 保护知识产权: 防止别人轻易扒走你的核心算法。
  • 游戏安全: 阻止外挂作者分析游戏逻辑。
  • 数据安全: 确保客户端数据的完整性,防止恶意篡改。

当然,世界上没有绝对的安全,只有相对的安全。咱们今天讲的,也只是提高破解的门槛,增加攻击者的成本。

废话不多说,直接上干货!

第一回合:反调试,让Debug摸不着头脑

反调试,顾名思义,就是阻止别人用开发者工具(比如Chrome DevTools)来调试你的JS代码。咱们的目标是:

  1. 让调试器卡住: 疯狂循环,耗尽资源。
  2. 检测调试器是否开启: 一旦发现,立刻采取行动。
  3. 干扰调试: 让调试器显示错误的信息。

1.1 无限循环大法

这是最简单粗暴的方法,利用debugger语句,让调试器陷入无限循环。

function antiDebug1() {
  setInterval(function() {
    debugger;
  }, 100);
}

antiDebug1();

这段代码,每隔100毫秒就触发一次debugger语句。如果开发者工具是打开的,它就会不断地暂停在debugger语句处,让你啥也干不了。

缺点: 容易被发现,直接注释掉或者在调试器里跳过即可。

1.2 利用console.log()的特性检测

console.log()在调试器打开时和关闭时的行为略有不同,我们可以利用这一点来检测调试器是否开启。

function antiDebug2() {
  let start = Date.now();
  console.log('%cDebug Check', 'font-size: 50px;');
  let end = Date.now();

  if (end - start > 100) {
    // 调试器打开时,console.log执行时间会变长
    alert("Detected Debugger!");
    // 可以执行一些反制措施,比如重定向页面
    window.location.href = "about:blank";
  } else {
    setTimeout(antiDebug2, 500);
  }
}

antiDebug2();

原理: 当开发者工具打开时,console.log语句的执行时间会变长,因为调试器需要做一些额外的工作(比如格式化输出)。我们通过测量console.log的执行时间,来判断调试器是否开启。

缺点: 不同浏览器、不同版本、不同电脑上,这个时间差可能会有差异,需要根据实际情况调整阈值(这里的100毫秒)。

1.3 利用Error对象检测

当调试器打开时,访问某些属性或者调用某些方法可能会抛出异常。我们可以利用try...catch语句来捕获这些异常,从而判断调试器是否开启。

function antiDebug3() {
  try {
    // 尝试访问一个不存在的属性
    console.log(window.outerWidth - window.innerWidth);
  } catch (e) {
    alert("Detected Debugger!");
    window.location.href = "about:blank";
  }

  setTimeout(antiDebug3, 500);
}

antiDebug3();

原理: 某些浏览器在开发者工具打开时,window.outerWidth - window.innerWidth 会抛出异常,我们可以利用这个特性来检测调试器。

缺点: 兼容性问题比较严重,不同浏览器行为不一致。

1.4 覆盖console方法

我们可以覆盖console对象的方法,让调试器显示错误的信息,或者干脆什么都不显示。

function antiDebug4() {
  console.log = function() {
    // 什么都不做,或者显示错误信息
    // alert("别想调试我!");
  };

  console.error = console.warn = console.info = console.debug = console.log;
}

antiDebug4();

原理: 直接篡改console对象的方法,让调试器失去作用。

缺点: 容易被发现,直接在调试器里恢复console对象即可。

第二回合:反篡改,保证代码原汁原味

反篡改,就是防止别人修改你的JS代码。咱们的目标是:

  1. 检测代码是否被修改: 一旦发现,立刻采取行动。
  2. 混淆代码: 增加代码的阅读难度,让攻击者难以理解。
  3. 代码完整性校验: 确保代码在加载过程中没有被篡改。

2.1 利用Hash值校验

我们可以计算JS代码的Hash值(比如MD5、SHA256),然后将这个Hash值存储在一个安全的地方(比如服务器端)。在代码执行前,重新计算Hash值,并与存储的Hash值进行比较,如果不一样,说明代码被篡改了。

// 假设这是你的JS代码
const originalCode = `
  function add(a, b) {
    return a + b;
  }
  console.log(add(1, 2));
`;

// 计算代码的SHA256 Hash值 (需要引入一个SHA256库,比如crypto-js)
function calculateSHA256(code) {
  return CryptoJS.SHA256(code).toString();
}

// 存储的Hash值 (从服务器端获取)
const storedHash = "你的代码的SHA256 Hash值"; // 替换成实际的值

// 在代码执行前进行校验
function checkCodeIntegrity() {
  const currentHash = calculateSHA256(originalCode);
  if (currentHash !== storedHash) {
    alert("代码被篡改!");
    // 可以执行一些反制措施,比如重定向页面
    window.location.href = "about:blank";
  } else {
    // 代码没有被篡改,继续执行
    eval(originalCode);
  }
}

checkCodeIntegrity();

原理: Hash算法具有唯一性,只要代码被修改,Hash值就会发生变化。

缺点:

  • 需要引入额外的Hash库(比如crypto-js),增加代码体积。
  • 攻击者可以修改storedHash变量,绕过校验。
  • 代码本身暴露在客户端,容易被分析。

改进:

  • 将Hash值存储在服务器端,每次加载页面时从服务器端获取。
  • 对代码进行混淆,增加攻击者的分析难度。

2.2 代码混淆

代码混淆,就是将代码变得难以阅读和理解,从而增加攻击者的分析难度。常见的混淆方法包括:

  • 变量名混淆: 将变量名、函数名替换成无意义的字符串。
  • 控制流混淆: 改变代码的执行流程,使其变得复杂。
  • 字符串加密: 将字符串进行加密,防止直接查看代码内容。
  • 死代码插入: 插入一些无用的代码,干扰攻击者的分析。

有很多在线的JS混淆工具可以使用,比如javascriptobfuscator.com

示例:

原始代码:

function calculateArea(width, height) {
  return width * height;
}

console.log(calculateArea(10, 20));

混淆后的代码:

var _0x4c91 = ['log', 'calculateArea', '200', '10', 'width', 'height', 'returnx20widthx20*x20height;', 'console'];
(function(_0x2a773b, _0x4c911b) {
  var _0x2a77 = function(_0x4062c1) {
    while (--_0x4062c1) {
      _0x2a773b['push'](_0x2a773b['shift']());
    }
  };
  _0x2a77(++_0x4c911b);
}(_0x4c91, 0x133));
var _0x2a77 = function(_0x4062c1, _0x4c911b) {
  _0x4062c1 = _0x4062c1 - 0x0;
  var _0x2a773b = _0x4c91[_0x4062c1];
  return _0x2a773b;
};

function calculateArea(_0x58d04f, _0x14c973) {
  return _0x58d04f * _0x14c973;
}
console[_0x2a77('0x0')](calculateArea(0xa, 0x14));

原理: 通过各种手段,让代码变得难以阅读和理解。

缺点:

  • 混淆后的代码体积会变大。
  • 混淆后的代码性能可能会下降。
  • 高级攻击者仍然可以通过逆向工程来分析代码。

2.3 代码完整性校验(Subresource Integrity,SRI)

SRI是一种浏览器安全特性,允许浏览器验证从CDN或其他第三方服务器加载的资源是否被篡改。

<script src="https://example.com/your-script.js"
        integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
        crossorigin="anonymous"></script>

原理: 浏览器会计算下载资源的Hash值,并与integrity属性中的Hash值进行比较,如果不一样,浏览器会拒绝执行该资源。

优点:

  • 简单易用,只需要在HTML中添加integrity属性即可。
  • 由浏览器自动完成校验,无需手动编写代码。

缺点:

  • 只能用于外部资源,不能用于内联JS代码。
  • 需要事先计算资源的Hash值,并存储在integrity属性中。

第三回合:反虚拟机,识别恶意环境

反虚拟机,就是检测JS代码是否运行在虚拟机中。咱们的目标是:

  1. 检测虚拟机特征: 比如CPU数量、内存大小、硬盘序列号等。
  2. 检测常用虚拟机软件: 比如VMware、VirtualBox等。
  3. 检测Hook工具: 比如Frida等。

3.1 检测CPU数量

虚拟机通常会限制CPU数量,我们可以通过navigator.hardwareConcurrency属性来获取CPU数量,并判断是否小于某个阈值。

function detectVM1() {
  const cpuCount = navigator.hardwareConcurrency;
  if (cpuCount <= 2) {
    alert("可能运行在虚拟机中!");
    // 可以执行一些反制措施
  }
}

detectVM1();

原理: 虚拟机通常会限制CPU数量,我们可以通过navigator.hardwareConcurrency属性来判断。

缺点: 用户也可以手动限制CPU数量,导致误判。

3.2 检测内存大小

某些浏览器提供了navigator.deviceMemory属性,可以获取设备的内存大小。虚拟机通常会限制内存大小,我们可以通过这个属性来判断是否运行在虚拟机中。

function detectVM2() {
  if (navigator.deviceMemory <= 4) {
    alert("可能运行在虚拟机中!");
    // 可以执行一些反制措施
  }
}

detectVM2();

原理: 虚拟机通常会限制内存大小,我们可以通过navigator.deviceMemory属性来判断。

缺点:

  • navigator.deviceMemory属性的兼容性不好,只有部分浏览器支持。
  • 用户也可以手动限制内存大小,导致误判。

3.3 检测特定文件或进程

我们可以尝试访问一些只有在虚拟机中才存在的文件或进程,如果访问成功,说明代码运行在虚拟机中。

注意: 这种方法需要用到一些浏览器不提供的API,需要借助一些插件或者Native代码来实现。

3.4 检测Hook工具

Hook工具(比如Frida)可以用来修改JS代码的执行流程,我们可以检测是否存在这些工具,从而判断代码是否运行在恶意环境中。

原理: Hook工具通常会在内存中留下一些痕迹,我们可以通过扫描内存来检测这些痕迹。

注意: 这种方法比较复杂,需要用到一些底层的API,需要借助一些插件或者Native代码来实现。

总结:

反制手段 原理 优点 缺点
无限循环 利用debugger语句让调试器陷入无限循环 简单粗暴 容易被发现,直接注释掉即可
console.log()检测 console.log()在调试器打开时和关闭时的行为略有不同 可以检测调试器是否开启 不同浏览器、不同版本、不同电脑上,时间差可能会有差异
Error对象检测 某些情况下,开发者工具打开时会抛出异常 可以检测调试器是否开启 兼容性问题比较严重,不同浏览器行为不一致
覆盖console方法 篡改console对象的方法,让调试器失去作用 可以让调试器显示错误的信息或者什么都不显示 容易被发现,直接在调试器里恢复console对象即可
Hash值校验 计算代码的Hash值,并与存储的Hash值进行比较 可以检测代码是否被篡改 需要引入额外的Hash库,攻击者可以修改storedHash变量,代码本身暴露在客户端
代码混淆 将代码变得难以阅读和理解 增加攻击者的分析难度 混淆后的代码体积会变大,混淆后的代码性能可能会下降,高级攻击者仍然可以通过逆向工程来分析代码
SRI 浏览器验证从CDN或其他第三方服务器加载的资源是否被篡改 简单易用,由浏览器自动完成校验 只能用于外部资源,需要事先计算资源的Hash值,并存储在integrity属性中
检测CPU数量 虚拟机通常会限制CPU数量 可以检测虚拟机环境 用户也可以手动限制CPU数量,导致误判
检测内存大小 虚拟机通常会限制内存大小 可以检测虚拟机环境 navigator.deviceMemory属性的兼容性不好,用户也可以手动限制内存大小,导致误判
检测特定文件/进程 尝试访问只有在虚拟机中才存在的文件或进程 可以检测虚拟机环境 需要用到一些浏览器不提供的API,需要借助一些插件或者Native代码来实现
检测Hook工具 检测是否存在Hook工具(比如Frida) 可以检测恶意环境 比较复杂,需要用到一些底层的API,需要借助一些插件或者Native代码来实现

最后:

JS“自卫反击战”是一个不断升级的攻防游戏。攻击者会不断地寻找新的破解方法,我们需要不断地学习新的反制技术。记住,没有绝对的安全,只有相对的安全。咱们的目标是:

  • 提高破解的门槛。
  • 增加攻击者的成本。
  • 保护我们的代码和数据。

今天的分享就到这里,希望对大家有所帮助! 咱们下期再见!

发表回复

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