2012/03/08

Android:難読化ツールのProguardを適用する


Proguardは
  • ソースコードの難読化
  • 未使用のコード部分の削除
を支援するツールです。
リバースエンジニアリングを困難にし、apkのサイズを小さくする効果があります。

難読化はAndroidのセキュリティを考える上でよく出てくるキーワードです。
今回はProguard適用方法と、どれほど難読化されるのかを見ていこうと思います。

●project.propertiesの編集
Proguardの適用は非常に簡単です。
※ただし、ProguardはADT8.0以上、SDK-r8以上の環境が必要です。

EclipseでAndroidプロジェクトを作成すると、プロジェクトルートの直下に
project.propertiesファイルが自動生成されています。


Proguardを有効にするためには、このプロパティファイルを編集します。

今回指定するProguardコンフィグファイルは、プロジェクトルート直下に自動生成され
るproguard.cfgを指定しましょう。
下記の一行をプロパティファイルの最終行に追加します。
proguard.config=proguard.cfg

これでProguard機能がproguard.cfgの内容に従って動作するようになりました。

# 2012/03/09 追記
apk生成時に「Conversion to Dalvik format failed with error 1」というエラーが発生して
正しく処理できない場合があります。その場合は下記のサイトが助けになるでしょう。
Androidアプリ開発情報まとめブログ

# 2012/05/18 追記
ADT17以降は、proguard.cfgの扱いが若干変わります。
詳しくは下記ページを参照下さい。
http://d.hatena.ne.jp/bs-android/20120325/1332662384


●Proguard機能を試す
Proguardはプロジェクトを右クリックした時に表示される[AndroidTools]>
[Export Signed Application Package]からapkを生成すると適用されます。

以上でProguardの適用は完了なのですが、実感が湧きづらいので本当に難読化されている
のか確認してみましょう。

今回、下記のようなコードを組みました。
これをProguardで難読化してみようと思います。
public class ProguardTestActivity extends Activity {
 private CountDownLatch mLatch = new CountDownLatch(2);

 @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        Logger.d("Debug dump Test");
        Logger.e("Debug dump Test");
        android.util.Log.e("ProguardTest", "test dump");
    }

    @Override
    protected void onResume() {
     super.onResume();

     new AsyncTask<CountDownLatch, Void, Void>() {
      @Override
      protected Void doInBackground(CountDownLatch... latch) {
       try {
        latch[0].await();
       } catch (InterruptedException e) {
        // N.O.P
        Logger.d("wake up.");
       }
       Logger.d("hello.");

       return null;
      }
  }.execute(mLatch);

  Handler handler = new Handler();
  handler.post(new Runnable() {
   @Override
   public void run() {
    mLatch.countDown();
   }
  });
  handler.postDelayed(new Runnable() {
   @Override
   public void run() {
    mLatch.countDown();
   }
  }, 2000);
    }
}

難読化されているかの確認は、前回の「Android:Windowsでapkを逆コンパイルする方法
を利用して、難読化処理有の.apkと難読化処理無の.apkとで比較することにします。

難読化有/無のapkから上記のコード部分を抽出したものが下記になります。

●難読化無し
public ProguardTestActivity()
{
    mLatch = new CountDownLatch(2);
}

public void onCreate(Bundle bundle)
{
    super.onCreate(bundle);
    setContentView(0x7f030000);
    Logger.d("Debug dump Test");
    Logger.e("Debug dump Test  ");
    Log.e("ProguardTest", "test dump");
}

protected void onResume()
{
    super.onResume();
    AsyncTask asynctask = new AsyncTask() {

        protected volatile transient Object doInBackground(Object aobj[])
        {
            return doInBackground((CountDownLatch[])aobj);
        }

        protected transient Void doInBackground(CountDownLatch acountdownlatch1[])
        {
            try
            {
                acountdownlatch1[0].await();
            }
            catch(InterruptedException interruptedexception)
            {
                Logger.d("wake up.");
            }
            Logger.d("hello.");
            return null;
        }

        final ProguardTestActivity this$0;

      
        {
            this$0 = ProguardTestActivity.this;
            super();
        }
    };

    CountDownLatch acountdownlatch[] = new CountDownLatch[1];
    acountdownlatch[0] = mLatch;
    asynctask.execute(acountdownlatch);
    Handler handler = new Handler();
    handler.post(new Runnable() {

        public void run()
        {
            mLatch.countDown();
        }

        final ProguardTestActivity this$0;

      
        {
            this$0 = ProguardTestActivity.this;
            super();
        }
    });

    handler.postDelayed(new Runnable() {

        public void run()
        {
            mLatch.countDown();
        }

        final ProguardTestActivity this$0;

      
        {
            this$0 = ProguardTestActivity.this;
            super();
        }
    }, 2000L);
}

private CountDownLatch mLatch;

ほとんど原型をとどめています。
これであればソースコードの解読はかなり容易にできそうです。


●難読化有り
public class ProguardTestActivity extends Activity
{

    public ProguardTestActivity()
    {
        a = new CountDownLatch(2);
    }

    static CountDownLatch a(ProguardTestActivity proguardtestactivity)
    {
        return proguardtestactivity.a;
    }

    public void onCreate(Bundle bundle)
    {
        super.onCreate(bundle);
        setContentView(0x7f030000);
        yuki.proguard.a.a("Debug dump Test");
        yuki.proguard.a.b("Debug dump Test");
        Log.e("ProguardTest", "test dump");
    }

    protected void onResume()
    {
        super.onResume();
        b b1 = new b(this);
        CountDownLatch acountdownlatch[] = new CountDownLatch[1];
        acountdownlatch[0] = a;
        b1.execute(acountdownlatch);
        Handler handler = new Handler();
        handler.post(new c(this));
        handler.postDelayed(new d(this), 2000L);
    }

    private CountDownLatch a;
}

難読化処理無しに比べると非常に読みづらいコードになっています。
よく見ると、AsyncTaskの内部クラスがcという名前のクラスに置き換えられています。
この時、Proguardによってc.javaが新規生成されており難読化を促進していることがわか
ります。
他にもLoggerがaというクラス名に変更されており、一見すると何のクラスかわかりません。

しかし、ログの内容や2000といった直値まではさすがに難読化できていません。
このことから暗号鍵のバイナリを定数等で宣言するのがどれだけ危険で、容易に盗まれて
しまうかが想像できます。


●Proguardの注意点
Proguardには注意点もあります。
Proguardはソースコード(メソッド名・クラス名も含む)を変更するため、リフレクション
等をしたコードでは、目的のクラスやメソッド名が見つからないといった不具合に繋がる
恐れがあります。
また、Proguardは不要と判断したソースコードの削除も行うため、JINやリフレクション
による参照のみのメソッドやクラスは、誤って不要と判断されるケースがあります。
こういった事態を防ぐために、proguard.cfgで難読化やソースの削除を抑止する方法が提
供されています。

以上です。