各位老铁,早上好啊!今天咱不聊妹子,聊点硬核的——JS“自卫反击战”,也就是如何写出能抵抗调试、篡改,还能检测虚拟机的JS代码。这玩意儿,江湖人称“Self-Defending”代码。
开玩笑归开玩笑,这东西在实际应用中还是挺重要的,比如:
- 保护知识产权: 防止别人轻易扒走你的核心算法。
- 游戏安全: 阻止外挂作者分析游戏逻辑。
- 数据安全: 确保客户端数据的完整性,防止恶意篡改。
当然,世界上没有绝对的安全,只有相对的安全。咱们今天讲的,也只是提高破解的门槛,增加攻击者的成本。
废话不多说,直接上干货!
第一回合:反调试,让Debug摸不着头脑
反调试,顾名思义,就是阻止别人用开发者工具(比如Chrome DevTools)来调试你的JS代码。咱们的目标是:
- 让调试器卡住: 疯狂循环,耗尽资源。
- 检测调试器是否开启: 一旦发现,立刻采取行动。
- 干扰调试: 让调试器显示错误的信息。
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代码。咱们的目标是:
- 检测代码是否被修改: 一旦发现,立刻采取行动。
- 混淆代码: 增加代码的阅读难度,让攻击者难以理解。
- 代码完整性校验: 确保代码在加载过程中没有被篡改。
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代码是否运行在虚拟机中。咱们的目标是:
- 检测虚拟机特征: 比如CPU数量、内存大小、硬盘序列号等。
- 检测常用虚拟机软件: 比如VMware、VirtualBox等。
- 检测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“自卫反击战”是一个不断升级的攻防游戏。攻击者会不断地寻找新的破解方法,我们需要不断地学习新的反制技术。记住,没有绝对的安全,只有相对的安全。咱们的目标是:
- 提高破解的门槛。
- 增加攻击者的成本。
- 保护我们的代码和数据。
今天的分享就到这里,希望对大家有所帮助! 咱们下期再见!