10/29のアドベントカレンダー担当のponyaです
今回はAndroidアプリのリバースエンジニアリング手法についてです
使用するツール
- Android SDK(Platform Toolsなど)
- jdb(JDKと共にインストール)
- gdb
- Android Studio(3.0以上推奨)
- apkx(https://github.com/b-mueller/apkx)
- apktool
- IDA Demo
- Androidエミュレータまたは実機
静的解析
Javaコードの静的解析
最初にサンプルをダウンロードします。
今回はOWASP Mobile Security Testing Guideのサンプルをお借りします。
$ wget https://github.com/OWASP/owasp-mstg/raw/master/Crackmes/Android/Level_01/UnCrackable-Level1.apk
$ adb install UnCrackable-Level1.apk
ダウンロードしたapkファイルをapkxを使用して展開およびデコンパイルを行います。
$ apkx UnCrackable-Level1.apk
UnCrackable-Level1/src以下にデコンパイル結果のソースコードが展開されました。これらのソースコードをIDE(Android Studio)を使って見てみましょう。
Android Studioを起動してCreate New Projectにより新規のプロジェクトを作成します。その際、Company domainはパッケージ名の最初の部分であるvantagepoint.sgを指定します。このように指定しておくことで、のちにデバッガーがデバッグ対象のプロセスを識別することができます。
正しく指定できていればPackage nameの部分がsg.vantagepoint.uncrackable1となるはずです。次の設定はAPIのバージョン指定ですが重要な設定ではないので、特に気にしなくて構いません。デフォルトのActivity設定に関してはAdd no Activityを指定し、finishで完了です。
Projectの構造を見ると画面のようなディレクトリ構造となっているはずです。
この中のjavaの下、sg以下を削除し、先ほどデコンパイルした結果のUnCrackable-Level1/src/sg以下をjavaの下にコピーします。すると、ディレクトリの構造が画面のようになり、ソースコードをAndroid Studio上で見ることができるようになりました。デコンパイル結果は完全ではないのでソースコードのあちこちでエラーが出ていますが、無視しても構いません。
ソースコードの詳しい解説はしませんが、大体の動きとしてはまず、MainActivityを起動した後Root環境の検出とデバッグ可能かどうかを検出します。もし、検出で引っかかった場合にはAlertDialogを作成してアプリを終了させます。つぎに、入力文字列に対してそれが復号した文字列と一致するかどうかチェックし、一致した場合にのみ正解のダイアログが出現します。
これらの回避方法については後の動的解析にて行うのでいったんはここで終了です。
Nativeコードの静的解析
Androidアプリは主にJavaで書いたコードを実行することを想定していますが、Android NDKによりNativeのコードを動かすことができます。Nativeのバイナリは主にlib/以下にアーキテクチャごと(x86、armeabi-v7aなど)に分けられて入っています。
先ほどのOWASP Mobile Security Testing GuideのリポジトリよりHelloWorld-JNI.apkをダウンロードし、同様にしてapkxにより展開、デコンパイルを行います。
$ wget https://github.com/OWASP/owasp-mstg/raw/master/Samples/Android/01_HelloWorld-JNI/HelloWord-JNI.apk
$ apkx HelloWord-JNI.apk
HelloWord-JNI/lib以下にアーキテクチャごとの.soファイルが入っています。x86用の.soファイルが入っているのでこれを解析に使用します。IDA Demoを使用してみてみると、画像のような関数を見ることができます。
この関数がJavaのコードから呼び出されていることがわかります。このようなファイルの解析は既存の.soファイルの解析手法と同様にできます。
動的解析
Javaコードの動的解析
デバッグによりJavaコードの動的解析をやってみます。AndroidではJavaコードのデバッグにJDWPを使用することができます。しかし、JDWPでやりとりすることができるようになるためにはアプリで"android:debuggable="true""が指定されていなければなりません。リリース済みアプリはほとんどの場合このオプションがついていないので、このオプションをつけてアプリを再度パッケージングします。
今度はapktoolを使用してUnCrackable-Level1.apkを展開してみましょう。デフォルトでは自動的にソースコードへのデコンパイルをしてくれますが、今回は必要ないのでデコンパイルなしで展開をします。
$ apktool d --no-src UnCrackable-Level1.apk
展開した結果生成されたディレクトリ以下にあるAndroidManifest.xmlを編集して、次のようにapplicationタグに"android:debuggable="true""を追加します。
<applicationandroid:allowBackup="true"android:debuggable="true"android:icon="@mipmap/ic_launcher"android:label="@string/app_name"android:theme="@style/AppTheme">
これで編集は完了したのでapkファイルに再度パッケージングしましょう。Androidは署名済みのapkファイルしかインストールすることができません。apktoolはパッケージングも行ってくれますが、署名は行ってくれないので手動でやります。
今回はAndroid Studioをインストールした際に入るkeystore(~/.android/debug.keystore)を使用します。このkeystoreのデフォルトのパスワードはandroid、キーの名前はandroiddebugkeyになっています。
$ cd UnCrackable-Level1
$ apktool b
$ zipalign -v 4 dist/UnCrackable-Level1.apk ../UnCrackable-Repackaged.apk
$ cd ..
$ apksigner sign --ks ~/.android/debug.keystore --ks-key-alias signkey UnCrackable-Repackaged.apk
無事にパッケージングと署名が完了したら、adbでエミュレータまたは実機にapkファイルをインストールしてみましょう。
$ adb install UnCrackable-Repackaged.apk
インストールが終了したらAndroidの設定の開発者オプションでWait for debuggerをOnにし、Select debug appでデバッグ対象のアプリを選択します。
設定が終わってアプリを起動すると、アプリはデッバガー待ちの状態になります。
デバッガーをアタッチするための設定を行います。デバッグ対象のアプリのPIDを調べ、adb forwardコマンドでローカルのポートへの転送設定を行い、jdbでアタッチします。jdbでアタッチした際にアプリが再開するのを防ぐためにsuspendコマンドでアプリの実行を即座に停止しています。
$ adb shell ps | grep uncrackable
u0_a59 1693 321 990860 32104 00000000 f743e302 S sg.vantagepoint.uncrackable1
$ adb forward tcp:7777 jdwp:1693
$ { echo "suspend"; cat; } | jdb -attach localhost:7777
これでjdbを使ってアプリにアタッチできました。今回のアプリではRoot環境やデバッグ可能かどうかを判定して、途中でアプリを終了してしまう部分があります。これを回避してみましょう。以下のメソッドのsetCancelable(false)の部分をtrueに書き換えることでダイアログをキャンセルできるようにします。
private void a(final String title) {
final AlertDialog create = new AlertDialog$Builder((Context)this).create();
create.setTitle((CharSequence)title);
create.setMessage((CharSequence)"This in unacceptable. The app is now going to exit.");
create.setButton(-3, (CharSequence)"OK", (DialogInterface$OnClickListener)new b(this));
create.setCancelable(false);
create.show();
}
jdbのコンソールでブレークポイントを設定し、resumeで再開します。ブレークポイントで止まるたびにlocalsで変数を確認し、flag=falseだった場合にflag=trueに設定します。この操作を何回か行うと、ダイアログの外をタップしてアプリ終了を防ぐことができます。
Set uncaught java.lang.Throwable
Set deferred uncaught java.lang.Throwable
Initializing jdb ...
> All threads suspended.
> stop in android.app.Dialog.setCancelable
Set breakpoint android.app.Dialog.setCancelable
> resume
All threads resumed.
>
Breakpoint hit: "thread=main", android.app.Dialog.setCancelable(), line=1,110 bci=0
main[1] locals
Method arguments:
Local variables:
flag = true
main[1] resume
All threads resumed.
>
Breakpoint hit: "thread=main", android.app.Dialog.setCancelable(), line=1,110 bci=0
main[1] locals
Method arguments:
Local variables:
flag = false
main[1] set flag = true
flag = true = true
こうしてアプリの終了を回避することができました。
Nativeコードの動的解析
デバッガーでNativeコードの動的解析を行います。サンプルアプリにはHelloWord-JNI.apkを使用します。Select debug appでデバッグ対象を変更し、アプリが起動したら、デバッガー待ちの状態にします。今回はgdbでNative実行部分にアタッチしますが、そのままアタッチしても、デバッグしたいコードがすでに終了してしまっています。
これを解決するため、最初にjdbでアタッチをしてから、gdbをアタッチします。先ほどと同様の手順で対象のプロセスにjdbをアタッチしてstop in java.lang.System.loadLibraryでNativeコードが読み込まれる部分でブレークポイントを設定します。resumeで再開してブレークポイントにヒットしたらそのままの状態にします。
> stop in java.lang.System.loadLibrary
> resume
All threads resumed.
Breakpoint hit: "thread=main", java.lang.System.loadLibrary(), line=988 bci=0
次にgdbserverのバイナリをAndroidに入れます。Android NDKをインストールしている場合にはそれぞれのアーキテクチャに対応したgdbserverのバイナリがあるのでそれをAndroidの適当な箇所にコピーします。
$ adb push $NDK/prebuilt/android-arm/gdbserver/gdbserver /data/local/tmp
gdbserverで対象プロセスにアタッチします。
$ adb shell
$ ps | grep helloworld
u0_a164 12690 201 1533400 51692 ffffffff 00000000 S sg.vantagepoint.helloworldjni
$ su
# /data/local/tmp/gdbserver --attach localhost:1234 12690
Attached; pid = 12690
Listening on port 1234
gdbを起動するホスト側でポートの転送設定を行います
$ adb forward tcp:1234 tcp:1234
これで準備完了です。先ほどのjdbのコンソールを開いてstep upを実行して、loadLibrary()がreturnするまで実行を進めます。
gdbを起動してgdbserverに接続します
$ gdb libnative-lib.so
(gdb) target remote :1234
Remote debugging using :1234
0xb6de83b8 in ?? ()
対象の関数にブレークポイントをセットして通常のgdbと同じように操作ができます。
(gdb) info sharedlibrary
(...)
0xa3522e3c 0xa3523c90 Yes (*) libnative-lib.so
(gdb) info functions
All defined functions:
Non-debugging symbols:
0x00000e78 Java_sg_vantagepoint_helloworldjni_MainActivity_stringFromJNI
(...)
0xa3522e78 Java_sg_vantagepoint_helloworldjni_MainActivity_stringFromJNI
(...)
(gdb) b *0xa3522e78
Breakpoint 1 at 0xa3522e78
(gdb) cont
まとめ
今回は静的解析、動的解析の手法の概要について紹介しました。特に動的解析ではNativeコードをデバッグする際にjdbからもgdbからもアタッチされている状態を作り出すことにより、両方のコードのデバッグを簡単にできるようにしました。また、この記事はTampering and Reverse Engineering on Androidを参考に書きました。この記事では書ききれなかった他の手法や詳しい解説などが書いてありますので興味を持った方はぜひお読みください。