一、反编译
反编译是一个将目标代码转换成源代码的过程。目标代码是一种用机器语言表示的代码,这种语言能通过实机或虚拟机直接执行。当C编译器编译生成一个对象的目标代码时,该目标代码是为某一特定硬件平台运行而产生的,在编译过程中,编译程序通过查表将所有符号的引用转换为特定的内存偏移量。目标代码只能在特定的CPU 上运行。而Java编译器为了保证目标代码的可移植性,并不将对变量和方法的引用编译为数值引用,也不确定程序执行过程中的内存布局,而是将这些符号引用信息保留在字节码中,由Java虚拟机在运行过程中创立内存布局,然后再通过查表来确定一个方法所在的地址。由于其相对简单的Java 虚拟机(与真实的微处理器相比)和规范的字节码格式,由Java字节码(Bytecode)反编译成源代码的过程相对于C语言来说要简单许多,因此,当前反编译Java程序颇为盛行。
在介绍Java反编译器之前,要提及JDK自带的一个工具javap,它是一个Java代码反汇编器。反汇编器和反编译器是不同的,使用javap反汇编的Java类文件可得到数据区定义、方法和类的引用等信息。例如,下面是对HelloWorld.class反汇编后的部分信息:
C:\JExamples>javap -c HelloWorld
Compiled from "helloworld.java"
public class HelloWorld extends java.lang.Object{
public HelloWorld();
Code:
0: aload_0
1: invokespecial #1; //Method java/lang/Object."<init>":()V
4: return
…
由此可见在Java字节码中蕴藏了大量的信息。Java反编译器就是利用类文件中的潜在信息和语言规范等猜测出源代码的。没有一个反编译器能够保证准确无误地翻译出源代码,而且每个反编译器自身也存在各种不同的漏洞。但是我们不可轻视反编译器的威力,它还是能比较准确地翻译出部分甚至全部的源代码。
下面是一个具体的例子(图1所示),利用Java反编译器Jode(Jode的下载位置:http://jode.sourceforge.net/download.html)成功地反编译了HelloWorld.class:
图1 Jode反编译HelloWorld.class
反编译对安全构成的威胁是显而易见的,因此源码保护也就必不可少。其实,反编译和代码保护是一场无休止的斗争,双方都在争斗中得以发展。
目前保护源码的方法大致可以归为三类:加密、模糊和定制Java的类装载器。所谓加密,就是在Java应用程序分发之前,使用加密工具进行加密。流行的加密工具有PGP(Pretty Good Privacy)和GPG(Gnu Privacy Guard)等。但最终用户在运行应用之前必须先进行解密,解密之后最终用户就有了一份不加密的类文件,所以加密只能对软件分发的中间环节进行有效保护,其实际效用大大减弱。
模糊技术(Obfuscator)就是对源代码进行模糊化处理的行为。经过模糊处理后的代码,将失去了一此可读性,程序员很难识别代码的用意。举例来说,有如下的源代码:
public class HillSort {
public static String[] sort(String[] stringArray) {
String tmp;
boolean exchange;
for (int i = 0; i < stringArray.length - 1; i++) {
exchange = false;
for (int j = stringArray.length - 2; j >= i; j--) {
if (stringArray[j + 1].compareTo(stringArray[j]) < 0) {
tmp = stringArray[j + 1];
stringArray[j + 1] = stringArray[j];
stringArray[j] = tmp;
exchange = true;
}
}
if (!exchange) { break; }
}
return stringArray;
}
…
}
利用模糊处理器Smokescreen (Smokescreen是一个Java模糊器软件,其下载位置:http://www.leesw.com/smokescreen/licensedownload.html,对HillSort.class进行模糊处理得到A.class,然后,再利用Jode对A.class反编译,得到的源代码如下:
public class A
{
public static String[] A(String[] strings) {
int i = 0;
GOTO flow_2_32_
flow_2_32_:
int i;
IF (i >= strings.length - 1)
GOTO flow_72_33_
String[] strings_0_ = strings;
boolean bool = false;
int i_1_ = strings_0_.length - 2;
for (;;) {
IF (i_1_ < i)
GOTO flow_75_34_
if (strings[i_1_ + 1].compareTo(strings[i_1_]) < 0) {
boolean bool_2_ = true;
String string = strings[i_1_ + 1];
strings[i_1_ + 1] = strings[i_1_];
strings[i_1_] = string;
bool = bool_2_;
}
i_1_--;
}
flow_70_35_:
String[] strings_3_ = null;
GOTO flow_71_36_
…
}
…
static {
…
}
}
模糊处理器把可读的有意义的变量名、方法名,有时甚至是类名、包名转换成没有意义的字符串,让人难以阅读程序,但对于 JVM 来说,其在本质上和原来的程序是一样的。在上例中类名HillSort变为A,方法名sort变为A,变量名stringArray变为strings,我们已经不可能简单地从这些名称了解这段代码的功能。
有的模糊处理器更进一步,甚至采用非法的字符串来替代类文件中的标记,有意地违反了Java的规范。这些古怪的用法也可能造成Java虚拟机不能作出合法的反应(尤其在浏览器中)。例如,一个像“=”这样的变量与 Java 的规范是相反的;一些虚拟机可以忽略它,而另一些不可以这样。
模糊处理器除了对符号名进行转换,还可能修改字节码值指令,以模糊方法中的指令控制流,使得反编译的工作更难。上例中就被加入大量的GOTO语句。
模糊处理器还会在字节码中添加一些俗称“炸弹”的代码,反编译器如果不能忽略或告警,常常可能导致自身崩溃。例如一些模糊处理器会在return语句后面插入无意义的代码,确保Java虚拟机不会被执行它,反编译器却可能没有识别这个障碍。
模糊处理器也有一些不足之处,main方法不会被模糊处理,这是既定的类入口函数,否则Java虚拟机无法运行该类,本地方法不会被模糊处理。另外,调用Class.forName()时指定的类名字符串,不会被模糊处理,模糊处理会提供模糊转换前后的名称映射表,手工修改此处的类名字符串为转换后的名称,即可解决该问题。
反编译和模糊处理的技术都在发展,越来越复杂。这方面的工具也越来越多,大多既支持命令行,也支持图形界面。
在介绍如何定制类装载器(ClassLoader)前,先要了解Java的运行机制。Java应用程序启动时,在运行指定类的main方法前,虚拟机首先要装载该类的字节码,然后还要执行链接和初始化的准备工作。例如运行HelloWorld时,Java虚拟机试图执行类HelloWorld的main方法,但发现该类并没有被装载,于是虚拟机使用 ClassLoader寻找并装载HelloWorld的字节码。如果这个装载过程失败,则抛出一个异常;如果HelloWorld被成功装载,HelloWorld的main方法才会进一步被调用。
Java运行时装入字节码的机制意味着可以对字节码和装载过程进行修改。一个称为ClassLoader的对象负责为JVM装载类的字节码。JVM给ClassLoader一个待装入类的名字(比如HelloWorld),然后由 ClassLoader负责找到类文件,装入原始数据,并把它转换成一个Class对象。
我们可以对类文件进行加密,当其通过定制ClassLoader装载时再进行解密,解密后的字节码保存在内存中,这样窃密者很难得到解密后的代码。定制ClassLoader既要完成原来所承担的工作,又要完成即时解密的任务。类装载器实现的核心代码如下:
import java.io.*;
import java.security.*;
import java.lang.reflect.*;
import javax.crypto.*;
import javax.crypto.spec.*;
public class XClassLoader extends ClassLoader {
private final String CRYPT_ALGORITHM = "DES";
private Cipher cipher = null;
public XClassLoader(String keyFile)
throws IOException, GeneralSecurityException {
byte keyData[] = Util.readFile(keyFile);
SecretKeyFactory keyFactory =
SecretKeyFactory.getInstance(CRYPT_ALGORITHM);
SecretKey secretKey =
keyFactory.generateSecret(new DESKeySpec(keyData));
cipher = Cipher.getInstance(CRYPT_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, secretKey, new SecureRandom());
}
public Class loadClass(String name, boolean resolve) throws
ClassNotFoundException {
Class clasz = null;
// 检查类是否已被装载
if (null != (clasz = findLoadedClass(name))) {
return clasz;
}
// 定制装载处理
try {
// 读取加密的类文件
byte encodedClass[] = Util.readFile(name + ".class");
if (null != encodedClass && null != cipher) {
// 解密
byte decodedClass[] = cipher.doFinal(encodedClass);
// 创建类实例
if (decodedClass != null) {
clasz = defineClass(name, decodedClass, 0, decodedClass.length);
}
}
} catch (Exception e) { }
// 如果上面没有成功,用默认的ClassLoader装入它
if (clasz == null) {
clasz = findSystemClass(name);
}
// 装入相关的类
if (resolve && clasz != null) {
resolveClass(clasz);
}
if (clasz == null) {
throw new ClassNotFoundException();
}
return clasz;
}
static public void main(String args[]) throws Exception {
if (args.length < 2) {
System.out.println(
"Usage: java XClassLoader keyFile Application [args0] [args1] ...");
return;
}
String keyFile = args[0]; //密匙文件
String appName = args[1]; //应用实例类名
String appArgs[] = null; //应用实例的命令行参数
if (args.length > 2) {
appArgs = new String[args.length - 2];
System.arraycopy(args, 2, appArgs, 0, args.length - 2);
}
// 创建解密的ClassLoader
XClassLoader xClassLoader = new XClassLoader(keyFile);
// 装载应用实例的字节码,并创建一个类实例
Class appClass = xClassLoader.loadClass(appName);
// 通过Reflection API,获取应用实例的main()方法引用
Method main = appClass.getMethod(
"main", new Class[]{ (new String[1]).getClass() } );
// 调用main()
main.invoke( null, new Object[] {appArgs});
}
}
public class Util
{
// 把文件读入byte数组
static public byte[] readFile( String filename )
throws IOException {
File file = new File( filename );
byte data[] = new byte[(int)file.length()];
FileInputStream fin = new FileInputStream( file );
int r = fin.read( data );
if (r != data.length)
throw new IOException( "Not complete reading file: " + file );
fin.close();
return data;
}
// 把byte数组写出到文件
static public void writeFile( String filename, byte data[] )
throws IOException {
FileOutputStream fout = new FileOutputStream( filename );
fout.write( data );
fout.close();
}
}
类加密器实现的核心代码如下:
import java.io.*;
import java.security.*;
import javax.crypto.*;
public class XClassEncode {
static private final String CRYPT_ALGORITHM = "DES";
static public String keyFile = "key.data";
// 生成密匙,并把密匙保存到文件
static private SecretKey generateKey()
throws IOException, GeneralSecurityException {
KeyGenerator kg = KeyGenerator.getInstance( CRYPT_ALGORITHM );
kg.init( new SecureRandom() );
SecretKey key = kg.generateKey();
Util.writeFile( keyFile, key.getEncoded());
return key;
}
static public void main(String args[]) throws Exception {
if(args.length < 1 ) {
System.out.println(
"Usage: java XClassEncode [class1] [clsss2] ...");
System.out.println(
"Output: key.data class1 clsss2 ...");
return;
}
// 生成密匙
SecretKey key = generateKey();
Cipher ecipher = Cipher.getInstance( CRYPT_ALGORITHM );
ecipher.init( Cipher.ENCRYPT_MODE, key, new SecureRandom() );
// 加密命令行中指定的每一个class文件
for (int i=0; i<args.length; ++i) {
System.out.println( "encode file: "+ args[i]);
// 读入类文件
byte classData[] = Util.readFile( args[i] );
// 加密
byte encodedClassData[] = ecipher.doFinal( classData );
Util.writeFile( args[i], encodedClassData );
}
}
}
运行示例如下:
C:\XClassLoader>javac XClassEncode.java XClassLoader.java Util.java
C:\XClassLoader>javac HelloWorld.java
C:\XClassLoader>java HelloWorld
Hello World!
C:\XClassLoader>java XClassEncode HelloWorld.class
encode file: HelloWorld.class
C:\XClassLoader>java HelloWorld
Exception in thread "main" java.lang.ClassFormatError:
…
C:\XClassLoader>java XClassLoader key.data HelloWorld
Hello World!
|