翻译:JSFuck []()!+

557 阅读9分钟

早前见过类似一串加号括号、却不可思议的执行了代码,但是没有深究,正好看到 JSFuck 这个库,它仅用6个字符就能实现 JS 代码,非常巧妙神奇,因此翻译 README,学习一些它的设计思路。

原文:SFuck []()!+

JSFuck 是一个基于 JavaScript 原子部分的小众和教育意义的编程风格。它仅使用6个不同字符去写和运行代码。

它不依赖浏览器,所有你甚至能在 Node.js 上运行它。

Demo: jsfuck.com

@aemkei朋友们 所作。

示例

以下代码将执行 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

[] – 方括号

让我们以开闭方括号为开始看可能做的事。它们对这个项目超级有用并被视作核心元素,因为他们提供了方法做:

  1. 处理数组
  2. 访问属性和方法。

[] – 数组字面量

创建新数组:

[]   // 空数组
[[]] // 包含一个元素的数组 (另一个数组)

[X][i] – 数组/对象访问

[][[]] // undefined, 等同 [][""]

然后我们将能做这个:

"abc"[0]     // 获取单个字符
[]["length"] // 获取属性
[]["fill"]   // 获取方法

[X][0] - 数组包裹技巧

通过将表达式包裹在数组里,然后在下标0处获取元素,我们能在在一个表达式上施加多个操作符。这意味着方括号 [] 能代替圆括号 () 隔离表达式。

          [X][0]           // X
++[ ++[ ++[X][0] ][0] ][0] // X + 3

+ – 加号

这个符号很有用,因为它允许我们做:

  1. 创建数字
  2. 加两个值
  3. 连接字符串
  4. 创建字符串

当前版本的 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 – 转成布尔

逻辑“非”运算符能被用来创建 falsetrue

 ![] // false
!![] // true

!X+[] – 获取 "true" 和 "false"

布尔值能被转成字符串:

 ![] +[] // "false"
!![] +[] // "true"

这给我们使用更多字符的机会:

a, e, f, l, r, s, t, u.

和上面的集合一起,我们拥有 {}()[]+. INacdefilnorstuvy 并能访问这些方法:

  • call
  • concat
  • constructor
  • entries
  • every
  • fill
  • filter
  • find
  • fontcolor
  • includes
  • italics
  • reduce
  • reverse
  • slice
  • sort

重要: 我们也能使用另一个符号比如 = 创建布尔值,因为它更强大(参考下面的“替代选择”章节)。

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

……以及更多的方法和属性:

  • arguments
  • big
  • bind
  • bold
  • name
  • small
  • some
  • sub
  • substr
  • substring
  • toString
  • trim

() – 圆括号

调用方法

自从我们访问了方法,我们能调用它们以获得更强大的能力。为了做这个我们这里需要介绍两个符号 ()

不带参数的示例:

""["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.

执行函数

使用 () 之外的执行函数方法:

  1. 使用反引号:`
  2. 处理事件:on...
  3. 构造函数:new ...
  4. 类型转换:toString|valueOf
  5. 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 不是第一个方法!全世界很多人在尝试打破这所谓的“墙”。这里更多阅读: