当前位置 - 股票行情交易網 - 國際漫評 - 如何有效防止Java程序源碼被人偷窺?

如何有效防止Java程序源碼被人偷窺?

Java程序的源代碼很容易被別人偷看 只要有壹個反編譯器 任何人都可以分析別人的代碼 本文討論如何在不修改原有程序的情況下 通過加密技術保護源代碼

  壹 為什麽要加密?

 對於傳統的C或C++之類的語言來說 要在Web上保護源代碼是很容易的 只要不發布它就可以 遺憾的是 Java程序的源代碼很容易被別人偷看 只要有壹個反編譯器 任何人都可以分析別人的代碼 Java的靈活性使得源代碼很容易被竊取 但與此同時 它也使通過加密保護代碼變得相對容易 我們唯壹需要了解的就是Java的ClassLoader對象 當然 在加密過程中 有關Java Cryptography Extension(JCE)的知識也是必不可少的

 有幾種技術可以 模糊 Java類文件 使得反編譯器處理類文件的效果大打折扣 然而 修改反編譯器使之能夠處理這些經過模糊處理的類文件並不是什麽難事 所以不能簡單地依賴模糊技術來保證源代碼的安全

 我們可以用流行的加密工具加密應用 比如PGP(Pretty Good Privacy)或GPG(GNU Privacy Guard) 這時 最終用戶在運行應用之前必須先進行解密 但解密之後 最終用戶就有了壹份不加密的類文件 這和事先不進行加密沒有什麽差別

 Java運行時裝入字節碼的機制隱含地意味著可以對字節碼進行修改 JVM每次裝入類文件時都需要壹個稱為ClassLoader的對象 這個對象負責把新的類裝入正在運行的JVM JVM給ClassLoader壹個包含了待裝入類(比如java lang Object)名字的字符串 然後由ClassLoader負責找到類文件 裝入原始數據 並把它轉換成壹個Class對象

 我們可以通過定制ClassLoader 在類文件執行之前修改它 這種技術的應用非常廣泛在這裏 它的用途是在類文件裝入之時進行解密 因此可以看成是壹種即時解密器 由於解密後的字節碼文件永遠不會保存到文件系統 所以竊密者很難得到解密後的代碼

 由於把原始字節碼轉換成Class對象的過程完全由系統負責 所以創建定制ClassLoader對象其實並不困難 只需先獲得原始數據 接著就可以進行包含解密在內的任何轉換

 Java 在壹定程度上簡化了定制ClassLoader的構建 在Java 中 loadClass的缺省實現仍舊負責處理所有必需的步驟 但為了顧及各種定制的類裝入過程 它還調用壹個新的findClass方法

 這為我們編寫定制的ClassLoader提供了壹條捷徑 減少了麻煩 只需覆蓋findClass 而不是覆蓋loadClass 這種方法避免了重復所有裝入器必需執行的公***步驟 因為這壹切由loadClass負責

 不過 本文的定制ClassLoader並不使用這種方法 原因很簡單 如果由默認的ClassLoader先尋找經過加密的類文件 它可以找到;但由於類文件已經加密 所以它不會認可這個類文件 裝入過程將失敗 因此 我們必須自己實現loadClass 稍微增加了壹些工作量

  二 定制類裝入器

 每壹個運行著的JVM已經擁有壹個ClassLoader 這個默認的ClassLoader根據CLASSPATH環境變量的值 在本地文件系統中尋找合適的字節碼文件

 應用定制ClassLoader要求對這個過程有較為深入的認識 我們首先必須創建壹個定制ClassLoader類的實例 然後顯式地要求它裝入另外壹個類 這就強制JVM把該類以及所有它所需要的類關聯到定制的ClassLoader Listing 顯示了如何用定制ClassLoader裝入類文件

 Listing 利用定制的ClassLoader裝入類文件

 以下是引用片段

// 首先創建壹個ClassLoader對象 ClassLoader myClassLoader = new myClassLoader(); // 利用定制ClassLoader對象裝入類文件 // 並把它轉換成Class對象 Class myClass = myClassLoader loadClass( mypackage MyClass ); // 最後 創建該類的壹個實例 Object newInstance = myClass newInstance(); // 註意 MyClass所需要的所有其他類 都將通過 // 定制的ClassLoader自動裝入?

 如前所述 定制ClassLoader只需先獲取類文件的數據 然後把字節碼傳遞給運行時系統 由後者完成余下的任務

 ClassLoader有幾個重要的方法 創建定制的ClassLoader時 我們只需覆蓋其中的壹個 即loadClass 提供獲取原始類文件數據的代碼 這個方法有兩個參數 類的名字 以及壹個表示JVM是否要求解析類名字的標記(即是否同時裝入有依賴關系的類) 如果這個標記是true 我們只需在返回JVM之前調用resolveClass

 Listing ClassLoader loadClass()的壹個簡單實現

 以下是引用片段

  public Class loadClass( String name boolean resolve ) throws ClassNotFoundException { try { // 我們要創建的Class對象 Class clasz = null; // 必需的步驟 如果類已經在系統緩沖之中 // 我們不必再次裝入它 clasz = findLoadedClass( name ); if (clasz != null) return clasz; // 下面是定制部分 byte classData[] = /* 通過某種方法獲取字節碼數據 */; if (classData != null) { // 成功讀取字節碼數據 現在把它轉換成壹個Class對象 clasz = defineClass( name classData classData length ); } // 必需的步驟 如果上面沒有成功 // 我們嘗試用默認的ClassLoader裝入它 if (clasz == null) clasz = findSystemClass( name ); // 必需的步驟 如有必要 則裝入相關的類 if (resolve && clasz != null) resolveClass( clasz ); // 把類返回給調用者 return clasz; } catch( IOException ie ) { throw new ClassNotFoundException( ie toString() ); } catch( GeneralSecurityException gse ) { throw new ClassNotFoundException( gse toString() ); } }?

 Listing 顯示了壹個簡單的loadClass實現 代碼中的大部分對所有ClassLoader對象來說都壹樣 但有壹小部分(已通過註釋標記)是特有的 在處理過程中 ClassLoader對象要用到其他幾個輔助方法

 findLoadedClass 用來進行檢查 以便確認被請求的類當前還不存在 loadClass方法應該首先調用它

 defineClass 獲得原始類文件字節碼數據之後 調用defineClass把它轉換成壹個Class對象 任何loadClass實現都必須調用這個方法

 findSystemClass 提供默認ClassLoader的支持 如果用來尋找類的定制方法不能找到指定的類(或者有意地不用定制方法) 則可以調用該方法嘗試默認的裝入方式 這是很有用的 特別是從普通的JAR文件裝入標準Java類時

 resolveClass 當JVM想要裝入的不僅包括指定的類 而且還包括該類引用的所有其他類時 它會把loadClass的resolve參數設置成true 這時 我們必須在返回剛剛裝入的Class對象給調用者之前調用resolveClass

  三 加密 解密

 Java加密擴展即Java Cryptography Extension 簡稱JCE 它是Sun的加密服務軟件 包含了加密和密匙生成功能 JCE是JCA(Java Cryptography Architecture)的壹種擴展

 JCE沒有規定具體的加密算法 但提供了壹個框架 加密算法的具體實現可以作為服務提供者加入 除了JCE框架之外 JCE軟件包還包含了SunJCE服務提供者 其中包括許多有用的加密算法 比如DES(Data Encryption Standard)和Blowfish

 為簡單計 在本文中我們將用DES算法加密和解密字節碼 下面是用JCE加密和解密數據必須遵循的基本步驟

 步驟 生成壹個安全密匙 在加密或解密任何數據之前需要有壹個密匙 密匙是隨同被加密的應用壹起發布的壹小段數據 Listing 顯示了如何生成壹個密匙 Listing 生成壹個密匙

 以下是引用片段

  // DES算法要求有壹個可信任的隨機數源 SecureRandom sr = new SecureRandom(); // 為我們選擇的DES算法生成壹個KeyGenerator對象 KeyGenerator kg = KeyGenerator getInstance( DES ); kg init( sr ); // 生成密匙 SecretKey key = kg generateKey(); // 獲取密匙數據 byte rawKeyData[] = key getEncoded(); /* 接下來就可以用密匙進行加密或解密 或者把它保存 為文件供以後使用 */ doSomething( rawKeyData );步驟 加密數據 得到密匙之後 接下來就可以用它加密數據 除了解密的ClassLoader之外 壹般還要有壹個加密待發布應用的獨立程序(見Listing ) Listing 用密匙加密原始數據

 以下是引用片段

  // DES算法要求有壹個可信任的隨機數源 SecureRandom sr = new SecureRandom(); byte rawKeyData[] = /* 用某種方法獲得密匙數據 */; // 從原始密匙數據創建DESKeySpec對象 DESKeySpec dks = new DESKeySpec( rawKeyData ); // 創建壹個密匙工廠 然後用它把DESKeySpec轉換成 // 壹個SecretKey對象 SecretKeyFactory keyFactory = SecretKeyFactory getInstance( DES ); SecretKey key = keyFactory generateSecret( dks ); // Cipher對象實際完成加密操作 Cipher cipher = Cipher getInstance( DES ); // 用密匙初始化Cipher對象 cipher init( Cipher ENCRYPT_MODE key sr ); // 現在 獲取數據並加密 byte data[] = /* 用某種方法獲取數據 */ // 正式執行加密操作 byte encryptedData[] = cipher doFinal( data ); // 進壹步處理加密後的數據 doSomething( encryptedData );  步驟 解密數據 運行經過加密的應用時 ClassLoader分析並解密類文件 操作步驟如Listing 所示 Listing 用密匙解密數據

// DES算法要求有壹個可信任的隨機數源 SecureRandom sr = new SecureRandom(); byte rawKeyData[] = /* 用某種方法獲取原始密匙數據 */; // 從原始密匙數據創建壹個DESKeySpec對象 DESKeySpec dks = new DESKeySpec( rawKeyData ); // 創建壹個密匙工廠 然後用它把DESKeySpec對象轉換成 // 壹個SecretKey對象 SecretKeyFactory keyFactory = SecretKeyFactory getInstance( DES ); SecretKey key = keyFactory generateSecret( dks ); // Cipher對象實際完成解密操作 Cipher cipher = Cipher getInstance( DES ); // 用密匙初始化Cipher對象 cipher init( Cipher DECRYPT_MODE key sr ); // 現在 獲取數據並解密 byte encryptedData[] = /* 獲得經過加密的數據 */ // 正式執行解密操作 byte decryptedData[] = cipher doFinal( encryptedData ); // 進壹步處理解密後的數據 doSomething( decryptedData );

  四 應用實例

 前面介紹了如何加密和解密數據 要部署壹個經過加密的應用 步驟如下

 步驟 創建應用 我們的例子包含壹個App主類 兩個輔助類(分別稱為Foo和Bar) 這個應用沒有什麽實際功用 但只要我們能夠加密這個應用 加密其他應用也就不在話下

 步驟 生成壹個安全密匙 在命令行 利用GenerateKey工具(參見GenerateKey java)把密匙寫入壹個文件 % java GenerateKey key data

 步驟 加密應用 在命令行 利用EncryptClasses工具(參見EncryptClasses java)加密應用的類 % java EncryptClasses key data App class Foo class Bar class

 該命令把每壹個 class文件替換成它們各自的加密版本

 步驟 運行經過加密的應用 用戶通過壹個DecryptStart程序運行經過加密的應用 DecryptStart程序如Listing 所示 Listing DecryptStart java 啟動被加密應用的程序

 以下是引用片段

import java io *; import java security *; import java lang reflect *; import javax crypto *; import javax crypto spec *; public class DecryptStart extends ClassLoader { // 這些對象在構造函數中設置 // 以後loadClass()方法將利用它們解密類 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的 // 實例 它就是我們的定制ClassLoader // 設置好ClassLoader以後 我們用它裝入應用實例 // 最後 我們通過Java Reflection API調用應用實例的main方法 static public void main( String args[] ) throws Exception { String keyFilename = args[ ]; String appName = args[ ]; // 這些是傳遞給應用本身的參數 String realArgs[] = new String[args length ]; System arraycopy( args realArgs args length ); // 讀取密匙 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 ); // 創建解密的ClassLoader DecryptStart dr = new DecryptStart( key ); // 創建應用主類的壹個實例 // 通過ClassLoader裝入它 System err println( [DecryptStart: loading +appName+ ] ); Class clasz = dr loadClass( appName ); // 最後 通過Reflection API調用應用實例 // 的main()方法 // 獲取壹個對main()的引用 String proto[] = new String[ ]; Class mainArgs[] = { (new String[ ]) getClass() }; Method main = clasz getMethod( main mainArgs ); // 創建壹個包含main()方法參數的數組 Object argsArray[] = { realArgs }; System err println( [DecryptStart: running +appName+ main()] ); // 調用main() main invoke( null argsArray ); } public Class loadClass( String name boolean resolve ) throws ClassNotFoundException { try { // 我們要創建的Class對象 Class clasz = null; // 必需的步驟 如果類已經在系統緩沖之中 // 我們不必再次裝入它 clasz = findLoadedClass( name ); if (clasz != null) return clasz; // 下面是定制部分 try { // 讀取經過加密的類文件 byte classData[] = Util readFile( name+ class ); if (classData != null) { // 解密 byte decryptedClassData[] = cipher doFinal( classData ); // 再把它轉換成壹個類 clasz = defineClass( name decryptedClassData  decryptedClassData length ); System err println( [DecryptStart: decrypting class +name+ ] ); } } catch( FileNotFoundException fnfe ) // 必需的步驟 如果上面沒有成功 // 我們嘗試用默認的ClassLoader裝入它 if (clasz == null) clasz = findSystemClass( name ); // 必需的步驟 如有必要 則裝入相關的類 if (resolve && clasz != null) resolveClass( clasz ); // 把類返回給調用者 return clasz; } catch( IOException ie ) { throw new ClassNotFoundException( ie toString() ); } catch( GeneralSecurityException gse ) { throw new ClassNotFoundException( gse toString() ); } } }對於未經加密的應用 正常執行方式如下 % java App arg arg arg

 對於經過加密的應用 則相應的運行方式為 % java DecryptStart key data App arg arg arg

 DecryptStart有兩個目的 壹個DecryptStart的實例就是壹個實施即時解密操作的定制ClassLoader;同時 DecryptStart還包含壹個main過程 它創建解密器實例並用它裝入和運行應用 示例應用App的代碼包含在App java Foo java和Bar java內 Util java是壹個文件I/O工具 本文示例多處用到了它 完整的代碼請從本文最後下載

  五 註意事項

 我們看到 要在不修改源代碼的情況下加密壹個Java應用是很容易的 不過 世上沒有完全安全的系統 本文的加密方式提供了壹定程度的源代碼保護 但對某些攻擊來說它是脆弱的

 雖然應用本身經過了加密 但啟動程序DecryptStart沒有加密 攻擊者可以反編譯啟動程序並修改它 把解密後的類文件保存到磁盤 降低這種風險的辦法之壹是對啟動程序進行高質量的模糊處理 或者 啟動程序也可以采用直接編譯成機器語言的代碼 使得啟動程序具有傳統執行文件格式的安全性

 另外還要記住的是 大多數JVM本身並不安全 狡猾的黑客可能會修改JVM 從ClassLoader之外獲取解密後的代碼並保存到磁盤 從而繞過本文的加密技術 Java沒有為此提供真正有效的補救措施

lishixinzhi/Article/program/Java/hx/201311/25751