天天看點

如何保護Java程式

作者:◇ 歐陽辰

Java是一種跨平台的、解釋型語言。Java 源代碼編譯中間“位元組碼”存儲于class檔案中。Class檔案是一種位元組碼形式的中間代碼,該位元組碼中包括了很多源代碼的資訊,例如變量名、方法名等。是以,Java中間代碼的反編譯就變得非常容易。目前市場上有許多免費的、商用的反編譯軟體,都能夠生成高品質的反編譯後的源代碼。是以,對開發人員來說,如何保護Java程式就變成了一個非常重要的挑戰。本文首先讨論了保護Java程式的基本方法,然後對代碼混淆問題進行深入研究,最後結合一個實際的應用程式,分析如何在實踐中保護Java程式。 

  反編譯成為保護Java程式的最大挑戰 

  通常C、C++等程式設計語言開發的程式都被編譯成目标代碼,這些目标代碼都是本機器的二進制可執行代碼。通常所有的源檔案被編譯、連結成一個可執行檔案。在這些可執行檔案中,編譯器删除了程式中的變量名稱、方法名稱等資訊,這些資訊往往是由記憶體位址表示,例如如果需要使用一個變量,往往是通過這個變量的位址來通路的。是以,反編譯這些本地的目标代碼就是非常困難的。 

  Java語言的出現,使得反編譯變得非常容易而有效。原因如下:1.由于跨平台的需求,Java的指令集比較簡單而通用,較容易得出程式的語義資訊;2.Java編譯器将每一個類編譯成一個單獨的檔案,這也簡化了反編譯的工作;3.Java 的Class檔案中,仍然保留所有的方法名稱、變量名稱,并且通過這些名稱來通路變量和方法,這些符号往往帶有許多語義資訊。由于Java程式自身的特點,對于不經過處理的Java程式反編譯的效果非常好。 

  目前,市場上有許多Java的反編譯工具,有免費的,也有商業使用的,還有的是開放源代碼的。這些工具的反編譯速度和效果都非常不錯。好的反編譯軟體,能夠反編譯出非常接近源代碼的程式。是以,通過反編譯器,黑客能夠對這些程式進行更改,或者複用其中的程式。是以,如何保護Java程式不被反編譯,是非常重要的一個問題。 

  常用的保護技術 

  由于Java位元組碼的抽象級别較高,是以它們較容易被反編譯。本節介紹了幾種常用的方法,用于保護Java位元組碼不被反編譯。通常,這些方法不能夠絕對防止程式被反編譯,而是加大反編譯的難度而已,因為這些方法都有自己的使用環境和弱點。 

  隔離Java程式 

  最簡單的方法就是讓使用者不能夠通路到Java Class程式,這種方法是最根本的方法,具體實作有多種方式。例如,開發人員可以将關鍵的Java Class放在伺服器端,用戶端通過通路伺服器的相關接口來獲得服務,而不是直接通路Class檔案。這樣黑客就沒有辦法反編譯Class檔案。目前,通過接口提供服務的标準和協定也越來越多,例如 HTTP、Web Service、RPC等。但是有很多應用都不适合這種保護方式,例如對于單機運作的程式就無法隔離Java程式。這種保護方式見圖1所示。 

如何保護Java程式

  圖1隔離Java程式示意圖 

  對Class檔案進行加密 

  為了防止Class檔案被直接反編譯,許多開發人員将一些關鍵的Class檔案進行加密,例如對注冊碼、序列号管理相關的類等。在使用這些被加密的類之前,程式首先需要對這些類進行解密,而後再将這些類裝載到JVM當中。這些類的解密可以由硬體完成,也可以使用軟體完成。 

  在實作時,開發人員往往通過自定義ClassLoader類來完成加密類的裝載(注意由于安全性的原因,Applet不能夠支援自定義的ClassLoader)。自定義的ClassLoader首先找到加密的類,而後進行解密,最後将解密後的類裝載到JVM當中。在這種保護方式中,自定義的ClassLoader是非常關鍵的類。由于它本身不是被加密的,是以它可能成為黑客最先攻擊的目标。如果相關的解密密鑰和算法被攻克,那麼被加密的類也很容易被解密。這種保護方式示意圖見圖2。 

如何保護Java程式

  圖2 對Class檔案進行加密示意圖 

  轉換成本地代碼 

  将程式轉換成本地代碼也是一種防止反編譯的有效方法。因為本地代碼往往難以被反編譯。開發人員可以選擇将整個應用程式轉換成本地代碼,也可以選擇關鍵子產品轉換。如果僅僅轉換關鍵部分子產品,Java程式在使用這些子產品時,需要使用JNI技術進行調用。 

  當然,在使用這種技術保護Java程式的同時,也犧牲了Java的跨平台特性。對于不同的平台,我們需要維護不同版本的本地代碼,這将加重軟體支援和維護的工作。不過對于一些關鍵的子產品,有時這種方案往往是必要的。 

  為了保證這些本地代碼不被修改和替代,通常需要對這些代碼進行數字簽名。在使用這些本地代碼之前,往往需要對這些本地代碼進行認證,確定這些代碼沒有被黑客更改。如果簽名檢查通過,則調用相關JNI方法。這種保護方式示意圖見圖3。 

  代碼混淆 

如何保護Java程式

  圖3 轉換成本地代碼示意圖 

  代碼混淆是對Class檔案進行重新組織和處理,使得處理後的代碼與處理前代碼完成相同的功能(語義)。但是混淆後的代碼很難被反編譯,即反編譯後得出的代碼是非常難懂、晦澀的,是以反編譯人員很難得出程式的真正語義。從理論上來說,黑客如果有足夠的時間,被混淆的代碼仍然可能被破解,甚至目前有些人正在研制反混淆的工具。但是從實際情況來看,由于混淆技術的多元化發展,混淆理論的成熟,經過混淆的Java代碼還是能夠很好地防止反編譯。下面我們會詳細介紹混淆技術,因為混淆是一種保護Java程式的重要技術。圖4是代碼混淆的示意圖。 

如何保護Java程式

  圖4 代碼混淆示意圖 

  幾種技術的總結 

  以上幾種技術都有不同的應用環境,各自都有自己的弱點,表1是相關特點的比較。 

  混淆技術介紹 

  表1 不同保護技術比較表 

如何保護Java程式

  到目前為止,對于Java程式的保護,混淆技術還是最基本的保護方法。Java混淆工具也非常多,包括商業的、免費的、開放源代碼的。Sun公司也提供了自己的混淆工具。它們大多都是對Class檔案進行混淆處理,也有少量工具首先對源代碼進行處理,然後再對Class進行處理,這樣加大了混淆處理的力度。目前,商業上比較成功的混淆工具包括JProof公司的1stBarrier系列、Eastridge公司的JShrink和4thpass.com的SourceGuard等。主要的混淆技術按照混淆目标可以進行如下分類,它們分别為符号混淆(Lexical Obfuscation)、資料混淆(Data Obfuscation)、控制混淆(Control Obfuscation)、預防性混淆(Prevent Transformation)。 

  符号混淆 

  在Class中存在許多與程式執行本身無關的資訊,例如方法名稱、變量名稱,這些符号的名稱往往帶有一定的含義。例如某個方法名為getKeyLength(),那麼這個方法很可能就是用來傳回Key的長度。符号混淆就是将這些資訊打亂,把這些資訊變成無任何意義的表示,例如将所有的變量從vairant_001開始編号;對于所有的方法從method_001開始編号。這将對反編譯帶來一定的困難。對于私有函數、局部變量,通常可以改變它們的符号,而不影響程式的運作。但是對于一些接口名稱、公有函數、成員變量,如果有其它外部子產品需要引用這些符号,我們往往需要保留這些名稱,否則外部子產品找不到這些名稱的方法和變量。是以,多數的混淆工具對于符号混淆,都提供了豐富的選項,讓使用者選擇是否、如何進行符号混淆。 

  資料混淆 

如何保護Java程式

  圖5 改變資料通路 

  資料混淆是對程式使用的資料進行混淆。混淆的方法也有多種,主要可以分為改變資料存儲及編碼(Store and Encode Transform)、改變資料通路(Access Transform)。 

  改變資料存儲和編碼可以打亂程式使用的資料存儲方式。例如将一個有10個成員的數組,拆開為10個變量,并且打亂這些變量的名字;将一個兩維數組轉化為一個一維數組等。對于一些複雜的資料結構,我們将打亂它的資料結構,例如用多個類代替一個複雜的類等。 

  另外一種方式是改變資料通路。例如通路數組的下标時,我們可以進行一定的計算,圖5就是一個例子。 

  在實踐混淆進行中,這兩種方法通常是綜合使用的,在打亂資料存儲的同時,也打亂資料通路的方式。經過對資料混淆,程式的語義變得複雜了,這樣增大了反編譯的難度。 

  控制混淆 

  控制混淆就是對程式的控制流進行混淆,使得程式的控制流更加難以反編譯,通常控制流的改變需要增加一些額外的計算和控制流,是以在性能上會給程式帶來一定的負面影響。有時,需要在程式的性能和混淆程度之間進行權衡。控制混淆的技術最為複雜,技巧也最多。這些技術可以分為如下幾類: 

  增加混淆控制 通過增加額外的、複雜的控制流,可以将程式原來的語義隐藏起來。例如,對于按次序執行的兩個語句A、B,我們可以增加一個控制條件,以決定B的執行。通過這種方式加大反彙編的難度。但是所有的幹擾控制都不應該影響B的執行。圖6就給出三種方式,為這個例子增加混淆控制。 

如何保護Java程式

  圖6 增加混淆控制的三種方式 

  控制流重組 重組控制流也是重要的混淆方法。例如,程式調用一個方法,在混淆後,可以将該方法代碼嵌入到調用程式當中。反過來,程式中的一段代碼也可以轉變為一個函數調用。另外,對于一個循環的控制流,為可以拆分多個循環的控制流,或者将循環轉化成一個遞歸過程。這種方法最為複雜,研究的人員也非常多。 

  預防性混淆 

  這種混淆通常是針對一些專用的反編譯器而設計的,一般來說,這些技術利用反編譯器的弱點或者Bug來設計混淆方案。例如,有些反編譯器對于Return後面的指令不進行反編譯,而有些混淆方案恰恰将代碼放在Return語句後面。這種混淆的有效性對于不同反編譯器的作用也不太相同的。一個好的混淆工具,通常會綜合使用這些混淆技術。 

  案例分析 

  在實踐當中,保護一個大型Java程式經常需要綜合使用這些方法,而不是單一使用某一種方法。這是因為每種方法都有其弱點和應用環境。綜合使用這些方法使得Java程式的保護更加有效。另外,我們經常還需要使用其它的相關安全技術,例如安全認證、數字簽名、PKI等。 

  本文給出的例子是一個Java應用程式,它是一個SCJP(Sun Certificate Java Programmer)的模拟考試軟體。該應用程式帶有大量的模拟題目,所有的題目都被加密後存儲在檔案中。由于它所帶的題庫是該軟體的核心部分,是以關于題庫的存取和通路就成為非常核心的類。一旦這些相關的類被反編譯,則所有的題庫将被破解。現在,我們來考慮如何保護這些題庫及相關的類。 

  在這個例子中,我們考慮使用綜合保護技術,其中包括本地代碼和混淆技術。因為該軟體主要釋出在Windows上,是以轉換成本地代碼後,僅僅需要維護一個版本的本地代碼。另外,混淆對Java程式也是非常有效的,适用于這種獨立釋出的應用系統。 

  在具體的方案中,我們将程式分為兩個部分,一個是由本地代碼編寫的題庫通路的子產品,另外一個是由Java開發的其它子產品。這樣可以更高程度地保護題目管理子產品不被反編譯。對于Java開發的子產品,我們仍然要使用混淆技術。該方案的示意圖參見圖7。 

如何保護Java程式

  圖7 SCJP保護技術方案圖 

  對于題目管理子產品,由于程式主要在Windows下使用,是以使用C++開發題庫通路子產品,并且提供了一定的通路接口。為了保護題庫通路的接口,我們還增加了一個初始化接口,用于每次使用題庫通路接口之前的初始化工作。它的接口主要分為兩類: 

  1. 初始化接口 

  在使用題庫子產品之前,我們必須先調用初始化接口。在調用該接口時,用戶端需要提供一個随機數作為參數。題庫管理子產品和用戶端通過這個随機數,按一定的算法同時生成相同的SessionKey,用于加密以後輸入和輸出的所有資料。通過這種方式,隻有授權(有效)的用戶端才能夠連接配接正确的連接配接,生成正确的SessionKey,用于通路題庫資訊。非法的客戶很難生成正确的SessionKey,是以無法獲得題庫的資訊。如果需要建立更高的保密級别,也可以采用雙向認證技術。 

  2. 資料通路接口 

  認證完成之後,用戶端就可以正常的通路題庫資料。但是,輸入和輸出的資料都是由SessionKey所加密的資料。是以,隻有正确的題庫管理子產品才能夠使用題庫管理子產品。圖8時序圖表示了題庫管理子產品和其它部分的互動過程。 

如何保護Java程式

  圖8 題庫管理子產品和其它部分的互動過程圖