作者:JShaman.com:w2sft
內(nèi)容預(yù)告:
本文將實例講解以下JS代碼混淆加密技術(shù):
方法名轉(zhuǎn)義和轉(zhuǎn)碼、成員表達式轉(zhuǎn)IIFE、函數(shù)標(biāo)準(zhǔn)化、數(shù)值混淆、布爾型常量值混淆、二進制表達式轉(zhuǎn)為調(diào)用表達式、字符串轉(zhuǎn)Unicode、局部變量變形、屏蔽輸出語句,以及:無限斷點、時間差檢測等反調(diào)試方案。
大綱:
理論層面:為什么要對JS代碼進行混淆加密?
技術(shù)層面:用JS編程實現(xiàn)對JS代碼混淆加密。
防逆向措施:檢測與對抗。
專業(yè)的混淆加密:JShaman。
彩蛋:字節(jié)碼加密技術(shù)。
理論層面:為什么要對JS代碼進行混淆加密?
1、問:JS代碼需要考慮安全性嗎?
答:當(dāng)然。
2、問:為什么?
答:JS因為應(yīng)用環(huán)境需要,功能設(shè)計目的等歷史原因,成為了一種代碼公開透明的語言。
前端JS代碼,直接暴露在瀏覽器中,任何訪問者,都可以隨意查看代碼。這就導(dǎo)致代碼可以被分析、復(fù)制、盜用等,進而引發(fā)安全問題,如被利用代碼bug攻擊、揭露功能邏輯、復(fù)制出雷同應(yīng)用等等。
互聯(lián)網(wǎng)早些年,安全場景如上。而發(fā)展到當(dāng)下,JS的應(yīng)用范圍更加廣泛,如NodeJS的興起,使很多后端服務(wù)、產(chǎn)品、項目也應(yīng)用了JS。
在后端的角度,如果項目或產(chǎn)品,提交給第三方時,是否要交出源碼?顯然不妥。
假設(shè)服務(wù)器被入侵,如果部署的后端服務(wù)產(chǎn)品源碼也是JS明文,那將導(dǎo)致更嚴重的安全問題。
更多的應(yīng)用領(lǐng)域,如小程序開發(fā)、H5應(yīng)用,含ThreeJS引擎類游戲,等,都廣泛應(yīng)用了JS。
在所有這些場景中,都不應(yīng)該忽視JS代碼的安全問題,都應(yīng)該且需要對JS代碼進行保護。
3、問:如何讓JS代碼變的安全?
答:對JS代碼進行保護:混淆&加密,使代碼不可讀。即:它人依然可以看到代碼,但看到的是加密的代碼、無法理解代碼,更無法修改。
深入并精準(zhǔn)的說:通過混淆加密,使代碼變的難以閱讀和理解。可能有人說,混淆后機器能執(zhí)行,人就能理解,只是需要的時間長短問題。這種極端的說法,從理論上來說沒錯,如果可以投入足夠長的時間,程序員甚至可以直接用0101寫代碼。而從實際角度而言,一段代碼如果保護后分析需要的時長,超過開發(fā)需要的時長,保護的目的就達到了,就會勸退99.9999%對它有想法的正常人類。
理論已探討完畢,接下來步入正題,探索如何對JS代碼進行混淆加密,可不僅僅是應(yīng)用層面,而是全面掌握:會用、知其然,知其所在然,還要動手編碼,實現(xiàn):用JS對JS代碼混淆加密。
接下來的內(nèi)容,將在NodeJS環(huán)境中,使用JS編程,實現(xiàn)對JS代碼的混淆加密。
技術(shù)層面:用JS編程實現(xiàn)對JS代碼混淆加密。
技術(shù)理論:如何實現(xiàn)?
確定實現(xiàn)方案之前,首先需要排除幾種不可用方案:
- Eval思路不可用:可以被下斷點調(diào)試或API HOOK,而輕松還原出原始代碼。
- 可逆加密方式不可用:加密方式可逆,則必然有解密函數(shù),只需定位于解密出口,即可得到原始代碼。
- 異步代碼獲取并執(zhí)行不可用:同樣可被調(diào)試或hook,得到代碼。
- 可取的方式:代碼混淆+數(shù)據(jù)加密。
混淆原理:非replace或regexp方式字符串替換,而是對JS源碼進行重編譯。從源碼,進行詞法分析、語法分析、得到AST(抽象語法樹),此處是重點,得到AST后,在AST中執(zhí)行關(guān)鍵混淆加密操作,如:字符算陣列化、字符加密、平展控制流、僵尸代碼值入、反調(diào)試埋雷、花指令插入等,最后,再將AST重建為JS代碼。這樣就得到了一份被更改的面目全非的安全JS代碼:不可讀、不可理解、不可修改、不可還原。
編程現(xiàn)實:用JS對JS代碼混淆加密。
由以上的理論可知,重點是混淆加密,而入口點及整體流程框架是AST操作。
JS代碼&AST。
在JS引擎之下,代碼編譯執(zhí)行大體流程是:
JS代碼→AST(抽象語法樹)→ByteCode(字節(jié)碼)→機器碼→解釋器→執(zhí)行。
AST設(shè)計之初并不是用于對JS代碼混淆加密,但AST卻很適合這個事情。
基于AST的JS代碼混淆加密大體流程:
JS代碼→AST→(基于AST的混淆加密)→JS代碼。
題外話:能在ByteCode階段進行加密嗎?某些情況下可以,比如NodeJS環(huán)境中的JS代碼,可以編譯為ByteCode。但在前端運行的JS代碼,且于DOM有交互的則不理想,小總結(jié)而言,有將JS代碼進行VM式的加密方法,但通用性較差,使用起來復(fù)雜。因為這些弊端,因此,不是普遍性的JS代碼保護方案。
注:在本結(jié)尾,會有一個彩蛋內(nèi)容,實例介紹NodeJS字節(jié)碼生成及運行。
圖1,NodeJS字節(jié)碼效果:
回到正題,JS代碼如何轉(zhuǎn)化為AST?
其實,沒有想象中那么復(fù)雜。得益于NodeJS成熟的生態(tài),已經(jīng)有多個已實現(xiàn)模塊可以完成這一操作。比較流行的如:esprima、babel,都可以實現(xiàn)對JS代碼進行詞法分析、語法分析、生成AST、AST操作、從AST再生成JS代碼。
用esprima進行JS代碼混淆加密。程序框架。
圖2、esprima框架demo:
如圖2所示,使用esprima進行JS代碼保護的原始功能框架。
代碼介紹:
Esprima實現(xiàn)將JS代碼轉(zhuǎn)化為AST;
estraverse對AST節(jié)點進行遍歷,混淆加密的邏輯操作都將在此環(huán)節(jié)實現(xiàn);
escodegen則是將操作后的AST轉(zhuǎn)為JS代碼輸出。
此demo代碼未對AST進行任何處理,所以圖中右側(cè)的執(zhí)行結(jié)果中可以看到,輸出的JS代碼與最初代碼完全一致。
AST是這樣子的。
前面已經(jīng)對AST進行了說明,AST具體是什么樣?
一個方便的辦法,是使用astexplorer.net,可以對輸入的代碼的AST即時同步顯示:
圖3、const a=1的AST:
Demo中使用的一行JS語句:“const a=1”,其AST即如圖中所顯示。
AST是一個JSON結(jié)構(gòu)。
Program表示程序,子節(jié)點body中,是變量定義kind是“const”,字面量是“a”,值是“1”。
看似雜亂,但很規(guī)整,細看便不難理解。
demo程序里,在節(jié)點操作處可以用console輸出AST,與astexplorer輸出一至,不過前者更方便些。
圖4、在程序中輸出AST:
借助Esprima修改AST實例:改“==”為“===”。
圖5:
代碼如上圖,這是一個很簡單的示例。
程序中,estraverse對示例代碼結(jié)點進行處理,當(dāng)匹配到“==”時,改為“===”。
為了明確修改節(jié)點細節(jié),再對前后代碼進行分析。由圖6、圖7看到,差異僅在節(jié)點中的operator。
圖6、代碼中使用“==”:
圖7、代碼中使用“===”:
借助Esprima修改AST實例:把parseInt改為標(biāo)準(zhǔn)語法。
parseInt方法,有兩個參數(shù),參數(shù)一是要轉(zhuǎn)化的值,參數(shù)二是可選擇項,是要轉(zhuǎn)化的進制類型。
圖8:
通過astexplorer,先了解parseInt的AST,未使用參數(shù)二時,AST如下:
圖9:
如果有第二參數(shù),則AST如下:
圖10:
那么,要將parseInt轉(zhuǎn)為標(biāo)準(zhǔn)形式即是要給只有一個參數(shù)的調(diào)用增加第二參數(shù)。
代碼及執(zhí)行結(jié)果如下:
圖11:
因為初入手的原因,以上描述較為細致,后續(xù)將簡化。
方法名轉(zhuǎn)義和轉(zhuǎn)碼。
如:console.log轉(zhuǎn)為console[log]形式。
通過在aspexplorer中比較可知,造成語句形式差異的原因是CallExpression成員中computed屬性值的不同。
圖12:
那么,只需修改節(jié)對應(yīng)節(jié)點的computed屬性值即可:
圖13:
而修改的條件,則是判斷AST節(jié)點是CallExpression。上面的例子中,也是使用相似的條件判斷方法,找出要修改內(nèi)容相對應(yīng)的AST節(jié)點。
再進一步,將方法名轉(zhuǎn)為十六進制字符,console[log]會成為:console['x6cx6fx67'],以此進一步降低代碼可讀性。
圖14、增加字符串轉(zhuǎn)16進制操作:
例程代碼輸出為:
圖15:
運行混淆后的代碼:
圖16:
從簡單的例程,可以初步學(xué)習(xí)到對AST的操作方法。
接下來,實現(xiàn)一個有點難度的功能。
成員表達式轉(zhuǎn)為IIFE
成員表達式通常指調(diào)用對象的成員,例如 console 對象的 log 成員。
IIFE,全稱為:Immediately Invoked Function Expression,在JAVAScript編程中,是指:立即調(diào)用函數(shù)表達式。
為了方便理解,先展示此功能實現(xiàn)后的效果:
圖17:
如上圖中,console的log、warn、error方法,以及字符串的toUperCase()方法,在保護后都會成為匿名自執(zhí)行的函數(shù),且方法名都以數(shù)組化的形式被另外存放,代碼相比之前混亂了許多。
圖18、IIFE代碼執(zhí)行效果:
實現(xiàn)方法如下:
主架構(gòu)與之前略有差異,traverse方法改為replace,enter事件改為leave事件,如下圖:
圖19:
對變量定義結(jié)點,如console.log輸出的信息,以及成員函數(shù),如console的log方法進行操作。
圖20、改寫字符串定義、成員函數(shù)調(diào)用:
Add_string函數(shù)把字符串信息、方法名,都寫入到一個新的字符串?dāng)?shù)組,并且把方法改為IIFE。
字符串?dāng)?shù)組建立、方法改為IIFE的具體實現(xiàn)如下圖:
圖21:
然后,把新增的數(shù)組加入到AST中,最重再重建代碼:
圖22:
這樣就完成了本例功能。
注:本例僅供功能演示,尚有不嚴謹?shù)倪壿嫞热绯蓡T方法IIFE化之前,除應(yīng)該判斷node.type為MemberExpression,還應(yīng)排除節(jié)點computed為true的情況,否則代碼執(zhí)行會發(fā)生錯誤。
正如前文中所述,能對AST進行操作的模塊不止esprima,babel也是個很好的選擇。
接下來的例子,將使用babel來完成。
Babel的使用方式與esprima極為相似,其代碼框架如下:
圖23:
同樣是:JS代碼→AST→節(jié)點處理→JS代碼。
用Babel修改AST實例:去除代碼中的console.log輸出語句。
代碼如下圖所示:
圖24、用Babel在AST中去除console.log節(jié)點:
匹配AST中的成員操作節(jié)點,且滿足條件callee的對像名為console,屬性方法名為log,如檢測掉,則remove該節(jié)點。
運行效果如下圖所示,測試代碼中含有console.log,修改后輸出中已經(jīng)被去除。
圖25:
嚴謹?shù)目紤]的話,需要注意對象掛載的識別,如global.console.log,此時remove則會剩下global,將導(dǎo)致語法錯誤,因此還應(yīng)該判斷父節(jié)點類型來排除這種情況。
指定局部變量變形
圖26、對min、number兩個局部變量變形:
相當(dāng)于是可設(shè)定、可配置的對某些變量進行變形。
反向思考,也可以排除對某些變量的處理,等同于白名單,類似于JShaman平臺中的“保留字”功能。
刪除代碼中的空行。
圖27:
EmptyStatement表示空語句AST節(jié)點。
字符串轉(zhuǎn)Unicode。
圖28:
代碼及執(zhí)行結(jié)果如上圖,原理為:判斷節(jié)字符串字面量節(jié)點是否為Unicode格式,如不是則轉(zhuǎn)為Unicode。
在這幾個例子中,可看到與esprima的差異,esprima使用的是enter、leave方法,Babel中是直接對要處理的節(jié)點類型操作,如上圖中的StringLiteral。
更條理化的寫法,上面的代碼可以修改如下,這個方法被稱為Babel-plugin(插件):
圖29:
二進制表達式轉(zhuǎn)為調(diào)用表達式
即BinaryExpression節(jié)點轉(zhuǎn)為CallExpression。
先看效果:
圖30,左側(cè)為二進制表達式,右側(cè)為調(diào)用表達式:
二進制表達式AST形式:
實現(xiàn)代碼:
圖31:
調(diào)用表達式AST形式:
圖32:
代碼中的這部分,即是將二進制表達式轉(zhuǎn)化為調(diào)用表達式:
圖33:
布爾型常量值混淆
代碼及效果如下圖:
圖34:
數(shù)值混淆
代碼及效果如下圖:
圖35:
JS代碼混淆加密,雖不至博大精深,但也屬于高段位技術(shù)。
在此分享部分淺顯方案,以展現(xiàn)其實用效果,用于說明混淆加密手段對于JS代碼加固的有效性。此外,還有更多高端的防護手段,如JShaman應(yīng)用的:平展控制流、時間限制、域名鎖定、僵尸代碼植入等。
圖36、JShaman的JS代碼保護配置功能:
防逆向措施:檢測與對抗。
對JS代碼進行混淆加密之后,代碼安全度得到相當(dāng)?shù)募訌姡€能更進一步,為了防止不法者進行逆向分析、破解,可在代碼中加入防破解對抗功能。這也是被JShaman應(yīng)用的方案。
- 無限斷點。
JS當(dāng)中有一個debugger指令,當(dāng)處于調(diào)試工具中時,如在瀏覽器中,會形成斷點,使調(diào)試中斷,利用此特性,在程序中加入無限的debugger,可使代碼無法被調(diào)試。
圖37、每100毫秒一個斷點:
瀏覽器中執(zhí)行效果如下,當(dāng)打開“調(diào)試器”時,程序會不停的中斷,導(dǎo)致無法跟蹤代碼:
圖38:
時間差檢測。
代碼及執(zhí)行效果如下圖所示:
圖39:
檢測原理是:
在代碼中加入console.log輸出和console.clear語句,未在調(diào)試工具中時,這兩句代碼執(zhí)行是不需渲染顯示的,執(zhí)行耗時短,但假如在瀏覽器中打開了開發(fā)者工具,則會因為顯示輸出并清除的操作而消耗較多時間,這會被程序察覺出耗時異常,從而檢測出是在被調(diào)試。
專業(yè)的混淆加密:JShaman
本文講述了部分JS代碼混淆加密技術(shù)及實現(xiàn),更多更專業(yè)的防護方案未有盡述,這里再展示一段經(jīng)JShaman保護的代碼,領(lǐng)略專業(yè)級的JS代碼安全。
圖40、測試代碼準(zhǔn)備進行混淆加密:
圖41、保護選項設(shè)置:
圖42、混淆加密后的JS代碼:
彩蛋:字節(jié)碼加密技術(shù)。
提示:JS字節(jié)碼(ByteCode)加密技術(shù),理論可行,但通用性較差,在此僅做技術(shù)介紹,不推薦做為項目或產(chǎn)品正式使用方案。
- 字節(jié)碼生成
在NodeJS中將JS代碼生成字節(jié)碼,方法很簡單,需借助google的V8引擎,V8引擎內(nèi)置有JS虛擬機。通過v8虛擬機,將JS代碼編譯為字節(jié)碼。全程僅需十幾行代碼,如下圖:
圖43:
關(guān)鍵處是cachedData,即字節(jié)碼。
- 運行字節(jié)碼
V8虛擬機是能夠識別和直接運行該字節(jié)碼的。
代碼如下,如同創(chuàng)建字節(jié)碼一樣簡單。
圖44:
生成的字節(jié)碼是非文本形式的,如強行打開,內(nèi)容如下圖:
圖45、字節(jié)碼文件內(nèi)容:
JS字節(jié)碼生成并運行效果如下:
圖46:
代碼改變世界,獻給廣大JS開發(fā)者。全文結(jié)束,感謝閱讀。