操作符混淆工具
- 2019 年 12 月 4 日
- 筆記
1 序
1.1 從一段神奇的JS程式碼說起
前段時間在公眾號看到一段神奇的程式碼,它長這個樣子:
(!(~+[])+{})[--[~[]][+[]]*[~+[]]+~~!+[]]+({}+[])[[~!+[]]*~+[]]
如果把這行程式碼扔到瀏覽器裡面執行以下,就會輸出sb
字元串。

這是什麼鬼,還有這種操作?
1.2 走進科學時間
上面的程式碼由!()*+-[]{}~
這11種符號組成,其實這些符號都是JS的操作符
,而上面的程式碼在執行後轉換成字元串則是因為:
- 當操作符作用的操作數類型不一致或者不是基本類型時,JS將自動完成類型轉化;
- 不同的操作符具有不同的優先順序
將上面的程式碼按照 操作符優先順序 進行區域劃分,大致可以分為以下的幾個部分。

可以看到實際上就是應用JS的類型隱式轉換生成字元串,然後從字元串里提取想要的字元。 至於為什麼上圖的葉節點為什麼是這樣生成的值,請參照 es5.github.io/ 9 Type Conversion and Testing
2 操作符程式碼混淆器
收到前文的啟發,本人萌發了一種「操作符程式碼混淆器」的想法。也就是利用上文提及的原理,將JS程式碼混淆成全部由操作符組成的「讓人看著頭疼的程式碼」。
這意味著一些簡單的字元層面上的程式碼注入防範工作完全無法對我們的程式碼生效
,因為我們的程式碼完全由「操作符」構成,根本就不包含敏感關鍵字。
"操作符程式碼混淆器"需要解決幾個關鍵性的問題:
- 操作符生成其他字元
- 字元串組裝成可執行程式碼
2.1 數字
生成數字實際上只要有一個數字0,我們完成可以通過自增操作符++
生成數字1-9,所以我們只需要
// '數字集合' $ = +[]; // 0 $ = { _: $++, // 0 __: $++, // 1 ___: $++, // 2 ____: $++, // 3 _____: $++, // 4 $_: $++, // 5 $__: $++, // 6 $___: $++, // 7 $____: $++, // 8 $_____: $++, // 9 }
2.2 英文字元
從前文我們可以知道其邏輯是根據不同的類型轉換成字元串時可以生成不同的描述性字元串
{} + [] // '[object Object]' ![] +[] // 'false' !![] + [] // 'true' [][$._] + [] //'undefined'
從上面我們可以得到 a b c d e f i j l n o r s t u
這些字元。但是這很明顯沒辦法包含所有英文字元,同時也沒辦法表示換行符等特殊字元。
2.3 通用字元
所以我們需要一個更加通用的方案來通過操作符生成其他字元。 基於我們現在已經得到的數字字元,我們可以使用八進位的表示方式來生成其他ASCII字元。
'\' + $.__ + $._____ + $.$__ + '\' // '147' 即 g '\' + $.__ + $.$ + $._ // '150' 即 h , 以此類推
但是這也隨之帶來一個問題,那就是我們為了使用八進位來表達ASCII字元,引入了'\'
這種常量字元串,這使得整個計劃優點不完美,但是作者目前沒有想到更好的實現方式。
// '字符集合' $$ = '\'; $$ = { _: $$ + $.__ + $._____ + $.__, // 141 a __: $$ + $.__ + $._____ + $.___, // 142 b ___: $$ + $.__ + $._____ + $.____, // 143 c ____: $$ + $.__ + $._____ + $._____, // 144 d _____: $$ + $.__ + $._____ + $.$_, // 145 e $_: $$ + $.__ + $._____ + $.$__, // 146 f $__: $$ + $.__ + $._____ + $.$___, // 147 g $___: $$ + $.__ + $.$_ + $._, // 150 h $____: $$ + $.__ + $.$_ + $.__, // 151 i $_____: $$ + $.__ + $.$_ + $.___, // 152 j $$_: $$ + $.__ + $.$_ + $.____, // 153 k $$__: $$ + $.__ + $.$_ + $._____, // 154 l $$___: $$ + $.__ + $.$_ + $.$_, // 155 m $$____: $$ + $.__ + $.$_ + $.$__, // 156 n $$_____: $$ + $.__ + $.$_ + $.$___, // 157 o $$$_: $$ + $.__ + $.$__ + $._, // 160 p $$$__: $$ + $.__ + $.$__ + $.__, // 161 q $$$___: $$ + $.__ + $.$__ + $.___, // 162 r $$$____: $$ + $.__ + $.$__ + $.____, // 163 s $$$_____: $$ + $.__ + $.$__ + $._____, // 164 t $$$$_: $$ + $.__ + $.$__ + $.$_, // 165 u $$$$__: $$ + $.__ + $.$__ + $.$__, // 166 v $$$$___: $$ + $.__ + $.$__ + $.$___, // 167 w $$$$____: $$ + $.__ + $.$___ + $._, // 170 x $$$$_____: $$ + $.__ + $.$___ + $.__, // 171 y $$$$$_: $$ + $.__ + $.$___ + $.___ // 172 z }
2.4 執行容器
前文我們只是將程式碼內容轉換成了字元串的形式,這麼一個字元串還需要能夠跑起來。 好,我們的第一想法可能是eval
方法
eval() 函數可計算某個字元串,並執行其中的的 JavaScript程式碼。
假設我們已經將程式碼轉換成了字元串,但是下面的用戶調用方式未免顯得太過沒有逼格。
var codeStr = '混淆過的程式碼'; // 用戶調用 eval(codeStr)
所以我們的目標是生成的程式碼用戶可以直接扔到瀏覽器裡面開始執行,即我們需要一個可以執行的函數容器:
// 字元串集合 $$$ = { _: {} + [], // '[object Object]' __: ![] + [], // 'false' ___: !![] + [], // 'true' ____: [][$._] + [] //'undefined' } // 替換字符集, 這裡不替換的話無法根據constructor找到Function $$.___ = $$$._[$.$_]; // c $$._____ = $$$.___[$.____] // e $$.$$____ = $$$.____[$.__]; //n $$.$$_____ = $$$._[$.__]; // o $$.$$$___ = $$$.___[$.__]; // r $$.$$$____ = $$$.__[$.____]; // s $$.$$$_____ = $$$.___[$._]; // t $$.$$$$_ = $$$.____[$._]; // u $$$._____ = $$.___ + $$.$$_____ + $$.$$____ + $$.$$$____ + $$.$$$_____ + $$.$$$___ + $$.$$$$_ + $$.___ + $$.$$$_____ + $$.$$_____ + $$.$$$___; // 'constructor' $$$.$_ = $$.$$$___ + $$._____ + $$.$$$_____ + $$.$$$$_ + $$.$$$___ + $$.$$____; // 'return' $$$.$__ = ($._)[$$$$._][$$$$._] // Function // 執行容器調用方式 $$$.$__($$$.$__($$$.$_ + '"' + '這裡是經過混淆後的程式碼' + '"'())(); // 實際上就是 // Function(Function()('return "' + '這裡是經過混淆後的程式碼' + '"')())()
3 結論
通過以上實現,基本實現了一個簡單程式碼混淆工具的邏輯,可以只使用操作符對程式碼進行混淆,但依舊遺留了一些問題
- 程式碼依賴字元串,生成的程式碼也會包含字元串常量,並不是完全的「操作符化」;
- 工具的程式碼本身很難閱讀,使得維護和開發非常困難,這個工作可以依賴構建工具進行優化;
- 目前只包含了對ASCII字元的處理,對字符集以外字元的處理是有問題的;
- 本工具的應用場景具有局限性
4 相關資料
一段神奇的javascript程式碼 運算符優先順序 Annotated ECMAScript 5.1 – 9 Type Conversion and Testing jjencode JS程式碼加密混淆工具 jjencode