AndroidのLVL(License Verification Library) Crack編

※ ここに書いてあることは間違ってることもあるかもしれません。頑張ってますが間違っていたらTwitterかコメントなどで教えていただけるとうれしいです。

前回のまとめ

とりあえず、LVL使うだけなら簡単だね、というとこまでなんとなく見ました。
前回 d:id:vvakame:20101009#1286604355 では、とりあえずLVLを導入してみました。
で、

LVLをとりあえず導入するだけで、(多分) Lv.1の人たちへの対策としてはOKです。
具体的には、rooted端末でadb pull とかでapkを抜いてすぐ返品するような人々は
LVLのおかげでアプリを使えなくできるはず。
やったことないから知らないけど…。

今回は、Lv.2のプログラムの構造とかがそれなりにわかっているクラッカーさんになりきって、
LVLを導入したアプリケーションをハックしてみたいと思います。

前提

前回のソース GitHub - vvakame/LVLMinimumSet at 419990f6baed65b1191b4967feefd984b7fd85cc からソースをちょっと変更します。
承認失敗したときアプリを殺すように変更 · vvakame/LVLMinimumSet@e2a24db · GitHub です。権限がない場合、アプリを終了させるようにしました。


動作イメージはこんなん。Toastが表示された後にSystem#exitを呼んで終了させています。

今回のソースも、MainActivity#onCreateでLVLのチェックコードを呼んでいます。これは、Googleの中の人が言っている通り、
クラッカーが簡単に倒す事ができる、悪いコードです。

方法

今回は自分のアプリをCrackして、その後どうすればCrackされにくくなるか考えてみます。
具体的には、DalvikVMのバイトコードを改竄してチェックを回避してみます。

やってみよう!

classes.dex をとりあえずdisassembleしてみる。

(前略)
Class #14            -  Class descriptor  : 'Lnet/vvakame/lvlmin/MainActivity;'
  Access flags      : 0x0001 (PUBLIC)  Superclass        : 'Landroid/app/Activity;'
  Interfaces        -
(中略)
  Virtual methods   -
    #0              : (in Lnet/vvakame/lvlmin/MainActivity;)
      name          : 'onCreate'
      type          : '(Landroid/os/Bundle;)V'
      access        : 0x0001 (PUBLIC)
      code          -
      registers     : 4
      ins           : 2
      outs          : 3
      insns size    : 18 16-bit code units
003188:                                        |[003188] net.vvakame.lvlmin.MainActivity.onCreate:(Landroid/os/Bundle;)V
003198: 6f20 0100 3200                         |0000: invoke-super {v2, v3}, Landroid/app/Activity;.onCreate:(Landroid/os/Bundle;)V // method@0001
00319e: 1501 037f                              |0003: const/high16 v1, #int 2130903040 // #7f03
0031a2: 6e20 8d00 1200                         |0005: invoke-virtual {v2, v1}, Lnet/vvakame/lvlmin/MainActivity;.setContentView:(I)V // method@008d
0031a8: 2200 4100                              |0008: new-instance v0, Lnet/vvakame/lvlmin/MainActivity$LicenseChecker; // class@0041
0031ac: 1201                                   |000a: const/4 v1, #int 0 // #0
0031ae: 7030 7d00 2001                         |000b: invoke-direct {v0, v2, v1}, Lnet/vvakame/lvlmin/MainActivity$LicenseChecker;.<init>:(Lnet/vvakame/lvlmin/MainActivity;Lnet/vvakame/lvlmin/MainActivity$LicenseChecker;)V // method@007d
0031b4: 6e10 7e00 0000                         |000e: invoke-virtual {v0}, Lnet/vvakame/lvlmin/MainActivity$LicenseChecker;.doCheck:()V // method@007e
0031ba: 0e00                                   |0011: return-void
(後略)

まぁdalvikのバイトコードなんか全然読めないですけど、意味はわかりますね。
Classの中からMainActivityを探して、onCreateを探してきただけです。
元のソースが手元にありますので、プログラムの構造的には、onCreateでlicenseChecker.doCheck();が呼ばれさえしなければ、
ライセンスのチェックが走ることはなく、アプリケーションを不正に利用し続けることが可能なのはわかっています。
Crackerは生ソースが手元にあるわけではないので、読み解くのは大変だと思いますが、オブファスケータに通していないため、
MainActivity$LicenseChecker;.doCheck:()V とか表示されていて、容易に推測が可能です。

LVLを殺すには、licenseChecker.doCheck()の部分を適当にNOPで埋めて実行されないようにしてやればいいと思います。
やってみましょう。

@zaki50 さんに教えていただいたbviで適当に書き換えました。
正しく改変できているか見るため、もっかいdisassembleしてみます。

E/dalvikvm(76718): ERROR: bad checksum (bb41d602 vs c1efd6fe)
ERROR: DEX parse failed

…orz
Checksumを書き換えて、再度試してみます。

  Virtual methods   -
    #0              : (in Lnet/vvakame/lvlmin/MainActivity;)
      name          : 'onCreate'
      type          : '(Landroid/os/Bundle;)V'
      access        : 0x0001 (PUBLIC)
      code          - 
      registers     : 4
      ins           : 2
      outs          : 3
      insns size    : 18 16-bit code units
003188:                                        |[003188] net.vvakame.lvlmin.MainActivity.onCreate:(Landroid/os/Bundle;)V
003198: 6f20 0100 3200                         |0000: invoke-super {v2, v3}, Landroid/app/Activity;.onCreate:(Landroid/os/Bundle;)V // method@0001
00319e: 1501 037f                              |0003: const/high16 v1, #int 2130903040 // #7f03
0031a2: 6e20 8d00 1200                         |0005: invoke-virtual {v2, v1}, Lnet/vvakame/lvlmin/MainActivity;.setContentView:(I)V // method@008d
0031a8: 2200 4100                              |0008: new-instance v0, Lnet/vvakame/lvlmin/MainActivity$LicenseChecker; // class@0041
0031ac: 1201                                   |000a: const/4 v1, #int 0 // #0
0031ae: 7030 7d00 2001                         |000b: invoke-direct {v0, v2, v1}, Lnet/vvakame/lvlmin/MainActivity$LicenseChecker;.<init>:(Lnet/vvakame/lvlmin/MainActivity;Lnet/vvakame/lvlmin/MainActivity$LicenseChecker;)V // method@007d
0031b4: 0000                                   |000e: nop // spacer
0031b6: 0000                                   |000f: nop // spacer
0031b8: 0000                                   |0010: nop // spacer
0031ba: 0e00                                   |0011: return-void

ちゃんとNOPになってるので多分おkでしょう。
これをapkにまとめて、adb installで突っ込んでみたら、lisenceChecker.doCheck()が実行されず、
起動させ続けることができました。


ずっとまってても怒られない!!

どうすればいいのか考えてみよう…

※ 間違っていることや更にいい方法があったら是非教えてください!つっこみ所はきっと山ほどあるよ!!

やったほうがいいこと
  • コールバックでアプリの機能を有効にする
    • LVLの検証コードを呼ばせないように改竄するのは、難読化していても比較的簡単だと思われる。そのため、コールバック(ILicenseResultListener.Stubの実装クラス)内で、アプリケーションの動作に必要な"何か"をEnableにしてやると良いかも。
  • オブファスケータ(難読化ツール)
    • proguardなどを利用する
    • Activity#onCreateなど、システムから呼び出されたりする部分の名称を難読化することはできないので注意が必要
  • ド低能なプログラマのようにコードを書く
    • LVLの検証コードと、アプリケーションの動作に必要なコードを混ぜこぜにして書く。可読性が可能な限り低いとよい
  • 検証結果をアプリの動作の一部にする
    • 検証結果に自アプリのパッケージ名が含まれているので、その文字列を利用してクラス名を作成しClassLoaderに突っ込んでオブジェクトを取得するようなコードを書くと強いような気がする
  • 複数回検証する
    • LVL検証結果のコードを複数回、複数箇所のコードでチェックする。1カ所潰した時に、アプリが正常に動作しなくなるようにコードを書くようにすると、複数箇所潰して回るのは非常にめんどくさい
    • どこか一カ所を改竄されただけで正常に利用できるようではまずい
      • Dialogを表示してMarketに強制的に飛ばすようなコードを書いた場合、setCancelable(true)を呼ぶコード入れられたりしちゃうかも?
      • Activityを別途用意してそっちで駄目な時の処理を全てやる。元のActivityは終了しておく とかがぐぐるの中の人おすすめの方法ぽい
  • プログラムのフローを撹乱する(Googleの中の人おすすめ)
    • HandlerやThreadを使って、if, for, method callのような追っかけやすいフローではない方法を利用する。
  • レスポンスコードのハッシュ化(Googleの中の人おすすめ)
    • 検証結果の内容は万人に同一の書式なので、書式を変換してから使えということだと思う
    • 例えばLICENSEDは public static final int LICENSED = 0x0; なので、0x0との比較を行っている箇所を探せばCrackの糸口になるかも
      • 書式変換してから扱うことで、常套手段を潰してやる感じ?
やっちゃいけないこと
  • 不必要な抽象化をしない
    • GoogleのサンプルコードのInterfaceのPolicyとかは、勝手に都合のいいPolicy実装を作られて突っ込まれる可能性があるのでよくない
    • LVLに関連するクラスは全てfinalにして、finalにしたクラスのオブジェクト以外はメソッドの引数にしないようにする
次回予定

比較的Crackされにくそうな俺実装を作成してみる