JS 反混淆

在使用某插件的過程中,大量個性化需求不能滿足,於是我有了更改源碼的衝動。翻遍所有角落,只找了一份壓縮混淆的 js 文件,能否反混淆,這是本節討論的重點。

一、場景復現

先來說說幾種我們迫切需要知道源碼的情況:
1.閱讀源碼,當然,大部分開源的代碼都是可以直接查看的;
2.對某插件做個性化的需求更改,這時候你渴望看到未混淆壓縮的代碼;
3.爲了增加代碼分析的難度,混淆(obfuscate)工具被應用到了許多惡意軟件(如 0day 掛馬、跨站攻擊等)當中。分析人員爲了掀開惡意軟件的面紗,首先就得對腳本進行反混淆(deobfuscate)處理。
4.當你準備抄襲別人代碼時,這個稍微有點不可描述🙈;

本文,我們假設是在前兩種場景的條件下,來探索一下 js 反混淆問題。

二、尋求方案

爲了快速的解決問題,我們首先嚐試一下現有方案:
1.jsnice
2.aliasing1
一個簡單的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
function chunkData(e, t) {
  var n = [];
  var r = e.length;
  var i = 0;
  for (; i < r; i += t) {
    if (i + t < r) {
      n.push(e.substring(i, i + t));
    } else {
      n.push(e.substring(i, r));
    }
  }
  return n;
}

 

解析後:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
 * @param {string} str
 * @param {number} step
 * @return {?}
 */
function chunkData(str, step) {
  /** @type {Array} */
  var colNames = [];
  var len = str.length;
  /** @type {number} */
  var i = 0;
  for (;i < len;i += step) {
    if (i + step < len) {
      colNames.push(str.substring(i, i + step));
    } else {
      colNames.push(str.substring(i, len));
    }
  }
  return colNames;
};

 

是不是反混淆之後,可讀性強了很多。

2.js 代碼混淆站長工具
我們先從 Lodash 找一段演示代碼,如下:

1
2
3
4
5
6
7
8
9
10
11
12
function arrayReduce(array, iteratee, accumulator, initAccum) {
  var index = -1,
      length = array == null ? 0 : array.length;

  if (initAccum && length) {
    accumulator = array[++index];
  }
  while (++index < length) {
    accumulator = iteratee(accumulator, array[index], index, array);
  }
  return accumulator;
}

 

普通混淆後,就變成了這樣:

1
function arrayReduce(RkeVlNGo1, ZPgGaWYOP2, uFMz3, dHyv4) {  var tTsFyC5 = -1,      sh6 = RkeVlNGo1 == null ? 0 : RkeVlNGo1["\x6c\x65\x6e\x67\x74\x68"];  if (dHyv4 && sh6) {    uFMz3 = RkeVlNGo1[++tTsFyC5];  }  while (++tTsFyC5 < sh6) {    uFMz3 = ZPgGaWYOP2(uFMz3, RkeVlNGo1[tTsFyC5], tTsFyC5, RkeVlNGo1);  }  return uFMz3;}

 

爲了測試一下如何反混淆,我們將混淆後代碼拷貝到 jsnice :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
 * @param {Object} collection
 * @param {?} callback
 * @param {Text} accumulator
 * @param {number} a
 * @return {?}
 */
function arrayReduce(collection, callback, accumulator, a) {
  /** @type {number} */
  var index = -1;
  var b = collection == null ? 0 : collection["length"];
  if (a && b) {
    accumulator = collection[++index];
  }
  for (;++index < b;) {
    accumulator = callback(accumulator, collection[index], index, collection);
  }
  return accumulator;
};

 

可以看到,普通混淆後,代碼還是可以做一些復原。好的,我們加大力度,採用加密壓縮方式:

1
eval(function(p,a,c,k,e,d){e=function(c){return(c<a?"":e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)d[e(c)]=k[c]||e(c);k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1;};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p;}('b a(2,7,4,6){9 3=-1,5=2==8?0:2.5;e(6&&5){4=2[++3]}d(++3<5){4=7(4,2[3],3,2)}c 4}',15,15,'||array|index|accumulator|length|initAccum|iteratee|null|var|arrayReduce|function|return|while|if'.split('|'),0,{}))

 

這次代碼明顯多了,而 jsnice 也反混淆失敗了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
eval(function(str, n, name, pair, func, opt_attributes) {
  /**
   * @param {number} i
   * @return {?}
   */
  func = function(i) {
    return(i < n ? "" : func(parseInt(i / n))) + ((i = i % n) > 35 ? String.fromCharCode(i + 29) : i.toString(36));
  };
  if (!"".replace(/^/, String)) {
    for (;name--;) {
      opt_attributes[func(name)] = pair[name] || func(name);
    }
    /** @type {Array} */
    pair = [function(timeoutKey) {
      return opt_attributes[timeoutKey];
    }];
    /**
     * @return {?}
     */
    func = function() {
      return "\\w+";
    };
    /** @type {number} */
    name = 1;
  }
  for (;name--;) {
    if (pair[name]) {
      /** @type {string} */
      str = str.replace(new RegExp("\\b" + func(name) + "\\b", "g"), pair[name]);
    }
  }
  return str;
}("b a(2,7,4,6){9 3=-1,5=2==8?0:2.5;e(6&&5){4=2[++3]}d(++3<5){4=7(4,2[3],3,2)}c 4}", 15, 15, "||array|index|accumulator|length|initAccum|iteratee|null|var|arrayReduce|function|return|while|if".split("|"), 0, {}));

 

這段代碼,地球人已經沒法讀懂了,亂七八糟的。那麼,問題來了,加密混淆的代碼真的沒有辦法復原嗎?

三、思維突破

從上面的演示可以看出來,加密之後的混淆,已經完全無法反混淆了。除非我們知道混淆算法,可是混淆方式不計其數,你需要知曉它的混淆方式,並制定出反混淆算法,那估計會累死。

如果我們這麼想的話,那麼就陷入了泥潭,甚至無法自救。那麼,突破點到底在哪裏呢?

衆所周知,JavaScript 是解釋性語言,它嚴重依賴遊覽器。不管 JavaScript 如何混淆,最終瀏覽器都會知道最真實的代碼。所以,我們還得以瀏覽器爲突破口。

首先,同步一下原始代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function arrayReduce(array, iteratee, accumulator, initAccum) {
  var index = -1,
      length = array == null ? 0 : array.length;

if(typeof(iteratee) ==='function') throw 'jartto-test';

  if (initAccum && length) {
    accumulator = array[++index];
  }
  while (++index < length) {
    accumulator = iteratee(accumulator, array[index], index, array);
  }
  return accumulator;
}
arrayReduce();

 

其次,確定源碼中是否包含在 eval 中,參考代碼如下:

1
eval(function(p,a,c,k,e,d){e=function(c){return(c<a?"":e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)d[e(c)]=k[c]||e(c);k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1;};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p;}('9 a(2,6,4,7){d 3=-1,5=2==e?0:2.5;8(b(6)===\'9\')h\'i-g\';8(7&&5){4=2[++3]}c(++3<5){4=6(4,2[3],3,2)}f 4}a();',19,19,'||array|index|accumulator|length|iteratee|initAccum|if|function|arrayReduce|typeof|while|var|null|return|test|throw|jartto'.split('|'),0,{}))

 

然後,查找關鍵字 throw,如果有,那就成功了一大步;
最後,改動源碼,讓源碼拋出異常,讓 Eval Code 還原出真實代碼;

可以看到,爲了演示,我們讓源代碼具有某些特性,然而實際情況會遠遠複雜於此;

思維啓蒙:一招破解混淆後的 JavaScript 代碼

四、相關工具介紹

我們先來看一下,目前常用的混淆工具:

反混淆工具:

瞭解更多,請參考:幾種常見的JavaScript混淆和反混淆工具分析實戰

五、瞭解混淆手段

1.base62 編碼,其最明顯的特徵是生成的代碼以 eval(function(p,a,c,k,e,r)) 開頭;

看到這裏,上述站長工具的加密方式,就不難理解了。


這類混淆的關鍵思想在於將需要執行的代碼進行一次編碼,在執行的時候還原出瀏覽器可執行的合法的腳本,然後執行之。看上去和可執行文件的加殼有那麼點類似。Javascript 提供了將字符串當做代碼執行(evaluate)的能力,可以通過 Function 構造器、eval、setTimeout、setInterval 將字符串傳遞給 js 引擎進行解析執行。

 

無論代碼如何進行變形,其最終都要調用一次 eval 等函數。解密的方法不需要對其算法做任何分析,只需要簡單地找到這個最終的調用,改爲 console.log 或者其他方式,將程序解碼後的結果按照字符串輸出即可。

2.隱寫術
嚴格說這不能稱之爲混淆,只是將 js 代碼隱藏到了特定的介質當中。如通過最低有效位(LSB)算法嵌入到圖片的 RGB 通道、隱藏在圖片 EXIF 元數據、隱藏在 HTML 空白字符等。

比如這個聳人聽聞的議題:《一張圖片黑掉你》在圖片中嵌入惡意程序,正是使用了最低有效位平面算法。結合 HTML5 的 canvas 或者處理二進制數據的 TypeArray,腳本可以抽取出載體中隱藏的數據(如代碼)。

隱寫的方式同樣需要解碼程序和動態執行,所以破解的方式和前者相同,在瀏覽器上下文中劫持替換關鍵函數調用的行爲,改爲文本輸出即可得到載體中隱藏的代碼。

3.複雜表達式
代碼混淆不一定會調用 eval,也可以通過在代碼中填充無效的指令來增加代碼複雜度,極大地降低可讀性。Javascript 中存在許多稱得上喪心病狂的特性,這些特性組合起來,可以把原本簡單的字面量(Literal)、成員訪問(MemberExpression)、函數調用(CallExpression)等代碼片段變得難以閱讀。

舉個簡單的例子,可能會更好理解:

  1. 訪問一個對象的成員有兩種方法——點運算符和下標運算符。調用 window 的 eval 方法,既可以寫成 window.eval(),也可以 window[‘eval’];
  2. 爲了讓代碼更變態一些,混淆器選用第二種寫法,然後再在字符串字面量上做文章。先把字符串拆成幾個部分:’e’ + ‘v’ + ‘al’;
  3. 這樣看上去還是很明顯,再利用一個數字進制轉換的技巧:14..toString(15) + 31..toString(32) + 0xf1.toString(22);
  4. 一不做二不休,把數字也展開:(0b1110).toString(4<<2) + (‘ ‘.charCodeAt() - 1).toString(Math.log(0x100000000) / Math.log(2)) + 0xf1.toString(11 << 1);
  5. 最後的效果:window(2*7).toString(4<<2) + (‘ ‘.charCodeAt() - 1).toString(Math.log(0x100000000) / Math.log(2)) + 0xf1.toString(11 << 1)‘)

在 js 中可以找到許多這樣互逆的運算,通過使用隨機生成的方式將其組合使用,可以把簡單的表達式無限複雜化。

到這裏,我已經暈頭轉向了。深入瞭解,請移步使用 estools 輔助反混淆 Javascript

六、寫在最後

JavaScript 作爲一個以函數式爲核心的多範式動態弱類型腳本語言,因爲它的靈活性,導致了源代碼在經過一些壓縮工具處理後,變得極難還原。

也有可能,當我們費勁心思還原出來的代碼,也許只是與源代碼運行流程一致的另一套代碼。當然,我們可以繼續探索,發掘未知的領域。

七、瞭解更多

 

轉載者注:

在線反混淆的網站

1.http://jsnice.org/

2.http://tool.chinaz.com/js.aspx   JS混淆加密壓縮-->解密

3.https://beautifier.io/

4.http://relentless-coding.org/projects/jsdetox/    無在線版

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