【勉強メモ】たのしいバイナリの歩き方(愛甲健二)
2013年発行の本。面白いと人から勧められたけど、自分にはハードルが高く、随分とほったらかしになってしまった。
最近になって読破しようと読み返してみたが、けっこう大変だった。全体として説明が雑というか不親切で、バッファーオーバーフローのみ図を使って説明しているが、その図も分かりにくい(もう少し手を加えれば分かりやすいのに)。DLLインジェクションやAPIフックのところでは図もないし説明も短い。「分かっている人」なら分かるんだろうけど、それが分かっているなら本を読む必要なんか無いわけで…。ネットで逐一調べなければ読み切れなかった。人を選ぶ本。
【 良かったところ】
- バッファーオーバーフロー、自作デバッガ、DLLインジェクション、APIフック等、扱っているテーマが面白い。
- 自分で手を動かして試すことができる。
【 不満なところ】
- 説明が不親切。高度になるほど説明が減る。
- ソースコードの背景が黒(書込みができない。誤字脱字も訂正しにくい)
- 誤字脱字が多い(自分のは第2版だが、今は修正されてるのか?)
- 不要にC++が使われている(自分がC++知らないからだけど、ほぼC言語で十分じゃない?)
第1章 リバースエンジニアリングでバイナリの読み方を身に付ける (p.13-)
- Stirling、Process Monitor、Wireshark、IDA、Ollydbg等の簡単なツールと、簡単な表層・動的・静的解析の紹介。アセンブリ言語をざっくり読んでみるというところまでやらせている。第1章にしてはハードル高い。
- アセンブラからC言語のソースコードをイメージできるか。
- WindowsAPIや関数にブレイクポイントをセットする。
第2章 シューティングゲームをチートから守るには (p.71-)
- うさみみハリケーン(汎用プロセスメモリエディタ。プロセス、スレッドからレジスタを表示・編集できる)を使って、サンプルのシューティングゲームの得点(を保持しているレジスタ)をいじる。
- Just-In-Timeデバッグ(Windowsがエラー終了した際に自動的にデバッガが起動してプロセスをアタッチする)によるクラッシュダンプの解析
アンチデバッグ手法の紹介
- IsDebuggerPresent関数、CheckRemoteDebuggerPresent関数によるデバッガ検知
- popfとSINGLE_STEP例外を利用したデバッガ検知(後で調べる)
- int 2dhを利用したデバッガ検知(後で調べる)
難読化
ジャンクコードを追加することでIDA等のコード解釈を実際とは異なるように見せる。例えば、下記では「EB」を追加することで、動作内容は変わらないが、call命令を隠せてしまう。
00401000 FF 15 00 20 40 00 call ds:__imp__IsDebuggerPresent@0 00401006 85 C0 test eax, eax 00401008 74 17 jz short loc_401021 ↓「JMP(0xEB)」を追加され、命令の区切りが変わり、違うプログラムのように見せる 00401000 EB FF jmp short near ptr main+1 00401002 15 00 20 40 00 adc eax, offset __imp__IsDebuggerPresent@0 00401006 85 C0 test eax, eax 00401008 74 17 jz short loc_401021
"EB"は「jmp rel8」。いわゆるshort jump。次にEIPが指すアドレスから、8bit(1Byte)が示す相対オフセット分ジャンプする。
8bitのオペランド部は、符号付きとして読み込まれる。符号付き整数の読み取りが2の補数表現によると考えれば、FFh=-1 となる。
「00401000 EB FF」を読み取った後、EIPが2byte分(EB FF)進んで「00401002」の値になり、その位置から-1することで「00401001」に飛ぶ。
「00401001」→「FF 15 00 20 40 00 call ~」が解釈され、EBを追加する前と同じ動作になる。
パック(pack)
実行ファイル内のコードやデータを圧縮し、それを展開するコードを先頭に付加して新たな実行ファイルとする。「UPX」や「ASPack」が有名(というか有名すぎて簡単にアンパックされるのでマルウェアにはほぼ使われない)
手動アンパック
Ollydbg(Ollydump)を使って、UPXを手動アンパック。
手動アンパックは「展開ルーチンが終わる瞬間(箇所)を見つける作業」とも言える。
第3章 ソフトウェアの脆弱性はこうして攻撃される (p.125-)
一般ユーザが所有者権限でプログラムを実行する仕組み(SETUID)
Linux系OSのパスワードは通常 /etc/master.passwd や /etc/passwd、/etc/shadow などに保存されており、root権限を持たなければ書き換えできない。しかしrootでなければパスワード変更できないのでは困るので、一般ユーザにも一時的に所有者権限を付与する仕組みが必要になる。setuid は実行ユーザではなくプログラム所有者権限でプログラムを実行させる仕組み(setgid もある)。setuid は呼出元のプロセスの実効ユーザID(Effective UID)を設定する(ただしroot権限が必要)。 setuidのついたプログラムに脆弱性があると、一般ユーザからroot権限を奪われる可能性が出てくる。
どのようにして権限は奪われるのか(バッファーオーバーフローによるシェル奪取)
プログラム上で関数をCALLし、EIP(プログラムの読取位置)が関数に飛ぶ際、関数終了後に元の処理位置に戻るために、戻り先アドレス(Return Address)をスタックに積んでいる。関数でどんな処理が行われようと、最終的にはスタックから戻り先アドレスを取り出し、元の処理位置にEIPを戻す。そこで、その戻り先アドレスを書き換えることができれば、自分が用意したプログラム(コード)を読み取らせることもできるわけだ。
関数上のローカル変数や配列にもスタックが使われている。C言語のstrcpy関数には、書込み先バッファのサイズを超えてもコピーをしてしまう脆弱性があるので、これを利用して同じスタック上にある戻り先アドレスを上書きしてしまえば良い。これがバッファオーバーフロー。コピーしたバッファに/bin/shを起動するコードを仕込んでおいて、飛ばし先をバッファ先頭に向ければ、シェルを起動させられる。さらに本書のターゲットプログラムにはSETUIDでroot権限が割り当てられているので、root権限のシェルが取れる。
CPU、OS、コンパイラバージョン等によって、上図の通りにはならない。ソースコード内で宣言していなくとも、buf[64]と呼出元EBPの間にさらにスタックが確保されている(セキュリティ的要素含む)場合があるので、このexploit.pyのままでうまく行くとは限らない。そもそもbuf[64]をスタック上に確保する際、隙間なく確保するというルールは特にないのだ。
【防御】StackGuard ~ スタック破壊を検知
- コンパイル時に、各関数の入口と出口にスタック破壊を検知するマシン語を挿入する(コンパイラ機能)
- スタックの戻り先アドレスと呼出元EBPの上に、ランダムな値(カナリー)を積んでおき、関数から抜ける(RET)前にその値が上書きされていないことを調べる。典型的なスタックオーバーフロー対策
- cl.exeなら「/GS-」、gccなら「-fno-stack-protector」を付けると無効化できる。
- Stack Canaryと呼ばれることも。
【防御】Exec-Shield ~ コード領域以外に実行権限持たせない
- メモリ領域の読み書き実行権限を制限することで、スタック領域に置かれたshellcodeの実行を防止。
- 「cat /proc/
/maps 」で、そのプロセスが使う各メモリ領域の権限を確認できる。 - DEP(Data Execution Prevention)と呼ばれることも。
- 「execstack」コマンドで、プログラムに対して設定できる
$ execstack -q sample3 # フラグ調査。"-"でスタック実行不可、"X"でスタック実行可能 $ execstack -s sample3 # Exec-Shieldをセットする $ execstack -c sample3 # Exec-Shieldをクリア(無効化)する
- gccでは「-z execstack」又は「-z noexecstack」オプション
$ gcc -z execstack sample3.c -o sample3 # Exec-Shield無効 $ gcc -z noexecstack sample3.c -o sample3 # Exec-Shield有効
【防御】ASLR ~ アドレスをランダムに配置
- ASLR: Address Space Layout Randomization - スタック、各モジュール、動的メモリ領域などのアドレス(配置先)をランダムに決定する仕組み。 - shellcodeもどこに配置されるのか分からないので、絶対アドレスを指定してEIPを飛ばすことができない。 - Linuxでは、以下のいずれかで設定可能。 - sysctl -w kernel.randomize_va_space=[0, 1, 2] (0:無効、1:ヒープ以外ランダム、2:全部ランダム) - /etc/sysctl.conf内で kernel.randomize_va_space=[0, 1, 2] - echo [0, 1, 2] > /proc/sys/kernel/randomize_va_space
【回避】Return-into-libc ~ libcの中にある関数を利用して攻撃
- Exec-Shieldの攻略法として発案。 -libc.soはUnix系のほとんどのプログラムで実行時にロードされるか、コンパイル時に静的リンクされる。
- Window系であれば、ntdll.dll や kernel32.dllとか。
- 任意のコードを実行できなくとも、最終的に任意のプログラムを実行できれば権限を奪える
- libc.so内のsystem関数、exec系関数へジャンプさせれば、/bin/sh等を実行できる
第4章 処理を自在に実行させるプログラミングのテクニック (p.175-)
デバッガを自作して動作を理解する(Windows)
デバッガの基本的な動作
- デバッギーをCreateProcessで起動。ただし、CREATE_SUSPENDED(サスペンド状態)で。 また、DEBUG_PROCESSフラグを指定すると、呼出側プロセスをデバッガ、作成プロセスをデバッギーとして扱う。
- ResumeThreadでサスペンド状態を解消し、プロセスを開始する。
- WaitForDebugEventで、デバッグイベントが発生するのを待ち、DEBUG_EVENT構造体に情報を格納する。デバッグイベントは、デバッギープロセスで発生するイベントで下表の9通り。
- デバッグイベントメッセージをメッセージループで捕まえて処理。 デバッガ処理中は、デバッギーの動作は停止している。デバッギーを再開するには、ContinueDebugEventで再開する。
イベント | 説明 |
---|---|
CREATE_PROCESS_DEBUG_EVENT | 新デバッギープロセスが作成又はデバッガにアタッチ |
EXIT_PROCESS_DEBUG_EVENT | デバッギーが終了 |
CREATE_THREAD_DEBUG_EVENT | デバッギー内で新スレッドが作成 |
EXIT_THREAD_DEBUG_EVENT | デバッギー内のスレッドが終了 |
LOAD_DLL_DEBUG_EVENT:デバッギー内でDLLをロード | |
UNLOAD_DLL_DEBUG_EVENT | デバッギー内でDLLがアンロード |
EXCEPTION_DEBUG_EVENT | デバッギー内で例外が発生 |
OUTPUT_DEBUG_STRING_EVENT | デバッギー内でOutputDebugString関数を呼出 |
RIP_EVENT | システムデバッグエラーが発生 |
逆アセンブラを実装する(udis86を利用)
逆アセンブラでは、他プロセスのメモリ空間等にアクセスする必要がある。Windowsではデバッガでアタッチしているかどうかに関わらず、プロセスのハンドルさえ得られれば他のプロセスメモリ空間を読み書きできる(ユーザに権限は必要)。ここでは、例外発生時(EXCEPTION_DEBUG_EVENT発生時)に逆アセンブルを行っている。
- OpenProcessでデバッギーのプロセスハンドルを取得する(権限が無ければ失敗する)。
- ReadProcessMemoryでデバッギーのプロセスメモリを読み込み、マシン命令を取得。マシン語を編集するならば、WriteProcessMemoryを使用。
- OpenThreadでスレッドハンドルを取得、GetThreadContextでコンテキスト(レジスタ値等)を取得。レジスタ変更を加えるならば、SetThreadContextで実行する。
他プロセス内で任意のコードを実行させる ~ コードインジェクション
他プロセス内で任意のコードを実行させる手法を総称して「コードインジェクション」という。特にDLLを使う場合を「DLLインジェクション」という。 - DLLインジェクション参考:「Three Ways to Inject Your Code into Another Process」
フックとは(本書内の説明が不足しているので補足)
Windowsではウィンドウに対して何らかのメッセージ(イベント)が発生すると、
- GetMessage()でメッセージキューからメッセージを取得し、MSG構造体へ格納
- TranslateMessage()で仮想キーメッセージを文字メッセージに変換し、再度キューへ投げる(必須ではない)。
- TranslateMessage()に該当しなかったメッセージをDispatchMessage()がウィンドウプロシージャへ送信する。
- ウィンドウプロシージャがメッセージに対応した処理を実施。特段の処理をしないものはDefWindowProc()でシステムに処理を任せる。
- アプリ終了時は、PostQuitMessage()でWM_QUITメッセージを送り、終了する。
という流れで処理を行う。このメッセージの流れにおいて、プログラミングできる部分はわずか(メッセージループとウィンドウプロシージャを組むだけ)であり、それ以外の部分には手を出すことができない。組み込みコントロール(ボタンとか)はそもそもプロシージャを内部に持つのでいじれない。例えば「特定のキー入力やマウスクリックを無効化する」ときにはどうすればよいか。
この問題に対して、異なるアプローチとして存在するのがフック(HOOK、引っかける、横取り)である。メッセージがウィンドウプロシージャに渡る前に、メッセージを引っかけて拾い上げてしまうイメージ。フックしたメッセージはウィンドウプロシージャならぬフックプロシージャに渡されて処理される。
ローカルフック
自身のスレッドのみに働くフック。それほど使い道は無いが、モードレスダイアログをモーダルダイアログに見せかけたり、手当たり次第に取得したい場合に使用。
グローバルフック(システムフック)
全てのスレッドにセットされて働く。つまり、全てのウィンドウの全てのメッセージを横取り可能。ユーティリティアプリに適任だが、危険度が高く、バグがシステム全体に影響を与えてしまう。ローカルフックと基本的な手順はほぼ同じだが、システムフックはDLLでなければならない。
主なフックタイプ
SetWindowsHookExでフックを設定する際にフックする場所や対象を定めるフラグ
フックタイプ | フックするメッセージ | 特徴 |
---|---|---|
WH_CALLWNDPROC | 全メッセージ | ウィンドウプロシージャが呼ばれる直前を取得 |
WH_CALLWNDPROCRET | 全メッセージ | ウィンドウプロシージャが呼ばれた後に取得 |
WH_CBT | ウィンドウが作られたりアクティブになったり | ウィンドウの作成・消滅等を監視 |
WH_DEBUG | フックプロシージャ次第 | 他のフックプロシージャを監視して、フックをデバッグ |
WH_GETMESSAGE | 全メッセージ | GetMessage()で取得されたメッセージをフック |
WH_JOURNALPLAYBACK | 全メッセージ | キー入力等の操作を任意の操作に置換 |
WH_JOURNALRECORD | 全メッセージ | キー入力等の操作を監視・記録 |
WH_KEYBOARD | キーボード入力 | GetMessage()で取得したキー入力をフック |
WH_MOUSE | マウス操作 | GetMessage()が取得したマウスメッセージを横取り |
WH_MSGFILTER | 全メッセージ | メニュー操作等の特別なメッセージを取得 |
WH_SHELL | システム関係 | システムやシェルに関わる情報を取得 |
WH_SYSMSGFILTER | メニューなど特定のメッセージ | WH_MSGFILTERのシステムフック専用版 |
フックプロシージャ
横取りしたメッセージを受け取って処理するための関数。通常のメッセージループのウィンドウプロシージャに相当。なので、ウィンドウプロシージャのような書式にする必要がある。また、フックタイプによって引数の解釈が異なる。 フックプロシージャは、特定のフックタイプに関連するフックタイプに関連するフックチェーンの中にセットされる。つまり、フックチェーンとはフックプロシージャのリストであり、現在のフックプロシージャは次のフックプロシージャをCallNextHookExで呼び出す義務がある。
SetWindowsHookEx でOSのシステムメッセージをフックする
OSのシステムメッセージをフック(横取り)するには以下の三つのAPIを使う。
// フックをセットする。 HHOOK SetWindowsHookEx( // フックハンドルを返す int idHook, // フックタイプ指定。フック箇所とフック対象を定める HOOKPROC lpfn, // フックプロシージャへのポインタ。横取りしたメッセージを受け取る HINSTANCE hMod, // フックプロシージャを実装するDLLのハンドル(システムフックの場合) DWORD dwThreadId // フック対象のスレッドID。0なら全スレッド対象 ); // 同一フックタイプの次のフックプロシージャを呼び出す。 LRESULT CallNextHookEx( HHOOK hhk, // 現在のフックハンドル(XP以降は無視される?) int nCode, // 現在のフックプロシージャに渡されたフックコード WPARAM wParam, LPARAM lParam ); // SetWindowsHookExでセットしたフックを外す。 BOOL UnhookWindowsHookEx( HHOOK hhk // 削除対象のフックハンドル );
サンプルコード
書籍ではC++だけど、必要性を感じなかったのでC言語版に変更。元コードではフックしても何もしていないから、フック時にもログを書き出してみた(大量に出た)。
logging2.c
// DLL作成をソースファイル一つで作成。さらにフック時にもログに書き出してみた #include <windows.h> #ifdef LOGING_EXPORTS #define LOGING_API __declspec(dllexport) // extern "C" はC++のときのみ #else #define LOGING_API __declspec(dllimport) // extern "C" はC++のときのみ #endif // DLLのハンドル用のグローバル変数 HINSTANCE dll_handle; // フックハンドル用のグローバル変数 HHOOK g_hhook = NULL; int WriteLog(TCHAR *szData){ TCHAR szTempPath[1024]; GetTempPath(sizeof(szTempPath), szTempPath); lstrcat(szTempPath, "loging.log"); // %TEMP%フォルダにloging.logを書き出す TCHAR szModuleName[1024]; GetModuleFileName(GetModuleHandle(NULL), szModuleName, sizeof(szModuleName)); TCHAR szHead[1024]; wsprintf(szHead, "[PID+%d][Module:%s] ", GetCurrentProcessId(), szModuleName); // %TEMP%loging.logを書込みモードで開く HANDLE hFile = CreateFile(szTempPath, GENERIC_WRITE, 0, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); if(hFile == INVALID_HANDLE_VALUE){ return -1; } // 開かれているファイルのファイルポインタを移動する SetFilePointer(hFile, 0, NULL, FILE_END); // loging.logへ書き出し DWORD dwWriteSize; WriteFile(hFile, szHead, lstrlen(szHead), &dwWriteSize, NULL); WriteFile(hFile, szData, lstrlen(szData), &dwWriteSize, NULL); CloseHandle(hFile); return 0; } // フックプロシージャ static LRESULT WINAPI GetMsgProc(int code, WPARAM wParam, LPARAM lParam){ WriteLog("Hook!\n"); return (CallNextHookEx(NULL, code, wParam, lParam)); } // SetWindowsHookExのラッパー LOGING_API int CallSetWindowsHookEx(VOID){ if(g_hhook != NULL){ return -1; } // フックのセット g_hhook = SetWindowsHookEx( WH_GETMESSAGE, // GetMessage()で取得されたメッセージをフック GetMsgProc, // フックプロシージャをGetMsgProcを指定 dll_handle, // フックプロシージャを実装するDLL(つまりloging.dll)のハンドル 0 // フック対象のスレッド(0なら全スレッド対象) ); if(g_hhook == NULL){ return -1; } return 0; } // UnhookWindowsHookExのラッパー LOGING_API int CallUnhookWindowsHookEx(VOID){ if(g_hhook == NULL){ return -1; } UnhookWindowsHookEx(g_hhook); g_hhook = NULL; return 0; } BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved){ // 当DLL自身のハンドルをグローバル変数に入れる dll_handle = hModule; switch( ul_reason_for_call ) { case DLL_PROCESS_ATTACH: // DLLがロードされたとき WriteLog("DLL_PROCESS_ATACH\n"); // loging.logへ書き出し break; case DLL_THREAD_ATTACH: // プロセスでスレッドが作成時 break; case DLL_THREAD_DETACH: // プロセスでスレッドが終了時 break; case DLL_PROCESS_DETACH: // プロセスが終了するとき WriteLog("DLL_PROCESS_DETACH\n"); // loging.logへ書き出し break; } return TRUE; }
cl.exeでビルド
> cl.exe /LD /DLOGING_API /Fe:logging2.dll dllmain.c logging2.c user32.lib # ⇒ logging2.dll と logging2.libが生成される
setwindowshook.c
#include <windows.h> #include <stdio.h> #include <tchar.h> int main(int argc, char *argv[]){ if(argc < 2){ fprintf(stderr, "%s <DLL Name>\n", argv[0]); return 1; } // 引数で与えられたDLL(loging.dll)をロードし、ハンドルをhに代入 HMODULE h = LoadLibrary(argv[1]); if(h == NULL){ return -1; } // loging.dllで定義された CallSetWindownHookEx関数 のアドレスを取得し、fcallへセット int (__stdcall *fcall) (VOID); fcall = (int (WINAPI *)(VOID))GetProcAddress(h, "CallSetWindowsHookEx"); if(fcall == NULL){ fprintf(stderr, "ERROR: Get CallSetWindowsHookEx\n"); goto _Exit; } // loging.dllで定義された CallUnhookWindowsHookEx関数のアドレスを取得し、ffreeへセット int (__stdcall *ffree)(VOID); ffree = (int (WINAPI *)(VOID))GetProcAddress(h, "CallUnhookWindowsHookEx"); if(ffree == NULL){ fprintf(stderr, "ERROR: Call UnhookWindowsHookEx\n"); goto _Exit; } // fcall(= CallSetWindowsHookEx) を呼んで、フックをセット if(fcall()){ fprintf(stderr, "ERROR: CallSetWindowsHookEx\n"); goto _Exit; } printf("Call SetWindowsHookEx\n"); getchar(); // ffree(= CallUnhookWindowsHookEx) を呼んで、フックを外す if(ffree()){ fprintf(stderr, "ERROR: CallUnhookWindowsHookEx\n"); goto _Exit; } printf("Call UnhookWindowsHookEx\n"); _Exit: FreeLibrary(h); return 0; }
setwindowshook.cをビルドして setwindowshook.exe を作成し、引数にlogging2.dllを指定して起動すると、以下のとおり動作。
- 引数のlogging2.dllをLoadLibraryでロード
- GetProcAddressでCallSetWindowsHookEx、CallUnhookWindowsHookExのアドレス取得
- CallSetWindowsHookExを起動。GetMessage()で取得されたメッセージをフック
- 標準入力を受け取ると、CallUnhookWindowsHookExでフックを外して終了
loging.log
[PID+4588][Module:C:\Work\Sct04\04_WriteAppInit\setwindowshook.exe] DLL_PROCESS_ATACH [PID+13096][Module:C:\Program Files\Intel\Intel(R) Rapid Storage Technology\IAStorIcon.exe] DLL_PROCESS_ATACH [PID+13096][Module:C:\Program Files\Intel\Intel(R) Rapid Storage Technology\IAStorIcon.exe] Hook! [PID+13096][Module:C:\Program Files\Intel\Intel(R) Rapid Storage Technology\IAStorIcon.exe] Hook! [PID+13096][Module:C:\Program Files\Intel\Intel(R) Rapid Storage Technology\IAStorIcon.exe] Hook! [PID+13096][Module:C:\Program Files\Intel\Intel(R) Rapid Storage Technology\IAStorIcon.exe] Hook! [PID+13096][Module:C:\Program Files\Intel\Intel(R) Rapid Storage Technology\IAStorIcon.exe] Hook! [PID+13096][Module:C:\Program Files\Intel\Intel(R) Rapid Storage Technology\IAStorIcon.exe] Hook! [PID+6132][Module:C:\WINDOWS\SysWOW64\DllHost.exe] DLL_PROCESS_ATACH [PID+6132][Module:C:\WINDOWS\SysWOW64\DllHost.exe] Hook! [PID+6132][Module:C:\WINDOWS\SysWOW64\DllHost.exe] Hook! …(100件以上 )… [PID+6132][Module:C:\WINDOWS\SysWOW64\DllHost.exe] Hook! [PID+6132][Module:C:\WINDOWS\SysWOW64\DllHost.exe] Hook! [PID+4588][Module:C:\Work\Sct04\04_WriteAppInit\setwindowshook.exe] DLL_PROCESS_DETACH [PID+6132][Module:C:\WINDOWS\SysWOW64\DllHost.exe] DLL_PROCESS_DETACH [PID+13096][Module:C:\Program Files\Intel\Intel(R) Rapid Storage Technology\IAStorIcon.exe] DLL_PROCESS_DETACH
レジストリのAppInit_DLLs にDLLのパスを登録しておく (p200-)
レジストリのAppInit_DLLsにDLLのパスを登録すると、OS起動直後にuser32.dllからロードされ、user32.dllを使うプロセスにマッピングされる(user32.dllを使わないプロセスにはマッピングされない)。
HKLM\SOFTWARE\Micorosoft\Windows NT\CurrentVersion\Windows\ - AppInit_DLLs : カンマ区切りでDLLのパスを登録 - LoadAppInit_DLLs : AppInit_DLLs有効/無効のフラグ
【補足】 64bitのWindows10上で、CプログラムのRegSetValueExを使って
HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows\AppInit_DLLs
に書込みをしたら、実際は
HKLM\SOFTWARE\WOW6432Node\Microsoft\Windows NT\CurrentVersion\Windows\AppInit_DLLs
に書き込まれていた。 これは、64bit Windows OS上で32bitプログラムを実行する「WOW64」という実行環境が作成するキー。 32bitアプリが「HKLM\SOFTWARE」へアクセスすると、
HKLM\SOFTWARE\Wow6432Node
にアクセスするよう、WOW64によって自動的にリダイレクトされる。
CreateRemoteThreadで他プロセス内にスレッドを生成する (p204-)
他プロセス内にスレッドを生成するCreateRemoteThreadを使い、LoadLibraryをスレッド実行して、システムフックを設定したDLLを他プロセスにロードさせる。そうやってターゲットプロセスにシステムフックを埋め込むことができる。
本書内でdllinjection.cppとなっているソースコードは、ダウンロードするサンプルだとinjectcode.cppとなっている。そして、それとは別にdllinjection.cppもあるが、それに該当するソースは本書内にはない。また、dllinjection.hというヘッダファイルもない。つまり
本書内 | 正 |
---|---|
injectcode.h | injectcode.h |
dllinjection.cpp(名前が異なる) | injectcode.cpp |
dllinjection.h | 存在しない |
本書内にはない(名前だけ) | dllinjection.cpp |
- CreateToolhelp32Snapshotでプロセス一覧のスナップショット取り、ターゲットのプロセスID取得
- OpenProcess() とプロセスIDから、ターゲットプロセスのハンドルを取得
- VirtualAllocEx() とプロセスハンドルから、ターゲットプロセスのメモリ空間に領域確保。ターゲットプロセスにロードさせたいDLLのパスを書き込む。
- CreateRemoteProcessでターゲットプロセス内に、LoadLibraryのスレッドを作成。LoadLibraryの引数(DLLパス)は3項で確保した領域に書いた文字列。
- WaitForSigleThreadでスレッド終了を待つ。
- VirtualFreeExでメモリ領域解放、CloseHandleでスレッド閉じる
ただ、Window10(64bit)上で iexplore.exe(32bit)に対して試してみると、CreateRemoteThreadで失敗する。既存プロセスではなく、プログラム内で新規プロセスを作成してそのプロセスへのインジェクションは成功する。ターゲットを notepad.exe(32.bit)にすると成功するので、iexplore.exeの方で何らかのプロテクトをしているのか? でも新規作成すればうまくいくことを考えると、やはり権限の問題か。
関数を挿入する (p211-)
DLLインジェクションでは、CreateRemoteThreadを使ってLoadLibraryを呼び出したが、DLLでなく関数(コード)そのものをプロセス内へコピーすれば、その関数をCreateRemoteThreadで実行できる。関数アドレス(&func)を先頭に4096byte(サイズは適当か?)をメモリ領域をコピーしている。
- インジェクションする関数(コード)とデータ(APIアドレス、文字列等)作成
- OpenProcess()でターゲットとなるプロセスのハンドルを取得
- VirtualAllocEx()でターゲットプロセスのメモリ空間にコードとデータ用の領域確保(4096byte)
- WriteProcessMemory()でコード・データをターゲットプロセスのメモリ空間に書込み 5. CreateRemoteThread()でターゲットプロセスのメモリ空間に書き込んだコードをスレッド起動
- WaitForSingleObject()でスレッド終了を待つ
- GetExitCodeThread()
- CloseHandleでスレッド閉じる、VritualFreeExでメモリ領域解放
コードインジェクションについても、本書ではiexplore.exe(32bit)に対して行っていたが、自分がやるとCreateRemoteThreadで失敗した。しかし、notepad.exe(32bit)に対しては成功した。
処理を任意のものに置き換える ~ APIフック (p216-)
フックはプログラムに独自処理を追加するが、APIフックは特にAPIに独自の処理を追加する。 APIフックには
- 対象となる関数の先頭数バイトを書き換えるタイプ(インラインフックとも呼ばれる)
- IAT(Import Address Table)を書き換えるタイプ(IATフックとも呼ばれる)
があるようだが、2については本書では割愛。
DetoursでかんたんにAPIフックを実現 (p216-)
- Microsoftリリース「Detours」:ユーザモードの任意関数をフックするためのライブラリ
- 関数の先頭数バイトをJMP命令に書き換え、一時的に別の関数に処理を飛ばしているらしい。
- APIフックは基本的にユーザランドにおいては、DLLが持つエクスポート関数に対して行うもの
- 非公開のAPIをフックしたり、カーネルランド(Ring0)で動作するドライバをフックする手法もあり
- 様々な環境下での様々な手法が存在する
helloworld.cpp
int _tmain(int argc, TCHAR *argv[]){ HMODULE h = LoadLibrary("detourshock.dll"); // APIフック用DLLをロード MessageBoxA( GetForegroundWindow(), // アクティブウィンドウをオーナーに指定 "Hello World!", "Message", MB_OK); FreeLibrary(h); return 0; }
detourshock.dll
// detourshock.dllがロードされると実行 BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved){ switch(ul_reason_for_call){ case DLL_PROCESS_ATTACH: DllProcessAttach(); // DLLがアタッチされた際に実行 break; case DLL_PROCESS_DETACH: DllProcessDetach(); break; } return TRUE; } // DLLがアタッチされた際に実行される関数。ターゲットの関数を迂回し、別関数を呼び出す。 int DllProcessAttach(VOID) { DetourRestoreAfterWith(); // ターゲット関数内import tableの復元(何のため?) DetourTransactionBegin(); // 迂回のための新しいトランザクションの作成 DetourUpdateThread(GetCurrentThread()); //指定スレッドをコミット時に更新 // TrueMeesgeBoxA(=MessageBoxA)にHookedMessageBoxAをアタッチ DetourAttach(&(PVOID&)TrueMessageBoxA, HookedMessageBoxA); if(DetourTransactionCommit() // 作成したトランザクションをコミット != NO_ERROR){ return -1; } return 0; }
helloworld.cppをコンパイルして実行すると、メッセージボックスのタイトルが変えられている。
※ Detour libについては、まだ理解ができていないので、後でまたよく読む。 https://github.com/microsoft/Detours/wiki
第5章 ツールを駆使してより深い世界へ
- Metasploit Framework (内容は少し古い)
- EMETでROP対策 (EMETはもうサポート終了して、Windows Defenderに統合済み)
- REMnuxでマルウェア解析
- ClamAVでマルウェアやExploit検知
- Zero Wine Tryoutsでマルウェア解析
おわりに
最初にも書いたが、自分の力不足故か読むのにかなり苦労した。それでも、いろいろと知らないテクニックを知ることができたので、得るものはあった。