株式会社Ninjastars 技術研究部

「もっと楽しく安全なゲームの世界を創る」

Android ARM バイナリ解析 入門

株式会社Ninjastars
セキュリティエンジニア:一瀬健二郎

今回はAndroidARMアーキテクチャのバイナリ解析を行います。
Android/iOS共にARMバイナリであることが多く、ARM解析の知識は脆弱性診断などでも必要になることが多々あります。
今回の記事がそうした需要にお答えできれば幸いです。

f:id:Ninjastars:20191002164812p:plain
Android ARMバイナリ解析に挑戦!

前提
AndroidStudioセットアップ済み
ndk-buildができる状態
adbコマンドが実行可能

環境
rootedな実機 or AndroidEmulator(ARM)

今回やること等
ARM(armv7)の簡単なcrkmeを解析します。(弊社主催Victor of Cyberistsで使用した問題をAndroid用にビルドしたもの)
ただし今回は、gdb等のデバッガなど使わず簡易的なデバッガを作って解析を行います。

ARMについての基礎知識は以下の方のブログをご参照ください。
inaz2.hatenablog.com

今回は使用しませんが、以下のサイトも有用です。
armconverter.com

セットアップ手順
実行ファイルとソースファイル(debuggerのみ)は以下からダウンロードください。。
drive.google.com

今回はAndroidStudioのエミュレータ(ARM)を利用して解析を行います。
AndroidStudioで仮想端末を作成する際、ABIにarmeabi-v7aを選択して端末を作成してください。

AndroidStudioでのセットアップ例:

  1. 右上のAVDManagerをクリック
  2. Create Virtual Deviceをクリック
  3. Pixel2を選択
  4. Other Images=>一番上の"Nougat APILevel25 armeabi-v7a"の項目のDownloadをクリック
  5. Finishをクリック

上記設定が出来たらAndroidエミュレータを起動してください。(場合によっては起動するだけで数分以上かかります。)
※通常Androidエミュレータでアプリの起動テスト等を行うときはx86を利用しますが、デバッグなどを行う関係上ARMにする必要があります。

まずcrkmeとdebuggerをエミュレータ内に転送し、実行権限を付与します。

adb push crkme /data/local/tmp
adb push debugger /data/local/tmp
adb shell chmod a+x /data/local/tmp/crkme
adb shell chmod a+x /data/local/tmp/debugger
adb shell
cd /data/local/tmp/crkme
./crkme

実行すると以下のようにkeyの入力を求められ、間違っていると終了してしまいます。

f:id:Ninjastars:20191002150904p:plain
crkmeの実行結果

静的解析
ARMアーキテクチャバイナリなので、IDAのFree版はサポートしていません。
今回はGhidraで解析を行ってみましょう。

f:id:Ninjastars:20191004164126p:plain
Ghidraでのcrkmeの解析

上記を見ると、
(1)scanf関数で入力文字列を16進数整数として受け取る
(2)decrypt関数に"B@AI@FC@"を引数として渡して呼び出す。
(3)入力数値r4とdecrypt関数の戻り値r0を比較する
(4)一致していればcorrect,一致していなければwrong
という処理になっていることが分かります。

000108f4 cmp r4 ,r0

ここで上記処理にブレークポイントを設置し、decypt関数の戻り値であるr0を取得してみましょう。

動的解析
※理解を深めたい方は実際にソースコードからビルドしてdebuggerを動作させてください。Android解析に慣れてない方は実際に手を動かして頂ければ充分です。

gdb等を利用すれば簡単にレジスタの値などはわかりますが、今回はptrace含めた理解のために自分で実装を行いcrkmeをデバッグします。
以下の方のブログで行っていることを簡易化したものをAndroid ARMの世界で行ってみます。
th0x4c.github.io

今回の対象バイナリのmain関数はthumbモードになっています。
thumbモードのソフトウェアブレークポイントはリトルエンディアンで{0x01,0xde}です。
またGhidra上では0x108f4という表示になっていますが、0x10000から始まっているのでcrkmeがマッピングされたアドレス+0x8f4が「cmp r4,r0」
のアドレスです。

  • やること
  1. PTRACE_ATTACHでターゲットのプロセスにアタッチ。
  2. PTRACE_PEEKTEXTで対象アドレスの値を保存。
  3. PTRACE_POKETEXT で対象アドレスの命令を {0x01, 0xde}に書換える。
  4. PTRACE_CONT でプロセスを再開させる。
  5. waitpid(2) でブレイクするのを待つ。
  6. プロセスが (3) で書換えたアドレスでブレイクする。
  7. PTRACE_GETREGSでR0レジスタの値を取得して出力する。
  8. PTRACE_DETACH でプロセスからデタッチする。

まずアタッチするためにcrkmeのプロセスidが必要です。コマンドは以下です。

ps | grep crkme

次にcrkmeの仮想メモリ空間のメモリマップを調べる方法は以下です。

cat /proc/crkmeのpid/maps | grep crkme

f:id:Ninjastars:20191004162446p:plain
上記のような結果の場合0xb21b4000+0x8f4で「cmp r4,r0」のアドレスは0xb21b48f4です。

説明は以上です。
AndroidStudioのエミュレータを起動した状態で、crkmeを起動してください。
次に下記のコマンドを入力してください。

adb shell
ps | grep crkme
cat /proc/(上記で調べたpid)/maps | grep crkme
cd /data/local/tmp
./debugger (上記で調べたpid) (上記で調べたcrkmeの先頭アドレス)+0x8f4

上記を実行すると下の画像のように入力すべき文字列は「20190630」であると分かります。(これは弊社主催CTFの開催日)

f:id:Ninjastars:20191004163438p:plain
デバッガの実行画像

上記をcrkmeに入力するとフラグが現れます。
ninja{g@me_s3curity_c0mp4ny}

最後にARMのデバッガを自作する際に問題となるのは、ARMアーキテクチャではptraceのPTRACE_SINGLESTEPが削除されているということです。
git.kernel.org
ptrace(PTRACE_SINGLESTEP...)等とコーディングしても効果がないのでご注意ください。

ソースコード
適当に作成したフォルダにjniフォルダを作り下記のAndroid.mk、Application.mk、debugger.cを作成してください。
コマンドプロンプト

cd 作成したフォルダ
ndk-build

でビルドが出来ます。

adb push /libs/armeabi-v7a/debugger /data/local/tmp

上記コマンドでエミュレータ内に転送できます。

Android.mk

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)
LOCAL_CFLAGS += -fPIE
LOCAL_LDFLAGS += -fPIE -pie
LOCAL_DISABLE_FATAL_LINKER_WARNINGS := true
LOCAL_MODULE    := debugger
LOCAL_SRC_FILES :=  debugger.c
LOCAL_LDLIBS := -lz -llog
include $(BUILD_EXECUTABLE)

Application.mk

APP_ABI :=  armeabi-v7a

debugger.c

#include <stdio.h>
#include <unistd.h>
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h> 

int main(int argc, char *argv[])
{
    
    pid_t pid=atoi(argv[1]);
    unsigned long address=strtoll(argv[2],NULL,0);

    //対象プロセスにアタッチ
    ptrace(PTRACE_ATTACH, pid, 0, 0);
    waitpid(pid, NULL, 0);
    int original_text;

    //対象アドレスのメモリの値を保存
    original_text = ptrace(PTRACE_PEEKTEXT, pid, address, NULL);

    //{0x01,0xde}を対象アドレスに書き込み
    int opcode = 0x0000de01;
    ptrace(PTRACE_POKETEXT, pid, address, ((original_text & 0xFFFF0000) | opcode ));
    
    //プロセスを再開させる
    int status;
    ptrace(PTRACE_CONT, pid, NULL, NULL);
    printf("Continuing.\n");

    //ブレイクするまで待機する
    waitpid(pid, &status, 0);

    if (WIFEXITED(status))
    {
      printf("Program exited normally.\n");
      exit(0);
    }

    if (WIFSTOPPED(status))
      printf("Breakpoint.\n");
    else
      exit(1);
    
    //レジスタの値を取得
    struct pt_regs regs;
    ptrace(PTRACE_GETREGS, pid, 0, &regs);
    printf("R0_RegisterValue:%x\n",regs.ARM_r0);

    //処理を書き戻す
    ptrace(PTRACE_POKETEXT, pid, address,original_text);
    
    //プロセスからデタッチする。処理が動き出す。
    ptrace(PTRACE_DETACH, pid, 0, 0);

}

まとめ
今回はAndroidでのARMバイナリの解析を行いました。
ARMアーキテクチャの解説などは省略させていただきましたが、実際に簡易デバッガを作りcrkmeを解析出来ました。
gdb等の汎用的なデバッガが存在する以上、原理などを知ることは不必要だと思われる方もいるかもしれません。
こういった解析ツールの内部の実装などを知ることにより、特に防御側に回った時に複合的な対策を検討できると思います。
弊社では今後もこうした技術の研究を行い、対策などに活かせるようにしていきます。

注意事項

本レポートに記載されている内容を許可されていないソフトウェアで行うと、場合によっては犯罪行為となる可能性があります。そのため、記事の内容を試す際には許可されたソフトウェアに対してのみ実施するようにしてください。

本レポートについて
お問い合せ
E-mail:ichise@ninjastars-net.com

株式会社Ninjastarsエンジニア
一瀬健二郎