吳 芳
(陜西省紡織科學(xué)研究所,西安710038)
C/C++等編程語言開發(fā)的程序被編譯成本機可執(zhí)行的二進制代碼。編譯器用內(nèi)存地址代替了程序中的變量名稱、方法名稱等信息。因此對這些本地二進制代碼進行反編譯從而得到源代碼是非常困難,甚至是不可能的。
Java是一種跨平臺的解釋型語言。Java編譯工具將Java源代碼編譯成為class文件(即Java字節(jié)碼),由 Java 虛擬機(Java Virtual Machine,JVM)負責(zé)對class文件進行解釋執(zhí)行。與本地目標代碼不同,class文件中仍然保留了方法名稱、變量名稱,并且通過這些名稱來訪問變量和方法,這些符號往往帶有許多語義信息。因此,對class文件進行反編譯就顯得比較容易。目前,市場上有許多Java的反編譯工具,有免費的,也有商用的,還有的是開放源代碼的。這些工具都能夠從class文件生成高質(zhì)量源代碼。所以,如何保護Java程序就變成了一個非常重要的挑戰(zhàn)。
目前主要有如下幾種Java字節(jié)碼保護技術(shù):隔離Java程序、字節(jié)碼混淆、轉(zhuǎn)換本地代碼、數(shù)字水印、自定義類加載器等。
隔離Java程序是指將關(guān)鍵的class文件放在服務(wù)器端,客戶端通過訪問服務(wù)器的相關(guān)接口來獲得服務(wù),而不是直接訪問class文件。這樣黑客就沒有辦法反編譯 class文件[1]。HTTP、Web Service、RPC等標準和協(xié)議都能夠支持通過接口提供服務(wù)。但是有很多應(yīng)用都不適合這種保護方式,例如對于單機運行的程序就無法隔離Java程序。
字節(jié)碼混淆主要是通過將定義的類、變量、方法和包的名字改為無意義的字符串、使用非法的字符代替變量符號和在軟件中添加一些無關(guān)的指令或永遠執(zhí)行不到的指令[2]等手段來增加反編譯和對反編譯后源代碼閱讀的難度。但這種方法并不能真正阻止反編譯,而且不論是開源的或是商用的混淆工具都有一定的規(guī)律可循,如果掌握這些規(guī)律,在反編譯時仍然可以得到一定質(zhì)量的源代碼。
轉(zhuǎn)換本地代碼即將Java程序像C/C++程序一樣編譯成本機可執(zhí)行的二進制代碼,目前TowerJ、jexegen、JET、JOVE、JToEXE 等工具都能做到這一點。但是這樣做使得Java程序失去其跨平臺的特性,而且這種技術(shù)目前并不十分成熟,因此不適用于大型應(yīng)用程序。
數(shù)字水印技術(shù)是在class文件中嵌入以數(shù)字水印形式存在的開發(fā)者簽名[3]。數(shù)字水印技術(shù)并不能阻止反編譯,但是能在確認某些程序是否屬于剽竊時提供有效的證據(jù)。
自定義類加載器是指首先將class文件進行加密處理,然后自己編寫一個Java類裝載器(即繼承java.lang.ClassLoader并重載其 loadClass方法)在class文件裝載時再進行解密處理。這種方法的缺點在于雖然經(jīng)過加密的class文件無法被反編譯,但自定義的類加載器本身卻不能防止被反編譯。因此,加密過的class文件仍然是不安全的。
另外,一些商用的專業(yè)數(shù)據(jù)加密軟件也能提供對Java字節(jié)碼的保護,如HASP、CodeMeter等,其原理是對Java程序和java.exe都進行加殼處理,在Java程序運行時需要連接加密狗進行解密。這種方法雖然安全性較高但是對于不使用java.exe來運行的Java程序(如Eclipse平臺使用的是javaw.exe)則無能為力。因此通用性較差。
通過第一節(jié)對Java字節(jié)碼保護現(xiàn)狀的分析可以得出結(jié)論:要找到一種更好的Java字節(jié)碼保護方法則必須滿足以下3個條件:
·能夠有效防止反編譯,使得Java程序的安全級別相當于本地應(yīng)用程序。
·保持Java程序的跨平臺特性。
·不修改JVM。
將class文件作為數(shù)據(jù)文件進行加密是比較容易的(關(guān)于數(shù)據(jù)加密的算法很多,在此不再贅述),實現(xiàn)了這一步即可阻止對class文件的直接反編譯。但問題的關(guān)鍵在于何時以何種方式將加密過的class文件安全地解密?首先來分析一下JVM的類加載機制。
Java語言是一種動態(tài)性的解釋型語言。Java編譯器將每個類和接口都編譯成一個單獨的class文件,這些文件對于JVM來說就是一個個可以動態(tài)加載的單元,這些文件只有在需要使用時才會被加載。當指定程序運行的時候,JVM就將class文件按照需求和一定的規(guī)則加載到JVM的內(nèi)存,并組織成為一個完整的Java應(yīng)用程序。
JVM初始化類加載器的過程如圖1所示。
圖1 JVM類加載器的初始化
JVM被激活后,會產(chǎn)生第一個類加載器BootstrapClassLoader,BootstrapClassLoader 是采用 C++語言編寫的本地代碼。BootstrapClassLoader首先加載Launcher.java之中的 ExtClassLoader,并設(shè)定其Parent為 null,代表其父加載器為 BootstrapClass-Loader。然后 BootstrapClassLoader再要求加載Launcher.java之中的AppClassLoader,并設(shè)定其Parent為之前產(chǎn)生的ExtClassLoader實例。在這三個類加載器中 BootstrapClassLoader和 ExtClassLoader用來加載JVM的系統(tǒng)類,AppClassLoader用來加載用戶自己的類。AppClassLoader首先通過其findclass方法查找需要加載的class文件,然后調(diào)用defineclass方法將class文件的內(nèi)容轉(zhuǎn)換成Class對象。AppClassLoader的defineclass方法最終調(diào)用的是其超類ClassLoader中的本地方法defineclass1。defineclass1方法的第二個參數(shù)就是class文件的內(nèi)容。
通過上述分析,如果有一種Hook機制能夠?qū)VM調(diào)用本地方法defineclass1的事件截獲,并在Hook函數(shù)中利用本地方法實現(xiàn)對class文件內(nèi)容的解密操作,然后將解密后的字節(jié)碼傳遞給JVM,這樣就能使解密過程僅在內(nèi)存中進行,從而實現(xiàn)對加密后的class文件的安全解密。
JNI的全稱是 Java Native Interface[4](Java 本地接口)。JNI是JDK的一部分,用于為Java提供本地代碼接口。JNI使得運行在JVM虛擬機上的Java代碼能夠操作使用其它語言編寫的應(yīng)用程序和庫,如C/C++以及匯編語言等。JNI在Java程序和C/C++程序間的調(diào)用原理如圖2所示。
圖2 JNI在Java程序和C/C++程序間的調(diào)用原理
通過JNI中提供的接口,本地代碼可以與JVM進行交互,其中有一個名為RegisterNatives的接口,這個接口可以注冊與某個類的方法關(guān)聯(lián)的本地代碼。
JVMTI的全稱是Java Virtual Machine Tool Interface[5](Java 虛擬機工具接口),它提供了一種監(jiān)視JVM狀態(tài)及控制JVM執(zhí)行的方法:JVMTI客戶端(又稱為代理或Agent)。JVMTI客戶端能夠從JVM監(jiān)聽到感興趣的事件。用戶可以通過JVM的-agentlib參數(shù)向JVM注冊JVMTI客戶端。下面對所要涉及的JVMTI函數(shù)和事件做簡要說明。
·Agent_OnLoad、Agent_OnUnload函數(shù):JVM 與代理交互的入口和出口函數(shù)。
·SetEventNotificationMode函數(shù):該函數(shù)可以向JVM注冊要監(jiān)聽的事件。
·SetEventCallbacks函數(shù):該函數(shù)可以為用SetEventNotificationMode注冊過的監(jiān)聽事件指定Hook函數(shù)。
·VMInit事件:JVM的初始化事件,這個事件的發(fā)生表示JVM初始化的完成。在該事件完成后,JVMTI客戶端就可以得到JNI和JVMTI環(huán)境。
通過對 JNI和 JVMTI的分析可知只要采用JVMTI技術(shù)編寫合適的JVMTI客戶端就能夠監(jiān)聽到class文件的加載事件;然后向該事件掛接一個采用JNI技術(shù)編寫事件的回調(diào)函數(shù),在回調(diào)函數(shù)中對字節(jié)碼進行解密處理,這樣就可以在JVM加載字節(jié)碼的時候利用本地代碼將其解密。
由于JVMTI客戶端和JNI回調(diào)函數(shù)都是本地代碼,并且解密的動作在JVM的內(nèi)存中完成,不在本地保留解密后的字節(jié)碼,因此解密動作的安全級別與本地代碼相當。
由于JNI和JVMTI都是JVM提供的機制,與平臺無關(guān),即JVMTI客戶端和 JNI回調(diào)函數(shù)在支持JVM的平臺上都可以實現(xiàn),所以這種方法能夠保持Java程序的跨平臺特性。
當JVM加載一個JVMTI客戶端時會自動調(diào)用Agent_OnLoad函數(shù),如果在這個函數(shù)中向JVM注冊VMInit事件及回調(diào)函數(shù),那么JVM初始化后JVMTI客戶端就會監(jiān)聽到VMInit事件并進入該事件的回調(diào)函數(shù)。在回調(diào)函數(shù)中利用JNI提供的RegisterNatives函數(shù)將ClassLoader.defineclass1方法注冊為自定義的一個代理函數(shù),這樣就可以攔截 JVM對ClassLoader.defineclass1方法的調(diào)用。當JVM在為了生成某個類的Class對象而調(diào)用ClassLoader.defineclass1方法時,它會調(diào)用自定義的代理函數(shù),在代理函數(shù)中實現(xiàn)對字節(jié)碼的解密。
JVMTI客戶端和JNI回調(diào)函數(shù)在支持JVM的平臺上都可以實現(xiàn),這里僅以Windows平臺和Linux平臺為例說明。
在Windows平臺上JVMTI客戶端以動態(tài)鏈接庫的形式出現(xiàn)。支持創(chuàng)建動態(tài)鏈接庫的開發(fā)工具很多,這里以Microsoft Visual C++為例說明。
首先創(chuàng)建一個動態(tài)鏈接庫項目,并將JDK中JVMTI和 JNI相關(guān)的頭文件(jvmti.h、jni.h、jni_md.h)引入到項目中以建立支持JVMTI和JNI的編程環(huán)境;然后創(chuàng)建Agent_OnLoad函數(shù),在該函數(shù)中通過GetEnv獲取JVMTI環(huán)境并向JVM預(yù)定VMInit事件的通知并設(shè)置VMInit事件的回調(diào)函數(shù),在回調(diào)函數(shù)中利用RegisterNatives將自定義的本地代理方法注冊到ClassLoader.defineclass1上;最后完成自定義的本地代理方法,在該方法中實現(xiàn)對字節(jié)碼的解密。解密后的字節(jié)碼存儲在JVM的內(nèi)存中,此時再調(diào)用java.dll中的_Java_java_lang_ClassLoader_define-Class1@32方法將解密后的字節(jié)碼轉(zhuǎn)換為Class對象并將這個對象作為Agent_OnLoad的返回值返回給JVM。
在運行加密過的class文件時,必須在JVM的運行參數(shù)中通過-agentlib參數(shù)指定JVMTI客戶端。
在windows平臺的源代碼基礎(chǔ)上做如下修改即可實現(xiàn)JVMTI客戶端向Linux平臺的移植:
(1)由于在Windows平臺從動態(tài)鏈接庫中調(diào)用函數(shù)需要LoadLibrary、GetProcAddress和FreeLibrary三個庫函數(shù),而g++編譯器不支持這三個庫函數(shù),所以需要將windows.h頭文件改為dlfcn.h頭文件,然后分別使用dlopen、dlsym和dlclose這三個庫函數(shù),具體實現(xiàn)和Windows平臺類似。
(2)利用g++編譯代碼時,需要使用shared參數(shù),這樣將代碼編譯成動態(tài)鏈接庫,利用-o libXXX.so輸出代理庫,其中輸出名稱的后綴為lib和.so,XXX為自定義名稱。
(3)在程序運行前,必須使用 set命令設(shè)置L-D_LIBRARY_PATH變量為代理庫的路徑。
和Windows平臺一樣,在運行加密過的class文件時,必須在JVM的運行參數(shù)中通過-agentlib參數(shù)指定JVMTI客戶端。
基于JVMTI的Java字節(jié)碼保護技術(shù)及其實現(xiàn)方案在保持Java程序跨平臺特性和不修改JVM的前提下達到了有效防止反編譯的目標。當然這種技術(shù)并非無懈可擊,采用極端的方式仍然可以對JVMTI客戶端程序進行破解,破解的難度取決于JVMTI客戶端采用的加密算法的強度。但正如Alex Kalinovsky所說[6]:成功的保護機制的關(guān)鍵是讓它復(fù)雜到95%的典型用戶很難破解,并迫使其余5%有經(jīng)驗的破解者花費大量的時間來破譯。換句話來說,目標是讓購買許可比花費在破解保護上的成本顯得更加劃算。
該方案在JVMTI客戶端程序?qū)VM類加載器執(zhí)行效率的影響方面沒有給出詳盡的分析或數(shù)據(jù),但可以肯定的是這種影響是存在的,其影響程度主要取決于解密算法的時間復(fù)雜度。這個問題值得相關(guān)工程技術(shù)人員進一步研究和探討。
[1] 歐陽辰.如何保護Java程序[EB/OL].2004-12-02.http://www.infosecurity.org.cn/article/websec/java/index.html.
[2] 陳剛.基于封裝包的Java源代碼安全保護[J].電子信息與對抗技術(shù),2006,21(3):45-48.
[3] 陳晗,趙軼群,繆亞波.Java字節(jié)碼的水印嵌入[J].微型電腦應(yīng)用,2004,20(20):56-58.
[4] Sun Microsystems.Java Native Interface Specification[EB/OL].http://java.sun.com/javase/6/docs/technotes/guides/jni/spec/niTOC.html.2007-06-20.
[5] Sun Microsystems.JVM(TM)Tool Interface 1.1.102[EB/OL].2007-06-20.http://java.sun.com/javase/6/docs/platform/jvmti/jvmti.html.
[6] Alex Kalinovsky,著.透視 JAVA[M].劉凌,周哲海,譯.北京:清華大學(xué)出版社,2005.