深入Java字節碼加密

問:
如果我把我的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運行解密後的字節碼文件,這樣就不會被反編譯了對嗎?
考慮到你是第一個提出這種想法的並且它又能正常運行,我表示遺憾和不幸,這種想法是錯誤的。

下面是一個簡單的類編碼器:

爲了闡明這種思想,我採用了一個實例和一個很通用的類加載器來運行它,該程序包括兩個類:
[code]
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[/code]我想通過加密相關的class文件並在運行期解密來隱藏my.secret.code.MySecretClass的執行。用下面這個工具可以達到效果(你可以到這裏下載Resources):

[code]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[/code]

這個累加載器(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 [my\secret\code\MySecretClass.class]

現在原先編譯的class文件已經被加密後的文件所替代了,如果我想運行原始類文件,需要使用EncryptedClassLoader來操作:
[code]
>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[/code]
現在可以確信,採用任何反編譯工具對加密後的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)方法,在裏面加入其他的類。正如下面:
...
[code] 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) {}
}
}
...[/code]注意if裏的語句可以過濾系統類加載器及其子類加載器,同樣在defineClass()方法可以正常工作的情況下才能載入類。很難以相信不只有一個類加載器實例加載一個類,可通過在文件名堆裏面加入類加載器標誌我還是最終把這一問題給解決了。:-)
最後一步是用包含java.lang.ClassLoader類的可執行文件臨時替換由JRE使用的文件rt.jar,你也可以使用-Xbootclasspath/p選項。
我再一次運行加密的程序,並恢復了所有的未加密的文件,這麼說可以很容易的把.class文件正確的反編譯。我先聲明我並沒有用EncryptedClassLoader類的內部機制來完成此壯舉的。

在這裏注意一點,假如我沒去使用一個系統類,我可以使用別的方法,比如自定義一個JVMPI代理來處理JVMPI_EVENT_CLASS_LOAD_HOOK事件。

學習小結:
我希望你能對本文有所興趣,你必須認識到得很重要的一點是在購買市面上任何反編譯工具前要三思而行,除非JVM體系結構進行改革以支持class字節碼在本地能進行譯碼轉換,你纔會更好的從傳統的困惑中走出來,上演一場字節碼的改革浪潮!
當然也有其他的更有效的方法:對類加載進行調試。儘可能地得到類加載的軌跡是很有價值的,特別是在類加載時你去捕獲異常情況下使用。因此,JAVA的誕生可能純粹是爲了開源項目,當然,其他一些體系結構(如:。NET)也正在傾向於反編譯。目前我就說說這種思想了.
發佈了0 篇原創文章 · 獲贊 0 · 訪問量 3061
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章