出处:掘金
原作者:前端微白
在开始技术细节前,让我们先看一组令人担忧的数据:
例如,可以使用 CSS 让文本不可选中,或者使用 JS 定期检查 DOM 的改变,以防止调试
CSS 防止文本被选中:
body {
-webkit-user-select: none; /* Chrome, Safari, Opera */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* Internet Explorer/Edge */
user-select: none; /* 非前缀版本,可以在支持的浏览器中使用 */
}
JS 定期检查 DOM 的改变:
const observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
console.log('Detected changes:', mutation);
// 可以在这里添加逻辑来处理检测到的DOM变化
});
});
observer.observe(document.body, {
childList: true, // 监听子元素的变化
attributes: true, // 监听属性的变化
subtree: true // 监听所有后代元素的变化
});
绕过方法:禁用 JS 运行
// 开发者工具状态检测函数
const detectDevTools = () => {
const threshold = 160; // 窗口大小变化的阈值
let lastTime = Date.now();
const interval = 1000
setInterval(() => {
const widthThreshold = window.outerWidth - window.innerWidth;
const heightThreshold = window.outerHeight - window.innerHeight;
if (widthThreshold > threshold || heightThreshold > threshold) {
// 开发者工具可能在侧面或底部打开
handleDevToolsOpen();
}
// 性能检测(开发者工具打开会降低性能)
const currentTime = Date.now();
if (currentTime - lastTime > interval + 100) {
// 控制台打开时 Date.now() 调用会变慢
handleDevToolsOpen();
}
lastTime = currentTime;
}, interval);
};
// 检测到开发者工具时的处理
const handleDevToolsOpen = () => {
// 1. 关闭当前窗口
// window.close();
// 2. 清空整个DOM
// document.documentElement.innerHTML = '';
// 3. 重定向到错误页面
// window.location.replace('https://example.com/debugging-forbidden');
// 4. 显示警告信息
document.body.innerHTML = `
<div class="anti-debug-warning">
<h1>⛔ 安全警告</h1>
<p>此页面禁止调试操作,请关闭开发者工具后刷新页面</p>
<p>如您是本网站合法用户,请<a href="javascript:location.reload()">点击此处</a>重试</p>
</div>
`;
};
// 禁用调试快捷键
const disableShortcuts = (e) => {
const forbiddenKeys = {
'F12': true,
'Ctrl+Shift+I': true,
'Ctrl+U': true,
'Ctrl+S': true // 监听 Ctrl+S 是为了禁止保存至本地,避免被 Overrides
};
if (
forbiddenKeys[e.key] ||
(e.ctrlKey && e.shiftKey && e.key === 'I') ||
(e.ctrlKey && e.key === 'u')
) {
e.preventDefault();
e.stopPropagation();
return false;
}
};
// 初始化检测
window.addEventListener('load', () => {
detectDevTools();
// 阻止键盘快捷键
document.addEventListener('keydown', disableShortcuts);
// 阻止鼠标右键
document.addEventListener('contextmenu', e => e.preventDefault());
});
绕过方法:
使用更完善的库 disable-devtool
该库有以下特性:
一定要使用匿名函数。否则别人直接在控制台重定义函数就完了,比如:function startDebug() {};
(() => {
function ban() {
setInterval(() => {
debugger;
}, 50);
}
try {
ban();
} catch (err) {}
})();
绕过方法:可以通过控制台中的 Deactivate breakpoints 按钮或者使用快捷键 Ctrl + F8 关闭无限 debugger
![[绕过基础版无限 debuger.webp]]
如果将 setInterval 中的代码写在一行,就能禁止用户断点,即使添加 logpoint 为 false 也无用。当然即使有些人想到用左下角的格式化代码,将其变成多行也是没用的
(() => {
function ban() {
setInterval(() => { debugger; }, 50);
}
try {
ban();
} catch (err) { }
})();
绕过方法:通过添加 add script ignore list 需要忽略执行代码行或文件
![[绕过进阶版无限 debuger.webp]]
可以通过将 debugger 改写成 Function("debugger")(); 的形式来应对。Function 构造器生成的 debugger 会在每一次执行时开启一个临时 js 文件。当然使用的时候,为了更加的安全,最好使用加密后的脚本
(() => {
function ban() {
setInterval(() => {
Function('debugger')();
}, 50);
}
try {
ban();
} catch (err) { }
})();
// 更加的晦涩难懂
(() => {
function block() {
setInterval(() => {
(function () {
return false;
}
['constructor']('debugger')
['call']());
}, 50);
}
try {
block();
} catch (err) { }
})();
绕过方法:
Function:Function.prototype.constructor=function(){}
Function = function () {}
使用工具来混淆 JS 代码,可以帮助隐藏代码的真实意图,并使得代码阅读和修改变得更加困难
实际开发中使用专业工具:
在线工具:
编码式 JS 代码加密,将 JS 代码分割、Unicode 编码化存储到数组中,使代码全部成为密文状态显示,执行时解密、重新编码并 eval 调用运行。加密后的代码中存在大量的"u"字符(Unicode 编码),所以命名为“U 加密”。 U 加密后代码可直接运行,使用方式与功能与加密前无异
在线使用:https://www.jshaman.com/tools/u-jiami.html
"U加密"采用独特的双重安全机制:
加密示例:
// JS 源码
(function (){
var domain = "jshaman.com";
var from_year = 2017;
var copyright = function(){
return "(c)" + from_year + "-" + (new Date).getFullYear() + "," + domain;
};
var console_log = console.log;
console_log(copyright())
})();
// 加密后的 JS 代码
var u=[`\u0028\u0066\u0075\u006e\u0063\u0074\u0069\u006f\u006e`,`\u0028\u0029\u007b`,`\u0076\u0061\u0072`,`\u0064\u006f\u006d\u0061\u0069\u006e`,`\u0022\u006a\u0073\u0068\u0061\u006d\u0061\u006e\u002e\u0063\u006f\u006d\u0022\u003b`,`\u0066\u0072\u006f\u006d\u005f\u0079\u0065\u0061\u0072`,`\u0032\u0030\u0031\u0037\u003b`,`\u0063\u006f\u0070\u0079\u0072\u0069\u0067\u0068\u0074`,`\u0066\u0075\u006e\u0063\u0074\u0069\u006f\u006e\u0028\u0029\u007b`,`\u0072\u0065\u0074\u0075\u0072\u006e`,`\u0022\u0028\u0063\u0029\u0022`,`\u0022\u002d\u0022`,`\u0028\u006e\u0065\u0077`,`\u0044\u0061\u0074\u0065\u0029\u002e\u0067\u0065\u0074\u0046\u0075\u006c\u006c\u0059\u0065\u0061\u0072\u0028\u0029`,`\u0022\u002c\u0022`,`\u0064\u006f\u006d\u0061\u0069\u006e\u003b`,`\u0063\u006f\u006e\u0073\u006f\u006c\u0065\u005f\u006c\u006f\u0067`,`\u0063\u006f\u006e\u0073\u006f\u006c\u0065\u002e\u006c\u006f\u0067\u003b`,`\u0063\u006f\u006e\u0073\u006f\u006c\u0065\u005f\u006c\u006f\u0067\u0028\u0063\u006f\u0070\u0079\u0072\u0069\u0067\u0068\u0074\u0028\u0029\u0029`,`\u007d\u0029\u0028\u0029\u003b`];var u2=[0,1,2,3,5,7,9,11,13,14,15,19,21,22,24,26,29,31,32,33];var u3=`u6[0] u6[1]u6[2] u6[3] = u6[5]u6[2] u6[7] = u6[9]u6[2] u6[11] = u6[13]u6[14] u6[15] + u6[7] + u6[19] + u6[21] u6[22] + u6[24] + u6[26]};u6[2] u6[29] = u6[31]u6[32]u6[33]`;for(u5=0; u5<u.length; u5++){u3 = u3.replace(new RegExp("u6\\["+u2[u5]+"\\]","g"), u[u5].replace("`","").replace("`",""));}eval(u3);
适用于需要轻量级保护的场景
"碎片化阵列加密" —— 一种基于词法拆解的 JS 混淆技术
在线使用:https://www.jshaman.com/tools/o0-jiami.html
核心原理:
eval 执行时按特定算法重组数组元素元素技术优势:
加密示例:
// JS 源码
(function (){
var domain = "jshaman.com";
var from_year = 2017;
var copyright = function(){
return "(c)" + from_year + "-" + (new Date).getFullYear() + "," + domain;
};
var console_log = console.log;
console_log(copyright())
})();
// 加密后的 JS 代码
(function(){let e = eval;let x = "(_o_____o_2 (){_o__0__o_____o_0 _o_____o_3 = _0__0__o_____o_4._o_____o_5_0__0_;_o_____o_0 _o_____o_6__o_____o_7 = _o_____o_8;_o_____o_0 _o_____o_9 = _o_____o_2(){_o__0__o_____o_10 _0__0_(_o_____o_11)_0__0_ + _o_____o_6__o_____o_7 + _0__0_-_0__0_ + (_o_____o_12 _o_____o_13)._o_____o_14() + _0__0_,_0__0_ + _o_____o_3;};_o_____o_0 _o_____o_1__o_____o_15 = _o_____o_1._o_____o_15;_o_____o_1__o_____o_15(_o_____o_9())_o__0_})();";let z = "";[["_9__6_","\\"] , ["_o__0_","\n"] , ["_0__0_","\""] , ["_0___o_","'"]].map(function(z){ x = x.replace(RegExp(z[0], "g"), z[1]);});z = "var,console,function,domain,jshaman,com,from,year,2017,copyright,return,c,new,Date,getFullYear,log".split(",");let y = z.length - 1;while(y > -1){ x = x.replace(RegExp("_o_____o_" + y, "g"), z[y]); y--;}e(x);}())
"进制编码加密" —— 一种基于数值进制转换的 JS 关键字混淆技术
在线使用:https://www.jshaman.com/tools/base32-encode.html
加密原理:
eval / alert)每个字母转换为 ASCII 码(数字).toString(32) 将 ASCII 码转为 32 进制字符串符串技术特点:
加密示例:
// JS 源码
eval(alert(1));
// 加密后的 JS 代码(32 进制编码)
eval(
(10).toString(32) + // a
(21).toString(32) + // l
(14).toString(32) + // e
(27).toString(32) + // r
(29).toString(32) // t
+ "(1)"
);
将 HTML 源码转化成 Unicode 编码形式,以此实现源码加密,而功能保持正常
在线使用:https://www.jshaman.com/tools/html-jiami.html
<html>
<head><title>测试</title></head>
<body>
<h1>Html源码加密</h1>
<script>
alert("加密测试");
</script>
</body>
</html>
加密:
<script>
function decodeUnicodeEntities(encodedStr){
return encodedStr.replace(/&#x([0-9a-fA-F]+);/g, function(match, hexValue){
return String.fromCharCode(parseInt(hexValue, 16));
});
}
let encodedString = "<html> <head><title>测试</title></head> <body> <h1>Html源码加密</h1> <script> alert("加密测试"); </script> </body> </html> ";
let decodedString = decodeUnicodeEntities(encodedString);
document.write(decodedString);
</script>
JS 代码中的 JSON 对像,或单独外部 JSON 文件,都可进行加密。加密后的 JSON,可以直接使用,与加密前一样
{
key1: [true, false, null],
//comment
"\u006B\u0065\u0079\u0032": {
"key2Sub": [1, 1.2, 2, "3", 1e10, 1e-3]
},
"key3": false,
"key4": "jshaman.com"
}
加密:
{"\u006B\u0065\u0079\u0031":[!![],![],null],"\u006B\u0065\u0079\u0032":{"\u006B\u0065\u0079\u0032\u0053\u0075\u0062":[456093^456092,1.2,950527^950525,"\u0033",1e10,1e-3]},"\u006B\u0065\u0079\u0033":![],"\u006B\u0065\u0079\u0034":"\u006A\u0073\u0068\u0061\u006D\u0061\u006E\u002E\u0063\u006F\u006D"}
将前端浏览器特有的语法进行标准化、统一化处理
例如:alert、console 转化为 window.alert、window.console
为什么要这样做?
alert(1),如仅对此一句代码加密,由于它会被示例全局顶层函数,出于代码稳定性考虑,通常是无法进行加密的,它不利于混淆加密window.alert(1),如此它便成了 window 的成员函数调用方式,便可进行加密将字串转化为 Unicode 转义序列形式。可用来 eval 等语句结合,实现代码加密、反调试
例如 var key = 123 转化为 Unicode 形式为:
\u0076\u0061\u0072\u0020\u006b\u0065\u0079\u0020\u003d\u0020\u0031\u0032\u0033\u003b\u000a
用 eval 可以执行
将代码进行 Base64 编码后,用 eval、atob 执行
"构造器隐写加密" —— 一种针对 new 操作符的轻量级混淆方案方案
技术实现:
// 原始代码
const now = new Date();
// 加密变体 A(数组索引式)
const now = [Date, Object, RegExp][0]();
// 加密变体 B(逗号表达式式)
const now = (RegExp, Object, Date)();
将关键算法移植到 WebAssembly,前端调用方式:
WebAssembly.instantiateStreaming(fetch('security.wasm'))
.then(obj => {
const protectLogic = obj.instance.exports.protectLogic;
// 调用受保护的逻辑
const result = protectLogic(123, 456);
console.log('安全计算结果:', result);
});
将网页的渲染过程放在服务器端,只返回最终渲染结果给客户端,隐藏源代码和逻辑
对于极其敏感的业务场景,考虑开发定制浏览器或客户端应用作为替代方案,将关键代码从浏览器环境中解放出来
"前端安全的本质是增加攻击成本,而不是追求绝对防御" —— 安全专家