Javaバイトコード暗号化のクラッキング

2003年5月9日

Q: .classファイルを暗号化し、カスタムクラスローダーを使用してその場でロードおよび復号化した場合、逆コンパイルは防止されますか?

A: Javaバイトコードの逆コンパイルを防ぐ問題は、言語自体とほぼ同じくらい古いものです。市場で入手可能なさまざまな難読化ツールにもかかわらず、初心者のJavaプログラマーは、知的財産を保護するための新しくて賢い方法を考え続けています。このJavaQ&Aの記事では、ディスカッションフォーラムで頻繁に再ハッシュされるアイデアに関するいくつかの神話を払拭します。

Java.classファイルを元のファイルに非常によく似たJavaソースに再構築するのが非常に簡単なことは、Javaバイトコードの設計目標とトレードオフに大きく関係しています。特に、Javaバイトコードは、コンパクトさ、プラットフォームの独立性、ネットワークモビリティ、およびバイトコードインタープリターとJIT(ジャストインタイム)/ HotSpot動的コンパイラーによる分析の容易さのために設計されました。間違いなく、コンパイルされた.classファイルはプログラマーの意図を表現しているため、元のソースコードよりも分析が容易である可能性があります。

逆コンパイルを完全に防止しない場合でも、少なくともそれをより困難にするために、いくつかのことができます。たとえば、コンパイル後の手順として、.classデータをマッサージして、バイトコードを逆コンパイル時に読みにくくするか、有効なJavaコードに逆コンパイルするのを難しくする(またはその両方)ことができます。前者では極端なメソッド名のオーバーロードを実行するなどの手法が適切に機能し、後者では制御フローを操作してJava構文では表現できない制御構造を作成するなどの手法が適切に機能します。より成功した商用難読化ツールは、これらの手法と他の手法を組み合わせて使用​​します。

残念ながら、どちらのアプローチも実際にJVMが実行するコードを変更する必要があり、多くのユーザーは、この変換によってアプリケーションに新しいバグが追加されることを恐れています(当然のことながら)。さらに、メソッドとフィールドの名前を変更すると、リフレクション呼び出しが機能しなくなる可能性があります。実際のクラス名とパッケージ名を変更すると、他のいくつかのJava API(JNDI(Java Naming and Directory Interface)、URLプロバイダーなど)が破損する可能性があります。名前の変更に加えて、クラスのバイトコードオフセットとソース行番号の関連付けが変更された場合、元の例外スタックトレースの回復が困難になる可能性があります。

次に、元のJavaソースコードを難読化するオプションがあります。しかし基本的に、これは同様の一連の問題を引き起こします。

難読化ではなく暗号化?

おそらく、上記のことから、「バイトコードを操作する代わりに、コンパイル後にすべてのクラスを暗号化し、JVM内でその場で復号化するとどうなるでしょうか(カスタムクラスローダーで実行できます)。その後、JVMは元のバイトコードですが、逆コンパイルしたりリバースエンジニアリングしたりするものは何もありませんよね?」

残念ながら、あなたがこのアイデアを最初に思いついたと考えることと、それが実際に機能すると考えることの両方において、あなたは間違っているでしょう。そして、その理由はあなたの暗号化スキームの強さとは何の関係もありません。

シンプルなクラスエンコーダ

このアイデアを説明するために、サンプルアプリケーションとそれを実行するための非常に簡単なカスタムクラスローダーを実装しました。このアプリケーションは、2つの短いクラスで構成されています。

public class Main {public static void main(final String [] args){System.out.println( "secret result =" + MySecretClass.mySecretAlgorithm()); }} //クラスパッケージの終わりmy.secret.code; インポートjava.util.Random; public class MySecretClass {/ ** *推測すると、秘密のアルゴリズムは乱数ジェネレーターを使用するだけです... * / public static int mySecretAlgorithm(){return(int)s_random.nextInt(); } private static final Random s_random = new Random(System.currentTimeMillis()); } //クラスの終わり

私の願望はmy.secret.code.MySecretClass、関連する.classファイルを暗号化し、実行時にその場で復号化することにより、の実装を隠すことです。そのために、私は次のツールを使用します(一部の詳細は省略されています。完全なソースはリソースからダウンロードできます)。

public class EncryptedClassLoader extends URLClassLoader {public static void main(final String [] args)throws Exception {if( "-run" .equals(args [0])&&(args.length> = 3)){//カスタムを作成現在のローダーを//委任の親として使用するローダー:final ClassLoader appLoader = new EncryptedClassLoader(EncryptedClassLoader.class.getClassLoader()、new File(args [1])); //スレッドコンテキストローダーも調整する必要があります:Thread.currentThread()。setContextClassLoader(appLoader);最終クラスapp = appLoader.loadClass(args [2]);最終メソッドappmain = app.getMethod( "main"、new Class [] {String [] .class});最終的な文字列[] appargs =新しい文字列[args.length-3]; System.arraycopy(args、3、appargs、0、appargs.length); appmain.invoke(null、新しいオブジェクト[] {appargs}); } else if( "-encrypt"。equals(args [0])&&(args.length> = 3)){...指定されたクラスを暗号化...} else throw new IllegalArgumentException(USAGE); } / ** * java.lang.ClassLoader.loadClass()をオーバーライドして、通常の親子*委任ルールを変更し、*システムクラスローダーの監視下からアプリケーションクラスを「奪う」ことができるようにします。 * / public Class loadClass(final String name、final boolean resolve)throws ClassNotFoundException {if(TRACE)System.out.println( "loadClass(" + name + "、" + resolve + ")");クラスc = null; //まず、このクラスがこのクラスローダーによってすでに定義されているかどうかを確認します//インスタンス:c = findLoadedClass(name); if(c == null){クラスparentsVersion = null; try {//これは少し非正統的です://親ローダーを介して試行ロードを実行し、親が委任されているかどうかを確認します。 //これにより、クラス名でフィルタリングしなくても、すべてのコアクラスと拡張クラスに//適切な委任が行われます。parentsVersion= getParent()。loadClass(name); if(parentsVersion.getClassLoader()!= getParent())c = parentVersion; } catch(ClassNotFoundException ignore){} catch(ClassFormatError ignore){} if(c == null){try {// OK、 'c'はシステム(ブートストラップまたは拡張機能ではない)ローダーによってロードされました(in //その定義を無視したい場合)または親が完全に失敗した場合;いずれにせよ、//自分のバージョンを定義しようとします。c= findClass(name); } catch(ClassNotFoundException ignore){//失敗した場合は、親のバージョンにフォールバックします// [この時点ではnullになる可能性があります]:c = ParentsVersion;}}} if(c == null)throw new ClassNotFoundException(name); if(resolve)resolveClass(c); cを返す; } / ** * java.new.URLClassLoader.defineClass()をオーバーライドして、クラスを定義する前に* crypt()を呼び出せるようにします。 * / protected Class findClass(final String name)throws ClassNotFoundException {if(TRACE)System.out.println( "findClass(" + name + ")"); //.classファイルがリソースとしてロード可能であるとは限りません。 //しかし、Sunのコードがそれを行う場合は、おそらくマイニングできます... final String classResource = name.replace( '。'、 '/')+ ".class";最終URLclassURL = getResource(classResource); if(classURL == null)throw new ClassNotFoundException(name); else {InputStream in = null; {in = classURL.openStream();を試してください。最終バイト[] classBytes = readFully(in); // "decrypt":crypt(classBytes);if(TRACE)System.out.println( "復号化[" +名前+ "]"); defineClass(name、classBytes、0、classBytes.length);を返します。 } catch(IOException ioe){throw new ClassNotFoundException(name); }最後に{if(in!= null)try {in.close(); } catch(Exception ignore){}}}} / ** *このクラスローダーは、単一のディレクトリからのカスタムロードのみが可能です。 * / private EncryptedClassLoader(final ClassLoader parent、final File classpath)throws MalformedURLException {super(new URL [] {classpath.toURL()}、parent); if(parent == null)throw new IllegalArgumentException( "EncryptedClassLoader" + "null以外の委任親が必要"); } / ***指定されたバイト配列のバイナリデータを復号化/復号化します。メソッドを再度呼び出すと、*暗号化が逆になります。 * / private static void crypt(final byte [] data){for(int i = 8;i <data.length; ++ i)データ[i] ^ = 0x5A; } ...その他のヘルパーメソッド...} //クラスの終了

EncryptedClassLoader2つの基本的な操作があります。特定のクラスパスディレクトリ内の特定のクラスセットを暗号化することと、以前に暗号化されたアプリケーションを実行することです。暗号化は非常に簡単です。基本的に、バイナリクラスの内容のすべてのバイトの一部のビットを反転することで構成されます。(はい、古き良きXOR(排他的論理和)はほとんど暗号化されていませんが、我慢してください。これは単なる例です。)

によるクラスローディングEncryptedClassLoaderはもう少し注意に値します。私の実装はjava.net.URLClassLoader、両方loadClass()をサブクラス化してオーバーライドし、defineClass()2つの目標を達成します。1つは、通常のJava 2クラスローダー委任ルールを曲げて、システムクラスローダーが実行する前に暗号化されたクラスをロードする機会を得ることです。もう1つは、内部でcrypt()呼び出される直前に呼び出すことdefineClass()ですURLClassLoader.findClass()

すべてをbinディレクトリにコンパイルした後:

> javac -d bin src/*。javasrc/ my / secret / code/*。java 

私はMainMySecretClassクラスの両方を「暗号化」します。

> java -cp bin EncryptedClassLoader -encrypt bin Mainmy.secret.code.MySecretClass暗号化[Main.class]暗号化[my \ secret \ code \ MySecretClass.class] 

のこれら2つのクラスはbin暗号化されたバージョンに置き換えられました。元のアプリケーションを実行するには、次の方法でアプリケーションを実行する必要がありますEncryptedClassLoader

> java -cpbinスレッド "main"のメイン例外java.lang.ClassFormatError:java.lang.ClassLoader.defineClass0(ネイティブメソッド)のメイン(不正な定数プールタイプ)java.lang.ClassLoader.defineClass(ClassLoader.java: 502)java.security.SecureClassLoader.defineClass(SecureClassLoader.java:123)at java.net.URLClassLoader.defineClass(URLClassLoader.java:250)at java.net.URLClassLoader.access00(URLClassLoader.java:54)atjava。 net.URLClassLoader.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 -runbinメイン復号化[メイン]復号化[my.secret.code.MySecretClass]シークレット結果= 1362768201

案の定、暗号化されたクラスで逆コンパイラー(Jadなど)を実行しても機能しません。

洗練されたパスワード保護スキームを追加し、これをネイティブの実行可能ファイルにラップして、「ソフトウェア保護ソリューション」に数百ドルを請求する時が来ましたね。もちろん違います。

ClassLoader.defineClass():避けられないインターセプトポイント

すべてClassLoaderのは、明確に定義された1つのAPIポイントであるjava.lang.ClassLoader.defineClass()メソッドを介してクラス定義をJVMに配信する必要があります。ClassLoaderAPIは、この方法のいくつかのオーバーロードがありますが、それらのすべてがに呼び出すdefineClass(String, byte[], int, int, ProtectionDomain)方法。これは、finalいくつかのチェックを行った後、JVMネイティブコードを呼び出すメソッドです。新しいを作成したい場合、クラスローダーがこのメソッドの呼び出しを回避できないことを理解することが重要です。Class

このdefineClass()メソッドは、Classフラットバイト配列からオブジェクトを作成する魔法を実行できる唯一の場所です。そして、何を推測するか、バイト配列には、十分に文書化された形式で暗号化されていないクラス定義が含まれている必要があります(クラスファイル形式の仕様を参照)。暗号化スキームを破ることは、このメソッドへのすべての呼び出しをインターセプトし、すべての興味深いクラスをあなたの心の欲望に合わせて逆コンパイルするという単純な問題です(別のオプション、JVMプロファイラーインターフェイス(JVMPI)については後で説明します)。