提到Java代碼加密,常見方式是使用代碼混淆工具,如proguard。混淆是一種邏輯層面的加密,被混淆的代碼仍可以反編譯,但由於命名與程序流程上的等效替換,使得程序的可讀性變的很差,導致代碼難以被理解和盜用。但若有方法使代碼根本無法被反編譯,效果顯然優於邏輯上的加密,而一種可以實現的方式就是字節碼加密。
Java代碼的實際運行與源代碼(*.java)關系不大,只依賴於編譯后的字節碼文件(*.class)。class文件的內容有非常緊湊和嚴格的約定,使JVM可以識別和執行代碼功能;反編譯工具也是利用這種約定的結構將字節碼反向解析成源碼。只要破壞class文件的結構,就能使這個文件完全失效,變得不可能被反編譯。
當然,這樣加密的class文件也無法被JVM正常加載,不過java的類加載機制是支持自定義,這就給解密留出了空間。可以用自定義的類加載器實現“先解密,后加載”,使JVM能“正常”執行被加密的class文件。以下就來實現上述字節碼加密策略。
首先寫一個加密器接口,方便實現各種字節碼加密算法,兩個接口方法就是將一個字節數組進行某種變換與逆變換:
以下是一個最簡單的實現:
這個加密器只是將首字節改為一個自定義的特殊字節magicIndex,以下是用字節碼查看工具查看某個java類字節碼的加密結果:
這么簡單的“算法”也能加密?以下是用Eclipse的反編譯插件JD查看加密文件的結果:
這印證了字節碼文件的內容是具有嚴格約束的結構,相差一個字節就能破壞整個文件。如使用稍復雜的加密算法,反編譯就變的完全不可能了。
接下來寫一個jar包加密方法,邏輯很簡單,就是將class文件從原包提取出來,加密后寫到新包里去
類似的,解密方法從一個Jar包中獲取一個(加密后的)class文件,並解密成JVM可識別的字節碼。這里作了一點小變化,把多種加密器寫在一個Map(CipherConfig.cipherMap)里,用加密文件的首字節作為標識:
如果是從文件系統(而不是Jar包)加載,還要更簡單一點,因為不需要通過ZipEntry迭代器來定位文件名。
下一步是怎樣調用解密方法,這涉及Java的類加載機制。通常自定義類加載都是通過繼承java.lang.ClassLoader類或其子類,重寫findClass(name)方法,再利用反射機制調用方法。但此處我們無從了解一個發布了的加密的項目中,各類的加載時機和順序,也不知道類之間的引用關系(這些是由JVM的執行機制確定的),因此通過通常的方式難以實現讓一個加密的Jar包在JVM上運行起來。為解決這個問題,這里采取一種聽起來有點“嚇人”的方法,那就是修改java系統類源碼。下面先給出實現方法。
自定義的類都是通過sun.misc.Lancher$AppClassLoader.loadClass(name)這個方法來加載。該方法的調用時機由JVM底層決定,返回值被直接加載到JVM內存。其源碼如下:
最后一行本質上就是調用底層原生方法(后續文章還會涉及)將字節碼加載到內存。如果對應類名的字節碼文件是加密的,這個方法將無法解析,並拋出ClassNotFoundException異常。修改的方式很簡單,當默認加載失敗后,嘗試用之前自定義的解密方法解密加載:
以上類修改以后,將編譯產生的bin/sun/misc/Launcher$AppClassLoader.class文件(注意命名空間必須與系統相同)替換掉jre\lib\rt.jar包中的對應文件,就能正常運行加密后的Jar包了。如將loadFromJar方法寫在自定義的其他類中(如工具類),那么這個類也要一同添加到rt.jar。並且命名空間也必須為sun.misc,否則系統將無法加載這個類本身。關於重寫系統類的命名空間問題,后續還會討論。
修改系統類的“代價”是程序發布時需要捆綁一個自定義的JRE,這看起來讓人“不太舒服”,但仔細分析的話其實利大於弊。第一,增強了程序的獨立性和完整性,不會產生JVM不兼容問題,而在沒有安裝Java的終端,附帶一個JVM則是必不可少的;第二,進一步增強加密強度,因為用通用的JRE無法運行加密代碼;第三,修改系統類甚至JVM源碼有助於了解Java虛擬機底層原理,而自定義系統類和JVM也使得開發者對代碼的控制能力大為。唯一的不足可能是捆綁JRE會增加程序的總體積,不過通過一些不算復雜的優化措施(可以搜索“綠色JRE”或“JRE瘦身”等關鍵字來了解),大部分程序所必須的JRE都可以壓縮到10MB以內,加上應用后續文章將討論的“輕客戶端模式”,這一點空間上的代價完全是可以接收的。
如果有對輕客戶端感興趣的同學,也可以加入我們的群:291694807。本群主要用於討論輕客戶端,使用技術包括qt、flex、java、vc等。
也可以關注我的微博 http://weibo.com/liuxue9527