在使用某插件的過程中,大量個性化需求不能滿足,於是我有了更改源碼的衝動。翻遍所有角落,只找了一份壓縮混淆的 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混淆和反混淆工具分析實戰
五、瞭解混淆手段
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)等代碼片段變得難以閱讀。
舉個簡單的例子,可能會更好理解:
- 訪問一個對象的成員有兩種方法——點運算符和下標運算符。調用 window 的 eval 方法,既可以寫成 window.eval(),也可以 window[‘eval’];
- 爲了讓代碼更變態一些,混淆器選用第二種寫法,然後再在字符串字面量上做文章。先把字符串拆成幾個部分:’e’ + ‘v’ + ‘al’;
- 這樣看上去還是很明顯,再利用一個數字進制轉換的技巧:14..toString(15) + 31..toString(32) + 0xf1.toString(22);
- 一不做二不休,把數字也展開:(0b1110).toString(4<<2) + (‘ ‘.charCodeAt() - 1).toString(Math.log(0x100000000) / Math.log(2)) + 0xf1.toString(11 << 1);
- 最後的效果:window(2*7).toString(4<<2) + (‘ ‘.charCodeAt() - 1).toString(Math.log(0x100000000) / Math.log(2)) + 0xf1.toString(11 << 1)‘)
在 js 中可以找到許多這樣互逆的運算,通過使用隨機生成的方式將其組合使用,可以把簡單的表達式無限複雜化。
到這裏,我已經暈頭轉向了。深入瞭解,請移步使用 estools 輔助反混淆 Javascript
六、寫在最後
JavaScript 作爲一個以函數式爲核心的多範式動態弱類型腳本語言,因爲它的靈活性,導致了源代碼在經過一些壓縮工具處理後,變得極難還原。
也有可能,當我們費勁心思還原出來的代碼,也許只是與源代碼運行流程一致的另一套代碼。當然,我們可以繼續探索,發掘未知的領域。
七、瞭解更多
轉載者注:
在線反混淆的網站
2.http://tool.chinaz.com/js.aspx JS混淆加密壓縮-->解密