読者です 読者をやめる 読者になる 読者になる

むらかみの雑記帳

Android とか iOS とかソフトウェア開発に関するネタ帳

Anti AntiLVL - AntiLVL への簡単な対抗方法について考えてみる

Android

故あって、Android の LVL を無効化する「AntiLVL」の対抗策を検討していたりします。

LVL (License Verification Library) は Google が提供する Android のライセンス検証用のライブラリで、簡単にいうと Google Play でちゃんと購入されたアプリかどうかをサーバに問い合わせて確認するというライブラリです。AntiLVL は、APK ファイルを書き換えてLVL を無効化してしまうというツールで、Google Play で有料アプリを購入してすぐに APK を取り出して注文キャンセルし LVL を無効化してしまえば不正にアプリを利用できるというわけです。そんなことする不届き者はとっとと氏ねばいいのにと思うわけですが。

AntiLVL での LVL 解除は非常に簡単にできてしまうので開発者としては何らかの対策が必要です。

署名検証すれば良いのでは?

AntiLVL は、APK を分解してデコンパイルし、バイトコードにパッチを当てて再度 APK に戻すことで LVL を解除しています。

APK の内容を改竄するので、当然開発者とは違う証明書で APK に署名することになるわけです。したがって、PackageManager を使って署名を確認し改竄されていないか確認すれば良さそうな気がしますよね。

ところが、AntiLVL は PackageManager を差し替えて署名を偽装するのでこれは容易ではありません。PackageManager を直接使わずリフレクション使えばいいじゃんと思うわけですが、AntiLVL はリフレクションの Method#invoke もフックするためこれもダメです。

JNI で native コードを書くか、jar ファイルをパッケージの中に入れておいて自前でクラスロードすれば回避できると思いますが、かなり面倒です。そこでここでは、署名検証せずに AntiLVL を回避する方法について考えてみます。

AntiLVL はどうやって LVL を解除しているのか

AntiLVL 1.4.0 の場合、LVL に含まれる ServerManagedPolicy と LicenseValidator の2つのクラスにパッチを当てています (これは AntiLVL の jar をバラして得られる XML ファイルにルールが書いてあります)

まず、ServerManagedPolicy の方。こちらは allowAccess() メソッドの返り値を強制的に true に変更します。具体的には、以下のようにしてパッチを行います。

  • 以下の条件のいずれかを満たすクラスは、ServerManagedPolicy クラスと判断します (他にもいくつか条件があるが省略)
    • "com.android.vending.licensing." という文字列が含まれる
    • "ServerManagedPolicy" という文字列が含まれる 
    • "validityTimestamp" という定数文字列が含まれる
  • この中に、以下のメソッドがあれば allowAccess() と判断します
    • public なメソッド
    • System.currentTimeMillis() を呼んでいる

LicenseValidator のほうは、verify() メソッド内の switch - case 文を書き換え、全部 LICENSED と判定するようにします。具体的には:

  • 以下のようにして verify() メソッドを探します。
    • メソッドのシグニチャが public * (java.security.PublicKey, java.lang.String, java.lang.String) というメソッドを探します
  • メソッド内の switch - case 文を書き換えます
    • java.security.Signature.getInstance() を呼び出していることを確認します。
    • このメソッドの中の switch 文内の case 節を書き換えます。

対策方法

ここまでわかれば、対策としては比較的簡単です。やり方はいくつかありますが、例えば以下のようにすればよいでしょう。当然 ProGuard の難読化は必要です。

  • ServerManagedPolicy クラスは
    • 文字列定数の PREFS_FILE, PREF_VALIDITY_TIMESTAMP を違う文字列に変更します (変更しても動作には影響ないはず。詳細はソース参照)
    • allowAccess() は System.currentTimeMillis() を直接呼ばず、別のメソッドを経由して呼ぶように変更
  • LicenseValidator クラスは
    • verify メソッドの switch - case 文を if 文に変更します。

とりあえず、この修正を加えれば AntiLVL は効かなくなります。

また、AntiLVL を罠にかけるために、わざと AntiLVL が書き換えるようなコードを仕込んでおくという手もあります。上記 ServerManagedPolicy そっくりのクラスを作っておき、allowAccess は必ず false を返すようにしておきます。AntiLVL で書き換えた APK では allowAccess の返り値が必ず true になるので、true になっていれば AntiLVL により書き換えられたことが判別できるので、ライセンス違反と判定すればいいでしょう。

いたちごっこなので AntiLVL 側がさらに対策してくる可能性もありますが、そもそもの問題はみなが同じ LVL ライブラリを使っていることが問題なので、みなが少しずつ違う実装にしていれば AntiLVL 側では対処できなくなるはずです。

PS. CashFlow は対策済みです。