JSfuck原理解析三——源碼解析

  通過前兩章我們已經瞭解了jsfuck的基本原理與實現,現在不妨先設想一下,假設我們要自己實現這麼一個加密代碼,應該如何去做。

  首先,我們拿到了一段明文代碼“alert(1)”,爲了把他變成jsf的模式,我們要按照第二章的描述對各個字符挨個加密,最後拼接成想要的代碼。那麼如果我們想把這個過程工程化,我們就需要一個map,裏面有每個字符對應的jsf代碼,這樣我們加密一串代碼只需要拼接就行了,事實上jsf的原作者也是這麼做的。

  打開github:https://github.com/aemkei/jsfuck,根目錄下有一個叫做jsfuck.js的文件,裏面部分代碼如下:

const SIMPLE = {
    'false':      '![]',
    'true':       '!![]',
    'undefined':  '[][[]]',
    'NaN':        '+[![]]',
    'Infinity':   '+(+!+[]+(!+[]+[])[!+[]+!+[]+!+[]]+[+!+[]]+[+[]]+[+[]]+[+[]])' // +"1e1000"
  };

  const CONSTRUCTORS = {
    'Array':    '[]',
    'Number':   '(+[])',
    'String':   '([]+[])',
    'Boolean':  '(![])',
    'Function': '[]["fill"]',
    'RegExp':   'Function("return/"+false+"/")()',
    'Object':	'[]["entries"]()'
  };

  const MAPPING = {
    'a':   '(false+"")[1]',
    'b':   '([]["entries"]()+"")[2]',
    'c':   '([]["fill"]+"")[3]',
    'd':   '(undefined+"")[2]',
    'e':   '(true+"")[3]',
    'f':   '(false+"")[0]',
    'g':   '(false+[0]+String)[20]',
    'h':   '(+(101))["to"+String["name"]](21)[1]',
    ……
    ……

  在這裏我們可以看到三個常量,其中的MAPPING確實如我們所想的一樣,是一個字符與代碼的對應,不過對應的值看起來並不是jsf的代碼,並且有些甚至是缺失的,比如:'P': USE_CHAR_CODE。這些字符我們不能如a、b、c那樣輕易的在現有的字符串中找到,需要特殊的處理。

  那麼jsf的源碼到底是怎麼工作的呢,接下來我們就來整體分析一下jsfuck.js文件的代碼。

  我們可以把代碼摺疊一下,得到下圖:

  由上圖可看出,代碼分爲三個部分,常量、方法、執行。

MAPPING補全:

  我們直接來看第三部分,方法的執行,第一個方法fillMissingDigits,字面意思可以看出是填補缺失的數字,代碼如下:

  function fillMissingDigits(){
    var output, number, i;

    for (number = 0; number < 10; number++){

      output = "+[]";

      if (number > 0){ output = "+!" + output; }
      for (i = 1; i < number; i++){ output = "+!+[]" + output; }
      if (number > 1){ output = output.substr(1); }

      MAPPING[number] = "[" + output + "]";
    }
  }

  這段代碼很簡單,從中可以看出,這個方法的功能是填補MAPPING中數字(0 - 9)的鍵值對,如:0: '[+[]]'、1: '[+!+[]]'。(在外面加了一個括號可以方便的轉換爲字符串。)

  那麼很自然就得出下面的fillMissingChars方法是填補確實的其他字符,下面的一串代碼也比較少,我也把它貼出來:

function fillMissingChars(){
  var base16code, escape;
  for (var key in MAPPING){
    if (MAPPING[key] === USE_CHAR_CODE){
      //Function('return"\\uXXXX"')()
      base16code = key.charCodeAt(0).toString(16);
      escape = ('0000'+base16code).substring(base16code.length).split('').join('+');
      MAPPING[key] = 'Function("return"+' + MAPPING['"'] + '+"\\u"+' + escape + '+' + MAPPING['"'] + ')()';
    }
  }
}

  看代碼可以知道,此方法處理了MAPPING中的所有值爲USE_CHAR_CODE的字符,原理比較簡單暴力,直接用到了charCodeAt,由於key是一個字符,就相當於返回了key這個字符的Unicode編碼,並轉換成16進制。我們拿'a'舉例,會得到'61'

  接下來的代碼做了三件事:

  1. 將得到的16進制數轉換成4位,缺少的位數在左方補0。(例如’61‘ => '0061')

  2. 分割字符串

  3. 用'+'合併字符串。(例如'0061' => '0+0+6+1')

  爲什麼要這麼做呢?下面的代碼我們就能得到答案,下面的代碼是拼接字符串,我們可以在MAPPING中找到MAPPING['"']的值爲:'("")["fontcolor"]()[12]',我們將字符串拼接之後就會得到如下字符串:

'Function("return"+("")["fontcolor"]()[12]+"\\u"+0+0+6+1+("")["fontcolor"]()[12])()'

  我們已經知道Function( code )()的作用,它可以將字符串作爲可執行代碼執行,那麼我們只需要把code部分組合出來就能得到:

return("")["fontcolor"]()[12]\\u0+0+6+1("")["fontcolor"]()[12])()

  這段字符串執行了之後,其實就是:return "\u0061",得到的Unicode字符就是'a'.

  到此爲止我們已經填充了所有的確實字符,但是他並不是我們需要的jsf代碼,而是半成品。

替換字符:

  從下面兩個方法的名字就能看出來,接下來要做的是把所有字符全部替換成[]()!+這些符號。

  我們先來看第一個替換方法:replaceMap。源代碼太長,就不貼了,大家可以看github的jsfuck.js文件的146行。我們可以看到這個方法依然是先定義了三個方法,然後執行了一個循環MIN(32) => MAX(126)細心的同學肯定發現了,這個定義好的範圍正好是ascii表的所有可顯示字符,相當於遍歷了一遍MAPPING的所有鍵值對。

  在循環中首先執行了String.fromCharCode方法,將數字轉換成了Unicode字符,相當於MAPPING中的鍵,然後在通過索引拿到對應的鍵中的值。

  首先執行了如下代碼:

for (key in CONSTRUCTORS){
      replace("\\b" + key, CONSTRUCTORS[key] + '["constructor"]');
    }

  我們拿 'A': '(+[]+Array)[10]'舉例,經過調用,最終的執行代碼是這樣的:

'(+[]+Array)[10]'.replace(
      new RegExp("Array", "gi"),
      '[]["constructor"]'
    );

  得到的結果是字符串:'(+[]+[]["constructor"])[10]'。

  這個替換我們可以理解爲,將含有最開始定義的CONSTRUCTORS下的key全部替換掉,替換原因是這些key包含的都是一個個對象,並不能經過簡單的字符串拼接就能得到。這樣我們距離最終的加密代碼又緊了一步。

  第二步與第一步意義相同,是將含有SIMPLE的key全部替換掉,這些是可以直接使用的jsf代碼成品。

  再接下來的是連着六個replace,分別爲:

  1. 將兩位以上的數字全部替換。(兩位以上的數字作爲Number替換,因爲如果我們直接用MAPPING,只會打得到相加的字符串,例:MAPPING[1] + MAPPING[1] = [+!+[]]+[+!+[]] = '11')

  2. 將‘(Number)’替換成MAPPING[key]。

  3. 將‘[Number]’替換成MAPPING[key]。

  4. 將'GLOBAL'替換成Function("return this")()。

  5. 將'+""'替換成+[]。(+""是爲了轉換成字符串)

  6. 將""替換成[]+[]。(括號中的空字符串)

  經過這一輪替換,我們得到的MAPPING離目標更近了一步,得到如下圖的MAPPING。

  下一步便是最後一個方法:replaceStrings。這個方法開頭便定義了一個正則表達式:

var regEx = /[^\[\]\(\)\!\+]{1}/g

  它匹配了所有的非目標字符( 非[]()!+ ),那麼我們就能猜測到,經過這一輪替換,將得到我們的最終map。那麼它是如何工作的呢?我們逐行來看。首先定義了一些變量和方法,然後執行了下面一段代碼:

      for (all in MAPPING){
        MAPPING[all] = MAPPING[all].replace(/\"([^\"]+)\"/gi, mappingReplacer);
      }

  這段代碼替換了MAPPING中的所有帶雙引號的字符串( “fill” => f+i+l+l ),完成之後是一個while循環,這個循環涉及到了兩個方法,我們來一一分析:

  1. findMissing

      function findMissing(){
        var all, value, done = false;
  
        missing = {};
  
        for (all in MAPPING){
  
          value = MAPPING[all];
  
          if (value.match(regEx)){
            missing[all] = value;
            done = true;
          }
        }

        return done;
      }

  代碼很簡單,循環了MAPPING,如果有值含有非jsf字符,則返回true,一遍循環之後,生成了一個對象missing,裏面是MAPPING的所有含有非jsf字符的key。

  2. valueReplacer:更簡單,用來替換非jsf字符,如果MAPPING[key]全是jsf字符,則直接替換,不是則不變。

  經過一遍又一遍的循環,最終將所有的非jsf字符替換成了jsf字符。最終的MAPPING就誕生了。

  不過這裏有一個問題,我們可以留意一下作者寫下的一段error提示:

        if (count-- === 0){
          console.error("Could not compile the following chars:", missing);
        }

  上述循環其實是有可能造成死循環的,如果幾個非jsf代碼的value相互引用,且最終無法轉換成jsf字符,就會造成死循環。其實我們最初看作者定義的常量的時候就能看出,這些被定義的常量都是經過篩選的,不會造成循環引用。

  最後一個方法就是暴露出encode方法,很簡單就不做分析了。

(填坑完畢)

拓展:

aaencode:js顏文字加密

゚ω゚ノ= /`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) - (゚Θ゚))+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ (゚Θ゚)+ (c^_^o)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ (゚ー゚)+ ((゚ー゚) + (゚Θ゚))+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((゚ー゚) + (゚Θ゚))+ (゚ー゚)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((゚ー゚) + (゚Θ゚))+ (゚ー゚)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((゚ー゚) + (゚Θ゚))+ ((゚ー゚) + (o^_^o))+ (゚Д゚)[゚ε゚]+((゚ー゚) + (゚Θ゚))+ (゚ー゚)+ (゚Д゚)[゚ε゚]+(゚ー゚)+ (c^_^o)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ (゚Θ゚)+ ((o^_^o) - (゚Θ゚))+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ (゚ー゚)+ (゚Θ゚)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((o^_^o) +(o^_^o))+ ((o^_^o) +(o^_^o))+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ (゚ー゚)+ (゚Θ゚)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((o^_^o) - (゚Θ゚))+ (o^_^o)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ (゚ー゚)+ (o^_^o)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((o^_^o) +(o^_^o))+ ((o^_^o) - (゚Θ゚))+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((゚ー゚) + (゚Θ゚))+ (゚Θ゚)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((o^_^o) +(o^_^o))+ (c^_^o)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((o^_^o) +(o^_^o))+ (゚ー゚)+ (゚Д゚)[゚ε゚]+(゚ー゚)+ ((o^_^o) - (゚Θ゚))+ (゚Д゚)[゚ε゚]+((゚ー゚) + (゚Θ゚))+ (゚Θ゚)+ (゚Д゚)[゚o゚]) (゚Θ゚)) ('_');

Brainfuck:Brainfuck是一種極小化的計算機語言,它是由Urban Müller在1993年創建的。jsfuck就是按照這種思維模式開發出來的。

圖靈完備:圖靈完備是指機器執行任何其他可編程計算機能夠執行計算的能力。圖靈完備也意味着你的語言可以做到能夠用圖靈機能做到的所有事情,可以解決所有的可計算問題。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章