PLAY DEVELOPERS BLOG

HuluやTVerなどの日本最大級の動画配信を支える株式会社PLAYが運営するテックブログです。

HuluやTVerなどの日本最大級の動画配信を支える株式会社PLAYが運営するテックブログです。

Android アプリ 難読化の落とし穴

こんにちは、OTTサービス事業部 プロフェッショナルサービス第1グループの上野です。
今年の4月で入社して10年になります。早いです。

Androidアプリを担当させて頂く事が多く、その時にハマりがちな難読化のお話について説明できればと思います。

難読化とは

アプリの挙動を変えずに、人間にとってソースを読みにくくする作業になります。
PlayerクラスをAクラス、playメソッドをaメソッドに置き換える、といったことになります。
これにより、逆コンパイルやリバースエンジニアリングで中身のソースを推測しにくくできます。

実際には以下のようにコードが変換されます。

mapping.txt

androidx.appcompat.app.ActionBarDrawerToggle$DelegateProvider -> a.a.a.b:
androidx.appcompat.app.AlertController -> androidx.appcompat.app.AlertController:
    android.content.Context mContext -> a
    int mListItemLayout -> O
    int mViewSpacingRight -> l
    android.widget.Button mButtonNeutral -> w
    int mMultiChoiceItemLayout -> M
    boolean mShowTitle -> P
    int mViewSpacingLeft -> j
    int mButtonPanelSideLayout -> K

Googleの公式リファレンスが充実しており、ここを見ながら調整を進めればほぼ間違いないと思います。
developer.android.com
Androidアプリは、逆コンパイルしてソースを確認するというのが簡単にできてしまいます。
その為、難読化の設定は、脆弱性診断でも必須の項目になります。
難読化と同時に最適化(使用していないコードの除外)も行われまして、アプリ容量も削減されますのでやっておくと良いでしょう!
今回は、その難読化した時に陥りがちな話をしていきます。

リリース前

アプリがクラッシュする

難読化の後で一番有りがちなのがビルドは成功するがアプリを起動するとクラッシュすることです。
通常のコードであれば上手く難読化できますが、外部ライブラリなどでこのクラス名やメソッドは変更してはいけない、となっているものが多いです。
そこが変換されてしまうとクラッシュが発生します。

使用しているライブラリでそのまま維持しておく必要があるものがないか、一つずつ確認をしてproguard-rules.txtに記載していきましょう。
大抵はGitHubのページなどに記載されています。

厄介なのは、ビルドは成功しますが そのコードを使う場所を通らないとクラッシュしないということです。
その為、難読化した場合、全ての画面、全てのコードを動かしてチェックが必要になります。

以下は、画像を読み込むGlideライブラリですが、難読化で以下を記載すると書かれています。
github.com

これがないとGlideを使う場所まで行ったときに、初めてクラッシュします。

コードがコードを自動で生成する場合、意図しない動作に

1例ですが、以下のようなクラスがあり、このクラスのメンバー変数を自動的にPOSTのBodyパラメーターに変換するようなコードを書いていたとします。
emailpasswordをコード側で入れてリクエストするような意図)

import com.google.gson.annotations.SerializedName;
public class MailValidationRequest {
    public String email;
    public String password;
}

その場合、難読化時に

import com.google.gson.annotations.SerializedName;
public class AB {
    public String a;
    public String b;
}

のようなコードが生成されまして、emailapasswordbというキーに入りサーバー側にリクエストされてしまう、という事もあります。
その為、こういった裏でコードを自動変換してくれるコードはより注意が必要です。

実際にはどのくらいの工数が必要か

これだけで1〜5日程度見ておかないと難しいかと思います。
大規模アプリだと、以下のような記載がproguard-rules.txtに2〜3倍と増えていくと思います。
この後にテストをする工数があると考えるとゾッとしますね・・!
最初から積み上げてやっておきましょう。

proguard-rules.txt

# RxJava
# https://github.com/artem-zinnatullin/RxJavaProGuardRules/blob/master/rxjava-proguard-rules/proguard-rules.txt 参照
-dontwarn sun.misc.Unsafe
-keepclassmembers class rx.internal.util.unsafe.*ArrayQueue*Field* {
   long producerIndex;
   long consumerIndex;
}
-keepclassmembers class rx.internal.util.unsafe.BaseLinkedQueueProducerNodeRef {
   long producerNode;
   long consumerNode;
}

# picasso
-dontwarn com.squareup.picasso.**

# glide
-dontwarn com.bumptech.glide.**
-keep public class * implements com.bumptech.glide.module.GlideModule
-keep public enum com.bumptech.glide.load.resource.bitmap.ImageHeaderParser$** {
    **[] $VALUES;
    public *;
}

# Eventbus
-keepclassmembers class ** {
    @org.greenrobot.eventbus.Subscribe <methods>;
}
-keep enum org.greenrobot.eventbus.ThreadMode { *; }

# Crashlytics用
# https://firebase.google.com/docs/crashlytics/get-deobfuscated-reports?platform=android 参照
-keepattributes SourceFile,LineNumberTable        # Keep file names and line numbers.
-keep public class * extends java.lang.Exception  # Optional: Keep custom exceptions.

リリース後

クラッシュが捕捉できなくなる

難読化後はリリース前にGoogle Play Consoleにmapping.txtのアップをしておきましょう。
support.google.com

これをしておかないとGoogle Play Consoleにクラッシュの通知が上がってきた場合、以下のようなログが送られてしまいます。

Non-fatal Exception: java.lang.Throwable: AdError [errorType: LOAD, errorCode: AdErrorCode [name: FAILED_TO_REQUEST_ADS, number: 1005], message: 初期化失敗]
       at feature.detail.videoplayer.ContentPlayerFragment$playerListener$1.onAdError(ContentPlayerFragment.kt:1000)
       at myplayer.player.PlayBackSession$8.onAdError()
       at myplayer.player.CompositeVideoPlayer.d()
       at myplayer.player.CompositeVODVideoPlayer.d()
       at myplayer.player.CompositeVODVideoPlayer$3.a()
       at myplayer.player.CompositeVODVideoPlayer$3.lambda$J769lS2XYVqU-00RdmD9h3-PKD4()
       ~~~~

クラッシュを追っていくとmyplayer.player.PlayBackSession.onAdError()まではメソッド名をソースで検索すれば分かります。
その次の行にいくとmyplayer.player.CompositeVideoPlayer.d()というのが出てきており、そこからは難読化された状態のメソッド名で上がってきており、追っていくのが困難になります。

Firebase Crashlyticsで集計している場合は、以下の設定を実施しておきましょう。
firebase.google.com

クラッシュが捕捉できないが、急ぎ調整したい場合

上記を行う権限がない or アプリをリリースしないといけないなどで、今すぐ調整したいなどの場合は、リリースバージョンに戻り、ビルドをしてマッピングファイルを出力して、そこからどのクラスで起きているか?を探るというので最低限場所は突き止められそうです。
ソースが同じ構成ならば、同じマッピングファイルが出力されます。(あくまで緊急用になります。)

以下が出力されるmapping.txtの場所です。

mapping.txt

android.support.v4.media.MediaBrowserCompat$MediaItem -> android.support.v4.media.MediaBrowserCompat$MediaItem:
    android.support.v4.media.MediaDescriptionCompat mDescription -> c
    int mFlags -> b
    1:1:void <clinit>():563:563 -> <clinit>
    1:3:void <init>(android.os.Parcel):538:540 -> <init>
    1:5:java.lang.String toString():556:560 -> toString
    1:2:void writeToParcel(android.os.Parcel,int):550:551 -> writeToParcel
    ~~~~

開くとこういった記述があり特定はできると思います。
ただこういった記述が1万行以上ありまして、それを紐解いていくのは現実的ではなく、お勧めしません...

まとめ

難読化の実施方法は、少し設定を入れ替えるだけですが、現状どのように動いているのかを意識しておかないと色々とつまづく事が多いです。
設定を入れれば動くが、裏でどういった仕組みが動いているのか?ということを理解して進められるのが、出来るエンジニアだと思います。

基本的には、社内テストに出す際から、難読化設定はONにして、アップを繰り返すようにしましょう! (ビルド時間はかかるので、通常開発時はしなくて良いと思います。)