Jocky混淆JAVA程式碼(保護你的JAVA專案)
一、前言
我們知道,Java是一種跨平臺的程式語言,其原始碼(.java檔案)被編譯成與平臺無關的位元組碼(.class檔案),然後在執行期動態連結。這樣,編譯後的類檔案中將包含有符號表,從而使得Java程式很容易被反編譯。相信每一個Java開發人員,都曾經用過諸如Jad之類的反編譯器,對Java的class
檔案進行反編譯,從而觀察程式的結構與實現細節。如此一來,對於那些需要嚴格進行智慧財產權保護的Java應用,如何有效的保護客戶的商業投資,是開發人員經常需要面對的問題。
於是就出現了Java混淆編譯器,它的作用是打亂class檔案中的符號資訊,從而使反向工程變得非常困難。
Jocky就是這樣一款優秀的Java混淆編譯器。
目前業界有不少商業的甚或是開源的混淆編譯器,但它們普遍存在一些這樣或者那樣的問題。一般而言,現有的混淆器都是對編譯好的
class檔案進行混淆,這樣就需要編譯和混淆兩個步驟。而事實上,並不是所有的符號都需要混淆。如果你開發的是一個類庫,或者某些類需要動態裝載,那些公共API(或者說:那些被publish出來的API)就必須保留符號不變,只有這樣,別人才能使用你的類庫。現有的混淆器提供了GUI或指令碼的方式來對那些需要保留的符號名稱進行配置,但如果程式較大時,配置工作將變得很複雜,而程式一旦修改,配置工作又要重新進行。某些混淆器能夠調整位元組碼的順序,使反編譯更加困難,但筆者經歷過混淆之後的程式執行出錯的情況。
而Jocky與其它混淆編譯器最大的不同之處在於:它是直接從原始碼上做文章,也就是說編譯過程本身就是一個混淆過程。
Jocky混淆編譯器是在Sun JDK中提供的Java編譯器(javac)的基礎上完成的,修改了其中的程式碼生成過程,對編譯器生成的中間程式碼進行混淆,最後再生成class檔案,這樣編譯和混淆只需要一個步驟就可以完成。另外可以在源程式中插入 符號保留指令 來控制哪些符號需要保留,將混淆過程與開發過程融合在一起,不需要單獨的配置。
如前文所述,混淆編譯是Jocky的首要用途。我們舉一個最簡單的例子,下面的SimpleBean是未經混淆的class檔案通過Jad反編譯以後獲得的原始檔:
1publicclass SimpleBean implements Serializable 2 3 private String name ="myname"; 4 5 private List myList =null; 6 7 publicvoid SimpleBean() { 8 myList =new ArrayList(10); 9 }10 11 publicvoid foo1() { 12 myList.add("name"); 13 }14 15 privatevoid foo2() { 16 }17 18 privatevoid writeObject(java.io.ObjectOutputStream out) 19 throws IOException { 20 21 }22 23} |
<未混淆的類檔案反編譯後的效果> |
下面是經Jocky混淆過的類檔案,通過Jad反編譯後產生的原始檔:
1publicclass SimpleBean implements Serializable { 2 3 private String _$2; 4 5 private List _$1; 6 7 public SimpleBean() { 8 _$2="myname"; 9 this; 10 JVM INSTR new #4<Class ArrayList>; 11 JVM INSTR dup ; 12 JVM INSTR swap ; 13 10; 14 ArrayList(); 15 _$1; 16 }17 publicvoid foo1() { 18 _$1.add("name"); 19 }20 21 privatevoid _$1() { 22 }23 24 privatevoid writeObject(ObjectOutputStream objectoutputstream){ 25 throws IOException { 26 }27} |
<Jocky混淆過的類檔案反編譯的效果> |
JDK 5.0在語法層面上有許多新增特色,能夠為簡化應用的開發帶來一些便利。譬如Generics、Enhanced for Loop以及 Autoboxing/Unboxing等。但另人遺憾的是,倘若利用這些新的語法開發應用,就意味著不能夠在JDK 1.4上執行,而JDK 1.4畢竟是目前最為普及的VM版本。幸運是,Jocky的另一個特色就是:通過引數配置,能夠把用JDK 5.0語法編寫的應用編譯成JDK 1.4上的類檔案版本。我們可以把經過 Jocky編譯的類檔案以UltraEdit開啟,可以發現在第8個位元組上(類檔案的major version)的數值是0x30,即十進位制的48,這是JDK 1.4所能夠理解的類檔案版本(JDK 5.0預設編譯的類檔案版本是49)。前提是:應用中不能夠使用JDK 1.4中所沒有的一些API。
使用Jocky非常簡單,獲得jocky.jar以後,只需要執行java -jar jocky.jar就可以啟動Jocky混淆編譯器,jocky的命令列引數和javac完全相同,但增加了一個新的引數-scramble,它的用法如下:
-scramble 混淆所有package private或private符號 -scrambleall 混淆所有符號 -scramble:<level> 混淆相應級別的符號 其中<level>指定混淆級別,可以是以下幾種級別: -scramble:none 不進行混淆 -scramble:private 對所有private訪問級別的元素進行混淆 -scramble:package 對所有private或package private元素進行混淆 -scramble:protected 對所有private, package private, protected元素進行混淆 -scramble:public 對所有的元素都進行混淆 -scramble:all 相當於-scramble:public 如果使用-scramble不帶級別引數,則相當於-scramble:package 近年來,Ant已經成為Java應用開發中打包工具的事實上的標準。在應用的開發過程中,我們往往都會有一個Ant指令碼,通過該指令碼,能夠對應用進行編譯、打包、釋出等一系列過程。因此,Jocky的最佳切入點便是對Ant的支援。
在Ant中使用Jocky非常簡單:
1.
將lib\jocky-ant.jar copy至ANT_HOME\lib目錄下。
2. 在ant指令碼中加入這樣一行程式碼,以引入Jocky
Task
3. 設定Jocky的一些基本屬性,包括:
jocky.jar包的位置,以及混淆級別,如下所示:
4.
當設定jocky的enable屬性為true時,此時,Ant指令碼中的javac編譯命令,便會被自動替換成Jocky編譯器;當設定enable屬性為false時,javac編譯命令將恢復成正常設定,示例指令碼如下:
1<project name="jocky" default="build"> 2<!-- 引入Jocky Ant Task,要確保jocky-ant.jar位於ANT_HOME\lib目錄下 --> 3<taskdef resource="jockytasks"></taskdef> 4<target name="build"> 5<!-- 設定jocky.jar的位置以及混淆級別,當enable為true時,javac task將被自動替換成Jocky混淆編譯器 --> 6<jocky jar=" F:\Works2\Jocky\jocky1.0\lib\jocky.jar" enable=" true" level=" private"></jocky> 7<!-- 下面的編譯,將使用Jocky混淆編譯器 --> 8<javac destdir="bin2" debug="on" source="1.5" target="1.4"> 9<src path="src"></src>10</javac>11<!-- 當enable為false時,javac task將被恢復成正常設定, Jocky編譯器不再起作用 -->12<jocky enable="false"></jocky>13<!-- 下面的編譯,將使用正常的Javac編譯器 -->14<javac destdir="bin3" debug="on" target="1.4">15<src path="src"></src>16</javac>17</target>18</project> |
<Jocky的Ant指令碼示例> |
注意: Jocky for Ant在Ant 1.6.5上開發,推薦使用該版本。
<Jocky在Eclipse中的右鍵選單> |
<Jocky在Eclipse中的屬性設定> |
1<project basedir="." default="build" name="jocky.example.jocky"> 2<property name="jocky.jar" value="f:\EclipseWTP1.0.8\workspace_jdk5_apusicstudio\org.apusic.jocky\jocky.jar"></property> 3<property name="jocky.output.dir" value="jocky"></property> 4<property name="jocky.scramble.level" value="package"></property> 5<property name="target" value="1.4"></property> 6<path id="project.classpath"> 7<pathelement location="bin"></pathelement> 8</path> 9<target name="init">10<jocky jar="${jocky.jar}" level="${jocky.scramble.level}"></jocky>11<mkdir dir="${jocky.output.dir}"></mkdir>12<mkdir dir="${jocky.output.dir}/bin"></mkdir>13</target>14<target name="clean">15<delete dir="${jocky.output.dir}/bin"></delete>16<delete dir="${jocky.output.dir}"></delete>17</target>18<target depends="init" name="build">19<echo message="${ant.project.name}: ${ant.file}"></echo>20<jocky enable="true"></jocky>21<javac destdir="${jocky.output.dir}/bin" target="${target}">22<src path="src"></src>23<classpath refid="project.classpath"></classpath>24</javac>25</target>26</project> |
<Jocky在Eclipse中自動生成的Ant指令碼示例> |
除了在命令列用 -scramble 引數控制符號混淆級別外,還可以在原始碼中使用符號保留指令來控制那些符號需要保留。符號保留指令是一個Java文件註釋指令,可以插入在類和類成員的文件註釋中,例如:
1/** 2 * This class should preserve. 3 * @preserve 4 */ 5public class Foo { 6 /** 7 * You can specify which field should be preserved. 8 * @preserve 9 */ 10 private int x; 11 12 /** 13 * This field is not preserved. 14 */ 15 private int y; 16 17 /** 18 * You can also preserve methods. 19 * @preserve 20 */ 21 public void hello() {} 22 23 /** 24 * This method is not preserved. 25 */ 26 private void collect() {} 27} |
<使用preserved指令的示例> |
如果沒有@preserve指令,則根據混淆級別及成員的訪問級別來確定符號是否保留。
對於類的符號保留指令可以附帶一個保留級別引數,來控制類成員的符號保留,包括:
事實上,即便不加@preserve指令,Jocky對Java語言特有的一些private級別的方法不進行混淆,譬如,在序列化時有特殊作用的writeObject及readObject方法等。但筆者強烈建議:
針對這些有特殊含義不能夠被混淆的 private級別的方法或者欄位,請以@preserve指令予以保護。
注1:建議通過IDE的JavaDoc設定,來輔助@preserve指令的書寫。
正如前文所說,Jocky是基於原始碼的混淆編譯器,因此,Jocky不支援分別編譯,必須對所有的原始檔同時進行混淆編譯。但事實上,倘若混淆級別控制在private級別上,該限制便不復存在。