讓 JavaSript 難以閱讀:jsfuck 與 aaencode


Posted by huli on 2016-07-16

前言

在進入今天的主題之前,先推薦大家看一個影片 WAT:A lightning talk by Gary Bernhardt from CodeMash 2012。在這個影片裡面,講者會為大家示範 JavaScript 到底有多「神奇」。而這些神奇的特性,也會跟我們之後所要介紹的兩個東西有關。

先從 Brainfuck 開始

大家有聽過 Brainfuck 嗎?顧名思義,就是會讓你超級頭痛的一個程式語言,只用下面這八個字元就可以寫出一個完整的程式:

  1. >
  2. <
  3. +
  4. -
  5. .
  6. ,
  7. [
  8. ]

而這幾種字元如果對應到 C 的程式碼,就是:

  1. ++ptr;
  2. --ptr;
  3. ++*ptr;
  4. --*ptr;
  5. putchar(*ptr);
  6. *ptr =getchar();
  7. while (*ptr) {
  8. }

(資料來源:wikipedia: Brainfuck
Brainfuck 內建會給你一組陣列,並且讓 ptr 指向陣列開頭,剩下的事情就交給我們自己了,舉例來說,輸出 Hello World 的程式長這樣:

++++++++++[>+++++++>++++++++++>+++>+<<<<-]
>++.>+.+++++++..+++.>++.<<+++++++++++++++.
>.+++.------.--------.>+.>.

如果想看更多範例可以參考維基百科,上面有附一些說明。由於 Brainfuck 並不是今天的重點,因此只是稍微跟大家介紹一下而已。

JSfuck

接著就是我們今天的第一個主角:JSfuck,我們先來看一段 JSfuck 的程式碼:

jsfunck

現在你知道它為什麼取做這個名稱了吧!
與 Brainfuck 相似,JSfuck 只有六個字元:

  1. [
  2. ]
  3. (
  4. )
  5. !
  6. +

可是 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)')(); // 可以順利執行

接著就是把 mapconstuctor 這兩個字用上面的方法湊出來,不是就可以了嗎?

做到這裡,相信大家應該比較瞭解 JSfuck 的原理了,就是用許多特別的技巧湊出文字、湊出 Function Constructor 來執行那段文字。

接著,我們介紹一個原理類似,但更可愛的東西!

用顏文字寫程式

aaencode

可愛吧!居然可以把 JavaScript 變成一堆顏文字!
aaencodeJSfuck 又有一點小差異了,因為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 野生工程師,相信分享與交流能讓世界變得更美好


#javascript #jsfuck #aaencode









Related Posts

Two Sum

Two Sum

畢業多年後再回到杜鵑花節顧攤,是什麼感覺?

畢業多年後再回到杜鵑花節顧攤,是什麼感覺?

C++ 教學(五) 迴圈

C++ 教學(五) 迴圈




Newsletter




Comments