前言
在進入今天的主題之前,先推薦大家看一個影片 WAT:A lightning talk by Gary Bernhardt from CodeMash 2012。在這個影片裡面,講者會為大家示範 JavaScript 到底有多「神奇」。而這些神奇的特性,也會跟我們之後所要介紹的兩個東西有關。
先從 Brainfuck 開始
大家有聽過 Brainfuck 嗎?顧名思義,就是會讓你超級頭痛的一個程式語言,只用下面這八個字元就可以寫出一個完整的程式:
- >
- <
- +
- -
- .
- ,
- [
- ]
而這幾種字元如果對應到 C 的程式碼,就是:
- ++ptr;
- --ptr;
- ++*ptr;
- --*ptr;
- putchar(*ptr);
- *ptr =getchar();
- while (*ptr) {
- }
(資料來源:wikipedia: Brainfuck)
Brainfuck 內建會給你一組陣列,並且讓 ptr 指向陣列開頭,剩下的事情就交給我們自己了,舉例來說,輸出 Hello World 的程式長這樣:
++++++++++[>+++++++>++++++++++>+++>+<<<<-]
>++.>+.+++++++..+++.>++.<<+++++++++++++++.
>.+++.------.--------.>+.>.
如果想看更多範例可以參考維基百科,上面有附一些說明。由於 Brainfuck 並不是今天的重點,因此只是稍微跟大家介紹一下而已。
JSfuck
接著就是我們今天的第一個主角:JSfuck,我們先來看一段 JSfuck 的程式碼:

現在你知道它為什麼取做這個名稱了吧!
與 Brainfuck 相似,JSfuck 只有六個字元:
- [
- ]
- (
- )
- !
- +
可是 JSfuck 與 Brainfuck 最大的差別就在於,JSfuck 其實是把你的 JavaScript 程式碼轉換成這樣的形式,而不是像 Brainfuck 那樣,每一個符號都有自己代表的動作。
接著就讓我們來看看 JSfuck 的原理到底是什麼吧!
Function Constructor
如果想要執行一段字串裡面的程式碼,可以怎麼做呢?你可能會用 eval,但其實還有一個方法,就是 Function Constructor,你可以傳入一段字串,那段字串就會被當做程式碼來運行,舉例來說:
new Function('alert(1)')();
上面這段程式碼就會做跟 alert(1) 一樣的事情
不只如此,其實連參數名稱都可以傳入!
new Function('a', 'b', 'return a+b;')(1, 2);
// 3
這一段先到這邊暫時打住,等等再回來看。但是現在知道一個很重要的事實了:只要你有一段文字,就可以用 Function Constructor 的方式去執行。
如何湊出程式碼?
那我們下一個要達成的目標就是,湊出所有的文字,並且都是用那六個字元組成,不就可以執行了嗎?
先從數字開始吧,看看怎樣可以湊出數字。但其實我們也只要湊出 0 跟 1 就好,因為其他正整數都可以透過這兩個數字拼湊起來。
+[]可以湊出 0,或者也可以換一個思路,![]會是 false,所以+![]也會是 0,有 0 之後,要變出 1 就不難了,因為 ![] 是 false,所以 !![] 就是 true。那 +!![] 就是數字的 1。
數字有了,接下來是文字跟符號,文字的話你可能會直接想到:String.fromCharCode,只要能湊出這段文字,你就能湊出其他任何文字或符號了。
但我們先來看看一個比較特別的方法,例如說 ![] 是 false,然後 []+[] 是空字串,所以把兩個加起來,![]+[]+[]就會是字串的 "false"(其實 ![]+[] 就可以了),那我是不是可以用:"false"[0] 取得 f 這個字元?
把字串 "false" 用上面的那串取代,就會變成:(![]+[]+[])[0],那 0 又可以用我們上面得出的+[]取代,就變:(![]+[]+[])[+[]],這樣你就成功用這幾個字元湊出 f 這個字了,酷吧!
其他的文字跟符號也是相同原理,你可以從各個 JavaScript 的程式碼裡面找到許多文字的蹤跡,例如說 undefined,如果你想知道所有的文字是怎麼湊出來的,可以參考:JSfuck 原始碼。
把上面結合起來
現在有了 Function Constructor 跟要執行的文字,是不是就可以完成我們想做的事了。可是 new Function() 要怎麼用這六個字元湊出來呢?
一個空的陣列[]有很多原生的 JavaScript function,像是 map 好了,[].map 就可以得到 map 這個 function,有了 function 之後,只要用 map.function.constructor,就可以拿到 Function Constructor了! 就像 "".constructor 也可以拿到 String Constructor 一樣。
而且.可以用[]取代,[].map會變成[]['map'],這樣結合下來,就變成:
[]['map']['constructor']
[]['map']['constructor']('alert(1)')(); // 可以順利執行
接著就是把 map 跟 constuctor 這兩個字用上面的方法湊出來,不是就可以了嗎?
做到這裡,相信大家應該比較瞭解 JSfuck 的原理了,就是用許多特別的技巧湊出文字、湊出 Function Constructor 來執行那段文字。
接著,我們介紹一個原理類似,但更可愛的東西!
用顏文字寫程式

可愛吧!居然可以把 JavaScript 變成一堆顏文字!
aaencode 跟 JSfuck 又有一點小差異了,因為aaencode可以用到的字元比較多,只是長得比較可愛而已,那既然JSfuck可以做到,aaencode沒什麼理由做不到。
接著讓我們仔細看一段 aaencode 轉出來的程式碼:
(因為有些特殊字元的關係,可能會顯示不出來,但不影響整體的閱讀,看個感覺就好)
゚ω゚ノ= /`m´)ノ ~┻━┻   //*´∇`*/ ['_']; o=(゚ー゚)  =_=3; 
c=(゚Θ゚) =(゚ー゚)-(゚ー゚); (゚Д゚) =(゚Θ゚)= (o^_^o)/ (o^_^o);
(゚Д゚)={゚Θ゚: '_' ,゚ω゚ノ : ((゚ω゚ノ==3) +'_') [゚Θ゚] ,゚ー゚ノ :(゚ω゚ノ+ '_')[o^_^o -(゚Θ゚)]
,゚Д゚ノ:((゚ー゚==3) +'_') [゚ー゚] }; (゚Д゚) [゚Θ゚] =((゚ω゚ノ==3) +'_') [c^_^o];(゚Д゚) ['c'] = 
((゚Д゚) +'_') [ (゚ー゚)+(゚ー゚)-(゚Θ゚) ];(゚Д゚) ['o'] = ((゚Д゚)+'_') [゚Θ゚];(゚o゚)=(゚Д゚)  ['c']+
(゚Д゚) ['o']+(゚ω゚ノ +'_')[゚Θ゚]+ ((゚ω゚ノ==3) +'_') [゚ー゚] + ((゚Д゚)   +'_') [(゚ー゚)+(゚ー゚)]+ (
(゚ー゚==3) +'_') [゚Θ゚]+((゚ー゚==3) +'_') [(゚ー゚) - (゚Θ゚)]+(゚Д゚) ['c']+((゚Д゚)+'_') [(゚ー゚)+(
゚ー゚)]+ (゚Д゚) ['o']+((゚ー゚==3) +'_')    [゚Θ゚];(゚Д゚) ['_'] =(o^_^o) [゚o゚] [゚o゚];(゚ε゚)=((
゚ー゚==3) +'_') [゚Θ゚]+ (゚Д゚) .゚Д゚ノ+((゚Д゚)+'_') [(゚ー゚) + (゚ー゚)]+((゚ー゚==3) +'_') [o^_^o -
゚Θ゚]+    ((゚ー゚==3) +'_') [゚Θ゚]+ (゚ω゚ノ +'_') [゚Θ゚]; (゚ー゚)+=(゚Θ゚); (゚Д゚)[゚ε゚]='\\'; (
゚Д゚).゚Θ゚ノ=(゚Д゚+ ゚ー゚)[o^_^o -(゚Θ゚)];(o゚ー゚o)=(゚ω゚ノ +'_')[c^_^o];(゚Д゚)     [゚o゚]='\"';(
゚Д゚) ['_'] ( (゚Д゚) ['_'] (゚ε゚+(゚Д゚)[゚o゚]+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ (゚ー゚)+ (゚Θ゚)+ (゚Д゚)[゚ε゚]+(
゚Θ゚)+ ((゚ー゚) + (゚Θ゚))+ (゚ー゚)+ (゚Д゚)[゚ε゚]+(゚    Θ゚)+ (゚ー゚)+ ((゚ー゚) + (゚Θ゚))+ (゚Д゚)[゚ε゚]+
(゚Θ゚)+ ((o^_^o) +(o^_^o))+     ((o^_^o) - (゚Θ゚))+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((o^_^o) +(
o^_^o))+ (゚ー゚)+ (゚Д゚)[゚ε゚]+((゚ー゚) + (゚Θ゚))+ (c^_^o)+ (゚Д゚)[゚ε゚]+((o^_^o) +(o^_^o))+ (
゚Θ゚)+ (゚Д゚)[゚ε゚]+((゚ー゚) + (゚Θ゚))+ (゚Θ゚)+ (゚Д゚)[゚o゚]) (゚Θ゚)) ('_');
仔細觀察會發現裡面其實有很多分號,是把很多行組合在一起,我們挑裡面比較短的一行來看:
(底下的程式碼因為特殊字元的關係有多做一點處理,跟原本的有些許差異)
o=(˙_˙)  =_=3;
看起是顏文字,但其實沒那麼簡單,我把空格隔多一點,你就知道我在講什麼了:
o = (˙_˙)   =  _  = 3;
其實就是:
_ = 3;
(˙_˙) = _;
o = (˙_˙)  ;
也就是讓 o, (˙_˙), _ 都是 3
所以 ˙_˙ 只是一個變數名稱,然後用 () 包起來變成顏文字,但這括弧在程式上其實沒有什麼意義
至於其他段的程式碼,做的事情也都大同小異,有興趣的讀者們可以自己再去分析,或者直接右鍵->檢視原始碼去看看是怎麼 encode 的。
結論
我第一次看到 aaencode 的時候也是:「哇!」嚇了一跳,不解為什麼用顏文字也可以寫程式,後來仔細看才了解到其實顏文字本來就是一堆符號組成的,可以寫出程式也是件很正常的事情。
但每次看到今天介紹的這兩種特別的寫法,還是很佩服作者,當初怎麼想到可以用這樣子來寫程式。希望這篇文章的介紹能讓大家對程式碼有點新的想法,說不定給了你靈感,可以開發出更厲害的寫法。
關於作者:
@huli 野生工程師,相信分享與交流能讓世界變得更美好


