JSFuck解密实战:从原理到工具,安全高效还原混淆代码
1. 从一个“天书”般的弹窗说起
那天下午,我正在调试一个前端项目,控制台突然弹出了一段极其诡异的代码:
[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]][([][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]])[+!+[]+[+[]]]+([][[]]+[])[+!+[]]+(![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[+!+[]]+([][[]]+[])[+[]]+([][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]])[+!+[]+[+[]]]+(!![]+[])[+!+[]]]((![]+[])[+!+[]]+(![]+[])[!+[]+!+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]+(!![]+[])[+[]]+(![]+[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]])[!+[]+!+[]+[+[]]]+[+!+[]]+(!![]+[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]])[!+[]+!+[]+[+[]]])()我的第一反应是:代码被混淆了,还是遇到了什么奇怪的攻击?但仔细一看,这段代码里只有六个字符:[、]、(、)、!、+。这立刻让我想到了一个在安全圈和前端圈都颇有名气的“奇技淫巧”——JSFuck。JSFuck是一种极端的JavaScript混淆技术,它仅用这六个字符就能编码出任何有效的JavaScript代码。对于安全分析、代码审计或者像我这样单纯好奇的程序员来说,遇到这种“天书”代码,解密就成了首要任务。这篇文章,我就结合自己多次处理JSFuck代码的经验,详细拆解两种最实用、最高效的解密方法:动态执行法和静态分析法,并深入探讨它们背后的原理、适用场景以及那些官方文档里不会写的实操细节与避坑指南。
2. 理解JSFuck:它如何用六个字符构建世界
在动手解密之前,我们必须先理解JSFuck的编码原理。这不仅仅是解密的前提,更能帮助我们在遇到变种或复杂情况时,具备独立分析和解决问题的能力。JSFuck的核心思想,是利用JavaScript语言中类型转换和运算符的“怪癖”,从最基本的false、true、undefined、NaN、[object Object]等值中,“提取”出构成任意字符串所需的每一个字符。
2.1 基础构建块:从布尔值到字符串
一切始于两个最简单的值:
![]的结果是false。!![]的结果是true。
通过+运算符和数组[],我们可以将它们转换为字符串:
(![]+[])等于"false"。(!![]+[])等于"true"。
现在,我们有了两个字符串:“false”和“true”。字符串可以通过下标([index])访问单个字符。例如:
"false"[0]是"f"。"true"[0]是"t"。
但在JSFuck的语法里,数字下标也需要用那六个字符构造出来。这就需要用到类型转换和数字的构造。
2.2 构造数字:类型转换的魔法
在JavaScript中,+运算符在涉及不同类型时,会触发类型转换。+[]会将空数组转换为数字0,因为空数组在转换为字符串时为"",而""转换为数字是0。所以:
+[]等于0。
有了0,我们可以通过!(逻辑非)和+(一元正或加法)来构造其他数字。!0是true,+true是1。但更常用的方法是利用+运算符的字符串连接特性:
+!+[]:+[]是0,!0是true,+true是1。所以+!+[]等于1。- 要得到
2,可以构造1+1。但1需要用JSFuck表示,即+!+[]。所以2是:+!+[] + +!+[]。但直接相加会得到数字2,我们需要的是字符串"2"来作为下标。更优雅的方式是利用数组和类型转换:[+!+[]]是一个包含数字1的数组,[+!+[]]+[]会得到字符串"1"。然后[+!+[]]+[+!+[]]进行字符串连接得到"11",取其长度[+!+[]]+[+!+[]][+[]]…… 这个过程开始变得复杂。实际上,JSFuck通常通过预定义的数字字符来构造其他数字下标,例如从"false"中取"a"(下标2),从"true"中取"r"(下标1)等,再组合成"10"、"11"这样的字符串,最后用+()或-[]等方式转换为数字。
一个更直接构造小数字的例子:!+[]+!+[],!+[]是true,true+true在数值运算中会被转换为1+1=2。但同样,需要确保它在正确的上下文中被求值为数字。
2.3 构造字符与函数调用
通过组合"false"、"true"、"undefined"(可通过[][[]]得到)、"[object Object]"(可通过[]+{}得到)等字符串,并利用构造出的数字下标,我们可以提取出几乎所有的字母、数字和符号。
例如,构造字母"a":
"false"[1]是"a"。在JSFuck中,1是+!+[],所以(![]+[])[+!+[]]就得到了"a"。
构造出字符后,将它们拼接起来就能得到任意字符串。而要执行代码,最关键的一步是构造函数调用。JSFuck通常利用[]["filter"]或[]["constructor"]这种方式来访问Array.prototype.filter或Function构造函数,再将其转换为字符串,从中提取出"constructor"这个单词的每一个字母,最终拼出"constructor"。有了"constructor",就可以通过[]["constructor"]得到Array构造函数,再通过[]["constructor"]["constructor"]得到顶层的Function构造函数。
最后,通过Function构造函数,将我们拼接好的表示代码的字符串传入,动态创建一个函数并立即执行,就实现了任意JS代码的运行。文章开头那段“天书”,其核心结构通常是[]["filter"]["constructor"]("alert(1)")()的JSFuck编码形式。
理解了这套“从无到有”的构建体系,我们就能明白,任何JSFuck代码本质上都是一个巨大的表达式,它最终会求值并执行一段隐藏的JavaScript代码。我们的解密目标,就是安全、准确地得到那段被隐藏的原始代码。
3. 方法一:动态执行解密法——让浏览器自己“坦白”
这是最直接、最快捷的解密方法,其核心思想是:既然JSFuck代码是有效的JavaScript,那么直接把它扔给JavaScript引擎(比如浏览器)去执行,然后捕获其输出或行为,不就知道它是什么了吗?
3.1 浏览器的开发者工具(Console)
这是我最推荐初学者使用的方法,简单暴力且直观。
操作步骤:
- 打开浏览器的开发者工具(F12),切换到Console(控制台)标签页。
- 将完整的JSFuck代码粘贴到控制台中。
- 按下回车键执行。
可能的结果与处理:
- 直接输出结果:如果JSFuck代码是类似
console.log("hello")的编码,那么控制台会直接打印出hello。 - 弹窗或页面变化:如果代码是
alert("pwned")或修改了DOM,你会看到弹窗或页面内容改变。这里要极度小心:在不确定代码用途前,切勿在重要网站或包含敏感信息的浏览器环境中执行未知JSFuck代码。最好使用一个全新的、无痕模式的浏览器窗口。 - 返回一个函数或值:有时代码执行后可能返回一个函数。你可以尝试在它后面加上
toString()来查看函数体,或者直接调用它(如果它是个函数)。例如,执行后得到一个函数ƒ anonymous() { alert(1) },你可以再输入(<这里显示的函数名>).toString()查看其源码。
注意:安全第一!动态执行法最大的风险在于你完全不知道这段代码会做什么。它可能是无害的
alert(1),也可能是窃取Cookie、发起恶意请求、进行加密挖矿(挖矿脚本)甚至利用浏览器漏洞的恶意代码。绝对不要在生产环境、登录了重要账户的浏览器中执行未知来源的JSFuck代码。务必在隔离的虚拟环境、无痕模式或专门的检测沙箱中进行。
3.2 利用Node.js环境执行
对于不涉及浏览器特定对象(如document,window,alert)的纯逻辑JSFuck代码,或者你想在命令行中自动化处理,Node.js是个好选择。
操作步骤:
- 创建一个新的JavaScript文件,例如
decode.js。 - 将JSFuck代码赋值给一个变量,或者直接使用
eval(谨慎!)。更安全的方式是利用vm模块创建一个隔离的沙箱环境。// decode.js - 相对安全的方式 const vm = require('vm'); const jsfuckCode = `[][(![]+[])[+[]]+...很长的一段代码...]()`; try { const script = new vm.Script(jsfuckCode); // 创建一个受限的上下文,可以限制访问权限 const context = vm.createContext({ console: console, // 允许输出 // 可以按需暴露其他安全对象 }); const result = script.runInContext(context); console.log('执行结果:', result); } catch (error) { console.error('执行出错:', error.message); } - 在终端中运行
node decode.js。
为什么用vm模块而不是直接eval?直接使用eval或Function构造函数执行未知代码,在Node.js中同样危险,恶意代码可以访问文件系统(fs)、网络(http)、环境变量等。vm模块的runInContext或runInNewContext可以提供一个隔离的沙箱环境,你可以控制暴露哪些全局对象给这段代码,极大地提高了安全性。例如,你可以不暴露require、process、__dirname等。
动态执行法的优缺点:
- 优点:速度快,几乎无需思考,对于大多数标准JSFuck编码的代码立竿见影。
- 缺点:
- 安全风险高:这是最大的弊端,你必须承担执行未知代码的风险。
- 无法处理反调试代码:有些经过混淆或加固的JSFuck代码会包含反调试逻辑,比如检测
console、debugger关键字,或者试图干扰开发者工具,导致执行失败或无法看到清晰结果。 - 对于非执行类代码不直观:如果JSFuck编码的是一大段函数定义或一个复杂对象,直接执行可能只返回一个函数引用,你需要进一步操作(如
.toString())才能看到源码,不够直接。
4. 方法二:静态分析解密法——像侦探一样拆解
当动态执行法因安全顾虑或代码本身的反制措施而失效时,静态分析就成了必由之路。这种方法不执行代码,而是像解谜一样,一步步分析JSFuck表达式的结构,将其还原成可读的JavaScript。这需要更多的耐心和对JSFuck原理的理解。
4.1 手动分步求值(在控制台中)
这是最基础的静态分析,适合较短或结构清晰的JSFuck代码。我们利用浏览器控制台的交互式特性,对表达式进行逐层求值。
操作步骤:我们以构造字母"a"的简单例子(![]+[])[+!+[]]来演示:
从内到外,逐层分解:
- 先看最内层:
![]求值为false。 (![]+[])即false + []。false转字符串为"false",[]转字符串为"",字符串连接后得到"false"。- 再看下标部分:
+!+[]。+[]是0,!0是true,+true是1。所以下标是1。 - 最终,
"false"[1]得到字符"a"。
- 先看最内层:
对于长代码:将一大段JSFuck代码复制到编辑器中,寻找重复的模式或括号匹配,将其拆分成多个子表达式。然后,在浏览器控制台里,从最小的、可独立求值的子表达式开始,逐步替换。
- 例如,发现一段频繁出现的
(![]+[])[+!+[]],你可以在控制台先执行它,得到结果"a"。 - 然后,在原始代码中,用
"a"替换所有(![]+[])[+!+[]](注意字符串引号)。这样,代码长度和复杂度就大大降低了。 - 重复这个过程,逐步将复杂的JSFuck结构替换为简单的字符串、数字或函数引用。
- 例如,发现一段频繁出现的
这个过程非常耗时,但对于理解JSFuck的构造逻辑和应对变种混淆非常有帮助。
4.2 使用专门的解密工具
手动分析对于长代码是不现实的。幸运的是,开源社区已经有一些优秀的JSFuck解密工具(或称为“反混淆器”)。
推荐工具及使用方式:
在线解密网站:
- 搜索 “jsfuck decoder” 或 “jsfuck deobfuscator” 可以找到很多在线工具。你只需将JSFuck代码粘贴进去,点击按钮即可得到解密结果。
- 优点:极其方便,无需安装。
- 缺点:存在安全风险。你无法保证这些网站不会记录你提交的代码(其中可能包含敏感信息)。对于来源不明的JSFuck代码(尤其是从可疑网站或邮件中获取的),切勿使用在线工具解密,以防源码泄露。
本地命令行工具:
- 例如,可以通过Node.js的npm安装一些本地包。虽然专门针对JSFuck的成熟工具不多,但一些通用的JavaScript反混淆工具(如
js-beautify)有时也能提供格式化帮助,而更高级的工具如javascript-deobfuscator可能内置了对JSFuck等特定混淆模式的支持。 - 使用示例(假设有名为
jsfuck-decode的cli工具):npm install -g jsfuck-decode # 假设的包名 jsfuck-decode -i obfuscated.js -o decoded.js - 优点:代码不离本地,安全性高,可集成到自动化流程中。
- 缺点:需要寻找、安装和信任特定的开源工具。
- 例如,可以通过Node.js的npm安装一些本地包。虽然专门针对JSFuck的成熟工具不多,但一些通用的JavaScript反混淆工具(如
集成到代码编辑器/IDE的插件:
- 有些编辑器插件支持简单的反混淆或代码格式化,可能对解读JSFuck有帮助。
静态分析法的优缺点:
- 优点:
- 绝对安全:不执行代码,零风险。
- 可应对反调试:无论代码里埋了多少检测
debugger或干扰执行的陷阱,静态分析都不受影响。 - 深入理解:通过分析过程,你能彻底看清混淆的每一层,对于学习JavaScript语言特性和代码混淆技术非常有价值。
- 缺点:
- 速度慢:尤其是手动分析,极其耗时耗力。
- 工具依赖:自动化解密工具可能无法处理非标准或自定义修改过的JSFuck变种。
- 复杂度高:对于极其复杂、嵌套很深的JSFuck代码,即使工具也可能解析失败或输出难以理解的结果。
5. 实战解密流程与避坑指南
结合两种方法,我通常采用一种“动静结合,由浅入深”的流程来处理遇到的JSFuck代码。
5.1 标准操作流程(SOP)
- 环境隔离:首先,打开一个全新的、无痕模式的浏览器窗口。关闭所有不必要的插件。这是你的安全沙盒。
- 初步观察:将JSFuck代码粘贴到一个文本编辑器(如VSCode)。粗略观察其结构:是否包含明显的
[][...][...](...)()模式?这通常是函数构造和调用的标志。代码总长度是多少?极长的代码可能功能复杂。 - 尝试安全动态执行:
- 在无痕浏览器的控制台,尝试执行一个最简单的片段,比如代码的最后一部分(可能是一个立即执行函数表达式)。有时,直接执行整段代码会触发恶意行为,但执行其返回的函数(先不调用)并用
.toString()查看会更安全。 - 如果代码是
(function(){...})()形式的JSFuck编码,你可以尝试只执行(function(){...})部分(去掉最后的调用括号),得到函数体后再.toString()。
- 在无痕浏览器的控制台,尝试执行一个最简单的片段,比如代码的最后一部分(可能是一个立即执行函数表达式)。有时,直接执行整段代码会触发恶意行为,但执行其返回的函数(先不调用)并用
- 使用本地工具静态分析:
- 如果动态执行有风险或失败,立即转向静态分析。优先使用可离线运行的开源解密工具。将代码保存为文件,在本地运行解密工具。
- 如果工具解密成功,仔细阅读输出。输出可能仍然是混淆的,但层级更简单,可能是
eval、Function构造函数包裹着另一层字符串。
- 手动辅助拆解:
- 对于工具解密后仍不清晰的部分,或者工具解密失败的情况,回到浏览器控制台进行手动分步求值。利用控制台的自动补全和查看对象功能。
- 技巧:对于形如
[]["filter"]的访问,你可以在控制台先计算"filter"这个字符串是如何拼出来的,然后计算[]["filter"]得到function filter() { [native code] },再计算[]["filter"]["constructor"]得到function Function() { [native code] }。这样一步步把“黑盒”打开。
- 验证与清理:
- 得到疑似原始代码的字符串后,将其格式化(使用Prettier或编辑器自带的格式化功能),提高可读性。
- 仔细审查解密出的代码,确认其功能。如果是恶意代码,分析其行为模式(如请求的URL、操作的DOM元素等)。
5.2 常见“坑点”与应对策略
坑点一:无限循环或内存炸弹。有些JSFuck代码可能包含
while(1){}或用于耗尽内存的递归。在动态执行时,这会导致浏览器标签页卡死。- 应对:在尝试动态执行前,先设置执行超时。在控制台执行可能难以控制,可以考虑写一个简单的HTML页面,用
setTimeout包裹执行代码,并在超时后强制停止。或者,直接采用静态分析法。
- 应对:在尝试动态执行前,先设置执行超时。在控制台执行可能难以控制,可以考虑写一个简单的HTML页面,用
坑点二:检测执行环境。恶意代码可能会检查
navigator.userAgent、window对象属性,甚至检测开发者工具是否打开(例如检查console对象的方法toString后的长度)。如果检测不通过,它可能不会执行核心逻辑或执行误导逻辑。- 应对:在静态分析时,注意寻找字符串拼接中可能出现的
"userAgent"、"console"、"debugger"等关键词。在安全的动态环境中(如Node.jsvm沙箱),可以模拟一个纯净的环境对象传递给代码。
- 应对:在静态分析时,注意寻找字符串拼接中可能出现的
坑点三:多层嵌套与编码。JSFuck编码的代码本身,其内部可能还包裹着另一层编码(如Base64、AAEncode、JJEncode等),或者解密出的代码是
eval(atob("..."))的形式。- 应对:解密后如果得到的是
eval(...)或Function(...),其参数往往是一个长字符串。将这个字符串提取出来,它可能就是最终的Payload。如果这个字符串看起来像Base64(以=结尾,字符集为A-Za-z0-9+/),就用atob解码;如果像十六进制,就尝试转换。准备好应对多轮解密的心理准备。
- 应对:解密后如果得到的是
坑点四:代码被分割或动态生成。有时,JSFuck代码不是完整的一段,而是通过多个
<script>标签或字符串拼接动态组合而成,或者需要从页面DOM中提取某些值作为密钥。- 应对:这种情况需要还原完整的执行上下文。静态分析可能更有效,你需要像拼图一样找到所有代码片段。在浏览器中,可以尝试在页面加载完成后,在控制台查看全局变量或特定的DOM元素内容,看是否有缺失的部分。
6. 进阶:当JSFuck遇上混淆与加固
在实际的网络攻防或代码保护场景中,单纯的JSFuck很少单独出现。它常常与其他混淆技术结合,形成更强的保护。
- 组合混淆:先对原始JavaScript代码进行变量名混淆、控制流平坦化、字符串加密等操作,然后再将混淆后的代码用JSFuck编码。这样,即使你解密了JSFuck层,得到的仍然是一堆难以阅读的混淆代码。
- 自修改代码:JSFuck代码执行后,可能会动态修改自身或环境,然后再执行真正的逻辑,这给动态调试增加了难度。
- 环境依赖:代码的执行逻辑严重依赖浏览器特定的对象、属性或事件,在Node.js沙箱中无法复现,迫使分析者必须在浏览器环境中进行动态分析。
面对这些情况,静态分析的优势就凸显出来。你需要的是一个分层的解密策略:
- 第一层:使用JSFuck解密工具或手动分析,得到第一层解密后的代码。
- 第二层:识别第二层混淆的类型(如控制流平坦化、字符串数组化、不透明谓词等)。这时可能需要使用更专业的反混淆工具,如基于AST(抽象语法树)分析的
de4js等。 - 第三层:对解密出的代码进行人工审计,理解其业务逻辑或恶意行为。
这个过程对分析者的JavaScript语言功底和耐心是极大的考验。我个人的习惯是,在静态分析到一定程度后,会在一个高度可控的虚拟环境或“蜜罐”页面中,尝试运行部分解密后的代码块,通过观察其行为(网络请求、DOM操作、控制台输出)来辅助理解,但这需要极其谨慎,确保环境隔离。
7. 不只是解密:JSFuck的启示与应用
处理JSFuck的过程,不仅仅是一个“解谜游戏”,它给我们带来很多关于JavaScript和Web安全的深刻启示。
对JavaScript语言特性的再认识:JSFuck极端地展示了JavaScript弱类型、动态类型转换的灵活性(或者说“怪异”之处)。理解这些特性,不仅能帮助我们解密,更能让我们在日常开发中避免写出可能产生隐式类型转换错误的代码,理解各种“奇技淫巧”的原理。
代码安全与混淆的博弈:JSFuck是一种极致的混淆,它虽然能增加代码的阅读难度,但并不能提供真正的安全保护。因为它的解码过程是确定性的,只要有合适的工具或耐心,总能被还原。这提醒我们,前端代码永远是公开的,任何依赖代码混淆来保护敏感逻辑(如加密算法、业务规则、API密钥)的做法都是不可靠的。真正的安全应该建立在后端验证、HTTPS传输、合理的API设计等基础上。
在CTF和漏洞分析中的价值:在网络安全竞赛(CTF)中,JSFuck常作为Web题目的一种形式出现,考察选手的代码分析和JavaScript功底。在真实的漏洞分析中,攻击者也可能使用JSFuck来隐藏漏洞利用代码(Exploit),以绕过简单的WAF规则或增加分析难度。作为防御方,掌握JSFuck的解密技巧是安全分析师的基本功之一。
最后,分享一个我自己的小技巧:当你面对一段特别复杂的JSFuck代码毫无头绪时,不妨尝试在代码中搜索"constructor"这个字符串的JSFuck编码片段。因为绝大多数JSFuck代码最终都要通过Function构造函数来执行代码,找到构造"constructor"的部分,就相当于找到了通往最终Payload的“钥匙孔”。从那里开始逆向拆解,往往会事半功倍。
