Javaプログラムの難読化手法

  1. ネットワークシステム研究室
  2. 指導教員 : 坂本 直志 准教授
  3. 08ec046 : 工藤 和也

目次


1.はじめに

近年では多種多様のプログラミング言語が存在し、Java言語はその中でも最も人気のあるプログラミング言語である(2012年2月現在)[1]。しかし、Javaプログラムは実行ファイルからソースコードを読み取ることのできる逆コンパイラが出回っている[2][3]。プログラムのソースコードはプログラム作成者の知的財産であり、他者の解析から保護しなければならない。そこで、ソースコードが知られてしまってもソースコードの意味を理解し難くする手法として難読化という方法が考えられている。

難読化とはソースコードの変数名を別の変数名に書き換えたり、処理順序を変更したりすることによってソースコードを読み難くし、プログラムの意味を理解し難くする方法である(図1)。このとき、ソースコードは書き換えるがプログラムの動作結果は等価である。

        int form = 1;
        int to = 10;
        int sum = 0;
        while(from <= to) {
            sum += from++;
        }
        System.out.println(sum);
  

        int b = 10;
        int c = 0;
        int a = 1;
        while(a <= b) {
            c += a++;
        }
        System.out.println(c);
  

図1. 変数名の置き換えと処理順序の変更

本研究ではこの難読化に着目し、プログラムの解析を困難にすることを目的とする。難読化の手法については、数値、文字、オブジェクトの3項目に分けて考える。また、本研究の難読化手法を自動化するため、正規表現を用いてJavaファイル(テキストファイル形式)を難読化後のプログラムに変更する方法を考える。

本論文は、本章を含めて4章と参考文献、付録から構成される。第2章では本研究に出てくる用語説明や本研究を理解する上での予備知識について述べ、第3章で本研究の具体的な内容について説明する。そして、第4章で本研究のまとめ及び今後の課題について述べる。参考文献では本論文で参考にした資料を記載し、付録に本研究で作成したプログラムを記載する。

2.準備

2.1.Javaとは

2.1.1.Javaの特徴

Java言語は1990年、サン・マイクロシステムズ(2010年1月にオラクルにより買収)が開発をはじめ、1996年1月に完成したプログラミング言語の1つである。1994年にJavaのアルファ版であるJava 1.0aが発表され、1995年にJava 1.0のベータ版が発表された。そして、1996年1月に完成版であるJava 1.0が発表された。C言語に似た表記法を採用し、既存の言語の欠点を踏まえて設計されたオブジェクト指向プログラミング言語である。Java言語でプログラミングをするためには、Java開発キットJDK(Java Development Kit)を用い、Javaプログラムで作成されたアプリケーションを実行するためには、Java実行環境JRE(Java Runtime Environment)を用いる。

Javaのプログラムは複数のクラスから構成され、プログラムの実行は各クラスが実体化したオブジェクト群が相互にメッセージをやりとりしながら行われる。クラスとは、オブジェクト指向においてオブジェクトの設計図にあたるものである。データ型には、参照型と基本型の2種類があるが、Javaのオブジェクトはすべて参照型である。

また、JavaはOSやマイクロプロセッサ等のプラットフォームに依存されにくい特徴を持っている。つまり、作成したJavaプログラムは多種多様のプラットフォーム上でも動作するということである。これはJava実行環境に含まれるJava仮想マシン上でプログラムが動作するためである。

2.1.2.Javaプログラムの説明

ここでは実際のJavaプログラムを説明する。付録AのPersonクラスはJavaプログラムの一例である。

Personクラスはオブジェクト生成時に名前を登録する。toStringメソッドで「Person : 名前」を戻り値として返し、getNameメソッドで「名前」を戻り値として返す。setNameメソッドでは名前を新しく登録しなおす。

2.1.3.Javaの用語

2.1.3.1.コンストラクタ

コンストラクタはオブジェクトの生成時に呼び出される特殊な関数で戻り値はない。一般的にインスタンスの初期化を行う。付録AのPersonクラスでは図2の部分がコンストラクタである。フィールドnameを引数として受け取った文字列で初期化している。

    public Person(String name) {
        this.name = name;
    }
        

図2. Personクラスのコンストラクタ

2.1.3.2.setter、getter

setter、getterはprivate型で宣言されたフィールドを外部のクラスから操作するためのメソッドである。付録Aのpersonクラスでは図3の部分がsetter、図4の部分がgetterである。setterではフィールドnameに引数として受け取った文字列を代入し、getterではフィールドnameに保存されている文字列を戻り値として返している。

    public void setName(String name) {
        this.name = name;
    }
        

図3. Personクラスのsetter

    public String getName() {
        return name;
    }
        

図4. Personクラスのgetter

2.1.3.3.メソッド

メソッドはクラスの実際の動作を行う場所で、図5のように記述する。戻り値がない場合は戻り値の型をvoidとし、returnにおいて戻り値は記述しない。引数はカンマで区切ることで複数にすることもできる。図6はメソッドの一例である。引数として受け取った2つのint型整数の和を計算し、その値を戻り値として返すメソッドである。

    [アクセス修飾子] [戻り値の型] [メソッド名] ([引数の型] [引数] ) {
        [処理] ;
        return [戻り値] ;
    }
        

図5. メソッドの記述方法

    public int add(int from, int to) {
        return from + to;
    }
        

図6. メソッドの一例

2.1.3.4.static

staticという修飾子がついているメソッドおよび変数は、後述するオブジェクトの生成を行わなくてもクラスの機能を利用することができ、そのクラスから生成された全てのオブジェクトで共有される。static修飾子がついた変数には[クラス名].[変数名]でアクセスし、static修飾子がついたメソッドには[クラス名].[メソッド名]でアクセスすることができる。図7はstatic変数、staticメソッドを利用したプログラムの一例である。ConuntIncrementクラスはオブジェクトが生成されるたびにcountの値を増やし、getCountメソッドでConuntIncrementクラスのオブジェクトが生成された回数を戻り値として返すクラスである。ConuntIncrementRunクラスではstaticなgetCountメソッドを呼び出している。

class ConuntIncrementRun {
    public static void main(String[] args) {
        System.out.println(ConuntIncrement.getCount());
    }
}
public class ConuntIncrement {
    private static int count = 0;
    ConuntIncrement(){
        count++;
    }
    public static int getCount() {
        return count;
    }
}
        

図7. static変数およびstaticメソッドの例

2.1.3.5.mainメソッド

mainメソッドはプログラムを動作させると最初に呼び出されるメソッドで、図8のように記述する。このメソッドから他のメソッドを呼び出してプログラムを動作させていく。

    public static void main(String[] args) {
        [処理]
    }
        

図8. mainメソッドの記述方法

2.1.3.6.オブジェクトの生成

クラスの機能を実際に利用するためにはそのクラスのオブジェクトの生成を行う必要がある。オブジェクトを生成するためには図9のように記述する。図10は付録AのPersonクラスのオブジェクトを生成する方法の一例である。

    [クラス名] [変数名] = new [クラスのコンストラクタ] ;
        

図9. オブジェクトの生成方法

    Person person = new Person(“Suzuki”);
        

図10. Personクラスのオブジェクトの生成

2.1.3.7.メソッドの呼び出し

プログラムが動作しているクラスと同一のクラスにあるメソッドは図11のように呼び出して利用することができる。他クラスのメソッドはオブジェクトの生成後、図12のように呼び出すことができる。メソッド呼び出しの評価値は呼び出したメソッドの戻り値となる。

    [メソッド名] ( [引数] ) ;
        

図11. 同一クラスのメソッドの呼び出し方法

    [変数名].[メソッド名] ( [引数] ) ;
        

図12. 他クラスのメソッドの呼び出し方法

2.2.解析しにくいプログラムとは

2.2.1.プログラムの解析とは

プログラムを実行した場合、プログラム実行者が知ることのできるのはプログラムの実行結果だけである。プログラムがその実行結果を出力するために行った途中の内部処理は知ることができない。例えば、図13は1から10までの和を求めるプログラムの一部である。このプログラムの出力結果は「55」であるが、プログラムを実行しただけでは「55」がどのように算出されているか、何を表しているのかを知ることができない。そこで、プログラムの解析とはプログラムの動作内容を把握し、プログラムの意味を理解しようとすることと定義する。

        int form = 1;
        int to = 10;
        int sum = 0;
        while(from <= to) {
            sum += from++;
        }
        System.out.println(sum);
      

図13. 1から10までの和を求めるプログラム

2.2.2.解析しにくいプログラムとは

解析しにくいプログラムとは、プログラムから実行結果が予想しにくい、メソッドがどのような処理をしているのかわかりにくい、変数がどのような意味を持っているのかわかりにくい等、プログラムの意味を理解するのに時間がかかるプログラムのことである。つまり、解析にかかる時間を長くすることを目的としたプログラムが解析しにくいプログラムということになる。図14は図13のプログラムの変数名を意味の持たないものにし、さらに宣言の順番を変え、解析を困難にしたプログラムの一例である。

        int b = 10;
        int c = 0;
        int a = 1;
        while(a <= b) {
            c += a++;
        }
        System.out.println(c);
      

図14. 解析しにくいプログラムの一例

また、Javaにはコーディング規約[11]というものがあり、Javaプログラムを読みやすくするためのプログラムの記述方法がある。Javaコーディング規約では「クラス名の最初は大文字にする」等の決まりごとがあるが、Javaコーディング規約の通りに記述しなくてもプログラムは動作する。そこで、Javaコーディング規約に反した記述方法でプログラムを記述することで、プログラムを読み難くし、プログラムの解析を困難にできる。例として付録BにJavaコーディング規約に則って記述したJavaプログラムを記し、付録CにJavaコーディング規約に反して記述したJavaプログラムを記す。

2.2.3.既存の難読化手法

2.2.3.1.変数名、メソッド名、クラス名の変更

ここで既存の難読化手法について説明する。

変数名、メソッド名、クラス名の変更による難読化手法は、意味のある名称になっている変数名、メソッド名、クラス名を意味のない名称に書き換える方法である(図15)。図15の例では OperationRunクラスはaクラスとなり、Operationクラス内の各メソッドはa, b, c, dとなる。また、メソッド内で使用されていた変数名もa, bとなる。

class OperationRun {
    public static void main(String[] args) {
        System.out.println(Operation.add(1, 2));
    }
}

public class Operation {
    public static int add(int num1, int num2) { return num1 + num2;}
    public static int subtract(int num1, int num2) { return num1 - num2;}
    public static double multiply(int num1, int num2) { return num1 * num2;}
    public static double divide(int num1, int num2) { return num1 / num2;}
}
        

class a {
    public static void main(String[] args) {
        System.out.println(Operation.a(1,2));
        }
}

public class Operation {
    public static int a(int a, int b) { return a + b;}
    public static int b(int a, int b) { return a - b;}
    public static double c(int a, int b) { return a * b;}
    public static double d(int a, int b) { return a / b;}
}
        

図15. 変数名、メソッド名、クラス名の変更による難読化

2.2.3.2.無意味なコードの挿入

無意味なコードの挿入による難読化する手法は、意味のない構文やメソッドを元のプログラムに挿入する方法である(図16)。図16の例ではプログラム中で使用されない変数aやbを作成したり、呼び出されることのないdummyメソッドを挿入したりしている。

この他にも、必ず同様の結果になる条件分岐や、発生することのない例外処理の追加等も無意味なコードの挿入にあたる。

class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello World !");
    }
}
        

class HelloWorld {
    public static void main(String[] args) {
        int a = 10;    int b = a + 20;
        System.out.println("Hello World !");
    }
    public void dummy () { /* 呼び出されないメソッド */ }
}
        

図16. 無意味なコードの挿入による難読化

2.2.3.3.数値、計算式の複雑化

計算式の複雑化による難読化手法は、プログラム中に存在する数値や計算式を複雑にする方法である(図17)。図17の例では、変数countに代入する値を、乱数を用いた値と複数の計算式によって求めている。また乗算、除算を使う代わりにシフト演算を用いて計算を行っている。ただし、シフト演算に関してはプログラムが見やすくなることもある。

        int count = 1;
        for(int i = 0; i <= 5; i++) {
            count = count * 2;
        }
        System.out.println(count);
        

        int temp = (int)Math.random() * 100;
        int temp2 = (temp + (2 << 1)) << 1;
        temp2 = (temp2 - (3 << 1)) >> 1;
        int count = temp2 - temp;
        for(int i = 0; i <= 5; i++) {
            count = count << 1;
        }
        System.out.println(count);
        

図17. 計算式の複雑化による難読化

2.2.3.4.制御構造の複雑化

制御構造の複雑化による難読化手法は、処理の内容や順序は変更せずに、繰り返し文や条件分岐などの制御構造を複雑にする方法である。ループの繰り返し回数を減らしてループの外で少なくなった処理を補ったり(図18)、ループの始点と終点を変更して条件分岐でループから抜けるようにしたりする方法などがある。図18の例では繰り返し処理の回数が10回から9回になっており、繰り返し文の外で少なくなった処理を補っている。

        int sum = 0;
        int dec = 0;
        for(int i = 1; i <= 10; i++) {
            sum = sum + i;
            dec = dec - i;
        }
        System.out.println(sum + ", " + dec);
        

        int sum = 0;
        int dec = 0;
        int i = 1;
        sum = sum + i;
        for(; i <= 9;) {
            dec = dec - i++;
            sum = sum + i;
        }
        dec = dec - i++;
        System.out.println(sum + ", " + dec);
        

図18. 制御構造の複雑化による難読化

2.2.3.5.処理順序の複雑化

処理順序の複雑化による難読化手法は、順序を入れ替えても問題ない部分を入れ替えることで処理順序を複雑にする方法である(図19)。順序を変更しても問題ないことを調べる必要がある。図19の例では最初に2つの変数を宣言してから値を代入していたものを、変数を1つ宣言した後すぐにその変数に値を代入するように変更している。また変数aよりも変数bについての処理を先に行っている。

        int a;
        double b;
        a = 1;
        b = 1.5;
        System.out.println(a + ", " + b);
        

        double b;
        b = 1.5;
        int a;
        a = 1;
        System.out.println(a + ", " + b);
        

図19. 処理順序の複雑化による難読化

2.2.3.6.処理順序の隠蔽化

処理順序の隠蔽化による難読化手法は、どのような順序でプログラムが動作しているかを隠蔽する手法である。例えば、1→2→3の順番で呼び出されるプログラムを1→3→2→1→3→2のように呼び出し、1番目、3番目、5番目に呼び出されたプログラムは無視するようにする(図20)。呼び出すメソッドの順序は変更されるが、メソッド内の処理順序が変わるわけではない。しかし、ユーザーの入力に対して、メソッドを呼び出す順番が変わってしまう場合にはそのことも考慮しなければならない。図20の例では何らかのメソッドを呼び出すたびに変数countの値を増やし、countの値が指定された値でない時はメソッド内の処理を行わないようにしている。

    public static void main(String[] args) {
        method001();
        method002();
        method003();
    }
    private static void method001() { /* 処理 */ }
    private static void method002() { /* 処理 */ }
    private static void method003() { /* 処理 */ }
        

    static int count = 0;
    public static void main(String[] args) {
        method001();    count++;
        method003();    count++;
        method002();    count++;
        method001();    count++;
        method003();    count++;
        method002();    count++;
    }
    private static void method001() { if(count == 0) { /* 処理 */ } }
    private static void method002() { if(count == 2) { /* 処理 */ } }
    private static void method003() { if(count == 4) { /* 処理 */ } }
        

図20. 処理順序の隠蔽化による難読化

2.3.難読化とは

難読化とはすでに完成されたJavaプログラムを解析しにくいプログラムに書き換えることである。難読化で重要となるのはすでに完成されたプログラムを書き換えるわけなので、元のプログラムを壊してはいけない。つまり、元のプログラムの仕様を変更しないということである。

難読化の特徴は次のとおりである。

  1. 元のプログラムより解析が困難になる。
  2. 一般的には元のプログラムよりもファイルサイズが大きくなる。
  3. 一般的には元のプログラムより処理時間が長くなる。

1.は難読化の目的である。解析が困難にならなければ難読化とは言えない。

2.はプログラムを解析しにくくするためのプログラムを加えるため、ファイルサイズが大きくなる。しかし、難読化の手法によっては逆にプログラムが短くなり、ファイルサイズが小さくなるものもある。

3.はプログラムを解析しにくくするためのプログラムが加わるため、そのプログラムの処理の数だけ処理時間が長くなる。しかし、難読化の手法によっては逆に処理時間が短くなるものある。

また、プログラムの解析を困難にする手法の一つに暗号化というものがある。プログラムを暗号化した場合、解析は困難になるが暗号化したプログラムはコンパイルの前に復号しなければプログラムを実行することはできない。しかし、難読化の場合はプログラムをそのままコンパイルし、実行することができる。

解析の困難さでは暗号化の方が優れているが、難読化したプログラムを暗号化するという様に併用することもできる。そのため、解析の困難さが暗号化に劣っているからといってデメリットにはならない。

2.4.シャローコピーとは

本研究の難読化手法で用いるシャローコピーについて説明する。

2.1で「Javaのオブジェクトはすべて参照型である。」と言った。これはオブジェクトを生成し変数に代入した場合、変数にはインスタンス(オブジェクトの実体)が代入されるわけではなく、インスタンスへの参照が代入されることを意味する。この参照型変数を「a = b;」のようにコピーした場合、インスタンスはコピーされずインスタンスへの参照がコピーされる(図21)。つまり、コピー先(コピー元)のインスタンスを変更すると、コピー元(コピー先)のインスタンスまで変更されたように見える。このようなコピー方法をシャローコピーと呼ぶ。

シャローコピー

図21. シャローコピー

これに対し、インスタンスそのものをコピーし(図22)、コピー先(コピー元)のインスタンスを変更しても、コピー元(コピー先)のインスタンスは変更されないコピー方法をディープコピーと呼ぶ。

ディープコピー

図22. ディープコピー

図23は簡単な例である。

ただし、CopyCheckクラスにはfieldというpublic int型のフィールドがあるとする。

        CopyCheck copycheck1 = new CopyCheck();
        CopyCheck copycheck2 = copycheck1;        /* シャローコピー */
        
        copycheck1.field = 10;
        copycheck2.field = 20;
        
        System.out.println(copycheck1.field);
        System.out.println(copycheck2.field);
    

図23. シャローコピーのプログラム例

図23のプログラムでインスタンスが普通にコピーされていれば、実行結果は「10」と「20」が表示されるはずである。しかし、実際の実行結果は「20」と「20」が表示される。これはcopycheck2がcopycheck1のインスタンスをシャローコピーし、copycheck1と同一のインスタンスを参照しているからである。そのため、copycheck2のfieldの値を変えた場合、copycheck1のfieldの値も変わっているかのように見えるのである。つまり、fieldと言う箱は1つしかなく、copycheck1.fieldもcopycheck2.fieldも同一のfieldを参照しているということである。

2.5.正規表現とは

本研究の難読化の自動化で用いる正規表現について説明する。

正規表現とは、文字列に含まれる指定したパターンにマッチする部分があるかどうかを調べることができるものである。正規表現で用意されている構文や特殊な文字を組み合わせる事で、複雑な条件を持つ検索パターンを簡潔に定義することが出来るのが特徴である。

Patternクラスで正規表現の構文を作り、Matcherクラスで指定した文字列とマッチするかどうかを調べる方法(図24)が標準的な利用方法である。図24のプログラムでは構文”a*b”と言うパターンにマッチする文字列を検索する。”a*b”という構文ではaという文字が0文字以上続いた後にbという文字が続く文字列を検索する。”aaaaab”という文字列はaという文字が5文字続いた後にbという文字が続いているのでパターンにマッチし、変数bにはtrueが代入される。

    Pattern p = Pattern.compile("a*b");
    Matcher m = p.matcher("aaaaab");
    boolean b = m.matches();
    

図24. 正規表現の利用例

Pattern (Java Platform SE 6)

<http://java.sun.com/javase/ja/6/docs/ja/api/java/util/regex/Pattern.html>

付録Dに正規表現を利用したプログラム例を記す。

付録DのPassHideクラスのpassChangeメソッドは文字列を引数として受け取り、正規表現を用いて[ ]で囲まれた英数字のパターンにマッチした文字列を[****]に置き換える。PassHideRunクラスではpassChangeメソッドを実際に利用するため「password : [ab12CD34]」と言う文字列を引数として渡し「password : [****]」という戻り値を受け取る。

3.本論

3.1.難読化手法

3.1.1.数値の難読化

3.1.1.1.アルゴリズム

本研究で作成した数値の難読化手法は、プログラム中に存在する数値の基底を乱数によって定め変換する方法である。具体的な方法は以下の通りである。

・123という数値を難読化する場合

  1. 乱数で決めた数値を7と仮定する。
  2. 123を7進数に変換し、234(7)に変換する。
  3. 234(7)を基底の7および各桁の4,3,2という4つの数値で表す。
  4. 数値を利用するときは7,4,3,2の値から逆の計算式を用いて、元の123の値を計算してから利用する。

この手法を用いることでプログラム中の数値を、別の数個の数値に分解して保存することができる。

3.1.1.2.実装方法

この手法を実装するために元のプログラムに新しくNumSetterクラス、Accessorクラス(付録E)を追加する。

NumSetteクラスはプログラム中に存在する数値を変換するクラスのオブジェクトを生成するクラスで、最初は何も記述しない。変換する数値を見つけたら、その数値を「NumSetter.accessor0.getNum();」と言うメソッド呼び出しに変更し、NumSetterクラスに「static Accessor accessor0 = new Accessor(数値);」と言う構文を追加する。

Accessorクラスは数値の変換と変換した数値を元の数値に戻すことができるクラスである。Accessorクラスのオブジェクト生成時に、インナークラスであるNumChangeクラスのオブジェクトを生成し、NumChangeクラスのenNumメソッドを呼び出す。enNumメソッドでは引数として受け取った数値を乱数によって定めた基数に変換し、各桁を保存する。AccessorクラスのgetNumメソッドではNumChangeクラスのdeNumメソッドを呼び出す。deNumメソッドでは、enNumメソッドによって変換された数値を、元の数値に戻す。

この手法を実装したプログラムが図25である。図25の例では最初の「1000」という数値を「NumSetter.accessor0.getNum();」に書き換え、NumSetterクラスに「static Accessor accessor0 = new Accessor(1000);」を書き足す。次に「3000」という数値を「NumSetter.accessor1.getNum();」に書き換え、NumSetterクラスに「static Accessor accessor1 = new Accessor(3000);」を書き足す。

class NumPrint {
    public static void main(String[] args) {
        int a = 1000;
        int b = 3000;
        System.out.println(a);
        System.out.println(b);
    }
}
        

class NumPrint {
    public static void main(String[] args) {
        int a = NumSetter.accessor0.getNum();
        int b = NumSetter.accessor1.getNum();
        System.out.println(a);
        System.out.println(b);
    }
}
class NumSetter {
    static Accessor accessor0 = new Accessor(1000);
    static Accessor accessor1 = new Accessor(3000);
}
        

図25. 数値の難読化手法

3.1.2.文字の難読化

3.1.2.1.アルゴリズム

本研究で作成した文字の難読化手法は、プログラム中のダブルクォーテーション(“ ”)で囲まれた文字列およびシングルクォーテーション(‘ ’)で囲まれた文字を全て文字コードで表す方法である。具体的な方法は以下の通りである。

・“Text”という文字列を難読化する場合

  1. Textという文字列のUnicodeから84, 101, 120, 116という数値を取り出す。
  2. 文字列の場所には取り出した数値を引数として渡すメソッド呼び出しに変更する。
  3. Unicodeの数値を引数として受け取り、文字列を戻り値として返すclassを実装する。

この手法を用いることで、プログラム中に存在する文字を数値化することができる。

3.1.2.2.実装方法

この手法を実装するために元のプログラムに新しくFromCharCodeクラス(付録F)を追加する。

変換する文字を見つけたらその文字を「FromCharCode.fromCharCode(文字コード)」に書き換える。

FromCharCodeクラスはfromCharCodeメソッドで引数として受け取った数値をUnicodeとする文字列を返すことができるクラスである。

この手法を実装したプログラムが図26である。図26の例では「"Hello"」という文字列を「FromCharCode.fromCharCode(72, 101, 108, 108, 111)」に書き換える。

class Hello {
    public static void main(String[] args) {
        System.out.println("Hello");
    }
}
        

class Hello {
    public static void main(String[] args) {
        System.out.println(FromCharCode.fromCharCode(72, 101, 108, 108, 111));
    }
}
        

図26. 文字の難読化手法

3.1.3.オブジェクトの難読化

3.1.3.1.アルゴリズム

本研究で作成したオブジェクトの難読化手法は、多くの変数を作成してプログラム中のオブジェクトのインスタンスに対してシャローコピーを行い、インスタンスを利用するときにシャローコピーをしたインスタンスを利用する手法である。具体的な方法は以下の通りである。

・ Obj obj = new Obj();を難読化する場合

  1. 難読化するオブジェクトと同じ型の変数objaddを作る。
  2. 次の行でobjadd = objを書き足しシャローコピーを行う。
  3. この処理以降に存在する、インスタンスobjに対し、乱数を用いてobjとobjaddのどちらかに変更する。

この手法を用いることで、プログラム中に存在するオブジェクトを数値化することができる。

3.1.3.2.実装方法

難読化するオブジェクトを見つけたら、その次の行で同じ型の変数の宣言をし、その変数に対して難読化するオブジェクトをシャローコピーする。そして、シャローコピー後の処理で利用されている元のオブジェクト変数をシャローコピーした変数に書き換える。

この手法を実装したプログラムが図27である。ただし、NumSetGetクラスは変更されていないので、難読化後のプログラムではNumSetgetクラスの記述を省略している。図27の例ではNumSetGetクラスのオブジェクトを生成し変数numSGに代入している処理「NumSetGet numSG = new NumSetGet();」の次の行で「NumSetGet numSGadd = numSG;」を書き足し、シャローコピーを行う。そして、シャローコピー後に利用されている「numSG.getNum()」は「numSGadd.getNum()」書き換える。つまり、フィールドnumに値を代入するときは、インスタンスnumSGのsetterを使用しているが、フィールドnumの値を取り出す時はインスタンスnumSGaddのgetterを利用している。

class NumSetGetRun {
    public static void main(String[] args) {
        NumSetGet numSG = new NumSetGet();
        numSG.setNum(10);
        System.out.println(numSG.getNum());
    }
}
public class NumSetGet {
    int num;
    void setNum(int num) { this.num = num; }
    int getNum() { return num; }
}
        

class NumSetGetRun {
    public static void main(String[] args) {
        NumSetGet numSG = new NumSetGet();
        NumSetGet numSGadd = numSG;
        numSG.setNum(10);
        System.out.println(numSGadd.getNum());
    }
}
        

図27. オブジェクトの難読化手法(a)

また、プログラムはインスタンスsumSGaddのsetterを使用し、インスタンスsumSGのgetterを利用することもできる(図28)。

class NumSetGetRun {
    public static void main(String[] args) {
        NumSetGet numSG = new NumSetGet();
        NumSetGet numSGadd = numSG;
        numSGadd.setNum(10);
        System.out.println(numSG.getNum());
    }
}
        

図28. オブジェクトの難読化手法(b)

3.2.難読化の自動化

3.2.1.自動化の必要性

プログラムを難読化する際、必要となるのが自動化である。元のプログラムが数行から数十行であれば手動で書き換えることも可能であるが、元のプログラムが数百行、数千行となってくると、手動で書き換えるのは時間がかかりミスも多くなる。時間やミスが多くなれば元のプログラムを壊しかねない。そこで本研究で考案した難読化手法を、正規表現を用いて自動化する。自動化を行うObfuscation4クラス、ClassSymbolクラス、UserFileクラス、ObfuscationTest4クラスはそれぞれ付録G、付録H、付録I、付録Jに記載する。

Obfuscation4クラスは本研究で考案した数値の難読化、文字の難読化、オブジェクトの難読化を行うクラスである。

ClassSymbolクラスはObfuscation4クラスでのオブジェクトの難読化時に必要となるクラスの名称、そのクラスで生成されたインスタンスの名称等の情報を保存するクラスである。

UserFileクラスは読み込まれたJavaファイルが正常に難読化できるかどうかをチェック、ファイルのバックアップの作成、文字列からJavaファイルの作成を行うクラスである。

ObfuscationTest4クラスはJavaファイルの絶対パスを引数として実行すると、UserFileクラスを用いてファイルのチェックを行い、難読化できるのならばファイルのバックアップを作成し、ファイルからプログラムを文字列として受け取る。そしてObfuscation4クラスに文字列を渡し難読化後の文字列が戻り値として受け取る。最後にUserFileクラスで難読化後のJavaファイルを元のJavaファイルと同じディレクトリに作成する。

3.2.2.数値の難読化に対しての自動化

数値の難読化はObfuscation3クラスのintegerCodeChangeメソッドで行う。

正規表現のパターン「"\\W\\d+"」で数値を検索する。これは非単語文字の次に数字が1つ以上続くパターンである。

メソッド内の処理動作は以下の通りである。

  1. パターン(「"\\W\\d+"」)を決定する。
  2. パターンとマッチした数値を保存するArrayListを作成する。
  3. String型の元のプログラムの文字列をStringBuffer型にコピーする。
  4. StringBuffer型の文字列内でパターンにマッチした数値をArrayListに保存してから、削除する。
  5. 同場所に「NumSetter.accessor0.getNum();」(数値は挿入する度に増加)を挿入する。
  6. StringBuffer型の文字列を全て調べる。
  7. StringBuffer型の文字列の最後に「class NumSetter {」を追加する。
  8. NumSetterクラス内に見つけた数値の個数分の「static Accessor accessor0 = new Accessor(****);」を挿入する(****はパターンにマッチした数値)。
  9. StringBuffer型の文字列の最後に「}」を追加する。
  10. StringBuffer型をString型の文字列にして返す。

さらにAccessorクラスを作成する。Accessorクラスの作成はObfuscation4クラスのmakeAccessorメソッドで行う。makeAccessorメソッドは文字列を引数として渡すと、その文字列の最後にAccessorクラスを文字列として追加するメソッドである。

これで数値を難読化することができる。

3.2.3.文字の難読化に対しての自動化

文字の難読化はObfuscation3クラスのstringCodeChangeメソッドで行う。

正規表現のパターン「"\".*?\""」および「"'.?'"」で数値を検索する。これはダブルクォーテーションの次に任意の文字が0個以上続き、その次がダブルクォーテーションであるパターン、およびシングルクォーテーションの次に任意の文字が0個か1個あり、その次がシングルクォーテーションであるパターンである。

メソッド内の処理動作は以下の通りである。

  1. パターン(「"\".*?\""」)を決定する。
  2. パターンとマッチした文字を保存するString型変数を作成する。
  3. StringBuffer型変数を作成する。
  4. StringBuffer型変数に「FromCharCode.fromCharCode(」を追加する。
  5. パターンとマッチした文字をString型変数に保存し、その文字のUnicodeを調べる。
  6. StringBuffer型変数に文字Unicodeを追加していき「FromCharCode.fromCharCode(***,***,***)」(***は文字のUnicode)のという形にする。
  7. パターンにマッチした文字を上記のStringBuffer型変数に置き換える。
  8. 全ての文字を検索したらパターンを「"'.?'"」にし、2〜7を繰り返す。
  9. 置き換え終わった文字列を返す。

さらにFromCharCodeクラスを作成する。FromCharCodeクラスの作成はObfuscation4クラスのmakeFromCharCodeメソッドで行う。makeFromCharCodeメソッドは文字列を引数として渡すと、その文字列の最後にFromCharCodeクラスを文字列として追加するメソッドである。

これで文字を難読化することができる。

3.2.4.オブジェクトの難読化に対しての自動化

オブジェクトの難読化はObfuscation3クラスのclassNameSearchメソッド及び、instanceNameSearchメソッドで行う。

classNameSearchメソッド内の処理は以下の通りである。

  1. 作成されたクラス名を検索する。
  2. インポートされたクラス名を検索する。
  3. 検索されたクラス名をArrayListに保存する。
  4. instanceNameSearchメソッドを呼び出す。

instanceNameSearchメソッド内の処理は以下の通りである。

  1. 「クラス名 + 文字列」となっている部分を検索する。
  2. 文字列の部分をインスタンス名としてArrayListに保存する。
  3. 「クラス名 + 任意 + , + 文字列」となっている部分を検索する。
  4. 同様に文字列の部分をインスタンス名としてArrayListに保存する。
  5. 「クラス名 + インスタンス名」となっている部分を検索する。
  6. 上記部分がメソッドの引数だった場合、メソッドの最初でシャローコピーを行う。
  7. メソッドの引数でない場合、「インスタンス名 + = + new」を検索する。
  8. 上記部分の次行でシャローコピーを行う。
  9. 0か1の乱数を発生させ、乱数が1の場合、シャローコピーした部分よりも後に出てきたインスタンス名をシャローコピーしたものに置き換える。
  10. インスタンスの数だけ5〜9を繰り返す。
  11. クラスの数だけ1〜10を繰り返す。

これでオブジェクトを難読化することができる。

3.2.5.結果

本研究で作成した自動化のプログラムはObfuscationTest4クラス(付録J)に難読化したいJavaファイルの絶対パスを引数として渡して実行すると、同じディレクトリに難読化されたJavaファイルを作成するプログラムである。このプログラムを用いて付録KにあるJavaプログラムを難読化し、その実行結果を付録Lに記載した。

数値に関しては、まずNumsetterクラスを読み、AccessorクラスのgetNumメソッドを読まなければならない。getNumメソッドではどの値が評価値となるのかを調べ、deNumメソッドを読み、どのような値が返されるのかを計算し、元の数値を読み取ることができる。元のプログラムが直接数値を代入されているのに対し、難読化後のプログラムではこのような手順で数値が代入される。元のプログラムよりも処理手順が多くなっているので元のプログラムより解析が困難になっていると言える。

文字に関しては、FromCharCodeクラスのfromCharCodeメソッドを呼び出して、そのメソッドから文字が返される。数値の難読化よりは処理数が少ないが、ソースコード上に元の文字がなく、文字は全てUnicodeで表されているので、文字を判別するためには、Unicodeと文字を対応させる必要がある。このことより、元のプログラムより解析が困難になっていると言える。

オブジェクトに関しては、インスタンスが利用される場面で、元のインスタンスとシャローコピーされたインスタンスが無作為に用いられているので、中身のデータの予想が付きにくい。本研究で用いた難読化手法だけでは、元のインスタンス名にaddを付けただけのインスタンス名になっていてシャローコピーされたインスタンスがわかりやすいが既存の方法と組み合わせ、インスタンス名を無作為に変更した場合、シャローコピーされたインスタンスが見分けにくくなる。このことより、元のプログラムより解析が困難になっていると言える。

文字数については、元のプログラム数が353文字なのに対し、難読化後の文字数は2750文字と大幅に増えている。しかし、そのほとんどは難読化された数値と文字を元に戻すための処理であるAccessorクラスとFromCharCodeクラスである。この2つのクラスは固定で増える文字数なので、その2つのクラスを除いた難読化後の文字数は590文字である。元のプログラムによって誤差はでるが、文字数に関しては、元のプログラムの約1.7倍+2160文字程度の文字数になる。

4.まとめ

4.1.総括

本研究ではJavaプログラムの解析が困難にする手法の1つである難読化の手法を考案した。難読化とはソースコードを元の動作仕様をくずさない様に変更する手法である。本研究で考案した難読化手法はソースコード上の数値、文字、オブジェクトの3つに対象として行う。

数値の難読化手法は10進数の数値の基数を変更し、変更後の数値を各桁の数値で保存する方法である。文字の難読化手法は文字をUnicodeに変更する方法である。オブジェクトの難読化は、オブジェクト変数にシャローコピーを行い、1つのインスタンスを複数のオブジェクト変数から操作できるようにする方法である。

また、本研究ではこれらの難読化手法をJavaファイルに適用することのできるプログラムを作成した。

4.2.今後の課題

4.2.1.難読化手法についての課題

数値の難読化ではソースコード上に元の数値が残ってしまっている。そこで、数値の難読化も文字の難読化と同様に「method(***,***,***)」(***は数値)のような記述方法にし、分解した後の数値をソースコードに記述し、元の数値をソースコード上に残さないようにする。

文字の難読化ではUnicodeをそのまま扱っているので、Unicodeの数値に何らかの処理を加えることでさらに解析を困難にすることができる。その方法として、文字の難読化を行ったあとで数値の難読化を行う。本研究で作成したプログラムでは数値の難読化を行った後に、文字の難読化を行っていたが、その処理を逆にするだけで、解析は困難になる。しかし、文字列の数だけ数値の難読化を行うので、難読化後のプログラムの文字数は大幅にあがる。

オブジェクトの難読化ではインスタンスobjをobjのままにするかobjaddに書き換えるかは乱数によって決定する。そのため、全てのインスタンスobjがobjのままだったり、全てobjaddになってしまったりすることがある。そのため、元のインスタンスとシャローコピーしたインスタンスが同じくらいになるようにする。

4.2.2.自動化についての課題

以下のパターンはマッチすることができなかったり、不具合が生じたりする。

  1. 「0.05」のような小数
  2. 「0x55」のような10進数以外の数値
  3. 「1_000_000」のようなアンダースコア付き数値
  4. 「/* “ */ /* “ */」のようにコメント文とダブルクォーテーションまたはジングルクォーテーションが入れ子になっている場合
  5. 「import java.util.*」のようにワイルドカード(*)を用いたオブジェクト
  6. 「Integer」のようなjava.lang.object内の宣言なしで利用できるオブジェクト
  7. 他スコープ内に同名のインスタンスや変数がある場合
  8. 「ArrayList<E>」のように「< >」付きのオブジェクト
  9. 「class Parent extends Child {}」のように継承等を利用しているオブジェクト

これらのパターンになっても自動化できるようにすることが今後の課題となる。

5.参考文献

6.付録

付録A

public class Person {
    private String name;
    public Person(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    @Override
    public String toString() {
        return "Person : " + name;
    }
}
  

付録B

public class House {
    private String addres;
    public House(String addres) {
        this.addres = addres;
    }
    public String getAddres() {
        return addres;
    }
    public void setAddres(String addres) {
        this.addres = addres;
    }
    @Override
    public String toString() {
        return "addres : " + addres;
    }
}
  

付録C

public class house {
private String ADDRES;
public house(String ADDRES) {
this.ADDRES = ADDRES;}
public String Add() {
return ADDRES;}
public void Add(String ADDRES) {
this.ADDRES = ADDRES;}
@Override
public String toString() {
return "addres : " + ADDRES;}}
  

付録D

import java.util.regex.Matcher;
import java.util.regex.Pattern;

class PassHideRun {
    public static void main(String[] args) {
        String str = "password : [ab12CD34]";
        System.out.println(str + " → " + PassHide.passChange(str));
    }
}

public class PassHide {
    public static String passChange(String str) {
        Pattern p = Pattern.compile("\\[\\w*\\]");
        Matcher m = p.matcher(str);
        return m.replaceAll("[****]");
    }
}
  

付録E

class Accessor {
    private NumChange nc, nc2 ,nc3;
    private boolean flag = false;
    private int flag2 = 0;
    
    public Accessor() {
        nc = nc2 = nc3 = new NumChange();
    }
    
    public Accessor(int y) {
        this();
        setNum(y);
    }
    
    public int getNum() {
        return flag ? (flag2 > 0) ? (getAbSize() != 0) ? nc.deNum() : nc2.dumyMethod() : flag2 : 0;
    }
    
    public void setNum(int y) {
        nc3.enNum(y);
    }
    
    public int getAbSize() {
        return nc.ab.size();
    }
    
    public int getAbFirst() {
        return nc.ab.get(0);
    }
    
    public int getBase() {
        return nc.base;
    }
    
    @Override
    public String toString() {
        return String.valueOf(nc.deNum());
    }
    
    class NumChange {
        private int base;
        private java.util.ArrayList<Integer> ab, ab2, ab3;
    
        public NumChange() {
            base = (int)(28 * java.lang.Math.random() + 2);
            ab = ab2 = ab3 = new java.util.ArrayList<Integer>();
        }
        
        private int deNum() {
            int x = 0;
            int y = ab3.size() - 1;
            for(int i = 0; i <= y; i++) {
                x = (ab2.get(y - i) < 0) ? x * -1 : x + (ab.get(y - i) * (int)java.lang.Math.pow(base, i));
            }
            return x;
        }
        
        private void enNum(int z) {
            flag = true;
            if(z != 0) {
                if(z < 0) {
                    ab3.add(-1);
                    enNum(java.lang.Math.abs(z) / base);
                    ab.add(java.lang.Math.abs(z) % base);
                } else {
                enNum(z / base);
                ab2.add(z % base);
                }
            }
            flag2++;
            return;
        }
        
        private int dumyMethod(){
            return 0;
        }
    }
}
  

付録F

class FromCharCode {
    private int[] codepoints = null;
    FromCharCode(int... codepoints) {
        this.codepoints = codepoints;
    }
    @Override
    public String toString(){
        return new String(codepoints, 0, codepoints.length);
    }
    public static String fromCharCode(int... codepoints){
        return new String(codepoints, 0, codepoints.length);
    }
}
  

付録G

package obfuscation;

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Obfuscation4 {
    final private static String LINE_SEP = System.getProperty("line.separator");
    private String src;
    public Obfuscation4(String trim) {
        src = trim;
        // TODO 自動生成されたコンストラクター・スタブ
    }
    // 文字列を文字コードに変換するメソッド
    public void stringCodeChange() {
        String[] regexs = {"\".*?\"", "'.?'"};
        for(String regex : regexs) {
            Pattern p = Pattern.compile(regex);
            Matcher m = p.matcher(src);
            StringBuffer cordtext = new StringBuffer();
            while(m.find()) {
                String str = src.substring(m.start() + 1, m.end() - 1);
                cordtext.append("FromCharCode.fromCharCode(");
                cordtext.append(strToCode(str));
                cordtext.append(str.codePointAt(str.length() - 1) + ")");
                src = m.replaceFirst(cordtext.toString());
                cordtext.delete(0, cordtext.length());
                m = p.matcher(src);
            }
        }
    }
    private String strToCode(String str) {
        StringBuilder sb = new StringBuilder();
        for(int k = 0; k < str.length() - 1; k++){
            sb.append(str.codePointAt(k) + ", ");
        }
        return sb.toString();
    }
    
    // 文字コードを文字列に変換するクラスを作成するメソッド
    public void makeFromCharCode() {
        src += LINE_SEP +
                "class FromCharCode {" + LINE_SEP +
                "    private int[] codepoints = null;" + LINE_SEP +
                "    FromCharCode(int... codepoints) {" + LINE_SEP +
                "        this.codepoints = codepoints;" + LINE_SEP +
                "    }" + LINE_SEP +
                "    @Override" + LINE_SEP +
                "    public String toString(){" + LINE_SEP +
                "        return new String(codepoints, 0, codepoints.length);" + LINE_SEP +
                "    }" + LINE_SEP +
                "    public static String fromCharCode(int... codepoints){" + LINE_SEP +
                "        return new String(codepoints, 0, codepoints.length);" + LINE_SEP +
                "    }" + LINE_SEP +
                "}" + LINE_SEP;
    }
    
    public void exec() {
        classNameSearch();
        integerCodeChange();
        makeAccessor();
        stringCodeChange();            // ダブルクォーテーション内の文字を文字コードに変換
        makeFromCharCode();      // 文字コードを文字列に変換するクラスの作成
    }
    
    @Override
    public String toString() {
        return src;
    }
    //数値をsetterで保存し、getterで呼び出すようにするメソッド
    private static final Pattern numPattern = Pattern.compile("\\W\\d+");
    
    public void integerCodeChange() {
        Matcher m = numPattern.matcher(src);
        ArrayList<Integer> num = new ArrayList<Integer>();
        StringBuffer srcbuf = new StringBuffer(src);
        String cordtext = "";
        int count = -1;
        while(m.find()) {
            num.add(Integer.valueOf(srcbuf.substring(m.start() + 1, m.end())));
            srcbuf.delete(m.start() + 1, m.end());
            cordtext = "NumSetter.accessor" + ++count + ".getNum()";
            srcbuf.insert(m.start() + 1, cordtext.toString());
            cordtext = "";
            m = numPattern.matcher(srcbuf.toString());
        }
        
        srcbuf.append(createStrForInteger(num));
        
        src = srcbuf.toString();
    }
    private String createStrForInteger(List<Integer> num) {
        StringBuilder srcbuf = new StringBuilder();
        srcbuf.append(LINE_SEP + "class NumSetter {" + LINE_SEP);
        int i = 0;
        for(Integer data : num) {
            srcbuf.append("    static Accessor accessor" + i++ 
                    + " = new Accessor(" + data + ");" + LINE_SEP);
        }
        srcbuf.append("}" + LINE_SEP);
        return srcbuf.toString();
    }
    
    //数値を分解して保存するsetterとgetterを作成するメソッド
    public void makeAccessor() {
        src += LINE_SEP +
                "class Accessor {" + LINE_SEP +
                "    private NumChange nc, nc2 ,nc3;" + LINE_SEP +
                "    private boolean flag = false;" + LINE_SEP +
                "    private int flag2 = 0;" + LINE_SEP +
                "    "y + LINE_SEP +
                "    public Accessor() {" + LINE_SEP +
                "        nc = nc2 = nc3 = new NumChange();"y + LINE_SEP +
                "    }" + LINE_SEP +
                "    "y + LINE_SEP +
                "    public Accessor(int y) {" + LINE_SEP +
                "        this();"y + LINE_SEP +
                "        setNum(y);"y + LINE_SEP +
                "    }" + LINE_SEP +
                "y    " + LINE_SEP +
                "    public int getNum() {" + LINE_SEP +
                "        return flag ? (flag2 > 0) ? (getAbSize() != 0) ? nc.deNum() : nc2.dumyMethod() : flag2 : 0;" + LINE_SEP +
                "y    }" + LINE_SEP +
                "y    " + LINE_SEP +
                "    public void setNum(int y) {" + LINE_SEP +
                "        nc3.enNum(y);" + LINE_SEP +
                "y    }" + LINE_SEP +
                "    " + LINE_SEP +
                "    public int getAbSize() {" + LINE_SEP +
                "        return nc.ab.size();" + LINE_SEP +
                "    }"y + LINE_SEP +
                "    "y + LINE_SEP +
                "    public int getAbFirst() {" + LINE_SEP +
                "        return nc.ab.get(0);"y + LINE_SEP +
                "    }" + LINE_SEP +
                "    " + LINE_SEP +
                "    public int getBase() {" + LINE_SEP +
                "y        return nc.base;" + LINE_SEP +
                "    }"y + LINE_SEP +
                "    " + LINE_SEP +
                "    @Override" + LINE_SEP +
                "    public String toString() {" + LINE_SEP +
                "        return String.valueOf(nc.deNum());" + LINE_SEP +
                "    }" + LINE_SEP +
                "y    " + LINE_SEP +
                "    class NumChange {" + LINE_SEP +
                "        private int base;" + LINE_SEP +
                "        private java.util.ArrayList<Integer> ab, ab2, ab3;" + LINE_SEP +
                "    " + LINE_SEP +
                "        public NumChange() {"y + LINE_SEP +
                "            base = (int)(28 * java.lang.Math.random() + 2);" + LINE_SEP +
                "            ab = ab2 = ab3 = new java.util.ArrayList<Integer>();"y + LINE_SEP +
                "        }" + LINE_SEP +
                "        " + LINE_SEP +
                "y        private int deNum() {" + LINE_SEP +
                "            int x = 0;" + LINE_SEP +
                "y            int y = ab3.size() - 1;" + LINE_SEP +
                "            for(int i = 0; i <= y; i++) {" + LINE_SEP +
                "                x = (ab2.get(y - i) < 0) ? x * -1 : x + (ab.get(y - i) * (int)java.lang.Math.pow(base, i));"y + LINE_SEP +
                "            }" + LINE_SEP +
                "            return x;"y + LINE_SEP +
                "        }" + LINE_SEP +
                "y        " + LINE_SEP +
                "        private void enNum(int z) {" + LINE_SEP +
                "            flag = true;"y + LINE_SEP +
                "            if(z != 0) {"y + LINE_SEP +
                "                if(z < 0) {"y + LINE_SEP +
                "y                    ab3.add(-1);" + LINE_SEP +
                "                    enNum(java.lang.Math.abs(z) / base);" + LINE_SEP +
                "                    ab.add(java.lang.Math.abs(z) % base);"y + LINE_SEP +
                "                } else {" + LINE_SEP +
                "y                enNum(z / base);" + LINE_SEP +
                "                ab2.add(z % base);" + LINE_SEP +
                "                }"y + LINE_SEP +
                "            }"y + LINE_SEP +
                "            flag2++;" + LINE_SEP +
                "            return;" + LINE_SEP +
                "        }" + LINE_SEP +
                "        " + LINE_SEP +
                "        private int dumyMethod(){" + LINE_SEP +
                "            return 0;" + LINE_SEP +
                "        }" + LINE_SEP +
                "    }" + LINE_SEP +
                "}" + LINE_SEP;
        }
    
    //classファイルを検出し、インスタンスにシャローコピーを行う
    private static final Pattern whiteSpace = Pattern.compile("\\s*");
    private static final Pattern suffix = Pattern.compile("\\.\\w+$");
    
    public void classNameSearch() {
        //classで始まる自作クラスのオブジェクト名を保存
        Pattern p = Pattern.compile("class\\s+\\w+\\s*\\{");
        Matcher m = p.matcher(src);
        ArrayList<ClassSymbol> str = getStr(m);

        //importで読み込んだクラスのオブジェクト名を保存
        p = Pattern.compile("import\\s+.+\\.\\w+\\s*;");
        m = p.matcher(src);
        
        //全てのimport名をstrに保存
        while(m.find()) {
            //ex. strtempに保存されるのは「java.io.File」等
            String strtemp = src.substring(m.start() + 6, m.end() - 1);
            Matcher m2 = whiteSpace.matcher(strtemp);
            strtemp = (m2.replaceAll(""));
            
            //strtempに保存されている最後の「.」から先の名前のみ保存する
            m2 = suffix.matcher(strtemp);
            m2.find();
            str.add(new ClassSymbol(strtemp.substring(m2.start() + 1, m2.end())));
        }
        //元のjavaファイルの文字列と保存したオブジェクト名を引数として渡す
        src = instanceNameSearch(src, str);
    }
    private ArrayList<ClassSymbol> getStr(Matcher m) {
        //全てのclass名をstrに保存
        ArrayList<ClassSymbol> str = new ArrayList<ClassSymbol>();
        while(m.find()) { 
            Matcher m2 = whiteSpace.matcher(src.substring(m.start() + 5, m.end() - 1));
            str.add(new ClassSymbol(m2.replaceAll("")));
        }
        return str;
    }
    private static final Pattern commaPattern = Pattern.compile(",\\s*\\w+[\\s\\W]");
    
    private String instanceNameSearch(String src, ArrayList<ClassSymbol> str) {
        StringBuffer srcbuf = new StringBuffer(src);
        int count = -1;
        String cordtext = "";
        Matcher m0, m;
        //オブジェクトの数だけ繰り返す
        for(ClassSymbol cs : str) {
            //オブジェクト名+インスタンス名となっている1行の先頭からインスタンスの終わりまでのパターンを検索
            m0 = cs.getSengenLinePattern().matcher(srcbuf);
            int index = 0;
            while(m0.find(index)) {
                int linecount = 0;
                index = m0.end() + 1;
                String sengenLine = m0.group();
                String indent = getStemp(sengenLine);
                
                //オブジェクト名+インスタンス名となっているパターンを検索
                m = cs.getSengenPattern().matcher(sengenLine);
                m.find();
                // index3 にインスタンスの終わりを保存
                int index3 = m.end() - 1;
                
                m = whiteSpace.matcher(sengenLine.substring(m.start() + 1 + cs.toString().length(), m.end() - 1));
                cs.add(m.replaceAll(""));
                linecount++;
                
                m = commaPattern.matcher(sengenLine);
                while(m.find(index3)) {
                    index3 = m.end() - 1;
                    
                    Matcher m2 = whiteSpace.matcher(sengenLine.substring(m.start() + 1, m.end() - 1));
                    cs.add(m2.replaceAll(""));
                    linecount++;
                }
                
                for(int j = 0; j < linecount; j++) {
                    count++;
                    m = cs.getHikisuuPattern(count).matcher(sengenLine);
                    int index2 = 0;
                    if(m.find()) {
                        cordtext = LINE_SEP + indent + "    " + cs.toString() + " " + cs.get(count) 
                                + "add = " + cs.get(count) + ";";
                        index2 = insertCodeText(srcbuf, cordtext, m0);
                        index += cordtext.length();
                    }
                    else {
                        m = cs.getSengenAndNewPattern(count).matcher(sengenLine);
                        if(m.find()) {
                            cordtext = LINE_SEP + indent + cs.toString() + " " + cs.get(count) 
                                    + "add = " + cs.get(count) + ";"y;
                            index2 = insertCodeText(srcbuf, cordtext, m0);
                            index += cordtext.length();
                        }
                        else {
                            cordtext = LINE_SEP + indent + cs.toString() + " " + cs.get(count) + "add;"y;
                            index2 = insertCodeText(srcbuf, cordtext, m0);
                            index += cordtext.length();
                            
                            m = cs.getNewPattern(count).matcher(srcbuf);
                            if(m.find(index2)) {
                                cordtext = LINE_SEP + getCodeText(whiteSpace.matcher(srcbuf), cs.get(count), m.start());
                                index2 = insertCodeText(srcbuf, cordtext, m);
                            }
                        }
                    }
                    replaceVariableName(srcbuf, count, index2, cs);
                }
            }
            m0 = cs.getSengenLinePattern().matcher(srcbuf);
        }
        return srcbuf.toString();
    }
    private int insertCodeText(StringBuffer srcbuf, String cordtext, Matcher m) {
        srcbuf.insert(m.end(), cordtext);
        return m.end() + cordtext.length();
    }
    private String getCodeText(Matcher m2, String instance, int start) {
        String cordtext;
        m2.find(start);
        
        cordtext = m2.group() + instance + "add = " + instance + ";";
        return cordtext;
    }
    private void replaceVariableName(StringBuffer srcbuf, int count,
            int index2, ClassSymbol cs) {
        String cordtext;
        Matcher m;
        m = cs.getInstancesNamePattern(count).matcher(srcbuf);
        while(m.find(index2)) {
                if((int)(Math.random() * 2) == 1) {
                srcbuf.delete(m.start() + 1, m.end() - 1);
                cordtext = cs.get(count) + "add";
                srcbuf.insert(m.start() + 1, cordtext);
                index2 = m.start() + 1 + cordtext.length();
                cordtext = "";
                m = cs.getInstancesNamePattern(count).matcher(srcbuf);
            }
            else {
                index2 = m.end();
            }
        }
    }
    private String getStemp(String temp) {
        String stemp = "";
        Matcher m;
        m = whiteSpace.matcher(temp);
        if(m.find()) {
            stemp = m.group();
        }
        return stemp;
    }
}
  

付録H

package obfuscation;

import java.util.ArrayList;
import java.util.regex.Pattern;

public class ClassSymbol {
    private String name;
    private ArrayList<String> instances;
    public ClassSymbol(String name) {
        super();
        this.name = name;
        this.instances = new ArrayList<String>();
    }
    @Override
    public String toString() {
        return name;
    }
    public void add(String replaceAll) {
        instances.add(replaceAll);
    }
    public String get(int count) {
        return instances.get(count);
    }
    
    public Pattern getSengenLinePattern() {
        return Pattern.compile(".*[\\s\\W]" + name + "\\s+\\w+[\\s\\W]*?.*[;{]");
    }
    
    public Pattern getSengenPattern() {
        return Pattern.compile("[\\s\\W]" + name + "\\s+\\w+[\\s\\W]");
    }
    
    public Pattern getHikisuuPattern(int count) {
        return Pattern.compile("\\(.*" + name + "\\s+" + instances.get(count) + ".*\\)\\s*\\{");
    }
    
    public Pattern getSengenAndNewPattern(int count) {
        return Pattern.compile(".*" + name + "\\s+.*" + instances.get(count) + "\\s*=\\s*new\\s+.+;");
    }
    public Pattern getNewPattern(int count) {
        return Pattern.compile(".*" + instances.get(count) + "\\s*=\\s*new\\s+.+;");
    }
    public Pattern getInstancesNamePattern(int count) {
        return Pattern.compile("[\\s\\W]" + instances.get(count) + "[\\s\\W]");
    }
}
  

付録I

package obfuscation;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class UserFile {
    UserFile() {
    }
    // Fileチェックメソッド
    public static boolean fileCheck(File file) {
        // ファイルが存在するかどうかを判定
        if ( !file.exists() ) {
            // ファイルが存在しない場合は処理終了
            System.out.println( "ファイルが存在しない" );
            return false;
        }

        // 指定されたパスがファイルかどうかを判定
        if ( !file.isFile() ) {
            // ディレクトリを指定した場合は処理終了
            System.out.println( "ファイル以外を指定" );
            return false;
        }

        // ファイルが読み込み可能かどうかを判定
        if ( !file.canRead() ) {
            // ファイルが読み込み不可の場合は処理終了
            System.out.println("ファイルが読み込み不可");
            return false;
        }

        // ファイルが書き込み可能かどうかを判定
        if ( !file.canWrite() ) {
            // ファイルが書き込み不可の場合は処理終了
            System.out.println("ファイルが書き込み不可");
            return false;
        }
        return true;
    }
    
    //  File読み込みメソッド
    public static String fileRead(File _file ) {
        StringBuffer fileRead = new StringBuffer("");
        try {
            BufferedReader br = new BufferedReader( new FileReader( _file ) );
            String str = null;
            while ( ( str = br.readLine() ) != null ) {
                    fileRead.append(str + System.getProperty("line.separator"));
            }
            br.close();
        } catch ( FileNotFoundException e ) {
             System.out.println( e );
        } catch ( IOException e ) {
             System.out.println( e );
        }
        return fileRead.toString();
    }
    
    // Fileバックアップメソッド
    public static boolean fileBackUp(File _file, String filepath) {
        String regex = "\\..+$";
        Pattern p = Pattern.compile(regex);
        Matcher m = p.matcher(filepath);
        filepath = m.replaceAll(".backup");
        try{
            if(_file.renameTo(new File(filepath))){
                System.out.println("ファイルバックアップ成功"); // ファイル名変更成功
            }else{
                System.out.println("ファイルバックアップ失敗"); // ファイル名変更失敗
                return false;
            }
        }
        catch(SecurityException e){
            System.out.println(e.getMessage());
        }
        catch(NullPointerException e){
            System.out.println(e.getMessage());
        }
        return true;
    }
    
    // File書き込みメソッド
    public static void fileWrite(File _file, String _text){
        try {
            FileWriter filewriter = new FileWriter( _file );
            filewriter.write( _text );

            // ファイルを閉じる
            filewriter.close();
        } catch ( IOException e ) {
             System.out.println( e );
        }
    }
}
  

付録J

package obfuscation;

import java.io.File;
import static obfuscation.UserFile.fileBackUp;
import static obfuscation.UserFile.fileCheck;
import static obfuscation.UserFile.fileRead;
import static obfuscation.UserFile.fileWrite;

public class ObfuscationTest4 {
    public static void main(String[] args) {
        if ( args.length == 0) {
            System.out.println( "javaファイルのパスを引数として入力してください。" );
            return;
        }

        // Fileクラスをインスタンス化
        File file = new File( args[0] );
        if(!fileCheck(file)){   // ファイルが正しいか確認
            return;
        }
        String readText = fileRead( file );         // ファイルの内容を文字列として読み込み
        if(!fileBackUp(file, args[0])) {            // 難読化前のファイルのバックアップ作成
            return;                                 // 作成できなかったら終了
        }
        
        Obfuscation4 changetext = new Obfuscation4(readText.trim());                   // 難読化用に新しい変数を用意
        changetext.exec();
        String replaceText = changetext.toString();                // すべての難読化処理の終了
        
        fileWrite( file, replaceText );             // 難読化後の文字列をファイルに書き込み
        System.out.println("終了しました");
    }
}
  

付録K

import java.util.Date; // オブジェクト難読化用

class NumWordObj {
    public static void main(String[] args) {
        int num = 100; // 数値の難読化用
        String str = "Text"; // 文字の難読化用
        Date date = new Date();
        date.setTime(1000);
        
        System.out.println(num);
        System.out.println(str);
        System.out.println(date.getTime());
    }
}
  

付録L

import java.util.Date; // オブジェクト難読化用

class NumWordObj {
    public static void main(String[] args) {
        int num = NumSetter.accessor0.getNum(); // 数値の難読化用
        String str = FromCharCode.fromCharCode(84, 101, 120, 116); // 文字の難読化用
        Date date = new Date();
        Date dateadd = date;
        dateadd.setTime(NumSetter.accessor1.getNum());
        
        System.out.println(num);
        System.out.println(str);
        System.out.println(date.getTime());
    }
}
class NumSetter {
    static Accessor accessor0 = new Accessor(100);
    static Accessor accessor1 = new Accessor(1000);
}

class Accessor {
    private NumChange nc, nc2 ,nc3;
    private boolean flag = false;
    private int flag2 = 0;
    
    public Accessor() {
        nc = nc2 = nc3 = new NumChange();
    }
    
    public Accessor(int y) {
        this();
        setNum(y);
    }
    
    public int getNum() {
        return flag ? (flag2 > 0) ? (getAbSize() != 0) ? nc.deNum() : nc2.dumyMethod() : flag2 : 0;
    }
    
    public void setNum(int y) {
        nc3.enNum(y);
    }
    
    public int getAbSize() {
        return nc.ab.size();
    }
    
    public int getAbFirst() {
        return nc.ab.get(0);
    }
    
    public int getBase() {
        return nc.base;
    }
    
    @Override
    public String toString() {
        return String.valueOf(nc.deNum());
    }
    
    class NumChange {
        private int base;
        private java.util.ArrayList<Integer> ab, ab2, ab3;
    
        public NumChange() {
            base = (int)(28 * java.lang.Math.random() + 2);
            ab = ab2 = ab3 = new java.util.ArrayList<Integer>();
        }
        
        private int deNum() {
            int x = 0;
            int y = ab3.size() - 1;
            for(int i = 0; i <= y; i++) {
                x = (ab2.get(y - i) < 0) ? x * -1 : x + (ab.get(y - i) * (int)java.lang.Math.pow(base, i));
            }
            return x;
        }
        
        private void enNum(int z) {
            flag = true;
            if(z != 0) {
                if(z < 0) {
                    ab3.add(-1);
                    enNum(java.lang.Math.abs(z) / base);
                    ab.add(java.lang.Math.abs(z) % base);
                } else {
                enNum(z / base);
                ab2.add(z % base);
                }
            }
            flag2++;
            return;
        }
        
        private int dumyMethod(){
            return 0;
        }
    }
}

class FromCharCode {
    private int[] codepoints = null;
    FromCharCode(int... codepoints) {
        this.codepoints = codepoints;
    }
    @Override
    public String toString(){
        return new String(codepoints, 0, codepoints.length);
    }
    public static String fromCharCode(int... codepoints){
        return new String(codepoints, 0, codepoints.length);
    }
}