问:
如果我把我的class文件加密,在运行时用指定的类加载器(class loader)装入并解密它,这样子能防止被反编译吗?
答:
防止JAVA字节码反编译这个问题在java语言雏形期就有了,尽管市面上存在一些反编译的工具可以利用,但是JAVA程序员还是不断的努力寻找新的更有效的方法来保护他们的智慧结晶。在此,我将详细给大家解释这一直来在论坛上有争议的话题。
Class文件能被很轻松的重构生成JAVA源文件与最初JAVA字节码的设计目的和商业交易有紧密地联系。另外,JAVA字节码被设计成简洁、平台独立性、网络灵活性,并且易于被字节码解释器和JIT (just-in-time)/HotSpot 编译器所分析。可以清楚地了解程序员的目的, Class文件要比JAVA源文件更易于分析。
如果不能阻止被反编译的话,至少可以通过一些方法来增加它的困难性。例如: 在一个分步编译里,你可以打乱Class文件的数据以使其难读或者难以被反编译成正确的JAVA源文件,前者可以采用极端函数重载,后者用操作控制流建立控制结构使其难以恢复正常次序。有更多成功的商业困惑者采用这些或其他的技术来保护自己的代码。
不幸的是,哪种方法都必须改变JVM运行的代码,并且许多用户害怕这种转化会给他们的程序带来新的Bug。而且,方法和字段重命名会调用反射从而使程序停止工作,改变类和包的名字会破坏其他的JAVA APIS(JNDI, URL providers, etc),除了改变名字,如果字节码偏移量和源代码行数之间的关系改变了,在恢复这有异常的堆栈将很困难。
于是就有了一些打乱JAVA源代码的选项,但是这将从本质上导致一系列问题的产生。
加密而不打乱
或许上述可能会使你问,假如我把字节码加密而不是处理字节码,并且JVM运行时自动将它解密并装入类加载器,然后JVM运行解密后的字节码文件,这样就不会被反编译了对吗?
考虑到你是第一个提出这种想法的并且它又能正常运行,我表示遗憾和不幸,这种想法是错误的。
下面是一个简单的类编码器:
为了阐明这种思想,我采用了一个实例和一个很通用的类加载器来运行它,该程序包括两个类:
public class Main
{
public static void main (final String [] args)
{
System.out.println ("secret result = " + MySecretClass.mySecretAlgorithm ());
}
} // End of class
package my.secret.code;
import java.util.Random;
public class MySecretClass
{
/**
* Guess what, the secret algorithm just uses a random number generator...
*/
public static int mySecretAlgorithm ()
{
return (int) s_random.nextInt ();
}
private static final Random s_random = new Random (System.currentTimeMillis ());
} // End of class
我想通过加密相关的class文件并在运行期解密来隐藏my.secret.code.MySecretClass的执行。用下面这个工具可以达到效果(你可以到这里下载Resources):
public class EncryptedClassLoader extends URLClassLoader
{
public static void main (final String [] args)
throws Exception
{
if ("-run".equals (args [0]) && (args.length >= 3))
{
// Create a custom loader that will use the current loader as
// delegation parent:
final ClassLoader appLoader =
new EncryptedClassLoader (EncryptedClassLoader.class.getClassLoader (),
new File (args [1]));
// Thread context loader must be adjusted as well:
Thread.currentThread ().setContextClassLoader (appLoader);
final Class app = appLoader.loadClass (args [2]);
final Method appmain = app.getMethod ("main", new Class [] {String [].class});
final String [] appargs = new String [args.length - 3];
System.arraycopy (args, 3, appargs, 0, appargs.length);
appmain.invoke (null, new Object [] {appargs});
}
else if ("-encrypt".equals (args [0]) && (args.length >= 3))
{
... encrypt specified classes ...
}
else
throw new IllegalArgumentException (USAGE);
}
/**
* Overrides java.lang.ClassLoader.loadClass() to change the usual parent-child
* delegation rules just enough to be able to "snatch" application classes
* from under system classloader's nose.
*/
public Class loadClass (final String name, final boolean resolve)
throws ClassNotFoundException
{
if (TRACE) System.out.println ("loadClass (" + name + ", " + resolve + ")");
Class c = null;
// First, check if this class has already been defined by this classloader
// instance:
c = findLoadedClass (name);
if (c == null)
{
Class parentsVersion = null;
try
{
// This is slightly unorthodox: do a trial load via the
// parent loader and note whether the parent delegated or not;
// what this accomplishes is proper delegation for all core
// and extension classes without my having to filter on class name:
parentsVersion = getParent ().loadClass (name);
if (parentsVersion.getClassLoader () != getParent ())
c = parentsVersion;
}
catch (ClassNotFoundException ignore) {}
catch (ClassFormatError ignore) {}
if (c == null)
{
try
{
// OK, either 'c' was loaded by the system (not the bootstrap
// or extension) loader (in which case I want to ignore that
// definition) or the parent failed altogether; either way I
// attempt to define my own version:
c = findClass (name);
}
catch (ClassNotFoundException ignore)
{
// If that failed, fall back on the parent's version
// [which could be null at this point]:
c = parentsVersion;
}
}
}
if (c == null)
throw new ClassNotFoundException (name);
if (resolve)
resolveClass (c);
return c;
}
/**
* Overrides java.new.URLClassLoader.defineClass() to be able to call
* crypt() before defining a class.
*/
protected Class findClass (final String name)
throws ClassNotFoundException
{
if (TRACE) System.out.println ("findClass (" + name + ")");
// .class files are not guaranteed to be loadable as resources;
// but if Sun's code does it, so perhaps can mine...
final String classResource = name.replace ('.', '/') + ".class";
final URL classURL = getResource (classResource);
if (classURL == null)
throw new ClassNotFoundException (name);
else
{
InputStream in = null;
try
{
in = classURL.openStream ();
final byte [] classBytes = readFully (in);
// "decrypt":
crypt (classBytes);
if (TRACE) System.out.println ("decrypted [" + name + "]");
return defineClass (name, classBytes, 0, classBytes.length);
}
catch (IOException ioe)
{
throw new ClassNotFoundException (name);
}
finally
{
if (in != null) try { in.close (); } catch (Exception ignore) {}
}
}
}
/**
* This classloader is only capable of custom loading from a single directory.
*/
private EncryptedClassLoader (final ClassLoader parent, final File classpath)
throws MalformedURLException
{
super (new URL [] {classpath.toURL ()}, parent);
if (parent == null)
throw new IllegalArgumentException ("EncryptedClassLoader" +
" requires a non-null delegation parent");
}
/**
* De/encrypts binary data in a given byte array. Calling the method again
* reverses the encryption.
*/
private static void crypt (final byte [] data)
{
for (int i = 8; i < data.length; ++ i) data [i] ^= 0x5A;
}
... more helper methods ...
} // End of class
这个累加载器(EncryptedClassLoader)有两个基本的操作,在给定的类路径下加密一系列Class文件并且运行一个先前加密的程序。加密后的文件很简单,有一些极讨厌的各个字节的位组成。(当然,XOR运算符不可能被加密,这只是一个范例,请多多包涵。)
通过EncryptedClassLoader来加载类需要注意一些问题,我实现的是继承自java.net.URLClassLoader并且重载了loadClass()和defineClass()两个方法来实现自己的两个功能。一个是专心于JAVA 2 类加载器的委托规则并且在系统类加载器做之前先加载一个经加密过的类;二是在执行defineClass()之前立即调用crypt()方法,否则会执行URLClassLoader.findClass()。
执行下面的语句:
>javac -d bin src/*.java src/my/secret/code/*.java
我把Main.class和MySecretClass.class进行了.加密:
>java -cp bin EncryptedClassLoader -encrypt bin Main my.secret.code.MySecretClass
encrypted [Main.class]
encrypted [mysecretcodeMySecretClass.class]
现在原先编译的class文件已经被加密后的文件所替代了,如果我想运行原始类文件,需要使用EncryptedClassLoader来操作:
>java -cp bin Main
Exception in thread "main" java.lang.ClassFormatError: Main (Illegal constant pool type)
at java.lang.ClassLoader.defineClass0(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:502)
at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:123)
at java.net.URLClassLoader.defineClass(URLClassLoader.java:250)
at java.net.URLClassLoader.access$100(URLClassLoader.java:54)
at java.net.URLClassLoader$1.run(URLClassLoader.java:193)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:186)
at java.lang.ClassLoader.loadClass(ClassLoader.java:299)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:265)
at java.lang.ClassLoader.loadClass(ClassLoader.java:255)
at java.lang.ClassLoader.loadClassInternal(ClassLoader.java:315)
>java -cp bin EncryptedClassLoader -run bin Main
decrypted Main
decrypted [my.secret.code.MySecretClass]
secret result = 1362768201
现在可以确信,采用任何反编译工具对加密后的Class文件都不会起作用的。
现在添加一个可靠的密码保护机制,把它打包成本地可执行文件,并且使其对外收费。这样子可以吗?当然不能这样了。
ClassLoader.defineClass():必然经过的接口
所有的类加载器必须经过明确地API把类定义传递到JVM里,这就需要java.lang.ClassLoader.defineClass()方法了。类加载器的API有多个这个方法的重载,但是所有的方法都会调用defineClass(String, byte[], int, int, ProtectionDomain),这是一个在经过一些简单验证后放入到JVM里的最终的方法。如果你想建立一个新的Class文件的话,这对于理解每个类加载器都会不可避免的调用该方法是很重要的。
你只能在方法defineClass()里把一些单调的字节数组生成Class对象,并且我们猜测这些字节数组文件会包含一些文档格式化(查看class文件格式规范well-document.d format)的未加密的class定义,通过拦截对该方法的所有调用可以很简单的破坏这种加密模式,并且很方便的反编译你感兴趣的Class文件。
做这种拦截并不困难,实际上破坏自己建立的保护模式比用工具更加迅速的。首先,我取得基于J2SDK的java.lang.ClassLoader源文件,并修改defineClass(String, byte[], int, int, ProtectionDomain)方法,在里面加入其他的类。正如下面:
...
c = defineClass0(name, b, off, len, protectionDomain);
// Intercept classes defined by the system loader and its children:
if (isAncestor (getSystemClassLoader ().getParent ()))
{
// Choose your own dump location here [use an absolute pathname]:
final File parentDir = new File ("c:/TEMP/classes/");
File dump = new File (parentDir,
name.replace ('.', File.separatorChar) + "[" +
getClass ().getName () + "@" +
Long.toHexString (System.identityHashCode (this)) + "].class");
dump.getParentFile ().mkdirs ();
FileOutputStream out = null;
try
{
out = new FileOutputStream (dump);
out.write (b, off, len);
}
catch (IOException ioe)
{
ioe.printStackTrace (System.out);
}
finally
{
if (out != null) try { out.close (); } catch (Exception ignore) {}
}
}
...
注意if里的语句可以过滤系统类加载器及其子类加载器,同样在defineClass()方法可以正常工作的情况下才能载入类。很难以相信不只有一个类加载器实例加载一个类,可通过在文件名堆里面加入类加载器标志我还是最终把这一问题给解决了。:-)
最后一步是用包含java.lang.ClassLoader类的可执行文件临时替换由JRE使用的文件rt.jar,你也可以使用-Xbootclasspath/p选项。
我再一次运行加密的程序,并恢复了所有的未加密的文件,这么说可以很容易的把.class文件正确的反编译。我先声明我并没有用EncryptedClassLoader类的内部机制来完成此壮举的。
在这里注意一点,假如我没去使用一个系统类,我可以使用别的方法,比如自定义一个JVMPI代理来处理JVMPI_EVENT_CLASS_LOAD_HOOK事件。
学习小结:
我希望你能对本文有所兴趣,你必须认识到得很重要的一点是在购买市面上任何反编译工具前要三思而行,除非JVM体系结构进行改革以支持class字节码在本地能进行译码转换,你才会更好的从传统的困惑中走出来,上演一场字节码的改革浪潮!
当然也有其他的更有效的方法:对类加载进行调试。尽可能地得到类加载的轨迹是很有价值的,特别是在类加载时你去捕获异常情况下使用。因此,JAVA的诞生可能纯粹是为了开源项目,当然,其他一些体系结构(如:。NET)也正在倾向于反编译。目前我就说说这种思想了.
matrix开源技术经Javaworld授权翻译并发布.
如果你对此文章有任何看法或建议,请到Matrix论坛发表您的意见.
注明: 如果对matrix的翻译文章系列感兴趣,请点击oreilly和javaworld文章翻译计划查看详细情况
您也可以点击-javamen查看翻译作者的详细信息. Java, java, J2SE, j2se, J2EE, j2ee, J2ME, j2me, ejb, ejb3, JBOSS, jboss, spring, hibernate, jdo, struts, webwork, ajax, AJAX, mysql, MySQL, Oracle, Weblogic, Websphere, scjp, scjd
标签: