ReflectivePELoaderを実装してみる
ReflectivePELoaderを実装してみる
ディスク上のEXEやDLLは,Windowsが提供するPEローダによって,セクションの展開やベース再配置,IATの解決など,実行可能な状態に調整されながらメモリ上にロードされる.
これはディスク上のPEファイルをそのままメモリ上にロードしただけでは実行することができないことを意味する.
マルウェアなどは解析妨害・検知回避のために,リソースファイルとしてPEファイルを保持し,あるいは一度ディスク上に暗号化されたファイルをドロップした後,自前のローダを使用してこれをメモリ上に展開し実行することがある.
このようなテクニックは一般に,ReflectivePELoaderと呼ばれる.
本稿では,以下のリポジトリを参考にこのReflectivePELoaderを実装してみる.
本稿が,マルウェア解析やテクニック理解への手助けになれば幸いだ.
ReflectivePELoaderの概要
WindowsのPEローダは大まかに以下のような手順を経て,ディスク上のPEファイルをメモリ上にロードする.
- ディスク上のPEファイルを開き、DOSおよびPEヘッダを確認する
- 対象ファイル内のPEHeader.OptionalHeader.ImageBaseに指定されたアドレスに、PEHeader.OptionalHeader.SizeOfImageで指定されたバイト分メモリを確保する
- セクションヘッダを解析し、IMAGE_SECTION_HEADER構造体のVirtualAddressに基づき、確保したメモリブロックに各セクションをコピーする
- ImageBaseと異なるメモリブロックが確保された場合、コードやデータセクションの種々の依存関係を調整する。(ベース再配置)
- ライブラリに必要なインポートを、対応するライブラリをロードして解決する。(IATの解決)
- PEのエントリーポイント(AddressOfEntryPoint)を呼び出す
本稿では,これら一連の処理をエミュレートしたローダを実装し,実際にリソースとしてローダ内部に組み込まれたEXEファイルを動かしてみる.
ローダが利用するPEファイルフォーマットの重要箇所
PEファイルはメモリ上にそのままロードすれば動くというものではなく,実行可能にするにはいくかの調整が必要となることは既に述べた.
以下にローダによる調整が必要な箇所を列挙する.
- セクションの展開
- イメージベースの再配置
- IATの解決
最低限これだけ押さえておけば,筆者の経験上ミニマルなEXEは動くはずである,
それぞれに必要な処理は以前の記事で既に解説済みであるため,必要であれば参照していただきたい.
ローダにロード対象ファイルをリソースとして持たせる
Visual Studio 2019で,あるプログラムにリソースとしてファイルを組み込むには以下のようにすればよい.
ソリューションエクスプローラから当該プロジェクトを選択. 「リソースファイル」を右クリックし,「追加」内の「リソース」を選択する.
リソースの追加ウィンドウから「インポート」を選択し,リソースにしたいファイルを選択する.
「インポート」からファイルを選択した場合,カスタムリソースとして扱われ,リソースの種類を定義する必要がある.
以下のようなフォルダ構成になっていればうまく構成できている.
この時点でコンパイルすると,リソースセクションにロード対象が組み込まれたファイルが出力される.
ResourceHackerで確認すると確かにリソースとして組み込まれていることが確認できる.
またPE-bearを使用するとファイル構造が視覚的になって分かりやすいのではないだろうか.
リソースの読み込み
組み込まれたリソースには以下のようにしてアクセスする.
HRSRC hResource = FindResource(NULL, MAKEINTRESOURCE(IDR_RESOURCETYPE1), L"ResourceType"); DWORD peSize = SizeofResource(NULL, hResource); HGLOBAL pe = LoadResource(NULL, hResource); LPVOID peBytes = VirtualAlloc(NULL, peSize, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE); memcpy(peBytes, pe, peSize);
FindResourceでリソースハンドルを取得し,VirtualAllocで確保したメモリ領域へコピーする. その後,コピーしたデータを実行可能なように調整を加えながら展開する.
FindResourceの引数には説明が必要だろう.
第1引数hModuleには、リソースが入ったモジュールのハンドルを指定する.今回は自分自身内のリソースなのでNULL指定でよい.
第2引数lpNameには、リソースの識別子を指定する.識別子はresource.hに自動的に定義されたものを使用する.
第3引数lpType にはリソースの型を指定する.今回はカスタムリソースであり,「カスタムリソースの種類」ウィンドウで入力した適当な名前"ResourceType"を指定する.
メモリ上に展開したEXEファイルの実行
ロードしたEXEファイルを実行するには,AddressOfEntryPointを使って関数ポインタを作成し,この関数を呼び出すことで実行できる.
using PEEntry = VOID(*)();
PEEntry PEmain = (PEEntry)((DWORD_PTR)peBase + ntHeaders->OptionalHeader.AddressOfEntryPoint);
PEmain();
コード
以下がコードの全体像.
#include <iostream> #include <Windows.h> #include "resource.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; int main() { HRSRC hResource = FindResource(NULL, MAKEINTRESOURCE(IDR_RESOURCETYPE1), L"ResourceType"); DWORD peSize = SizeofResource(NULL, hResource); HGLOBAL pe = LoadResource(NULL, hResource); LPVOID peBytes = VirtualAlloc(NULL, peSize, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE); memcpy(peBytes, pe, peSize); PIMAGE_DOS_HEADER dosHeaders = (PIMAGE_DOS_HEADER)peBytes; PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)((DWORD_PTR)peBytes + dosHeaders->e_lfanew); SIZE_T peImageSize = ntHeaders->OptionalHeader.SizeOfImage; LPVOID peBase = VirtualAlloc((LPVOID)ntHeaders->OptionalHeader.ImageBase, peImageSize, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE); DWORD_PTR deltaImageBase = (DWORD_PTR)peBase - (DWORD_PTR)ntHeaders->OptionalHeader.ImageBase; std::memcpy(peBase, peBytes, ntHeaders->OptionalHeader.SizeOfHeaders); PIMAGE_SECTION_HEADER section = IMAGE_FIRST_SECTION(ntHeaders); for (size_t i = 0; i < ntHeaders->FileHeader.NumberOfSections; i++) { LPVOID sectionDestination = (LPVOID)((DWORD_PTR)peBase + (DWORD_PTR)section->VirtualAddress); LPVOID sectionBytes = (LPVOID)((DWORD_PTR)peBytes + (DWORD_PTR)section->PointerToRawData); std::memcpy(sectionDestination, sectionBytes, section->SizeOfRawData); section++; } IMAGE_DATA_DIRECTORY relocations = ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC]; DWORD_PTR relocationTable = relocations.VirtualAddress + (DWORD_PTR)peBase; DWORD relocationsProcessed = 0; 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)peBase + relocationRVA), &addressToPatch, sizeof(DWORD_PTR), NULL); addressToPatch += deltaImageBase; std::memcpy((PVOID)((DWORD_PTR)peBase + relocationRVA), &addressToPatch, sizeof(DWORD_PTR)); } } PIMAGE_IMPORT_DESCRIPTOR importDescriptor = NULL; IMAGE_DATA_DIRECTORY importsDirectory = ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]; importDescriptor = (PIMAGE_IMPORT_DESCRIPTOR)(importsDirectory.VirtualAddress + (DWORD_PTR)peBase); LPCSTR libraryName = ""; HMODULE library = NULL; while (importDescriptor->Name != NULL) { libraryName = (LPCSTR)importDescriptor->Name + (DWORD_PTR)peBase; library = LoadLibraryA(libraryName); if (library) { PIMAGE_THUNK_DATA thunk = NULL; thunk = (PIMAGE_THUNK_DATA)((DWORD_PTR)peBase + importDescriptor->FirstThunk); 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 { PIMAGE_IMPORT_BY_NAME functionName = (PIMAGE_IMPORT_BY_NAME)((DWORD_PTR)peBase + thunk->u1.AddressOfData); DWORD_PTR functionAddress = (DWORD_PTR)GetProcAddress(library, functionName->Name); thunk->u1.Function = functionAddress; } ++thunk; } } importDescriptor++; } using PEEntry = VOID(*)(); PEEntry PEmain = (PEEntry)((DWORD_PTR)peBase + ntHeaders->OptionalHeader.AddressOfEntryPoint); PEmain(); CloseHandle(pe); HeapFree(GetProcessHeap(), 0, peBytes); return 0; }
デモ
以下は実際に動作させてみた様子.
うまく動作できているようだ.
デバッガを使ってメモリ内を見てみると確かにロードされていることがわかる.
MALISOUSという文字は,筆者が分かりやすいようにロード対象のリソースファイルにハードコードしたもの. (綴りを間違っているのはご容赦願いたい)
リソースファイルの扱いで少々オバーヘッドがあるが,ReflectivePELoaderのコンセプトは再現できたのではないだろうか.