花之慶次 2007-11-7 23:47
	JAVA語言安全行研究--Java的反編譯
Java誕生於1995年,是一門較年輕的語言。它以平臺無關性,安全性,面向對象,分佈式,鍵壯性等特點贏得了眾多程式員的青睞。特別是它簡潔的面向對象的語言風格,更讓許多人對它愛不釋手。但人們在使用Java的過程中,會發現它有幾個致命的弱點:運行速度慢,用戶使用不便,源代碼保護機制不夠安全。特別是在保護源代碼方面,Java是基於解釋一種叫Java字節碼的中間代碼來運行其程式的,而且Jvm比電腦的微處理器要簡單的多,文檔也很齊全,結果造成其目標程式很容易被反編譯,而且所得代碼和其原始代碼十分相似,甚至可以一模一樣,可讀性相當好。這就給Java的代碼保護帶來了不利。但要實現Java程式的保護,也不是不可能的,經研究和總結,至少有三種實現方式:1.混淆器;2.網路載入重要類;3加密重要類。
 
1 混淆器
 
目前,開發人員使用的比較多的保護代碼的方法是用混淆器。混淆器是採用一些方法將類,變數,方法,包的名字改為無意義的字符串;使用非法的字符代替符號;貼加一些代碼使反編譯軟體崩潰;貼加一些無關的指令或永遠執行不到的指令等使反編譯無法成功或所得的代碼可讀性很差。這樣就實現了反反編譯的目的。我們來做個演示。原始代碼如下: 
 
import Java.io.*; 
 
import Java.security.*; 
 
public class sKey_kb{ 
 
public static void main(String args[]) throws Exception{ 
 
FileInputStream f=new FileInputStream("key1.dat"); 
 
ObJectInputStream b=new ObJectInputStream(f); 
 
Key k=(Key)b.readObJect(); 
 
byte[] kb=k.getEncoded(); 
 
FileOutputStream f2=new FileOutputStream("keykb1.dat"); 
 
f2.write(kb); 
 
for(int i=0;i 
System.out.print(kb[i]+","); 
 
} } } 
 
使用混淆器後,再用Jad反編譯得代碼如下: 
 
import Java.io.*; 
 
import Java.security.Key; 
 
public class sKey_kb{ 
 
public skey() {} 
 
public static void main(String args[]) { 
 
FileInputStream fileinputstream=new FileInputStream(ma); 
 
ObJectInputStream obJectinputstream=new ObJectInputStream(fileinputstream); 
 
Key key=(Key)b.readObJect(); 
 
byte abyte0[]=key.getEncoded(); 
 
FileOutputStream fileoutputstream=new FileOutputStream(na); 
 
fileoutputstream.write(abyte0); 
 
for(int i=0;i 
System.out.print(abyte0[i]+oa); 
 
}
 
private static String a(String s){ 
 
int i=s.length(); 
 
char ac[]=new char[i]; 
 
for(int J=0;J 
return new String(ac); 
 
}
 
private static String ma="u5AA1u5AAFu5AF3u5AFBu5AE4u5AAEu5AABu5ABE"; 
 
private static String na="u5AA1u5AAFu5AB3u5AA1u5AA8u5AFBu5AE4u5AAEu5AABu5ABE"; 
 
private static String oa="u5AE6"; 
 
public static{ 
 
ma=a(ma); 
 
na=a(ma) 
 
oa=a(oa); 
 
} } 
 
混淆後,再反編譯所仍然能得到源代碼,但顯然,所得代碼與原始代碼比,變得難以讀懂,代碼中多了其他的方法,文件名等資訊也被打亂了。並且,把以上代碼寫進sKey_kb.Java中,無法通過編譯。 
 
但是,如果在編寫軟體時,在軟體中寫入某些註冊資訊,或一些簡單的演算法,通過反編譯,還是有可能得到這些資訊的,從而未能達到保護軟體的目的。反編譯器與混淆器之間的鬥爭是永無止盡的。所以從其他角度去保護Java的源代碼是很有必要。 
 
2 網路載入重要類
 
在Java中提供了一個ClassLoader類,這個類可以讓我們使用類載入器將所需要的Java字節碼文件載入到Jvm中。我們通過重寫這個類,可以實現從網路通過url載入Java字節碼文件。這樣,我們就可以把一些重要的,隱秘的class放在網路伺服器上,通過密碼去檢驗是否有許可權下載該類。從而實現Java代碼保護的目的。其次在Java中正好提供了URLClassLoader這個類,通過此類,正好可以實現我們的目的。URLClassLoader類的基本使用方法是通過一個URL類型的數組告訴URLClassLoader類的對像是從什麼地方載入類,然後使用loadclass()方法,從給定的URL中載入字節碼文件,獲得它的方法,然後再執行。 
 
具體步驟如下: 
 
1.創建URL 
 
URL url[]={ 
 
new URL("file:///c:/classloader/web"), 
 
new URL("http://www.asp.zJc.zJut.edu.cn/Javaclass/") 
 
}; 
 
2.創建URLClassLoader對象 
 
URLClassLoader cl=new URLClassLoader(url); 
 
3.使用URLClassLoader對象載入字節碼文件 
 
Class class=cl.loadClass("class1"); 
 
4.執行靜態方法 
 
Class getarg[]={ 
 
(new String [1]).getClass() }; 
 
Method m=class.getMethod("main",getarg); 
 
String[] myl={"arg1 passed","arg2 passed"); 
 
ObJect myarg[]={myl}; 
 
m.invole(null,myarg); 
 
 
 
3 加密重要類
 
使用網路載入重要類的方法固然有一定的用處,但是,在遇到無網路的情況時,還是無法解決我們的問題。對於這種情況,我們只能把所有文件放在本地電腦上。那麼,對此我們該怎麼做才能保護好Java代碼呢?
 
其實,要實現這一點,並不難,只需要對一些重要的類實行加密就可以了。當然,在裝載時,加密的類是需要解密才能被ClassLoader識別的。所以,我們必須自己創建ClassLoader類。在標準Java api中ClassLoader有幾個重要的方法。創建定制ClassLoader時,我們只需覆蓋其中的一個,即loadClass,添加獲取原始類文件數據的代碼。這個方法有兩個參數:類的名字,以及一個表示JVM是否要求解析類名字的標記(即是否同時裝入有依賴關係的類)。如果這個標記為true,我們只需在返回JVM之前調用resolveClass。
 
原代碼如下:
 
public Class loadClass( String name, boolean resolve ) 
 
throws ClassNotFoundException { 
 
try { 
 
Class clasz = null; 
 
//步驟1:如果類已經在系統緩衝之中,我們就不需要再次裝入它 
 
clasz = findLoadedClass( name ); 
 
if (clasz != null) 
 
return clasz; 
 
byte classData[] = /* 通過某種方法獲取字節碼數據 */; 
 
if (classData != null) { 
 
clasz = defineClass( name, classData, 0, classData.length ); 
 
} 
 
//步驟2:如果上面沒有成功, 
 
if (clasz == null) 
 
clasz = findSystemClass( name ); 
 
//步驟3:如有必要,則裝入相關的類 
 
if (resolve && clasz != null) 
 
resolveClass( clasz ); 
 
return clasz; 
 
} catch( IOException ie ) { 
 
throw new ClassNotFoundException( ie.toString() ); 
 
} catch( GeneralSecurityException gse ) { 
 
throw new ClassNotFoundException( gse.toString() ); 
 
} } 
 
代碼中的大部分對所有ClassLoader對象來說都一樣,但有一小部分是特有的。在處理過程中,ClassLoader對象要用到其他幾個輔助方法:findLoadedClass:用來進行檢查,以便確認被請求的類當前是否存在,loadClass方法應該首先調用它。defineClass:獲得原始類文件字節碼數據之後,調用defineClass把它轉換成對象,任何loadClass實現都必須調用這個方法。findSystemClass:提供默認ClassLoader的支援。如果用來尋找類的定制方法不能找到指定的類,則可以調用該方法嘗試默認的裝入方式。resolveClass:當JVM想要裝入的不僅包括指定的類,而且還包括該類引用的所有其他類時,它會把loadClass的resolve參數設置成true。這時,我們必須在返回剛剛裝入的Class對象給調用者之前調用resolveClass。 
 
 
 
接下來就是加密解密部分。Java加密擴展即Java Cryptography Extension,簡稱JCE,是Sun的加密服務軟體,包含了加密和密匙生成功能。我們可以用DES演算法加密和解密字節碼。用JCE加密和解密數據是要遵循一些基本步驟的(可以參考<>,這裡就不祥述了)。
 
加密完成後,就是通過解密來獲取原始類的Java字節碼。可以通過一個DecryptStart程式運行經過加密的應用。 
 
具體方法如下:
 
public class DecryptStart extends ClassLoader 
 
{ 
 
private SecretKey key; 
 
private Cipher cipher; 
 
public DecryptStart( SecretKey key ) throws GeneralSecurityException,IOException { 
 
this.key = key; 
 
String algorithm = "DES"; 
 
SecureRandom sr = new SecureRandom(); 
 
System.err.println( "[DecryptStart: creating cipher]" ); 
 
cipher = Cipher.getInstance( algorithm ); 
 
cipher.init( Cipher.DECRYPT_MODE, key, sr ); 
 
} 
 
// main過程:我們要在這裡讀入密匙,創建DecryptStart的 
 
static public void main( String args[] ) throws Exception { 
 
String keyFilename = args[0]; 
 
String appName = args[1]; 
 
String realArgs[] = new String[args.length-2]; 
 
System.arraycopy( args, 2, realArgs, 0, args.length-2 ); 
 
System.err.println( "[DecryptStart: reading key]" ); 
 
byte rawKey[] = Util.readFile( keyFilename ); 
 
DESKeySpec dks = new DESKeySpec( rawKey ); 
 
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance( "DES" ); 
 
SecretKey key = keyFactory.generateSecret( dks ); 
 
DecryptStart dr = new DecryptStart( key ); 
 
System.err.println( "[DecryptStart: loading "+appName+"]" ); 
 
Class clasz = dr.loadClass( appName ); 
 
String proto[] = new String[1]; 
 
Class mainArgs[] = { (new String[1]).getClass() }; 
 
Method main = clasz.getMethod( "main", mainArgs ); 
 
ObJect argsArray[] = { realArgs }; 
 
System.err.println( "[DecryptStart: running "+appName+".main()]" ); 
 
main.invoke( null, argsArray ); 
 
} 
 
 
 
雖然應用本身經過了加密,但啟動程式DecryptStart沒有加密。攻擊者可以反編譯啟動程式並修改它,把解密後的類文件保存到磁片。降低這種風險的辦法之一是對啟動程式進行高品質的模糊處理。或者,啟動程式也可以採用直接編譯成機器語言的代碼,使得啟動程式具有傳統執行文件格式的安全性.比如使用Java的Jini技術,來實現解密部分,就可以作到。當然,這是需要付出一定的代價的,就是喪失了Java的最大特點--平臺無關性。不過,Jni技術可以用c語言在多種平臺實現,我們可以在不同的平臺編寫不同的啟動程式。
 
 
 
4 綜合實例:
 
對於一些需要網路支援的軟體來說,可以建立一個Web站點,在站點上存放該軟體的關鍵類,並且建立用戶管理機制,用戶直接登陸網站進行確認,是許可用戶,則發放解密key文件,讓其下載關鍵類,在本地解密運行。這樣作的優點是建立的Web站點可以有效的管理密鑰以及用戶資料。從而起到加強保護軟體源代碼的作用,並方便軟體升級。用C/S結構是不錯的選擇。
 [url]http://big5.ccidnet.com:89/gate/big5/java.ccidnet.com/art/3539/20071105/1264799_1.html[/url]