株式会社Ninjastars
セキュリティエンジニア:一瀬健二郎
今回はAndroidの共有ライブラリであるsoファイルのインジェクションについて解説します。
soインジェクションではptraceを利用して対象プロセスのメモリに書き込んだり、レジスタの操作などを行いsoファイルをインジェクションします。
インジェクションする一連の流れがptraceやARMアーキテクチャ等の理解の参考になるため、今回ご紹介させていただきます。
前提
AndroidStudioセットアップ済み
ndk-buildができる状態
adbコマンドが実行可能
環境
rootedな実機 or AndroidEmulator(ARM)
※AndroidOSのVersionは7未満。7以降はAndroidのセキュリティ機構により今回の手法でのdlopenが失敗するため。
今回やること等
他のプロセスに対してsoファイルをインジェクションして、そのプロセスで「”Hello,World”」を出力させます。
ARMについての基礎知識は以下の方のブログをご参照ください。
inaz2.hatenablog.com
弊社ブログ記事もご参照ください。
Android 他のプロセスのメモリを読み書きする - 株式会社Ninjastars 技術研究部
Android ARM バイナリ解析 入門 - 株式会社Ninjastars 技術研究部
基礎知識
Android,Linux等ではdlopen関数を利用するとsoファイルを動的にロードすることが可能です。
Man page of DLOPEN
void *dlopen(const char *pathname, int mode)
対象プロセスにsoファイルをインジェクションするためには、対象プロセスでdlopenを呼び出させれば良いということになります。
自プロセスでない他のプロセスで任意の関数を呼び出すということは通常は出来ませんが、ptraceシステムコールを利用すれば可能になります。
ptraceを利用し対象プロセスにアタッチが成功すると、対象プロセスは停止します。
この停止している間に以下を実行します。
- プログラムカウンタをdlopenのアドレスに書き換える
- dlopenの引数であるsoファイルのパスをメモリに書き込む
- レジスタにdlopenの引数をセットする
- 戻りアドレスを0に設定する(※ここが重要)
この状態でプロセスを再開するとdlopenが実行されsoファイルがロードされた後、不正な戻りアドレスがセットされていたことにより制御が戻り対象プロセスは再度停止します。
ここで事前に保存して置いた最初の状態のレジスタの値をセットし、再開させることでプログラムは正常に動き出すという形になります。
ARMアーキテクチャでの引数の渡し方
r0,r1,r2,r3の順で第一、第二、第三、第四引数に対応している。戻り値はr0レジスタにセットされる。5個以上の引数の場合スタックを利用する。
セットアップ手順
ソースファイルは下部にあるので、各自ビルドを行ってください。
ファイルの説明:
sleep:soファイルがインジェクトされる側
inject:soファイルをインジェクトする側
libhello.so:インジェクトされるsoファイル
今回は簡単のためsleep側でpidの値,mmapで確保したアドレス、dlopenのアドレスを出力します。
またdlopen後のdlcloseなどは省略させていただきました。
今回はAndroidStudioのエミュレータ(ARM)を利用して解析を行います。
AndroidStudioで仮想端末を作成する際、ABIにarmeabi-v7aを選択して端末を作成してください。
AndroidStudioでのセットアップ例:
- 右上のAVDManagerをクリック
- Create Virtual Deviceをクリック
- Pixel2を選択
- Other Images=>一番上の"Marshmallow APILevel23 armeabi-v7a"の項目のDownloadをクリック
- Finishをクリック
上記設定が出来たらAndroidエミュレータを起動してください。(場合によっては起動するだけで数分以上かかります。)
※通常Androidエミュレータでアプリの起動テスト等を行うときはx86を利用しますが、デバッグなどを行う関係上ARMにする必要があります。
まず各ファイルをエミュレータ内に転送し、実行権限を付与します。
adb push sleep /data/local/tmp adb shell chmod a+x /data/local/tmp/sleep adb push inject /data/local/tmp adb shell chmod a+x /data/local/tmp/inject adb push libhello.so /data/local/tmp
sleepを実行します。
adb shell cd /data/local/tmp ./sleep
コマンドプロンプトをもう一つ開き以下を実行します。(引数は環境に合わせて変更してください。)
./inject 1110 0xb6fdac85 0xb6f09000
するとsoファイルがインジェクトされ、以下のようにHello,World!が表示されます。
コマンドプロンプトで以下のコマンドを実行し確認します。
cat /proc/pid(環境に合わせて)/maps | grep libhello.so
今回と同じ考えを利用すると、対象プロセスでdlopen以外の任意の関数を呼ぶことやインジェクトしたsoファイルの任意の関数を実行することもできます。
下記の方の記事に詳細が解説されているため、是非ご参照ください。
qiita.com
ソースコード
適当に作成したフォルダにjniフォルダを作り下記のAndroid.mk、Application.mk、hello.c、inject.c、sleep.cを作成してください。
コマンドプロンプトで
cd 作成したフォルダ
ndk-build
でビルドが出来ます。
Android.mk
LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_CFLAGS += -fPIE LOCAL_LDFLAGS += -fPIE -pie LOCAL_MODULE := sleep LOCAL_SRC_FILES := sleep.c include $(BUILD_EXECUTABLE) ##################################### include $(CLEAR_VARS) LOCAL_CFLAGS += -fPIE LOCAL_LDFLAGS += -fPIE -pie LOCAL_MODULE := inject LOCAL_SRC_FILES := inject.c include $(BUILD_EXECUTABLE) ##################################### include $(CLEAR_VARS) LOCAL_MODULE := hello LOCAL_SRC_FILES := hello.c include $(BUILD_SHARED_LIBRARY)
Application.mk
APP_ABI := armeabi-v7a
hello.c
#include<stdio.h> //この属性を設定することで、soファイルが読み込まれたとき関数が実行される __attribute__((constructor)) void hello() { printf("Hello,World!\n"); }
inject.c
#include <stdio.h> #include <unistd.h> #include <sys/ptrace.h> #include <sys/types.h> #include <sys/wait.h> #include <fcntl.h> #include <dlfcn.h> //cpsrレジスタの下位5bit目=>0:ARM,1:Thumbモード #define CPSR_T_MASK ( 1u << 5 ) 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); //対象プロセスの仮想プロセスメモリ空間をオープン char file[64]; sprintf(file, "/proc/%ld/mem", (long)pid); int fd = open(file, O_RDWR); //対象プロセスのレジスタの値を取得 struct pt_regs old_regs; struct pt_regs regs; ptrace(PTRACE_GETREGS, pid, 0, &old_regs); ptrace(PTRACE_GETREGS, pid, 0, ®s); //mmapで確保したアドレスにsoファイルのパスを書き込み unsigned long mmap_addr = strtoll(argv[3],NULL,0);; char * librarypath = "/data/local/tmp/libhello.so"; lseek(fd, mmap_addr, SEEK_SET); write(fd, librarypath, strlen(librarypath)); unsigned long dlopen_addr = strtoll(argv[2],NULL,0); //リンクレジスタ(関数の戻りアドレス)を0にすることで、対象プロセスはアドレスエラー後に中断され、制御がデバッグプロセスに戻る regs.ARM_lr = 0; //プログラムカウンタをdlopenのアドレスにする regs.ARM_pc = dlopen_addr; //mmapで確保したアドレスを引数1にセット regs.ARM_r0 = mmap_addr; //RTLD_NOWを引数2にセット regs.ARM_r1 = RTLD_NOW; //プログラムカウンタの1bit目でARMかThumbモードか判定 if (regs.ARM_pc & 1) { //Thumbモード regs.ARM_pc &= (~1u); regs.ARM_cpsr |= CPSR_T_MASK; } else { //ARMモード regs.ARM_cpsr &= ~CPSR_T_MASK; } //レジスタに値をセット ptrace(PTRACE_SETREGS,pid,0,®s); //プロセスを再開させる int status; ptrace(PTRACE_CONT, pid, NULL, NULL); printf("Continuing.\n"); waitpid(pid,&status,0); while (status != 0xb7f) { if (ptrace(PTRACE_CONT, pid, NULL, NULL) == -1) { return -1; } waitpid(pid,&status,0); } //レジスタの値を元に戻す ptrace(PTRACE_SETREGS,pid,0,&old_regs); //デタッチする ptrace(PTRACE_DETACH, pid, 0, 0); }
sleep.c
#include<stdio.h> #include<stdlib.h> #include<dlfcn.h> #include<sys/mman.h> int main() { void *libdl; printf("mypid is %d\n",getpid()); libdl=dlopen("libdl.so", RTLD_NOW); int dlopen_addr=dlsym(libdl,"dlopen"); printf("dlopen addres:%x\n",dlopen_addr); int addr = mmap(0,0x400,PROT_READ|PROT_WRITE|PROT_EXEC,MAP_ANONYMOUS|MAP_PRIVATE,0,0); printf("memory address:%x\n",addr); fflush(stdout); while(1) { printf("sleep now...\n"); sleep(5); } return 0; }
まとめ
例えば動的解析ツールであるFridaは最初に対象プロセスにsoインジェクションを行い、対象プロセスの各関数をフックします。
こういった解析ツールの原理を知ることで敵を知ることができ、セキュリティ施策に役立てることが可能であると思います。
弊社では地道な研究こそが対策の近道であると考えており、日々研究を行っています。
こうした記事が読者の皆様のセキュリティ施策に役立てば幸いです。
注意事項
本レポートに記載されている内容を許可されていないソフトウェアで行うと、場合によっては犯罪行為となる可能性があります。そのため、記事の内容を試す際には許可されたソフトウェアに対してのみ実施するようにしてください。
本レポートについて
お問い合せ
E-mail:ichise@ninjastars-net.com
株式会社Ninjastarsエンジニア
一瀬健二郎