メモリからDLLを読み込んでみる
メモリからDLLを読み込んでみる
DLL(dynamic link library)は通常、Windows APIのLoadlibraryやLoadlibraryEXを使ってディスク上から読み込んで使用する。 このLoadLibraryやLoadLibraryExは、ファイルシステム上のファイルでのみ機能し直接メモリからDLLをロードすることはできない。またこれを端的に実現できる公式のWindows APIも存在しない。 メモリからDLLを読み込むには疑似的なPEローダーを実装する必要がある。
ゲームやマルウェアの開発者は、解析難度の向上やアンチウイルスソフトによる検出回避を狙ってしばしばこういった手法をとることがある。 この記事では、メモリからDLLを読み込むために必要なPEファイルの構造について簡易に解説した後、以下のリポジトリを参考に実際にメモリからDLLを読み込ませてみる。
本記事がPEファイルフォーマットの理解と、それを利用した各種テクニックへの理解につながれば幸いだ。
ディスク上からの読み込み
ディスクからのDLL読み込みには、LoadLibraryを使用する。 LoadLibraryを発行すると、Windowsは大まかに次のような処理を行う。
- 指定されたファイルを開き、DOSおよびPEヘッダーを確認する。
- 対象ファイル内のPEHeader.OptionalHeader.ImageBaseに指定されたアドレスに、PEHeader.OptionalHeader.SizeOfImageで指定されたバイト分メモリを確保する。
- セクションヘッダーを解析し、IMAGE_SECTION_HEADER構造体のVirtualAddressに基づき、確保したメモリブロックに各セクションをコピーする。
- ImageBaseと異なるメモリブロックが確保された場合、コードやデータセクションの種々の依存関係を調整する。(ベース再配置)
- ライブラリに必要なインポートを、対応するライブラリをロードして解決する。(IATの解決)
- フラグDLL_PROCESS_ATTACHを使用してDLLのエントリポイント(AddressOfEntryPoint)を呼び出す。
LoadLibraryなどは上のような処理を経て、ディスク上に作成されたDLLを実行可能な形に調整しながらメモリ上に読み込む。
この記事では簡便のために、ディスク上のDLLファイルをメモリ上に読み込んだ後、実行可能な形にメモリ上で再配置し、実際に動作させてみる。
DLLの準備
アタッチするとメッセージボックスがポップアップする簡単なDLLを用意する。
#include<Windows.h> #pragma comment(lib, "user32") BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: MessageBoxW(NULL, L"Hello?", L"Title", MB_OK); break; case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break; } return TRUE; }
DLLファイルの読み込み
次に、このDLLファイルをメモリから読み込むプログラムを準備する。 まずはディスク上のDLLをオープンし、メモリ上に配置する。
HANDLE dll = CreateFileA("[PATH]\\TestDLL.dll", GENERIC_READ, NULL, NULL, OPEN_EXISTING, NULL, NULL); DWORD64 dllSize = GetFileSize(dll, NULL); LPVOID dllBytes = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dllSize); ReadFile(dll, dllBytes, dllSize, NULL, NULL);
これだけではDLLをファイルとして読み込んだにすぎない。 動作させるにはローダーが行う処理をエミュレートする必要がある。 以降その処理を解説をする。
セクションのコピー
まず先んじてメモリ上にセクションのコピーを行う。NumberOfSectionsだけセクションをメモリ上にコピーする。 イメージファイルがロードされたアドレスにVirtualAddressを加算したアドレスへSizeOfRawData分セクション用の領域を確保する。
// セクションのロード ///////////////////////////////////////// PIMAGE_SECTION_HEADER section = IMAGE_FIRST_SECTION(ntHeaders); for (size_t i = 0; i < ntHeaders->FileHeader.NumberOfSections; i++) { LPVOID sectionDestination = (LPVOID)((DWORD_PTR)section->VirtualAddress + (DWORD_PTR)dllBase); LPVOID sectionBytes = (LPVOID)((DWORD_PTR)dllBytes + (DWORD_PTR)section->PointerToRawData); WriteProcessMemory(GetCurrentProcess(), sectionDestination, sectionBytes, section->SizeOfRawData, NULL); section++; } /////////////////////////////////////////
PE内のセクションと実際にメモリ上にロードされたセクションの関係は以下の図が分かりやすい
イメージベースの再配置
リンカ―によってPEファイル内に解決されたIamgeBaseと、ローダーによって実際にロードされるImageBaseが異なることがある。 リンカ―はPEファイル内のIamgeBaseに沿って各種の依存関係を解決している。 もしローダーがこのImageBaseと異なるアドレスへのロードを選択した場合、ローダーによってアドレスの依存関係の修正が行われる。 この処理をベース再配置と呼ぶ。
以前記事にしたProcess Hollowingでも同様の解説を行っているがここで改めて触れておく。
NtUnmapViewOfSecitonによるProcess Hollowingをやってみる - Snoozy
まずリンカ―によってPEファイル内に指定されたIamgeBaseと、ローダーによって実際にロードされるImageBaseの差Deltaを求める。 この差が0でない場合、ベース再配置が必要で、.relocセクションに格納されている再配置テーブルに基いて修正を開始する。
.relocセクションに格納されている再配置テーブルはベース再配置が必要なアドレスのリストである。 各アドレス値を取得し、Deltaだけアドレスを加算する。この値でテーブルを上書きしていくことでベース再配置が完了する。
// ベース再配置 ///////////////////////////////////////// IMAGE_DATA_DIRECTORY relocations = ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC]; DWORD_PTR relocationTable = relocations.VirtualAddress + (DWORD_PTR)dllBase; DWORD relocationsProcessed = 0; // Deltaを求める DWORD_PTR deltaImageBase = (DWORD_PTR)dllBase - (DWORD_PTR)ntHeaders->OptionalHeader.ImageBase; while (relocationsProcessed < relocations.Size) { PBASE_RELOCATION_BLOCK relocationBlock = (PBASE_RELOCATION_BLOCK)(relocationTable + relocationsProcessed); relocationsProcessed += sizeof(BASE_RELOCATION_BLOCK); DWORD relocationsCount = (relocationBlock->BlockSize - sizeof(BASE_RELOCATION_BLOCK)) / sizeof(BASE_RELOCATION_ENTRY); PBASE_RELOCATION_ENTRY relocationEntries = (PBASE_RELOCATION_ENTRY)(relocationTable + relocationsProcessed); for (DWORD i = 0; i < relocationsCount; i++) { relocationsProcessed += sizeof(BASE_RELOCATION_ENTRY); if (relocationEntries[i].Type == 0) { continue; } DWORD_PTR relocationRVA = relocationBlock->PageAddress + relocationEntries[i].Offset; DWORD_PTR addressToPatch = 0; ReadProcessMemory(GetCurrentProcess(), (LPCVOID)((DWORD_PTR)dllBase + relocationRVA), &addressToPatch, sizeof(DWORD_PTR), NULL); addressToPatch += deltaImageBase; WriteProcessMemory(GetCurrentProcess(), (PVOID)((DWORD_PTR)dllBase + relocationRVA), &addressToPatch, sizeof(DWORD_PTR), NULL); } } /////////////////////////////////////////
IATの解決
IAT(Import Address Table)はインポートするAPIのエントリーポイントのリストである。 DLLはこのIATを参照しつつAPIを呼び出すことになる。 たとえば今回のようにメッセージボックスをポップアップさせるDLLであれば、User32.dllからエクスポートされるMessageBox関連のAPIを使用する。正常に動作させるためには事前にDLLをメモリ上にロードし、エクスポートされるAPIのアドレスをIATに解決しておかなければならない。
IATの走査手順をまとめておく。
まずインポートが必要なDLL分ループを回す。 インポート情報はIMAGE_IMPORT_DESCRIPTOR構造体の配列で表される。 リンクが必要なDLLの数+1だけ、IMPORT_IMAGE_DESCRIPTOR構造体が連なる。 最後の1つはすべてNULLであり終端の識別に使用する。
インポートが必要なDLLが存在する場合はLoadlibraryを使って実際にDLLをメモリにロードする。 IMAGE_IMPORT_DESCRIPTOR構造体のメンバFirstThunkはIMAGE_THUNK_DATA構造体へのRVAである。IMAGE_THUNK_DATA構造体がIAT及びINTとして使われる構造体で、この構造体にインポートしたいAPIの名前または実際のメモリ上のアドレスが格納される。
APIが名前でインポートされているか序数でインポートされているかで処理を分岐させる。これを取得するためにIMAGE_SNAP_BY_ORDINALというマクロを使い、Ordinal の最上位ビットが立っているかどうかで判別する。 また、最上位ビットをマスクして序数値を取得するために、IMAGE_ORDINAL というマクロがありこれを利用する。 序数によるインポートである場合はGetProcAddressに序数を、API名によるインポートである場合はAPI名を引数に渡すことで、メモリにロードしたDLLからエクスポートされる各APIへのエントリーポインタへのアドレスを取得する。
// IATの解決 ///////////////////////////////////////// IMAGE_DATA_DIRECTORY importsDirectory = ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]; PIMAGE_IMPORT_DESCRIPTOR importDescriptor = (PIMAGE_IMPORT_DESCRIPTOR)(importsDirectory.VirtualAddress + (DWORD_PTR)dllBase); LPCSTR libraryName = ""; HMODULE library = NULL; // インポートが必要なDLL分ループ while (importDescriptor->Name != NULL) { // DLL名をもとにメモリにモジュールをロード libraryName = (LPCSTR)importDescriptor->Name + (DWORD_PTR)dllBase; library = LoadLibraryA(libraryName); if (library) { // PIMAGE_THUNK_DATA構造体、すなわちIATへのポインタを取得 PIMAGE_THUNK_DATA thunk = (PIMAGE_THUNK_DATA)(importDescriptor->FirstThunk + (DWORD_PTR)dllBase); // インポートするAPI分ループ while (thunk->u1.AddressOfData != NULL) { // 序数に基づいてインポート if (IMAGE_SNAP_BY_ORDINAL(thunk->u1.Ordinal)) { LPCSTR functionOrdinal = (LPCSTR)IMAGE_ORDINAL(thunk->u1.Ordinal); thunk->u1.Function = (DWORD_PTR)GetProcAddress(library, functionOrdinal); } else { // API名に基づいてインポート PIMAGE_IMPORT_BY_NAME functionName = (PIMAGE_IMPORT_BY_NAME)(thunk->u1.AddressOfData + (DWORD_PTR)dllBase); DWORD_PTR functionAddress = (DWORD_PTR)GetProcAddress(library, functionName->Name); //printf("IAT Resolving 0x%p -> 0x%p\n", thunk->u1.Function + (DWORD_PTR)dllBase ,functionAddress); thunk->u1.Function = functionAddress; } ++thunk; } } importDescriptor++; } /////////////////////////////////////////
IATがどう遷移するかは以下の画像がわかりやすい。
DLLがディスク上にある状態ではIAT、INTともに同じ構造体を指す。
DLLのIATが解決されると、IATは実際のアドレスが格納されたIMAGE_THUNK_DATA構造体を指すようになる。
出典:Exciting Journey Towards Import Address Table (IAT) of an Executable
DLLの実行
以上でDLLを実行可能にするために必要な最低限の処理は済んだ。 最後にDLLの実行だ。 ファイル内のAddressOfEntryPointが実行開始アドレスであるため、DLL_PROCESS_ATTACHフラグを付与して関数ポインタとして実行すればよい。
// ロードしたDLLの実行 ///////////////////////////////////////// DLLEntry DllEntry = (DLLEntry)(ntHeaders->OptionalHeader.AddressOfEntryPoint + (DWORD_PTR)dllBase); (*DllEntry)((HINSTANCE)dllBase, DLL_PROCESS_ATTACH, 0); /////////////////////////////////////////
コード
以下が完成したコード。
#include <Windows.h> #include<stdio.h> typedef struct BASE_RELOCATION_BLOCK { DWORD PageAddress; DWORD BlockSize; } BASE_RELOCATION_BLOCK, * PBASE_RELOCATION_BLOCK; typedef struct BASE_RELOCATION_ENTRY { USHORT Offset : 12; USHORT Type : 4; } BASE_RELOCATION_ENTRY, * PBASE_RELOCATION_ENTRY; using DLLEntry = BOOL(WINAPI*)(HINSTANCE dll, DWORD reason, LPVOID reserved); int main() { // DLLファイルのロード HANDLE hdll = CreateFileA("[PATHTODLL]\\TestDLL.dll", GENERIC_READ, NULL, NULL, OPEN_EXISTING, NULL, NULL); DWORD64 dllSize = GetFileSize(hdll, NULL); LPVOID dllBytes = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dllSize); ReadFile(hdll, dllBytes, dllSize, NULL, NULL); // NTヘッダーのロード PIMAGE_DOS_HEADER dosHeaders = (PIMAGE_DOS_HEADER)dllBytes; PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)((DWORD_PTR)dllBytes + dosHeaders->e_lfanew); SIZE_T dllImageSize = ntHeaders->OptionalHeader.SizeOfImage; LPVOID dllBase = VirtualAlloc((LPVOID)ntHeaders->OptionalHeader.ImageBase, dllImageSize, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE); WriteProcessMemory(GetCurrentProcess(), dllBase, dllBytes, ntHeaders->OptionalHeader.SizeOfHeaders, NULL); // セクションのロード ///////////////////////////////////////// PIMAGE_SECTION_HEADER section = IMAGE_FIRST_SECTION(ntHeaders); for (size_t i = 0; i < ntHeaders->FileHeader.NumberOfSections; i++) { LPVOID sectionDestination = (LPVOID)((DWORD_PTR)section->VirtualAddress + (DWORD_PTR)dllBase); LPVOID sectionBytes = (LPVOID)((DWORD_PTR)dllBytes + (DWORD_PTR)section->PointerToRawData); WriteProcessMemory(GetCurrentProcess(), sectionDestination, sectionBytes, section->SizeOfRawData, NULL); section++; } ///////////////////////////////////////// // ベース再配置 ///////////////////////////////////////// IMAGE_DATA_DIRECTORY relocations = ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC]; DWORD_PTR relocationTable = relocations.VirtualAddress + (DWORD_PTR)dllBase; DWORD relocationsProcessed = 0; // Deltaを求める DWORD_PTR deltaImageBase = (DWORD_PTR)dllBase - (DWORD_PTR)ntHeaders->OptionalHeader.ImageBase; while (relocationsProcessed < relocations.Size) { PBASE_RELOCATION_BLOCK relocationBlock = (PBASE_RELOCATION_BLOCK)(relocationTable + relocationsProcessed); relocationsProcessed += sizeof(BASE_RELOCATION_BLOCK); DWORD relocationsCount = (relocationBlock->BlockSize - sizeof(BASE_RELOCATION_BLOCK)) / sizeof(BASE_RELOCATION_ENTRY); PBASE_RELOCATION_ENTRY relocationEntries = (PBASE_RELOCATION_ENTRY)(relocationTable + relocationsProcessed); for (DWORD i = 0; i < relocationsCount; i++) { relocationsProcessed += sizeof(BASE_RELOCATION_ENTRY); if (relocationEntries[i].Type == 0) { continue; } DWORD_PTR relocationRVA = relocationBlock->PageAddress + relocationEntries[i].Offset; DWORD_PTR addressToPatch = 0; ReadProcessMemory(GetCurrentProcess(), (LPCVOID)((DWORD_PTR)dllBase + relocationRVA), &addressToPatch, sizeof(DWORD_PTR), NULL); addressToPatch += deltaImageBase; WriteProcessMemory(GetCurrentProcess(), (PVOID)((DWORD_PTR)dllBase + relocationRVA), &addressToPatch, sizeof(DWORD_PTR), NULL); } } ///////////////////////////////////////// // IATの解決 ///////////////////////////////////////// IMAGE_DATA_DIRECTORY importsDirectory = ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]; PIMAGE_IMPORT_DESCRIPTOR importDescriptor = (PIMAGE_IMPORT_DESCRIPTOR)(importsDirectory.VirtualAddress + (DWORD_PTR)dllBase); LPCSTR libraryName = ""; HMODULE library = NULL; // インポートが必要なDLL分ループ while (importDescriptor->Name != NULL) { // DLL名をもとにメモリにモジュールをロード libraryName = (LPCSTR)importDescriptor->Name + (DWORD_PTR)dllBase; library = LoadLibraryA(libraryName); if (library) { // PIMAGE_THUNK_DATA構造体、すなわちIATへのポインタを取得 PIMAGE_THUNK_DATA thunk = (PIMAGE_THUNK_DATA)(importDescriptor->FirstThunk + (DWORD_PTR)dllBase); // インポートするAPI分ループ while (thunk->u1.AddressOfData != NULL) { // 序数に基づいてインポート if (IMAGE_SNAP_BY_ORDINAL(thunk->u1.Ordinal)) { LPCSTR functionOrdinal = (LPCSTR)IMAGE_ORDINAL(thunk->u1.Ordinal); thunk->u1.Function = (DWORD_PTR)GetProcAddress(library, functionOrdinal); } else { // API名に基づいてインポート PIMAGE_IMPORT_BY_NAME functionName = (PIMAGE_IMPORT_BY_NAME)(thunk->u1.AddressOfData + (DWORD_PTR)dllBase); DWORD_PTR functionAddress = (DWORD_PTR)GetProcAddress(library, functionName->Name); //printf("IAT Resolving 0x%p -> 0x%p\n", thunk->u1.Function + (DWORD_PTR)dllBase ,functionAddress); thunk->u1.Function = functionAddress; } ++thunk; } } importDescriptor++; } ///////////////////////////////////////// // ロードしたDLLの実行 ///////////////////////////////////////// DLLEntry DllEntry = (DLLEntry)(ntHeaders->OptionalHeader.AddressOfEntryPoint + (DWORD_PTR)dllBase); (*DllEntry)((HINSTANCE)dllBase, DLL_PROCESS_ATTACH, 0); ///////////////////////////////////////// CloseHandle(hdll); HeapFree(GetProcessHeap(), 0, dllBytes); return 0; }
デモ
実際に動作させてみた様子が以下の動画。
IATが順次解決され、最終的にDLLが適切に読み込まれている様子が確認できる。IATを解決してメモリからDLLをロードしている様子 pic.twitter.com/2bhdRm3GzP
— ry0kvn (@ry0kvn) 2020年1月19日
参考サイト
http://www.openrce.org/reference_library/files/reference/PE%20Format.pdf