JS 混淆与编码

变量改名、JSFuck、AAEncode、JJEncode 都是「换一身皮」:语义完全没变,只是难读了。这里把四种形式变换放在一起,附一个把编码解回源码字符串的还原框。

只换标识符的名字,字符串、注释、属性名和内置全局都原样保留;结果仍是一份语义相同的代码。

输入 181 个字符,变量名混淆。所有处理都在本机浏览器里完成,代码不会上传到任何服务器。

使用说明

变量名混淆做的是形式变换,不改变语义。它先把源码切成一段段 token——字符串字面量、模板字符串、正则字面量、注释、标识符各自成类——再只对标识符那一类做替换,同时跳过 JavaScript 关键字、语言内置的名字(window、document、console、Math、JSON 这些)和点号访问的属性名。之所以用分词扫描而不是全局正则替换,是因为正则分不清「一个名字是标识符还是字符串里的文字」:代码里只要有一句 "price 是单价" 这样的中文说明,全局替换就会把字符串里的 price 也改掉。模板字符串要单独处理,只有 ${} 里的表达式参与改名,外面的文字保持原样。改完之后如果代码里有顶层声明,页面上会额外提醒一句:别的脚本或 HTML 事件属性按原名调用时就会找不到它。这里要说清它和压缩、美化的区别:压缩删掉注释与多余空白让代码变小,美化重排缩进与换行让代码好读,两者都不动名字;混淆正相反,只改名字、不删任何东西,目标既不是更小也不是更好读,而是更难读,所以它和压缩是两件可以各自独立做的事。要压缩体积或把代码重新排版,配合JS 压缩与美化一起用即可。

JSFuck 的思路完全不同:它不换名字,而是把整段源码当成字符串,用一个只用六个字符 []()!+ 写成的表达式现场拼出来,再交给 Function 编译执行。拼的过程是:用 ![] 和 !![] 拿到布尔值再转成字符串 "false"、"true",从里面按下标抠出 f、a、l、s、e 这些字母;用 [][[]] 拿 undefined 补上 d、i、n、u;数组和下标取到 Array.prototype.filter,再从它身上取 constructor,就得到了 Function 构造函数;剩下拼不出来的字符,先取到内置的 escape 或 Number.prototype.toString,用十六进制转义或进制转换把任意字符算出来。输出的形状是「函数构造器(源码字符串)()」,整段只由那六个字符组成,不含字母、数字、引号、花括号,粘到控制台可以直接执行——这是真正的 JSFuck,而不是把代码转成字符串再 Base64 那种假编码。

AAEncode 和 JJEncode 属于同一类:源码被写成字符串,交给一个只由特定字符集写成的骨架,骨架自己拼出源码字符串再调用 Function。AAEncode 用的是日式颜文字,靠一个正则给变量铺出 undefined、靠几个颜文字算出数字 3,再由 3 异或出别的数字,把源码拆成八进制和十六进制转义拼起来;JJEncode 用 $ 和下划线等少量 ASCII 字符,通过给全局对象挂一串属性、再用下划线做取值的语法糖把字符串拼出来。两者都不改变语义,也都不带任何保密性:只要知道是哪一种编码,把结果交给对应的解码器就能拿回原文。

体积是这类编码绕不过去的代价,页面上会给实测倍率。JSFuck 最夸张,因为它没有变量可以复用,字符串里的每个字符都要把那一长串表达式完整重写一遍,短片段大约一百多倍,几十个字符的代码就能到上千倍,所以输入超过一千个字符时这里会直接拒绝,避免生成一个兆字节级的文本框。AAEncode 大约六十到八十倍,极短片段会到一百多倍,中文这类非 ASCII 字符要先转成 %uXXXX 转义,会再长一截。JJEncode 最紧凑,大致二十到七十倍。需要说明的是网上常说的「四十到七十倍」只适用于极短的片段,实现细节不同倍率差别很大,以页面上算出来的为准。

页面上的还原框只做「尽力而为」的解码,并且刻意设了三道限制。第一道是字符集白名单:JSFuck 只允许 []()!+,AAEncode 只允许颜文字骨架用到的那批字符,JJEncode 只允许 $ 与下划线等少量 ASCII;开头识别出种类、后面却混进了白名单之外的字符,就直接拒绝解析,因为那多半已经不是编码而是真代码了。第二道是无论成败都还原被替换掉的原型:解码靠临时换掉 Function.prototype.constructor、用它接住源码字符串,所以最后必须在 finally 里换回来,否则会污染这个页面后续所有构造函数的调用。第三道是只截字符串、不执行源码:白名单和假构造器都只是把门关小,浏览器里并没有能拦住恶意代码的真沙箱,所以这一页的定位始终是「解自己确认来源的代码」,而不是「安全的在线运行器」。想先把一段代码的结构看清楚,可以配合代码高亮;改名时常踩的正则边界,用正则测试可以先验证一遍。

为什么这些编码没有安全价值

混淆的对手是「读代码的人」,不是「解密的人」。变量改名之后,压缩器、格式化工具、美化插件都能把结构和调用关系还原出来;JSFuck、AAEncode、JJEncode 更彻底一些,因为它们真的在跑之前要把源码字符串交给 Function,所以任何解码器只要替换掉 Function 构造函数就能老老实实把原文交出来——本页的还原框就是干这个的。换句话说,它们能挡住不熟悉的人几分钟,挡不住任何一个写好脚本的人。

所以别把它们当安全边界:密钥、接口地址、签名算法放在前端代码里,无论怎么混淆都守不住,浏览器必须能解开才能用,那用户也能解开;真要保密就把逻辑放服务端。本页的还原功能也只做字符集白名单内的编码还原,混进别的代码直接拒绝,并且全程不执行里面的代码——但即便如此也不要拿它去跑来路不明的内容,任何在线工具都不该被用来运行不信任的代码。想在传输层做真正的保密,用AES 加解密那类方案,或者干脆走 HTTPS 让传输本身就被加密。

常见问题

混淆算加密吗?能防抄袭吗?

都不算。这里四种做法都只改变代码的写法,运行语义一个字没变,压缩、美化、格式化这些工具一轮下去就恢复可读;字符集编码更是连解码器都不用,把结果粘到本页的还原框就能拿回原文。真正的加密要能保密,而混淆只是让人多花时间读,所以别把它当安全措施,也别用它保护密钥或接口地址——那些东西一旦下发给浏览器就守不住。

相关:AES 加解密

变量改名会不会把字符串和注释里的名字一起改掉?

不会。改名走的是分词扫描,先切出字符串字面量、模板字符串、正则字面量、注释和标识符,只对标识符那一类做替换。所以 "price 出现在字符串里" 这句话原样保留,注释里的名字也原样保留;模板字符串里只有 ${} 里的表达式参与改名,其余文字不动。用正则全局替换就会在这里出错,因为字符串里出现的名字会被误伤。

相关:JS 压缩与美化

为什么我的对象属性名没被改,别人的却被改了?

点号访问的名字按约定都当成属性名保留,因为改掉 this.owner 或 document.querySelector 几乎没有收益:前者改了还要同步改所有调用点,后者根本不能改。真正会被改的是变量名和函数名。反过来,用字符串下标访问的属性(obj["owner"])本来就在字符串里,同样不会被动。还有一条保守规则:某个名字只要在字符串、正则或模板串的正文里出现过,也会被自动冻结——因为动态访问(obj[name]、eval 拼字符串)从代码上看不出来,宁可少改也不要改坏。想确认某个名字一定不动,把它写进「自定义保留标识符」就行。

顶层声明的名字为什么有的改了、有的没改?

带 import、export 的文件按模块处理,顶层名字可能被别的模块按原名引用,所以默认一个都不改,只有函数内部的名字参与重命名。普通脚本没有这个约束,顶层名字可以改,但页面上会给出提示:如果 HTML 事件属性或另一个脚本按原名调用它,改名之后就会找不到。想强制一起改,勾上「连模块绑定一起改」,但要自己确认整个项目的引用都跟着改。

相关:HTML 与 CSS 压缩

JSFuck 的体积为什么会涨几百倍?

因为 JS 里没有任何可用字符时,每个字符都要用 []()!+ 现场拼出来:字母要从 false、true、undefined 这些字符串里按下标抠,冷门字符还得先拼出 escape 或 Number.prototype.toString,再算出一个十六进制转义序列。一个字符的表达式动辄几百到上千字符,所以短片段差不多一百多倍,几十个字符的代码就能到上千倍。这是 JSFuck 这种写法的固有代价,不是实现写歪了。

相关:Base64 编码解码

生成的 JSFuck 真的能跑吗?

能。结果是形如「函数构造器(源码字符串)()」的表达式:用 [] 取到 Array.prototype.filter,再从它身上取 constructor 拿到 Function 构造函数,把源码当函数体编译出来再立刻调用。整个过程只用 []()!+ 六个字符,输出里不含字母、数字、花括号和引号,整段粘到浏览器控制台里可以直接执行。需要说明的是它依赖 escape 这个老内置函数和 Number.prototype.toString 的规范输出,主流引擎都支持。

相关:Unicode 字符查询

AAEncode 和 JJEncode 有什么区别,该用哪个?

两者的思路一样,都是把源码拼成字符串再交给 Function 执行,差别在用的字符集和体积。AAEncode 用日式颜文字(゚ω゚ノ 那一类),可读性最差、字符集最怪,体积通常在源码的六十到八十倍之间,极短片段还会更高;JJEncode 只用 $ 和下划线等少量 ASCII 字符,体积只在几十倍上下,但结果看起来最像正常代码,也最容易被自动格式化工具认出来。想放进只允许少量字符的环境,或者只想让体积小一点,选 JJEncode。

还原功能是怎么把编码解开的?在页面上跑不认识的代码安全吗?

三种编码的骨架最后都要调用 Function 把源码字符串编译出来,所以还原的做法是临时换掉 Function.prototype.constructor,用一个只会记录参数、返回空函数的假构造器接住那次调用,源码字符串在真正执行之前就被截下来。即便如此它仍然不是安全边界:本页只接受字符集完全落在骨架白名单里的内容,混进其他字符就直接拒绝解析,并且无论成败都会还原原型。请只用来解自己确认来源的代码,永远不要把来路不明的代码交给任何在线工具运行。

相关:代码高亮