2017年10月29日 | ブログ記事

Androidアプリのリバースエンジニアリング手法

ponya

10/29のアドベントカレンダー担当のponyaです

今回は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を参考に書きました。この記事では書ききれなかった他の手法や詳しい解説などが書いてありますので興味を持った方はぜひお読みください。

この記事を書いた人
ponya

この記事をシェア

このエントリーをはてなブックマークに追加

関連する記事

2018年7月7日
ますにかえる ハッカソン参加記
yusuke
2017年12月27日
Splatoon2~ボムの使い方~
shigurure
2017年12月26日
RustでMCMC(Metropolis-Hasting)
David
2017年12月26日
NinjaFlickerが完成しました
gotoh
2017年12月25日
Project Obelisk [traP Advent Calendar 2017]
nari
2017年12月25日
tobiom

活動の紹介

カテゴリ

タグ