Linux/Android マルチスレッド対応のデバッガの実装
株式会社Ninjastars
セキュリティエンジニア:一瀬健二郎
このブログでも紹介した『ptrace入門』等を基に、簡易的なデバッガやstraceの作成に挑戦した方も居られると思います。
今回は一歩進めてシングルスレッドを想定したものから、マルチスレッドに対応した簡易デバッガを作成します。
マルチスレッドに対応させることで、実際のアプリケーションなどに対してある程度実用的なものを作成することが可能になります。

今回やること
マルチスレッド対応の簡易デバッガを作ります。
※今回の内容はある程度ptraceについてやデバッガの基本的な原理が分かっている方向けです。
ptraceについては冒頭でも触れた『ptrace入門』という書籍で体系的にまとめられています。
【書評】ptrace入門 大山恵弘著 - 株式会社Ninjastars 技術研究部
導入
まず64bitのOS上で32bitをコンパイルするため32bit用のライブラリをインストール必要があります。
sudo apt-get install libc6-dev-i386
gcc -m32 -no-pie debugger.c -o debugger gcc -m32 -no-pie singledebugger.c -o singledebugger gcc -m32 -no-pie tracer.c -o tracer -lpthread
上記ファイルについての内容は以下です。
- debugger:マルチスレッドに対応した簡易的なデバッガ
- singledebugger:シングルスレッドのみ対応の簡易的なデバッガ
- tracer:簡易的なマルチスレッドプログラム
実験
シングルスレッドのみ対応、マルチスレッド対応のデバッガでtracerのfunc関数内のgetchar呼び出し直後にソフトウェアブレークポイントを設置します。
objdumpでtracerを逆アセンブルします。
objdump -d -M intel tracer

コンパイル環境によって異なりますが、上記の画像のような場合0x804858fにソフトウェアブレークポイントを設置します。
Linxu環境でのデバッガによるブレークポイント設置は下記の方の記事が参考になります。
th0x4c.github.io
また弊社ブログでもAndroid、ARMの内容ですがソフトウェアブレークポイントについて説明しています。
Android ARM バイナリ解析 入門 - 株式会社Ninjastars 技術研究部
シングルスレッドのみ対応のデバッガによる解析
まずtracerを実行しましょう。
./tracer
プロセスIDは
ps -A | grep tracer
コマンドで調べます。
例えばプロセスIDが11274だとすると以下で該当アドレスにソフトウェアブレークポイントを設置できます。
sudo ./singledebugger 11274 0x804858f
この状態でtracerでEnterを押してみましょう。

デバッガでアタッチしたスレッドとは別のスレッドでSIGTRAPが発生したことにより、例外を捕捉できずにプログラムが強制終了してしまいました。
このようにマルチスレッドプログラムをデバッグする場合、予め全スレッドにアタッチする必要があります。
マルチスレッド対応のデバッガによる解析
同じようにtracerを実行し、debuggerでブレークポイントを設置します。
debuggerの実行結果は以下のようになり、正常にデバッグできていることが分かります。

マルチスレッドに対応するにあたり実装面において今回重要な点は以下の三つです。
- 予め全スレッドにアタッチした(attach_all_thread)。
- アタッチ後はSIGSTOPシグナルを送り、プロセスを停止させた。
- waitpidの第一引数を-1に、第三引数を『__WALL』にした。
2.について
ptraceシステムコールでは対象プロセスが停止していないとメモリの読み書きは出来ません。
そのため全スレッドにアタッチ後SIGSTOPシグナルを送り、停止させています。
3.について
waitpidの第一引数を-1にすることにより、子プロセスのどれかが停止するまで待機することができます。
また第三引数を『__WALL』にすることでスレッドを含めて待つことができるようになります。
詳しくはManのページを参照ください。
linuxjm.osdn.jp
今回は簡易的なものの紹介にとどめましたが、基本的なことさえ分かればあとは手間と実装力の問題かと思われます。
デバッガ自作はOSやコンパイラ自作と同じように大変学びのある興味深い世界だと思われるので、挑戦する仲間が増えれば嬉しい限りです。
まとめ
インターネット上のデバッガ関連記事の多くがシングルスレッドを想定したものだったため、今回この記事を書かせていただきました。
私自身も常に試行錯誤しながらデバッガや低レイヤーの勉強を行っています。
このブログが同じように勉強されている方等に対して技術的な悩みの解決や、学ぶ楽しさを知る糸口となれば幸いです。
ソースコード
tracer.c
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <pthread.h> #include <unistd.h> #include <sys/types.h> void* func() { printf("please input key.\n"); getchar(); } int main() { pthread_t thread; void* ret; pthread_create(&thread,NULL,(void*)func,NULL); pthread_join(thread,&ret); return 0; }
debugger.c
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <asm/unistd.h> #include <dirent.h> #include <sys/ptrace.h> #include <sys/types.h> #include <sys/wait.h> #include <sys/user.h> int attach_all_thread(int pid) { char _taskdir[255]; DIR *taskdir; sprintf(_taskdir, "/proc/%d/task",pid); taskdir=opendir(_taskdir); if (taskdir) { struct dirent *d; d=readdir(taskdir); while (d) { int tid=atoi(d->d_name); int status; ptrace(PTRACE_ATTACH,tid,0,0); int id = waitpid(-1,&status,__WALL); ptrace(PTRACE_CONT,id,0,0); d=readdir(taskdir); } closedir(taskdir); } return 0; } int main(int argc, char *argv[]) { pid_t pid=atoi(argv[1]); unsigned long address=strtoll(argv[2],NULL,0); //対象プロセスの全スレッドにアタッチ attach_all_thread(pid); //対象プロセスを停止させる syscall(__NR_tkill, pid, SIGSTOP); int s; waitpid(pid,&s,0); int original_text; //対象アドレスのメモリの値を保存 original_text = ptrace(PTRACE_PEEKTEXT, pid, address, NULL); printf("OriginalText:%x\n",original_text); //{0xCC}を対象アドレスに書き込み int opcode = 0x000000CC; ptrace(PTRACE_POKETEXT, pid, address, ((original_text & 0xFFFFFF00) | opcode )); ptrace(PTRACE_CONT, pid, NULL, NULL); printf("Continuing.\n"); //プロセスを再開させる int status; while(1) { //ブレイクするまで待機する int tid = waitpid(-1, &status, __WALL); if (WIFEXITED(status)) { printf("Program exited normally.\n"); exit(0); } if (WIFSTOPPED(status)) { printf("Breakpoint.\n"); } else exit(1); int signum = WSTOPSIG(status); printf("SignalNumber:%d\n",signum); if(signum==SIGTRAP) { printf("SIGTRAP\n"); //レジスタの値を取得 struct user_regs_struct regs; ptrace(PTRACE_GETREGS, tid, 0, ®s); printf("EIP_RegisterValue:%x\n",regs.eip); //処理を書き戻す ptrace(PTRACE_POKETEXT, tid, address,original_text); //int3は既に実行されており、1命令分進んでいる。 //よって1命令分eipを減算する。 regs.eip--; ptrace(PTRACE_SETREGS,tid,0,®s); //ここでは再度ブレークポイントを設置するためSINGLESTEPさせている。 ptrace(PTRACE_SINGLESTEP,tid,0,0); waitpid(tid,&status,__WALL); ptrace(PTRACE_POKETEXT, tid, address, ((original_text & 0xFFFFFF00) | opcode )); //プロセスを再開する ptrace(PTRACE_CONT, tid, 0, 0); } else if(signum==19 ||signum==21) { //シグナル番号19と21は無視する。 ptrace(PTRACE_CONT, tid, 0, 0); } else { ptrace(PTRACE_CONT,tid,0,signum); } } }
singledebugger.c
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/ptrace.h> #include <sys/types.h> #include <sys/wait.h> #include <sys/user.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); printf("OriginalText:%x\n",original_text); //{0xCC}を対象アドレスに書き込み int opcode = 0x000000CC; ptrace(PTRACE_POKETEXT, pid, address, ((original_text & 0xFFFFFF00) | 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 user_regs_struct regs; ptrace(PTRACE_GETREGS, pid, 0, ®s); printf("EIP_RegisterValue:%x\n",regs.eip); //処理を書き戻す ptrace(PTRACE_POKETEXT, pid, address,original_text); regs.eip--; ptrace(PTRACE_SETREGS,pid,0,®s); //プロセスからデタッチする。処理が動き出す。 ptrace(PTRACE_DETACH, pid, 0, 0); }
注意事項
本レポートに記載されている内容を許可されていないソフトウェアで行うと、場合によっては犯罪行為となる可能性があります。そのため、記事の内容を試す際には許可されたソフトウェアに対してのみ実施するようにしてください。
本レポートについて
お問い合せ
E-mail:ichise@ninjastars-net.com
株式会社Ninjastarsエンジニア
一瀬健二郎
Windows int3アンチデバッグの原理
株式会社Ninjastars
セキュリティエンジニア:一瀬健二郎
今回はWindowsでのアンチデバッグとして入門的な手法であるint3によるデバッガ検出についてお話しさせていただきます。
実際に本質的な部分だけ取り出した原始的なデバッガを作成し、何故int3でデバッガを検出できるのか解説いたします。
アンチデバッグとしての実効性はほぼありませんが、デバッガの原理の理解の手助けとなれば幸いです。

環境
Windows10
Visual Studio C++開発環境
今回やること
int3アンチデバッグを実装したプログラムを簡易デバッガでアタッチして何故検出されるのか考えます。
導入
まずint3.cppとdebugger.cppをVisualStudioでビルドしてください。
プログラムの説明
int3.exe
1.起動すると自身のプロセスIDを出力。
2.入力待機
3.デバッグされていれば「detect!」、されていなければ「not detect!」を出力する。
debugger.exe
1.入力したプロセスIDのプロセスにアタッチ。
2.デバッグイベントが発生するまで待機。以下ループ。
3a例外が発生したら例外番号とプロセスID、スレッドIDを出力して実行を再開。この際例外は対象プロセスに渡さない。
3b.発生したデバッグイベントが例外でなかったら、対象プロセスにそのまま渡す。
実験
(1)検知される場合
1.int3.exeとdebugger.exeを起動して出力されたint3.exeに出力されたプロセスIDをdebugger.exeに入力してください。
2.int3.exeでEnterを押すと「detect!」と出力されデバッガが検知されます。
(2)検知されない場合
42行目を以下のように書き換え。(1から2へ) 1.ContinueDebugEvent(debug_event.dwProcessId, debug_event.dwThreadId, DBG_CONTINUE); 2.ContinueDebugEvent(debug_event.dwProcessId, debug_event.dwThreadId, DBG_EXCEPTION_NOT_HANDLED);
debugger.cppを上記のように再度書き換えビルドし、再度1と同じことをしてください。
今度は「not detect!」と出力されると思います。
原理
デバッガはソフトウェアブレークポイントを設置するとき、x86・x64アーキテクチャの場合int3(0xCC)をメモリの該当アドレスに書き込んで設置します。
当該プログラムでint3に処理が移ったとき、例外が発生し処理がデバッガに移ります。
デバッガはレジスタやメモリの値を取得するなどした後、ContinueDebugEventの第三引数をDBG_CONTINUEにして実行を再開します。
これは捕捉した例外を対象プロセスに渡さずに実行を再開するというものです。
これを逆手にとってint3アンチデバッグでは例外が発生する前提でプログラムを実装しておき、本来発生するはずの例外が発生しなかったら検知するというものです。
ただしデバッガ自身が設置したソフトウェアブレークポイントのアドレスを保存しておき、自身が設置したものか否かを判断して実験のようにContinueDebugEventに渡す引数を変えれば簡単に回避可能です。
まとめ
デバッガは例外を捕捉してそのまま渡したり無かったことにしたりするなど、イメージ的には通信をインターセプトするプロキシツールのようなことが出来ます。
またブレークポイントを設置してのレジスタの取得・操作やプロセスメモリの取得・書き換えなど大変便利なことが可能です。
勿論これはそのままアプリケーションやゲームに対する悪意ある攻撃にも利用可能です。
弊社ではデバッガの原理を理解し解析や診断に役立てるとともに、悪意ある攻撃に対する対策を施すためにもこういった勉強は継続的に行っています。
読者の皆様に置かれましても、今回の記事がそうしたことに少しでも役立てば幸いです。
ソースコード
int3.cpp
#include <windows.h> #include <iostream> using namespace std; int main() { //自身のプロセスIDを出力 cout << "PID:" << GetCurrentProcessId() << endl; getchar(); int isDebug = 1; __try { __asm int 3 } __except(EXCEPTION_EXECUTE_HANDLER) { isDebug = 0; } if (isDebug) { cout << "detect!" << endl; } else { cout << "not detect!" << endl; } getchar(); return 0; }
debugger.cpp
#include <iostream> #include <windows.h> using namespace std; int main() { DWORD pid; cout << "Please input PID." << endl; cin >> pid; //対象プロセスにアタッチする。 HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, false, pid); if (hProcess == NULL) { cout << "OpenProcess Error" << endl; ExitProcess(-1); return -1; } if (!DebugActiveProcess(pid)) { cout << "DebugActiveProcess Error" << endl; ExitProcess(-1); return -1; } cout << "PID:" << pid << endl; cout << "HANDLE:" << hProcess << endl; DEBUG_EVENT debug_event; while (true) { //デバッグイベントが発生するまで待機。 if (WaitForDebugEvent(&debug_event, INFINITE)) { if (debug_event.dwDebugEventCode == EXCEPTION_DEBUG_EVENT) { cout << "DebugEventCode:" << debug_event.dwDebugEventCode << endl; cout << "pid:" << debug_event.dwProcessId << endl; cout << "tid:" << debug_event.dwThreadId << endl; //例外をなかったことにして実行を再開。 ContinueDebugEvent(debug_event.dwProcessId, debug_event.dwThreadId, DBG_CONTINUE); } else { //デバッグイベントをそのまま渡して実行を再開。 ContinueDebugEvent(debug_event.dwProcessId, debug_event.dwThreadId, DBG_EXCEPTION_NOT_HANDLED); } } } return 0; }
注意事項
本レポートに記載されている内容を許可されていないソフトウェアで行うと、場合によっては犯罪行為となる可能性があります。そのため、記事の内容を試す際には許可されたソフトウェアに対してのみ実施するようにしてください。
本レポートについて
お問い合せ
E-mail:ichise@ninjastars-net.com
株式会社Ninjastarsエンジニア
一瀬健二郎
【書評】ptrace入門 大山恵弘著
株式会社Ninjastars
セキュリティエンジニア:一瀬健二郎
今回はAmazonでKindle書籍として販売されている『ptrace入門』という本をご紹介いたします。
Linuxにおけるptraceシステムコールについて体系的に記述されており、環境もUbuntu18.04 64bit向けとほぼ最新の情報です。
値段は改定される可能性がありますが、現在100円と非常にお得になっています。

この本について
電気通信大学情報工学科の授業で著者が学生向けに配布していたものを、現在の状況に合わせて加筆修正したもの。
ptraceシステムコールの入門書であり、読み物というより教科書に近い。
著者は筑波大学システム情報系准教授の大山恵弘先生。
読んでみた感想
各章ごとの課題で簡易的なシステムコールトレーサ、システムコールサンドボックス、デバッガなどを作成していきます。
順を追った無駄のない適切な課題設定で理解しやすく非常に勉強になります。
デバッガの利用法ではなく実装にまで踏み込んでおり、尚且つ情報がほぼ最新というのは非常に貴重だと思います。
注意点としては
・ソースコードが全て掲載されている訳ではない
・課題に対する解答がない
上記から一人で読み進めていくにはある程度以上のC言語・低レイヤーの知識と根気が必要になると思われます。
総合的には内容も非常に良く値段も100円とお財布に優しいので、ptraceやデバッガの教科書としてとても良いと思いました。
まとめ
私自身知らないことだらけであり、日々勉強の毎日です。
丁度ptraceやデバッガ作成についての教科書的なものを探していたので、この書籍の内容が非常に参考になりました。
同じようにこのブログを始めとした弊社の情報発信が、読者の皆様にとって何か有益なものとなれば幸いです。
注意事項
本レポートに記載されている内容を許可されていないソフトウェアで行うと、場合によっては犯罪行為となる可能性があります。そのため、記事の内容を試す際には許可されたソフトウェアに対してのみ実施するようにしてください。
本レポートについて
お問い合せ
E-mail:ichise@ninjastars-net.com
株式会社Ninjastarsエンジニア
一瀬健二郎
Android soインジェクションについて
株式会社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エンジニア
一瀬健二郎
Android ARM バイナリ解析 入門
株式会社Ninjastars
セキュリティエンジニア:一瀬健二郎
今回はAndroidのARMアーキテクチャのバイナリ解析を行います。
Android/iOS共にARMバイナリであることが多く、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でのセットアップ例:
- 右上のAVDManagerをクリック
- Create Virtual Deviceをクリック
- Pixel2を選択
- Other Images=>一番上の"Nougat APILevel25 armeabi-v7a"の項目のDownloadをクリック
- 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
実行すると以下のようにkeyの入力を求められ、間違っていると終了してしまいます。
静的解析
ARMアーキテクチャバイナリなので、IDAのFree版はサポートしていません。
今回はGhidraで解析を行ってみましょう。

上記を見ると、
(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」
のアドレスです。
- やること
- PTRACE_ATTACHでターゲットのプロセスにアタッチ。
- PTRACE_PEEKTEXTで対象アドレスの値を保存。
- PTRACE_POKETEXT で対象アドレスの命令を {0x01, 0xde}に書換える。
- PTRACE_CONT でプロセスを再開させる。
- waitpid(2) でブレイクするのを待つ。
- プロセスが (3) で書換えたアドレスでブレイクする。
- PTRACE_GETREGSでR0レジスタの値を取得して出力する。
- PTRACE_DETACH でプロセスからデタッチする。
まずアタッチするためにcrkmeのプロセスidが必要です。コマンドは以下です。
ps | grep crkme
次にcrkmeの仮想メモリ空間のメモリマップを調べる方法は以下です。
cat /proc/crkmeのpid/maps | grep crkme

上記のような結果の場合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の開催日)

上記を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, ®s); 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エンジニア
一瀬健二郎
ゲームセキュリティ勉強会「Defense against Game Hack ~Cheat Society~」開催報告
株式会社Ninjastars
セキュリティエンジニア:吉村碧海
2019年9月17日(火)に株式会社Ninjastars主催のゲームセキュリティ勉強会
「Defense against Game Hack ~Cheat Society~」を開催いたしました。
約50名の方にご参加いただきました!
改めて沢山のご来場、誠に有難うございます。
おかげさまで多くの来場者を迎え、
好評のうち無事終えることができました。
今後もCTF大会や勉強会など開催予定ですので、是非ご参加ください。
勉強会の内容

会場の風景

会場の様子
注意事項
本ブログに記載されている内容を許可されていないソフトウェアで行うと、場合によっては犯罪行為となる可能性があります。そのため、記事の内容を試す際には許可されたソフトウェアに対してのみ実施するようにしてください。
本レポートについて
お問い合せ
E-mail yoshimura.aoi@ninjastars-net.com
株式会社Ninjastarsエンジニア
吉村 碧海
ホワイトハッカー入門 ~CTFでも使えるReversingの基礎~
株式会社Ninjastars
セキュリティエンジニア:吉村碧海
CTFのpwnやreversing問題ではIDAやGhidra等の解析ツールで実行バイナリを逆アセンブルし、プログラムの内部動作を解析する技術(リバースエンジニアリング技術)が求められます。
今回は皆様にリバースエンジニアリング技術への理解を深めてもらうために、簡単なソースコードとそれをコンパイルした実行バイナリを逆アセンブルし、その際の結果と機械的な動作を解説していきたいと思います。
解説の前に少しだけ自己紹介をさせていただきます。
私はNinjastarsにてリバースエンジニアリングやプログラミングを学びながらお仕事をさせて頂いております。
今回初めてブログ投稿の任を任されました。
まだまだ若輩者ですが、私の解説を是非御一読頂ければ幸いでございます。

今回使用するソフト
IDA free
gdb-peda
今回は途中からgdbの拡張スクリプトであるgdb-pedaを使ってデバッグしていきます。
もしまだgdb-pedaをインストールされていない方は以下のリンクをご参照下さい。
gdb-peda インストール for Ubuntu - miyagawNote
まず最初に簡単なソース文を用意したのでご覧ください。
#include<stdio.h> int main() { int x=12345; int y; printf("please input password.\n"); scanf("%d",&y); if(x==y) printf("correct! flag is Ninjastars\n"); else printf("wrong...\n"); return 0; }
はい、ビックリする程簡単ですね。
下記コマンドでコンパイルを行ってください。
gcc -m32 -no-pie -o sample sample.c
gccのコンパイルオプションについては当ブログで解説を行っております。
自作ゲーム:チートチャレンジ2 - 株式会社Ninjastars 技術研究部
コンパイルに成功したら、ターミナルで実行してみます。
パスワードの入力が求められたので、先程のソースから[12345]という数字を入力すればcorrect...つまり問題に正解してフラグをゲットできることが読み取れるのでここはその数字を与えてみましょう。

はい、無事に正解することができました。
今回はソース文を予め知っていたのでパスワード入力をパスすることができたのですが、実際の環境では基本的に実行プログラムだけ渡されるので、それを逆アセンブルして解析する必要があります。
なので今回も静的解析ツールIDAで解析をかけていきましょう!
IDAで解析をかけた結果がこちらになります。

一般的にフラグの文字列は暗号化等の処理を加えて隠されている場合が多いのですが、今回の問題は解説の簡略化のため読み取れる形で表示されています。
ソースを一見しただけでは簡単な処理に見えましたが、実際の環境の中では様々な動作をしていることがわかると思います。
そして今回の問題のキー部分となるのが[cmp [ebp+var_10], eax]の結果により処理が2つに分岐している部分です。

片方はwrong...と表示されフラグが表示されていませんが、もう片方は"flag is Ninjastars"という文字列が表示されています。
[cmp [ebp+var_10], eax]ではscanfの直前で定義されたeaxと、その前の
[mov [ebp+var_10], 3039h]にて代入された3039hという値を比較しています。

ちなみに3039hは16進数表記の値です。10進数に直すと[12345]という値になっているので、ソースを知らずともIDAだけで問題を解決することができます!
このように逆アセンブルして解析をし、それによって得たパスワードを入力して問題を解く方法を皆様も理解できたかと思います。
しかし、それだけではボリュームとして些か物足りないと思われるので今回はもう一つ、解法をご紹介したいと思います。
それは12345の数字を用いずにフラグを入手できる方法です。
ここからは動的解析ツールgdb-pedaを使っていきたいと思います。
まずターミナル上でgdbで実行ファイルを開いてみましょう。
その後[r]コマンドでgdb上で一度プログラムを実行してみます。

パスワードが求められました。ここはあえて[12345]以外の適当なパスワードを打ち込んでみましょう。

はい、wrong...と表示され不正解になってしまいました。
先にIDAで解析した通り、[12345]を入力しなければ、間違った分岐に進んでしまいます。
しかし、gdbではプログラムの実行時の途中で一時停止することが可能です。
今回はwrong!の分岐に飛ぶ直前で一時停止をしたいので、その部分のアドレスを先程のIDAの解析結果から探してみましょう。

直前部分[jnz short loc_8048549]の部分のアドレスはIDAでは[08048533]と表示されていますね。
gdbのコマンド[b *(アドレスの値)]で一時停止部分、ブレークポイントを設けることができます。
アドレスは16進数表記なので0x08048533、というわけで今回は[b *0x08048533]のコマンドを使用します。
再びgdbの画面に戻り、[b *0x08048533]でブレークポイントを作成します。
そうしたら[r]コマンドと適当なパスワード[11111]でその部分まで実行をしてみましょう。

一時停止部分で止まりました。
今回はcmp命令の演算結果による分岐先を変更したいので、CPUのステータスレジスタの一種であるフラグレジスタを変更していきます。
一時停止中に[i r]のコマンドでレジスタの中身を詳しく表示することができます。
その中に eflags 0x206 [ PF IF ] とあります。
一見しただけでは何を意味しているかわからないと思うので参考サイトをご用意しました。
このサイトを丸ごと解説して皆様に更なる理解を深めて頂くのも結構なのですが、それではこの記事の文字数と私の記述時間がエライことになりそうなので、フラグレジスタの説明部分だけかいつまんで解説させていただきます...

画像の表を見て参照をしていただくと、レジスタの中身 0x206 [ PF IF ] とはPF : パリティフラグと IF : 割り込み可能フラグがセットされていることがわかります。
cmp命令は値が等しい、つまり比較して0であればいいので、フラグレジスタの中身にZF : ゼロフラグを追加しましょう。
[set $eflags=(フラグレジスタの値)]のコマンドでフラグレジスタの値を変更することができます。
ZFは6ビット目のフラグなので2^6=64、16進数に変換して0x40。
この値を元の値[0x206]に足した[0x246]がセットするべきフラグの値であることがわかります!
というわけで[set $eflags=0x246]で一時停止部分でのフラグレジスタに書き込んで、[c]のコンテニューコマンドでプログラムの続行をしてみましょう。

flagが表示され問題に正解することができました!
このようにプログラムの分岐の結果自体を書き換えてパスワードを知らずともflagを入手する方法もあります。
CTFの問題等でも多種多様なアプローチと解答例があるので皆様も色々な知識を深めていただければ幸いでございます。
拙い文章ではございましたが、御一読ありがとうございました!
注意事項
本レポートに記載されている内容を許可されていないソフトウェアで行うと、場合によっては犯罪行為となる可能性があります。そのため、記事の内容を試す際には許可されたソフトウェアに対してのみ実施するようにしてください。
本レポートについて
お問い合せ
E-mail yoshimura.aoi@ninjastars-net.com
株式会社Ninjastarsエンジニア
吉村 碧海
イベント開催のお知らせ
弊社のゲームセキュリティイベント開催のお知らせをさせていただきます。
この度、9月17日(火) 19:00-22:00から
株式会社Ninjastars主催、第2回のゲームセキュリティイベント
「Defense against Game Hack Cheater's Society」
を弊社オフィスにて開催いたします。
今回はCheater's Societyという題材で
RMT業者やチート代行サービスをはじめとする
チーター側の社会の実情に焦点を当てたセミナーとなります。
参加申し込みはこちらから受け付けております。
https://forms.gle/RbKnmH2FYW6LA8eQ8
皆様のご参加を心よりお待ちしております。