在某些情况下, java 开发者可能希望保护自己的劳动成果,防止自己编写的源代码被竞争对手或者其他组织和个人轻易获取而危害自己的利益,最简单有效的办法就是对编译后的 java 类文件进行混淆处理。本文介绍一款这样的工具 yguard 。
yGruard 是一个功能比较强大的 java 类文件的混淆工具,特别适合与 ant 工具集成使用。
本文对 yguard 的基本元素做一些简单的介绍,并列举了一些简单的 ant 任务例子,在实际工程项目中可以参考这些样例。
1. 安装
在使用 yGruard 之前,必须首先确保系统中已经有可以正常使用的 Ant 工具。 Ant 工具的使用的介绍不属于本文的范畴。
然后在官方网站 http://www.yworks.com/products/yguard 下载 yguard 的工具包,解压出 yguard.jar 类库, yguard 当前只有一个 jar 包。
解压出之后,把它放置在你的 ant 工具能够找到的路径中。你可以使用绝对路径,但是在下面的例子中。
为了在你现有的 Ant 任务中使用 yguard 进行混淆工作,你可以采取下边两种做法之一。一、你可以在你的 build 脚本中直接插入下面的代码片。
<task def nam e ="obfu s cate " class name=" c om.yworks.yg uard.Obf u scatorTask" classpath="yguar d.jar" / >
<target n ame= "o bfusc a te" > <obf u scate > <!-- 根据你的需求修改 ob fus c ate 元素属 性 -- > </o bfuscat e > </targ e t>
|
二、你也可以把 taskdef 元素放置在 obfuscate 元素中,示例如下:
<targe t name="ob fuscate " > <taskdef name= "obfusca t e" classname="c o m.ywo r ks.yguar d.O b fuscatorTas k " clas s path="yg uard . jar" /> <obfuscate> <!-- 根据你的需求修改 o bf uscat e 元素属性 -- > < /o bfu s cat e> </ta r get>
|
2. 创建初始 ant 任务
我们从经典的 helloworld 示例入手,逐步学习使用 yguard 混淆器工具。
2.1. 编写 helloword 程序
我们先编写一个简单的 HelloWorld 程序,代码如下:
package helloworld;
public class HelloWorld { public String publicAttribute = "i am a public attrubute.";
protected String protectedAttribute = "i am a protected attrubute.";
private String privateAttribute = "i am a private attrubute.";
public void publicSayHello() { System.out.println("hello world in public . "); }
protected void protectedSayHello() { System.out.println("hello world in protected . "); }
private void privateSayHello() { System.out.println("hello world in private . "); }
public static void main(String args[]) { HelloWorld hello = new HelloWorld(); hello.publicSayHello(); } }
|
这个 HelloWorld 程序有不同可见性的属性,不同可见性的方法。在下面的过程中,我们将逐步演示 yguard 的强大功能。
2.2. 创建 ant 任务
创建一个初始的 ant 脚本任务。
<?xml version="1.0" encoding="UTF-8"?> <project name="project" default="init" basedir="."> <!-- 设置我们 Ant 任务的初始化工作 --> <target name="init"> <!-- 工程名称为 helloworld--> <property name="project_name" value="helloworld" /> <!--java 源文件所在路径是当前路径的 src--> <property name="srcDir" value="." /> <!-- 编译生成的 class 文件在当前路径的 classes 目录下 --> <property name="classDir" value="classes" /> <!--jar 包名称为本工程的名称加上 .jar 后缀名 --> <property name="jar" value="${project_name}_temp.jar" /> <!--jar 包名称为本工程的名称加上 .jar 后缀名 --> <property name="obfjar" value="${project_name}.jar" /> <!--yguard 混淆器工作产生的日志文件 --> <property name="obfuscationlog" value="${project_name}_obf_log.xml" /> <mkdir dir="${classDir}" /> </target>
<!-- 编译 --> <target name="compile" depends="init"> <javac srcdir="${srcDir}" destdir="${classDir}"> </javac> </target>
<!-- 打包 --> <target name="jar" depends="compile"> <jar jarfile="${jar}" basedir="${classDir}" includes="helloworld/**"> </jar> </target>
<!-- 删除所有已经编译的 class 文件 --> <target name="clean" depends="init"> <delete dir="${classDir}" includeEmptyDirs="true" /> </target> </project>
<!-- build.xml 文件结束 --> |
2.3. 执行 ant 任务
执行这个脚本,可以看到控制台有如下相似的输出:
Buildfile: D:/work/lspworkspace/helloworld/build.xml init : init : compile : [ javac ] Compiling 1 source file to D:/work/lspworkspace/helloworld/classes init : compile : [ javac ] Compiling 1 source file to D:/work/lspworkspace/helloworld/classes jar : [ jar ] Building jar: D:/work/lspworkspace/helloworld/helloworld_temp.jar BUILD SUCCESSFUL Total time: 4 seconds
|
如果你是完全 copy 的话,这个过程理论上是不会出错的,如果有错误,请考虑是不是你的环境配置问题。
3. 第一个混淆任务
3.1. 执行混淆任务
我们按照前面讲到的安装方法,在这个可执行的 Ant 任务中加入混淆任务,加入的具体内容如下:
<!-- 混淆任务 --> <target name="obfuscate" depends="jar"> <taskdef name="obfuscate" classname="com.yworks.yguard.ObfuscatorTask" classpath="yguard.jar" />
<!-- 不同工程需要修改的地方 --> <obfuscate mainclass="${mainclass}" logfile="${obfuscationlog}" replaceclassnamestrings="true"> <inoutpair in="${jar}" out="${obfjar}" /> </obfuscate> </target> |
我们执行这个修改过的 ant 任务,在设定的输出目录下,至少会产生如下几个文件,
helloworld.jar ,混淆后的 jar 文件;
helloworld_temp.jar ,混淆前的原始 jar 文件;
helloworld_obf_log.xml ,产生的日志文件。
下面我们打开 jar 包,可以发现包名、类名混淆前后是不一样的,我们反编译其中 class 文件,
这是我们在混淆之前根据编译后的 class 文件反编译产生的源文件。
package helloworld; import java.io.PrintStream; public class HelloWorld { public HelloWorld() { publicAttribute = "i am a public attrubute."; protectedAttribute = "i am a protected attrubute."; privateAttribute = "i am a private attrubute."; }
public void publicSayHello() { System.out.println("hello world in public . "); }
protected void protectedSayHello() { System.out.println("hello world in protected . "); }
private void privateSayHello() { System.out.println("hello world in private . "); }
public static void main(String args[]) { HelloWorld helloworld = new HelloWorld(); helloworld.publicSayHello(); }
public String publicAttribute; protected String protectedAttribute; private String privateAttribute; } |
这是我们根据混淆后的 class 文件产生的源代码,我们可以发现类的包名、方法名、属性名已经发生了变化。
package A; import java.io.PrintStream;
public class A { public A() { B = "i am a public attrubute."; A = "i am a protected attrubute."; C = "i am a private attrubute."; }
public void B() { System.out.println("hello world in public . "); }
protected void C() { System.out.println("hello world in protected . "); }
private void A() { System.out.println("hello world in private . "); }
public static void A(String as[]) { A a = new A(); a.B(); }
public String B; protected String A; private String C; } |
下面,我们来学习 obfuscate 及其相关元素。
4. Obfuscate 元素
Obfuscate 是整个混淆任务定义的元素,下面我们对它及其子元素进行详细的介绍。
4.1. Obfuscate 元素
在当前版本中, Obfuscate 元素有如下一些属性,
l
mainclass
:简写你的应用的主程序类名,主类的类名和它的主方法名都会被修改。你可能想仅仅暴露主方法(main) ,如果你的jar 文件描述文件中MANIFEST.MF 包含了
Main-Class
属性,yguard 将会自动调整成混淆后的主类名。
l logfile :混淆过程产生的日志文件名称,这个日志文件包含了混淆过程的任何警告信息和映射信息。
l conservemanifest
:(取值为boolean 类型-true/false ),表示混淆器是否应改保持jar 包的
manifest 清单文件不变。缺省值为 false ,表示这个清单文件将会被混淆器修改用来反映新的信息摘要。
l
replaceClassNameStrings
:
(也是一个boolean 属性值),设定yguard 是否需要取代某些硬编码的字符串。(本文英文水平有限,这个属性没有理解清楚)。
4.2. inoutpair 元素
inoutpair 元素,每一个混淆任务( obfuscation task )都必须至少有一个 inoutpair 元素。该元素指定了要被混淆的源 jar 包和产生的目标 jar 包的名称和路径,要注意的是只有 class 文件能够被混淆,其他的如资源文件,只是简单的从源 jar 包 copy 到目标 jar 包。
In 属性指定包含了需要被混淆 jar 包。
Out 属性指定了混淆后产生的新 jar 包。
4.3.
Externalclasses
元素
如果这个被混淆的jar 包需要依赖其他外部class 文件或者jar 包,externalclasses 用来指定这些被依赖的实体的具体路径,这些实体不会被混淆。样例代码片如下:
<!-- 设置所有用到得第三方库,不混淆它们 --> <externalclasses>
<pathelement location="${third-party-lib-path}/log4j-1.2.8.jar" />
<pathelement location="${third-party-lib-path}/commons-logging-1.0.4.jar" />
<pathelement location="${third-party-lib-path}/jbossall-client.jar" />
<pathelement location="${third-party-lib-path}/commons-beanutils.jar" />
<pathelement location="${third-party-lib-path}/commons-pool.jar" />
</externalclasses>
|
4.4.
Property
元素
Property
元素用来给混淆引擎一些提示, 不同的
yGuard 版本可能会有不同的提示,混淆任务可以根据这些提示来控制混淆的过程。
它有两个强制的属性:
l Name 混淆任务能够理解的一个特定键。
l Value 键对应的值。
Yguard 1.5.2.0_03 ,支持下面的属性:
l Error-checking 属性告诉 yguard 有任何错误的时候就终止正常任务。当前可用的值为:
Pedantic 表示错误发生,混淆任务将失败。
l
Naming-scheme
属性告诉yguard 在混淆过程中用到的命名模式,这个属性只能取下列值之一:
Ø
Small
产生尽可能短的名字,这能使产生的结果jar 包尽可能的小。
Ø
Best
产生的名字在反编译后看起来可能象是被过去分词化,使用该模式产生的jar 包不一定能在所有的文件系统下能够被成功的解压,而且使用该模式也会占用很多的空间使得结果jar 很可能变的很大(通常是两倍左右大小)。
Ø
Mix
混合了前面两种模式,它产生的jar 包有一个比较合理的大小,也很难反编译。
l
Language-conformity
属性告诉yguard 产生大多数反编译器能够被反编译的名字,换而言之,yguard 产生的class 文件能够被今天多数虚拟机验证和执行;但是在反编译时会产生一些完全没用的垃圾语句来混淆。该属性当前可能的取值:
Ø
Compatible
产生的名字(包括java 、jar 、manifest 文件),,大多数编译器都能理解,大多数文件系统能解压。
Ø
Legal
产生的名字一部分反编译器可以理解。
Ø
Illegal
产生的名字可能虚拟机工作正常,但是可能使一些工具(如jbuilder )崩溃。
l
Obfuscation-prefix
用指定的包名称混淆现在的包名称。
l
Expose-attributes
指明除标准属性之外的应该被暴露在外的属性列表,缺省情况下,yguard 会从方法中删除不需要的属性,如:deprecated 。这个值可能是一个逗号分割的列表,该列表在
Section 4.7 of the VM Specification of the .class File Format 中定义。 保持“deprecated” 的代名片如下:
<property name="expose-attributes" value="Deprecated"/>
4.5. Expose 元素
Expose 是 obfuscate 的子元素,它用来规定 classes 、 methods 、 fields 、 attributes 这些元素是否应该被暴露。
Obfuscator 任务可能会删除很多 class 文件不需要的信息,这些零散的信息可能不应该被反编译。有一些 boolean 值形式的属性来控制它们是否可以暴露,也就是不被混淆或删除。
这些设置将影响所有的 class 文件,控制这些属性的最好办法是在 classes 元素中使用 attribute 元素。
l
sourcefile 原始源文件的名称信息是否应该包含在被混淆的类中,缺省值为false ,表示信息将被删除,如果取值为true ,表示原始源文件的名称信息将保留,反编译后一般在文件头以注释的方式在头注释的最后一行出现。
l
Linenumbertable 在混淆后的类文件中是否保存原始类文件的每个操作字节码对应的原始源文件行号信息的相关信息。缺省值为false ,表示不保存。
l localvariabletable
决定是否保留本地变量表,这个表包含了每一个在在原始源代码中使用的本地变量名称和混淆后的类中变量名称的映射关系。缺省值为false ,表示将删除该信息。
l Localvariabletypetable 是否保留本地变量类型表,这个表包含了在原始源代码中使用的本地变量的类型与混淆后的类中变量的映射关系。缺省值为false ,不保存,也就是说,混淆过程中将删除该信息。
4.6.
Class
元素
Class
元素用来设定如何暴露类的相关信息,这些信息包括类的名称、方法的名称、属性的名称。下面的两个属性来决定特定的类混淆级别。
l
Methods
属性决定可见性在什么级别的方法可以被暴露。
l
Fields
属性决定可见性在何种级别的属性字段可以被暴露。
下表列出了属性可能的取值,以及对应于该值,类的哪些元素将被暴露。‘* ’表示给定可见性的元素会被暴露,‘- ’表示相应的元素将被混淆。
Value/Visibility |
public |
protected |
friendly |
private |
none |
- |
- |
- |
- |
public |
* |
- |
- |
- |
protected |
* |
* |
- |
- |
friendly |
* |
* |
* |
- |
private |
* |
* |
* |
* |
我们可以看到 none 表示所有的元素都会被混淆。
指定类的混淆有 3 中方式:
1. 使用完全限定的 java 类名指定特定的类。如:
<class name="mypackage.MyClass"/>
|
2. 使用 patternset 元素设定多个java 类,patternset 的include 和exclude 元素必须符合java 语法,允许使用通配符。如:
<class> <patternset> <include name="com.mycompany.**.*Bean"/> <exclude name="com.mycompany.secretpackage.*"/> <exclude name="com.mycompany.myapp.SecretBean"/> </patternset> </class>
|
上边的代码表示将暴露除 com.mycompany.secretpackage 包中类和 com.mycompany.myapp.SecretBean 类之外包 com.mycompany 及其子包中所有的类。
<class>
<patternset>
<include name="com.mycompany.myapp.MainClass"/>
<include name="org.w3c.sax?."/>
<exclude name="org.w3c.sax?.**.*$*"/>
</patternset>
</class>
|
这段代码片表示将暴露MainClass 类,org.w3c.sax1, org.w3c.sax2, org.w3c.saxb 这种命名形式的包中出内部类之外的所有类。
注:‘$ ’符号是java 中外部类和内部类的分隔符,在ant 中‘$ ’是一个特殊字符,如果我们想把‘$ ’字符作为参数传递给ant 任务,必须使用连续两个‘$ ’也就是'$$' 来表示。
3 .最后一种方法是我们用类的可见性来设定,如:
<class classes="protected"> <patternset> <include name="com.mycompany.myapi."/> </patternset> </class>
|
上例表示将暴露包com.mycompany.myapi 及其子包中的所有public 、protected 元素(也就是说保留它们不变,不混淆这些元素)。
<class classes="protected" methods="protected"
fields="protected">
<patternset>
<include name="**.*"/>
</patternset>
</class>
|
这个例子演示了如何暴露public 类型API 的通用方法,表示所有的public 和protected 类型的类,以及它们中所有声明为public 和protected 的属性和方法都会被暴露。
最后一个例子演示了怎么只暴露类的公共方法,而不暴露其类名和属性。
<class classes="none" methods="public" fields="none">
<patternset>
<include name="com.mycompany.myapi."/>
</patternset>
</class>
|
4.7. Method 元素
Method 用来设定被暴露的方法签名,一般我们用不到这样的级别。这个元素有两个属性:
l class
属性用来指定拥有该方法的类,通常使用java 语法的完全限定类名,这个属性可以被忽略,
l name
属性指明了被暴露(不混淆)的方法。注意:方法的参数和返回类型必须是完全限定的java 类名,即使是void 类型。
一些例子如下:
<method class="com.mycompany.myapp.MyClass"
name="void main(java.lang.String[])"/>
<method class="com.mycompany.myapp.MyClass"
name="int foo(double[][], java.lang.Object)"/>
<method name="void writeObject(java.io.ObjectOutputStream)">
<patternset>
<include name="com.mycompany.myapp.data.*"/>
</patternset>
</method>
<method name="void readObject(java.io.ObjectInputStream)">
<patternset>
<include name="com.mycompany.myapp.data.*"/>
</patternset>
</method>
|
暴露MyClass 的main 方法和foo 方法,com.mycompany.myapp.data 包中所有类的所有的writeObject 和readObject 方法都将被暴露,
4.8. Field 元素
4.9.
Sourcefile
元素
Sourcefile 设定源文件可以暴露哪些信息,用在堆栈跟踪上,这个元素没有属性,
<sourcefile>
<patternset>
<include name="com.mycompany.myapp.**"/>
</patternset>
</sourcefile>
|
包com.mycompany.myapp 及其子包中所有类的类名将被暴露,注意这将阻止正常的混淆,因为源文件的名字和未混淆的类名捆绑的很紧密。
<sourcefile>
<property name="mapping" value="y"/>
<patternset>
<include name="com.mycompany.myapp.**"/>
</patternset>
</sourcefile>
|
这将把包名称映射成一个字母y 。 (mapping 属性是当前版本sourcefile 元素支持唯一属性)。
4.10.
Linenumbertable
元素
在上边expose 元素中也有一个linenumbertable ,在那里它是一个属性,在这里它指定哪些类的行号表将被保留而不被混淆,主要用在堆栈跟踪。
<linenumbertable>
<patternset>
<include name="com.mycompany.myapp.**"/>
</patternset>
</linenumbertable>
|
这将暴露com.mycompany.myapp 及其子包中所有类的行号,这些类的sourcefile
属性也必须被暴露,否则,JVM 将显示未知的行号信息Unknown source
。
<linenumbertable>
<property name="mapping-scheme" value="scramble"/>
<property name="scrambling-salt" value="1234"/>
<patternset id="CompanyPatternSet">
<include name="com.mycompany.myapp.**"/>
</patternset>
</linenumbertable>
<sourcefile>
<property name="mapping" value="y"/>
<patternset refid="CompanyPatternSet"/>
</sourcefile>
|
4.11.
Adjust
元素
Adjust 元素主要是把资源文件中引用的被混淆的类的名称作适当的调整,这是一个比较有用的元素。
注意:它将仅仅调整inoutpair 中out-jar 元素设定的包,也就是混淆过后生成的jar 包。它有3 个属性:
l
replaceName 属性设定资源名称是否应该被调整,缺省值为false ;
l
replaceContent 设定资源的内容是否应该被调整,缺省值为false ;
l
replacePath 设定资源文件的路径是否应该被调整,缺省值为true 。
一些例子如下:
<!-- 调整 jar 包中所有 java 属性文件的名称 --> <adjust replaceName="true">
<include name="**/*.properties" />
</adjust>
<!-- 调整 jar 包的某个 xml 文件中的引用的类名 --> <adjust file="plugins.xml" replaceContent="true" />
<!-- 不调整 jar 包中资源文件的路径 com/mycompany/myapp/resource. --> <!-- 包 com.mycomSpany.myapp 也可能被混淆 --> <adjust replacePath="false">
<include name="com/mycompany/myapp/resource/*" />
</adjust>
|
5. 一个实际项目中的例子
这个例子可以作为我们大多数普通项目的模板,根据情况稍加修改就可以了。
在我的项目中,我有 3 个文件夹:
l Src-lib 保存未混淆前的 jar 包,我的文件夹下有 framework-common.jar , framework-ejb.jar , cbm-common.jar , cbm-ejb.jar 四个 jar 包需要混淆;
l Target-lib 放置混淆后产生的 jar 包;
l third-party-lib 放置我的项目用到的第三方库,以及 yguard 混淆工具库。
另外有一个定义ant 任务的build.xml 文件,文件内容如下:
<?xml version="1.0" encoding="gb2312"?>
<project name="project" default="init" basedir=".">
<!-- 设置我们 Ant 任务的初始化工作 -->
<target name="init">
<!-- 工程名称 -->
<property name="project_name" value="cpht" />
<!-- 源 jar 包所在路径 -->
<property name="src-lib" value="src-lib" />
<!-- 目标 jar 包所在得路径 -->
<property name="target-lib" value="target-lib" />
<!--yguard 混淆器工作产生的日志文件 -->
<property name="obfuscationlog" value="${project_name}_obf_log.xml" />
<!-- 第三方库所在的路径 -->
<property name="third-party-lib-path" value="third-party-lib" />
</target>
<!-- *********************************** 混淆任务 **************************-->
<target name="obfuscate">
<taskdef name="obfuscate" classname="com.yworks.yguard.ObfuscatorTask" classpath="${third-party-lib-path}/yguard.jar" />
<!-- 不同工程需要修改的地方 -->
<obfuscate logfile="${obfuscationlog}" >
<!-- 有错误时将中断该 target-->
<property name="error-checking" value="pedantic" />
<property name="language-conformity" value="illegal" />
<!-- 采用混合方式产生名字 -->
<property name="naming-scheme" value="mix" />
<!-- 设置需要暴露哪些信息,这些信息将不会被混淆 -->
<expose>
<!-- 仅保持类名不变,完全混淆类的其他信息 -->
<class classes="private" methods="none" fields="none">
<patternset id="catic">
<include name="com.catic.**" />
<include name="catic.**" />
</patternset>
</class>
<!-- 保持所有的属性 ( 过时的方法,文件名,行号、本地变量表 ) ,在系统允许过程中,可能会发生错误信息, 如果不保存这些信息,出错时很难跟踪代码,为方便调试保留了这些信息 -->
<attribute name="Deprecated, SourceFile, LineNumberTable, LocalVariableTable">
<patternset refid="catic" />
</attribute>
</expose>
<!-- 设置所有用到得第三方库,不混淆它们 -->
<externalclasses>
<pathelement location="${third-party-lib-path}/log4j-1.2.8.jar" />
<pathelement location="${third-party-lib-path}/commons-logging-1.0.4.jar" />
<pathelement location="${third-party-lib-path}/jbossall-client.jar" />
<pathelement location="${third-party-lib-path}/commons-beanutils.jar" />
<pathelement location="${third-party-lib-path}/commons-pool.jar" />
</externalclasses>
<!-- 输入和输出 jar 包,可以有多行 --> <!--********************* 输入、输出设置开始 *************************-->
<inoutpair in="${src-lib}/framework-common.jar" out="${target-lib}/framework-common-obf.jar" />
<inoutpair in="${src-lib}/framework-ejb.jar" out="${target-lib}/framework-ejb-obf.jar" />
<inoutpair in="${src-lib}/cbm-common.jar" out="${target-lib}/cbm-common-obf.jar" />
<inoutpair in="${src-lib}/cbm-ejb.jar" out="${target-lib}/cbm-ejb-obf.jar" />
<!--********************* 输入、输出设置结束 *************************-->
<!-- 调整 jar 包的资源文件中的引用的类名 -->
<adjust replaceContent="true">
<include name="**/*.xml" />
<include name="./**/*.xml" />
</adjust>
</obfuscate>
<!-- 不同工程需要修改的地方 -->
</target>
<!-- *********************************** 混淆任务结束 ************************-->
</project>
<!-- build.xml 文件结束 --> |
第一target 是设置环境变量,第二个target 是执行混淆任务。
<property name="naming-scheme" value="mix" /> 配置混淆时产生的名字的方式为混合方式,我们前面讲到有3 中方式small,best ,mix ,这里我们选择mix 。
Expose 元素设置了我们只保持类名不变,类的方法和属性需要混淆,在系统运行过程中,可能会发生一些意想不到的错误信息,如果完全混淆类,出错时很难跟踪代码,为方便调试保留了类名、行号表、本地变量表等这些信息。
Externalclasses 元素指定了我们使用到的第三方库。
Inoutpair 元素设置我们要混淆的jar 包,以及混淆后产生的对应jar 包名称和路径。
Adjust 元素调整资源文件中引用的类信息为我们混淆后的类信息。
6. 结束语
本文只是简单的介绍了yguard 的使用,更多用法请参考官方相关的文档。
一般来讲,我们只需要作这些简单事情就足够了,因为没有任何文档参考的情况下,没有混淆的代码已经不容易看懂了,如果再稍微混淆一下,相信java
世界中,没有多少人会真正花大量的精力去阅读这种经混淆后再反编译产生的代码。