Androidアプリのクラッキング対策


はじめましてComicCafeAppです。初めてQiitaに投稿します。
趣味でAndroidアプリを開発していて、現在リリースしているアプリはComicCafeCalcNoteCalcNoteProの3つです。この内、CalcNoteProが有料アプリで360円で販売しています。去年の11月にv1.0.0をリリースしてからアップデートを繰り返して徐々にユーザー数が増えてきた矢先、CalcNoteProがクラックされるという不幸に見舞われました。いろいろ対策を考えた結果、DexProtectorというツールを使って対策をしました。対策したバージョンをリリースしてからまだ数日ですが、幸い今のところクラックされていないようです。情報共有の為にDexProtectorの使い方をメモしておくことにします。

クラックされるとどうなるか

アプリがクラックされるとライセンスチェックの仕組みや署名のチェック処理などが全て無効化されてapkを入手すれば誰でもインストールして使える状態になります。そしてapkファイルは無数にあるアップローダーにアップロードされ、そのリンクがTwitterで拡散されたり、ブログやホームページで紹介されたりします。試しにアップローダーからapkをダウンロードしてみましたが、設定画面のアプリのバージョンを表示している箇所に丁寧にもクラッカーの名前が刻まれていました。悲しいですね。

一般的なクラック対策

GooglePlayで有料アプリをリリースする場合、LVLを使ってライセンスチェックをするのが一般的だと思います。さらに追加で署名を検証したり、GooglePlayからインストールされたかどうかのチェックをしたりしてクラック対策をします。そして最後にProGuardを使ってコードを難読化してこれらのクラック対策用のコードを攻撃者が解析しづらくします。

// GooglePlayからインストールされたか検証
String installer = context.getPackageManager().getInstallerPackageName(context.getPackageName());
TextUtils.equals("com.android.vending", installer);

// 署名の検証
PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES);
Signature signature = packageInfo.signatures[0];
byte[] cert = signature.toByteArray();
String sha1 = ByteUtils.computeSHA1Hash(cert);
return TextUtils.equals("予め計算しておいた署名のハッシュ値", sha1);

ProGuardは難読化だけではなくapkのサイズを削減してくれますし、とても素晴らしいツールですが、クラック対策という点では以下の理由より不十分だと思われます。

  • Applicationクラスや、Activityクラスから派生したクラスは難読化できません。
  • 文字列リテラルは難読化されません。上の例だと"com.android.vending"という文字列が丸見えです。
  • クラックを検出した時、アプリケーションを終了するためにActivity#finish()を呼び出しますが。これも難読化できません。
  • そもそも難読化というのは読みづらくするだけなので、読もうと思えば読めます。全体を読むのは無理でも難読化されていない部分からクラック対策の処理部分を当たりをつけて解読することは十分可能です。

さらなるクラック対策

私なりに考えてみて思いついた対策は以下になります。

  • Applicationクラスや、Activityクラスから派生したクラスにはロジックを書かない
  • 文字リテラルを暗号化しておき、実行時に復号化して使用する
  • Activity#finish()など呼び出し位置を特定されたくないメソッド呼び出しはリフレクションを使う
  • クラックを検出した時は、その場でアプリを終了するのではなく、機能を制限したり、タイミングをずらして例外を投げて終了する
  • NDKを使って解析を難しくする

実際、これらの対策を組み込んで実装してみて感じたことは

  • 本来アプリケーションとは関係ないコードが増えコードの可読性が落ちる
  • なんらかのバグによりクラック対策のコードが正規のユーザーに影響を与えないか不安
  • クラック対策のために追加したコードをテストするのが難しい

やはり開発者としてはコードはキレイに保ちたいですし、今後のことを考えるとトリッキーなクラック対策のコードによってメンテナンス性が低下するのはとても容認できません。

その他の対策

プログラミング以外の対策としてDMCA侵害申し立てが考えられます。アップローダーに対して著作権を侵害しているファイルの削除を要求することができます。しかし複数のアップローダーにapkがアップロードされているので面倒ですし、やりとりは英語になります。
次に思いついたのはGoogleにDMCA侵害申し立てを行い、検索のインデックスからクラックされたCalcNoteを紹介しているサイトを削除してもらうことです。ここから日本語で簡単に申請することができます。実際に何回か削除申請を行い削除してもらうことに成功したのですが、何回も申請しているうちにGoogleの担当者に「検索インデックスから削除しても大本の配布しているサイトは生き続けるので、直接アップローダにDMCA申請したら?」という本末転倒なことを言われ申請するのをやめました。

餅は餅屋

自力でクラック対策を行うことに限界を感じた私は専用のツールに頼ることにしました。ツールを買ってお金で解決できるならそれでいいじゃないか!餅は餅屋です。
調べたところ、この手のツールはあまり種類が無く、一番有名なのはProGuardを開発している会社がリリースしているDexGuardというやつです。昔は5万円ぐらいだったみたいなのですが今は価格は非公開で要問合せってことになっています。嫌な予感しかしませんが、どこかに価格情報がないかとググってみた結果は驚きのEUR 10313、まさかの100万超えです。どうやら中途半端なお金では解決してもらえないようです。しかしリンク先に少し機能は落ちますがDexProtectorというツールが紹介されており価格は$750ぽっきり。DexGuardの値段を聞いた後だとえらく安く思えますが、消費税込みで8.5万円です。普通に高いですね。まぁ他に選択肢は無さそうだったので、かなり迷いましたが買うことにしました。

DexProtector購入の注意点

DexProtectorのホームページに行くとトライアルを申し込んで、購入すればいいだけのように見えますが、GooglePlayの登録しているGmailを使ってトライアルを申し込んでもスルーされて返信は一切きません。2回ほど申し込んでみましたが華麗にスルーされます。ホームページにはUse your company/organization email address to prove good intentions.と書いてあります。
仕方がないので会社のメールで申し込んだら1時間もしないで返信がありました。どうやら販売する相手にも簡単な審査があるようです。念の為、「会社として購入するのではなく、個人として購入して使いたい」との旨を伝えたところ問題ないとの回答でした。ちなみに実際に購入するときも会社のメールじゃないと返信はきませんので注意が必要です。私はIT系の会社に勤務しているのですが、全然関係ない会社でも審査に通るかは不明です。

DexProtectorの機能

DexProtectorにはStandard版とEnterprise版があり、私が購入したのはStandard版です。Enterprise版は価格が非公開で要問合せとなっているのでかなり高額だと予想されます。Standard版とEnterprise版は比較表を見ればわかりますが、Standard版ではMultidexのサポート、NDKのサポート、Root化やエミュレータの検出などが使えません。あと比較表ではResource Encryptionは使えることになっていますが、resフォルダ以下のstringリソースなどは暗号化できないので注意が必要です。幸いassetsフォルダ以下は暗号化することができるので、暗号化したいリソースをassetsフォルダ以下に移動して、ロケールに応じて適切なリソースを読み込むような処理を自力で実装すれば同様のことを実現可能です。とりあえずStandard版で使える機能をみていきましょう。

String Encryption

Javaのコード内にある文字列リテラルを暗号化する機能です。画面に表示される文字列などはリソースファイルで管理していると思いますが、ログメッセージやFlavorを表す文字列("Pro"とか"Free")はデコンパイルすると丸見えで攻撃者に重要なヒントを与えることになります。

Class Encryption

classファイル自体を暗号化する機能です。暗号化したクラスはdex2jarJD-GUIを使ってデコンパイルしても表示されません。恐らく暗号化されたclassファイルをassetsとして管理して、実行時に独自のクラスローダーで復号化してるんだと思います。詳しい仕組みや暗号化のアルゴリズムは非公開なのでわかりませんが、解析をより一層難しくしてくれそうな感じです。頼もしい。

Hide Access

特定のメソッド呼び出しなどを隠蔽する機能です。先程説明したリフレクションを使ってActivity#finish()を呼び出すみたいな感じだと思います。

Resource Encryption

リソースファイルを暗号化する機能です。Standard版ではresフォルダ以下のリソースファイルの暗号化をサポートしていないので、assetsフォルダ以下を暗号化します。

DexProtectorのデメリット

当然、DexProtectorを使う上でのデメリットも存在します。

  • 個人開発者にとっては8.5万円は高いです。テスト用の端末が2台ぐらい買えそうです。
  • apkのファイルサイズが約1MBほど増加します。これは復号化に必要な処理やライブラリがapkに追加されるのが原因だと思われます。
  • ビルド時間が大幅に増加します。導入前はFree版とPro版のビルドで5分程度だったものが、導入後は20分ぐらいかかるようになりました。
  • アプリの起動が遅くなります。アプリを起動するには復号化の処理が必要になるのでアプリケーションの起動時間に悪影響を及ぼします。マニュアルにも書いてありますが、暗号化するクラスはライセンスチェックの処理や盗まれたくないアルゴリズムが書いてあるクラスに限定すべきです。
  • クラック被害から100%守ってくれるわけではありません。最終的にクラックされるかどうかはクラッカーのスキルに依存します。大金を使ったけどクラックされる可能性もあるわけですね。

DexProtectorの導入手順

1. アクティベーション

DexProtectorを入手したら、まずアクティベーションする必要があります。これはトライアル版でも製品版でも同じ手順です。アクティベーションが成功するとHomeディレクトリにライセンスファイルが生成されます。このファイルはアクティベーションした環境でのみ有効で、PCを買い替えたらサポートに連絡してアクティベーションをやり直す必要があるそうです。

java -jar dexprotector.jar -activate
# この後、アクティベーションコードを入力する

2. Androidプロジェクトに組み込む

DexProtectorは専用のGUIアプリでも使えますしMavenプラグインやGradleプラグインも提供しています。私はAndroidStudioで開発しているのでGradleプラグインを使ってDexProtectorをプロジェクトに組み込みました。
まず、プロジェクトのルートにlibsフォルダを作成して、dexprotector.jardexprotector-gradle-plugin.jarをコピーします。
次にプロジェクトルートにあるbuild.gradleに以下の記述を追加します。

buildscript {
    repositories {
        flatDir { dirs 'libs' }
    }
    dependencies {
        classpath ':dexprotector-gradle-plugin:'
        classpath ':dexprotector:'
    }
}

次にappフォルダの下にあるbuild.gradleに以下の記述を追加します。

apply plugin: 'dexprotector'

最後にDexProtectorの設定ファイルであるdexprotector.xmlappフォルダの下に作成します。

dexprotector.xml
<dexprotector>
    <verbose>true</verbose>
    <optimize>true</optimize>
    <signMode>release</signMode>

    <stringEncryption>
        <filters>
            <filter>glob:com/burton999/calcnote/XXXX.class</filter>
        </filters>
    </stringEncryption>
    <classEncryption>
        <filters>
            <filter>glob:com/burton999/calcnote/YYYY.class</filter>
        </filters>
    </classEncryption>

    <resourceEncryption>
        <assets>
            <filters>
                <filter>*.xml</filter>
            </filters>
        </assets>
    </resourceEncryption>
</dexprotector>

dexprotector.xmlは見て分かるように、何を暗号化するかをfilterで設定するだけです。filterには正規表現や、否定(◯◯以外は暗号化する)も使うことができますが、パフォーマンスの悪化を考慮して暗号化する対象は少なくすべきですので、直接クラスを指定する方法がシンプルで良いと思います。ちなみにDexProtectorには難読化する機能はないのでProGuardと併用して使います。

DexProtectorを導入してみての感想

今回、自分のアプリがクラックされたということで、いろいろ調べて対策を講じてみましたが冷静になって考えてみると、何も対策しないという選択が一番正解だったと思います。CalcNoteProは360円ですので1つ売れて私に入るお金は約250円です。DexProtectorを購入する為に使った8.5万円という金額は340ダウンロードに相当します。そしてこの記事を書いてる2016/9/22現在、CalcNoteProの総ダウンロード数はたったの400ダウンロードに過ぎません。これは今まで得た収入のほとんどをDexProtectorにつぎ込んでしまったことになります。またライセンスの更新に毎年$250かかります。
実際のところCalcNoteがクラックされてから売上が落ち込んだということは一切ありませんし、ほとんどのユーザーは海賊版ではなくGooglePlayから正規にダウンロードしていると思います。そもそも海賊版を使うという行為は、マルウェア感染などのリスクもあるわけで、たった360円のアプリでは割に合いません。おそらく海賊版を使う人の割合は100人に1人、いや1000人に1人といったレベルかもしれません。そう考えると高いお金を払ってまで対策する意味は無いのだと思います。私がDexProtectorの購入に踏み切った理由は、クラッカーの名前入りの海賊版をリリースされて悔しかったことと、技術的にDexProtectorのようなツールの効果に興味があったためです。まだDexProtectorを導入した段階でこれから実際にクラックされるかどうかはしばらく様子も見ないと分かりませんが、少なくとも「対策してやったぜ!」という満足感は得ることができました。

最後に

今回、クラック対策の調査、DexProtectorの評価と導入に多くの時間とお金を費やしました。本来これらの貴重な時間はアプリ開発に使われるべきです。各開発者がクラック対策の為に無駄な時間とお金を費やすのはAndroidプラットフォーム全体として見ても大きな損失で望ましいことではないと思います。個人的にはこういった問題は開発者個人ではなくプラットフォームを提供しているGoogleが先導して対策して欲しいと強く感じました。
もしこの記事を読んでDexProtectorの効果に興味を持ったなら、是非、CalcNoteProを購入してデコンパイルしてみてください。きっとライセンスチェックの処理がどこにあるのか見つけられないはずです。

追記 (2016/10/20)

DexProtectorを導入したバージョンをリリースしてから1ヶ月が経過しました。
日々、エゴサーチをしてクラックされたCalcNoteが出回ってないか確認していますが、今のところクラックを確認できていません。
ただ、クラックを試みようとしたと思われるログが多数 Firebase Crash Reporting に残っていました。

Exception java.lang.VerifyError: Rejecting class com.google.android.a.a.t because it failed compile-time verification (declaration of 'com.google.android.a.a.t' appears in /data/app/com.burton999.notecal.pro-1/base.apk)
    com.burton999.notecal.ui.activity.CalcNoteActivity.onCreate ()
    android.app.Activity.performCreate (Activity.java:6876)
    android.app.Instrumentation.callActivityOnCreate (Instrumentation.java:1135)
    android.app.ActivityThread.performLaunchActivity (ActivityThread.java:3207)
    android.app.ActivityThread.handleLaunchActivity (ActivityThread.java:3350)
    android.app.ActivityThread.access$1100 (ActivityThread.java:222)
    android.app.ActivityThread$H.handleMessage (ActivityThread.java:1795)
    android.os.Handler.dispatchMessage (Handler.java:102)
    android.os.Looper.loop (Looper.java:158)
    android.app.ActivityThread.main (ActivityThread.java:7229)
    java.lang.reflect.Method.invoke (Method.java)
    com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run (ZygoteInit.java:1230)
    com.android.internal.os.ZygoteInit.main (ZygoteInit.java:1120)
    de.robv.android.xposed.XposedBridge.main (XposedBridge.java:102)

これがクラックを試みた痕跡だと判断した理由は以下の3点です。

  1. スタックトレースが起動すらできない致命的なエラーであること。
  2. 影響ユーザー数が全ユーザー数の10%弱ほどあるのに、バグリポートや苦情が一切無いこと。
  3. 特定の端末ではなく、Android4〜6の複数の端末で発生していること。

クラッカーがどのような手法を用いているかは分かりませんが、今のところDexProtectorがきちんと仕事をしてくれているようです。
また、DexProtectorを導入後に、DexProtectorのバージョンアップがあり。リリースノートには

all the protection mechanisms are significantly improved in terms of performance.

とありました。
開発者が自身で実装しなくても、クラック対策の仕組みが日々向上してくのは本当にうれしい限りです。
あとはDexGuardのように急に理不尽な値上げをしないことを祈るばかりです。
基本的に海賊版対策は終わりがなく、まだまだ油断はできませんが、とりあえずDexProtectorを買って本当に良かったと感じています。

最後に、この記事が海賊版対策で悩んでる開発者の助けになれば幸いです。
もしCalcNoteがクラックされてしまった際には、必ずこの記事を更新することをお約束します。
よって、この記事にクラックされた報告が無い限りはDexProtectorはしっかり仕事をしてくれていると判断してください。