前言
在進入今天的主題之前,先推薦大家看一個影片 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 野生工程師,相信分享與交流能讓世界變得更美好