早前见过类似一串加号括号、却不可思议的执行了代码,但是没有深究,正好看到 JSFuck 这个库,它仅用6个字符就能实现 JS 代码,非常巧妙神奇,因此翻译 README,学习一些它的设计思路。
原文:SFuck
[]()!+
JSFuck 是一个基于 JavaScript 原子部分的小众和教育意义的编程风格。它仅使用6个不同字符去写和运行代码。
它不依赖浏览器,所有你甚至能在 Node.js 上运行它。
Demo: jsfuck.com
示例
以下代码将执行 alert(1) 语句:
[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[
]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]][([][(![]+[])[+[]]+([![]]+[][[]
])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+
(!![]+[])[+!+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[][(![]+[])[+[]]+([![]]+[][[]])[+
!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![
]+[])[+!+[]]])[+!+[]+[+[]]]+([][[]]+[])[+!+[]]+(![]+[])[!+[]+!+[]+!+[]]+(!![]
+[])[+[]]+(!![]+[])[+!+[]]+([][[]]+[])[+[]]+([][(![]+[])[+[]]+([![]]+[][[]])[
+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!!
[]+[])[+!+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[][(![]+[])[+[]]+([![
]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[
]+!+[]]+(!![]+[])[+!+[]]])[+!+[]+[+[]]]+(!![]+[])[+!+[]]]((![]+[])[+!+[]]+(![
]+[])[!+[]+!+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]+(!![]+[])[+[]]+(!
[]+[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])
[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]])[!+[]+!+[]+[+[]]]+[+!+[]]+(
!![]+[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[
])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]])[!+[]+!+[]+[+[]]])()
基础
false => ![]
true => !![]
undefined => [][[]]
NaN => +[![]]
0 => +[]
1 => +!+[]
2 => !+[]+!+[]
10 => +[[+!+[]]+[+[]]]
Array => []
Number => +[]
String => []+[]
Boolean => ![]
Function => []["filter"]
run => []["filter"]["constructor"]( CODE )()
eval => []["filter"]["constructor"]("return eval")()( CODE )
window => []["filter"]["constructor"]("return this")()
在 这里 查看完整列表。
如何工作
注意: 请随意加入这里讨论:gitter.im/aemkei
[] – 方括号
让我们以开闭方括号为开始看可能做的事。它们对这个项目超级有用并被视作核心元素,因为他们提供了方法做:
- 处理数组
- 访问属性和方法。
[] – 数组字面量
创建新数组:
[] // 空数组
[[]] // 包含一个元素的数组 (另一个数组)
[X][i] – 数组/对象访问
[][[]] // undefined, 等同 [][""]
然后我们将能做这个:
"abc"[0] // 获取单个字符
[]["length"] // 获取属性
[]["fill"] // 获取方法
[X][0] - 数组包裹技巧
通过将表达式包裹在数组里,然后在下标0处获取元素,我们能在在一个表达式上施加多个操作符。这意味着方括号 [] 能代替圆括号 () 隔离表达式。
[X][0] // X
++[ ++[ ++[X][0] ][0] ][0] // X + 3
+ – 加号
这个符号很有用,因为它允许我们做:
- 创建数字
- 加两个值
- 连接字符串
- 创建字符串
当前版本的 JSFuck 大量使用它但是我们不确定他是否是最基本的。
转成数字
+[] // 0 - 数字 0
数字自增
使用上面提到的数组包裹技巧:
++[ 0 ][ 0 ] // 1
++[ [] ][ +[] ] // 1
获取 undefined
通过下标在空数组获取元素将返回 undefined:
[][ 0 ] // undefined
[][ +[] ] // 获取第一个元素 (undefined)
[][ [] ] // 查找属性 ""
获取 NaN
将 undefined 转数字将导致非数字:
+[][[]] // +undefined = NaN
数字相加
1 + 1 // 2
++[[]][+[]] + ++[[]][+[]] // 2
一个利用 ++ 的简短方式:
++[ 1][ 0] // 2
++[++[[]][ 0]][ 0] // 2
++[++[[]][+[]]][+[]] // 2
使用这个技术,我们能得到全部数字:
0, 1, 2, 3, 4, 5, 6, 7, 8, 9
+[] – 转成字符串
结合加号和方括号将把其他值转成字符串:
[] +[] // "" - 空串
+[] +[] // "0"
[][[]] +[] // "undefined"
++[][[]] +[] // "NaN"
++[[]][+[]] +[] // "1"
"word"[i] – 获取单个字符
既然我们有字符串了,我们也能获取单个字符:
"undefined" [ 0] // "u"
[ "undefined" ][ 0][ 0] // "u"
[ undefined +[] ][+[]][+[]] // "u"
[ [][[]] +[] ][+[]][+[]] // "u"
"undefined" [ 1 ] // "n"
[[][[]]+[]][+[]][ ++[[]][+[]] ] // "n"
由于我们有 "NaN" 和 "undefined",我们已获得如下字符:
N,a,d,e,f,i,n,u.
+ – 组合字符
现在我们能组合字符为新单词了。
// can be written using []+ only:
"undefined"[4] // "f"
"undefined"[5] // "i"
"undefined"[6] // "n"
"undefined"[3] // "d"
// combine using +
"f"+"i"+"n"+"d" // "find"
"e" – 指数标记的数字
鉴于我们从 "undefined" 中获得 "e" 了,我们能利用指数标记构造一个非常大的数字,然后获得 Infinity 引用:
+("1e309") // Infinity
+("1e309") +[] // "Infinity"
+("11e100") // 1.1e+101
+("11e100") +[] // "1.1e+101" (获得 `.` 和 `+`)
+("0.0000001") // 1e-7
+("0.0000001") +[] // "1e-7" (获得 `-`)
产生字符:
I,f,i,n,t,y,.,+,-.
[]["method"] – 访问方法
新组合字符能形成方法名。它们能以方括号标记所访问:
[]["f"+"i"+"n"+"d"] // 这里 "f" 是 "false" 的第一个字符,以此类推
[]["find"] // 和点语法相同
[] .find
注意:使用 "undefined", "NaN" 和 "Infinity" 中的字符,我们仅能获得 Array.prototype.find 一个方法。
method+[] – 获取方法定义
我们能将方法转为字符串然后获得它的定义字符串:
[]["find"] +[]
这将返回如下字符串:
"function find() { [native code] }"
注意:原生函数的字符串表示不是 ECMAScript 标准的一部分并且在浏览器间有不同。例如,火狐输出稍有不同,它有额外的换行 \n。
产生字符:
a,c,d,e,f,i,n,o,t,u,v,{,},(,),[,]
产生方法:
.concat.find.
! – 逻辑非运算符
这是原 JSFuck 集合中的第四个字符,用于创建布尔值。
注意:这个符号也能被其他的所替代,比如 < 或 =。参考下面的“替代选择”章节
!X – 转成布尔
逻辑“非”运算符能被用来创建 false 和 true:
![] // false
!![] // true
!X+[] – 获取 "true" 和 "false"
布尔值能被转成字符串:
![] +[] // "false"
!![] +[] // "true"
这给我们使用更多字符的机会:
a, e, f, l, r, s, t, u.
和上面的集合一起,我们拥有 {}()[]+. INacdefilnorstuvy 并能访问这些方法:
callconcatconstructorentrieseveryfillfilterfindfontcolorincludesitalicsreducereverseslicesort
重要: 我们也能使用另一个符号比如 = 创建布尔值,因为它更强大(参考下面的“替代选择”章节)。
X["constructor"] – 基本类型包裹名称
利用 .constructor 我们拥有创建实例的引用。对应基本值,它返回对应的内建包裹名:
0 ["constructor"] // Number
"" ["constructor"] // String
[] ["constructor"] // Array
false ["constructor"] // Boolean
[].find ["constructor"] // Function
使用 +[] 把它们转成字符串并找回它们的函数名,以便获得更多字符:
0["constructor"]+[] // "function Number() { ... }"
可用的新字符:
m, b, S, g, B, A, F。
……以及更多的方法和属性:
argumentsbigbindboldnamesmallsomesubsubstrsubstringtoStringtrim
() – 圆括号
调用方法
自从我们访问了方法,我们能调用它们以获得更强大的能力。为了做这个我们这里需要介绍两个符号 ( 和 )。
不带参数的示例:
""["fontcolor"]() // "<font color="undefined"></font>"
[]["entries"]() +[] // "[object Array Iterator]"
新字符:
j, <, >, =, ", /
带多个参数调用方法
带多个参数调用方法不是那么容易 - 实现它你可以用下面的 技术(由 trincot 发现) - 例如:
调用字符串方法 "truefalse".replace("true","1") 能写成 ["true", "1"].reduce("".replace.bind("truefalse")),最后:
["true"]["concat"]("1")["reduce"](""["replace"]["bind"]("truefalse"))
调用数组方法 [1,2,3].slice(1,2) 能写成 [1,2].reduce([].slice.bind([1,2,3])),最后:
[1]["concat"](2)["reduce"]([]["slice"]["bind"]([1,2,3]))
“链式”带多个参数调用方法
为了能在右边调用(带多个参数)前一个方法结果上的方法,你能使用这个 技术(由 trincot 发现)- 例如:"truefalse".replace("true","1").replace("false","0") 能写成:
"truefalse"
.split().concat([["true", "1"]]).reduce("".replace.apply.bind("".replace))
.split().concat([["false", "0"]]).reduce("".replace.apply.bind("".replace))
最后:
"truefalse"
["split"]()["concat"]([["true"]["concat"]("1")])["reduce"](""["replace"]["apply"]["bind"](""["replace"]))
["split"]()["concat"]([["false"]["concat"]("0")])["reduce"](""["replace"]["apply"]["bind"](""["replace"]))
“链式”带多个参数调用数组方法
为了以右侧(链式)调用数组方法,我们使用和字符串相似的技术但多了些额外技巧(详情 这里),举例如下:
[3,4,5].slice(1,2).concat(6) 能写成[[3,4,5]].concat([[1,2]]).reduce([].slice.apply.bind([].slice)).concat(6) (和字符串类似)但是现在我们需要寻找右侧包裹数组 [3,4,5] 的方法并获得 [[3,4,5]],它能按如下方式完成 [3,4,5].map([].constructor).concat([[[]]])[0].slice(-1),于是我们得到:
[3,4,5]
// 调用:slice(1,2)
.map([].constructor).concat([[[]]])[0].slice(-1)
.concat([[1,2]]).reduce([].slice.apply.bind([].slice))
// 调用下一个方法(链式)
.concat(6)
最后(移除点和逗号后):
[3]["concat"](4)["concat"](5)
["map"]([]["constructor"])["concat"]([[[]]])[0]["slice"](-1)
["concat"]([[1]["concat"](2)])["reduce"]([]["slice"]["apply"]["bind"]([]["slice"]))
["concat"](6)
number.toString(x) – 获取任意小写字母
数字的 toString 方法有一个可选的指定基数的参数(2至36)。当基数为36时我们得到任意 小写 字母:
10["toString"](36) // "a"
11["toString"](36) // "b"
...
34["toString"](36) // "y"
35["toString"](36) // "z"
显出字符:abcdefghijklmnopqrstuvwxyz
Function("code")() – 执行代码
函数 constructor 是 JSFuck 中的主键:它用字符串作为参数然后返回一个以这个串为函数体的匿名函数。所以它是你执行任意字符串形式的代码的基础。这和 eval 相似,不需要指向全局作用域的引用(即 window)。我们能获得函数 constructor 例如用 []["find"]["constructor"]。
这是 JS-to-JSFuck 编译器的第一个主要步骤和必不可少的部分。
Function("return this")() – window
当执行 function anonymous() { return this } 时,我们得到指向全局作用域的调用上下文:window!
获得 window 引用对 JSFuck 而言是另一个巨大进步。伴随方括号字符,我们仅能挖掘可用对象:数字,数组,一些函数…… 伴随全局作用域引用,我们现在能访问任意全局变量和它们的内部属性。
创建正则表达式对象
我们能创建正则表达式例如 /pattern/g 如下:
[]["fill"]["constructor"]("return RegExp")()("pattern","g")
移除逗号后(使用不需 bind 的 多参数技术)看起来如下:
["pattern"]["concat"]("g")["reduce"]([]["fill"]["constructor"]("return RegExp")())
替代选择
组合字符
代替 + 我们可以使用 .concat 来组合字符串。
"f"["concat"]("i")["concat"]("l")["concat"]("l") // fill
问题:我们需要组合 "c","o","n","c","a" 和 "t" 获得 "concat"。
布尔值
! 可以被更“强大”的不止一处用途的字符代替。
= – 布尔 + 赋值
X == X // true
X == Y // false
X = Y // 赋一个新值
> – 布尔 + 创建数字
X > Y // true
X > X // false
X >> Y // 数字
一个更复杂的例子是仅用 []>+ 获取字母 "f":
[[ []>[] ] + [] ] [[]>>[]] [[]>>[]]
[[ false ] + [] ] [ 0] [ 0]
[ "false" ] [ 0] [ 0]
"false" [ 0]
数字
代替 + 我们可以用布尔和移位操作符创建数字:
true >> false // 1
true << true // 2
true << true << true // 4
问题:一些数字(比如 5)较难获得。但是当使用字符串时是可能的,例如 "11" >> true。
Problem: Some number (like 5) are harder to get. But it is possible when using strings, eg "11" >> true.
执行函数
使用 () 之外的执行函数方法:
- 使用反引号:
` - 处理事件:
on... - 构造函数:
new ... - 类型转换:
toString|valueOf - symbol 数据类型:
[Symbol...]
使用反引号
替代使用开闭圆括号,我们可以用反引号 ` 执行函数。在 ES6 中它们被用于插入字符串,为带标签的模板字符串表达式服务。
([]["entries"]``).constructor // Object
这会提供给我们从 "Object" 得到的字符以及访问它的方法。
很不巧,我们仅能传单个字符串(从我们基础字符表中例如 []!+)作为参数。不可能带多参数或一个预编译字符串来调用方法。为了实现这个,我们不得不使用 ${} 做表达式插入,但这会引入新字符。
反引号的可能性 在此 Gitter 聊天室 被详细讨论。
映射到类型转换
另一个不需圆括号执行函数的方法是映射到 .toString 或 .valueOf 方法上并隐式调用。
A = []
A["toString"] = A["pop"]
A+"" // 将执行 A.pop
注意:没办法传参数且要求 = 在我们的基础字符表里。它仅对返回基础类型的方法有用。
目前位置仅有用途是在火狐中连接 toSource 以获得特殊字符比如反斜杠 \。
触发事件句柄
函数或方法也能通过赋值给事件句柄被执行。有数个实现方法,例如:
// 开始时覆盖 onload 事件
onload = f
// 写图片标签
document.body.innerHTML = '<img onerror=f src=X />'
// 抛出和处理错误
onerror=f; throw 'x'
// 触发事件
onhashchange = f; location.hash = 1;
注意:我们需要 = 符号来赋值给句柄。
问题:我们还没有访问 window 或 DOM 元素以绑定事件句柄。
构造函数
我们也能使用 new 操作符调用函数,当它是假的对象类型:
new f
问题:new 操作符在我们的基础符号集合中(至今)不可用。
Symbol
symbol 是一个唯一和不可修改的数据类型,可能被用作对象属性标识符。这能用来隐式调用函数。
f[Symbol.toPrimitive] = f; f++;
f[Symbol.iterator] = f; [...f];
注意:我们需要 = 符号赋值函数。
问题:我们不能利用我们简化的字符集合访问到 Symbol。
更多阅读
JSFuck 不是第一个方法!全世界很多人在尝试打破这所谓的“墙”。这里更多阅读:
- Esolang Wiki: JSFuck
- sla.ckers.org – Original Discussion
- Xchars.js – Sylvain Pollet-Villard
- Non Alphanumeric JavaScript – Patricio Palladino
- Non-alphanumeric code – Gareth Heyes
- Executing non-alphanumeric JavaScript without parenthesis – Portswigger