今回は Windows で DLL ファイルを作成してプロセスにロードする方法をいくつか試してみました。
なお、本記事は法律および倫理に違反する情報を公開するものではありません。
もくじ
Hello World DLL をつくる
Visual Studio で新しい DLL プロジェクトを作成し、dllmain.cpp を以下のコードに置き換えることで、ロード時に Hello, World!
という文字列をメッセージボックスで表示する DLL を作成できます。
#include <Windows.h>
// Exported function
extern "C" {
__declspec(dllexport) void ShowMessage() {
MessageBoxA(NULL, "Hello, World!", "DLL Message", MB_OK | MB_ICONINFORMATION);
}
__declspec(dllexport) void ShowMessage2() {
MessageBoxA(NULL, "Hello, New World!", "DLL Message", MB_OK | MB_ICONINFORMATION);
}
}
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH: {
ShowMessage();
break;
};
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
このとき、メッセージボックスを取得する関数である ShowMessage は extern __declspec(dllexport) void ShowMessage()
によってエクスポートされた関数として定義されます。
参考:__declspec(dllexport) を使った DLL からのエクスポート | Microsoft Learn
この ShowMessage 関数は、DLL_PROCESS_ATTACH
時に呼び出されるため、DLL がプロセスにロードされた際に実行されます。
DLL のサンプルコードをビルドする際には、今回は特に必要のない Precompiled ヘッダを無効化しておきます。
DLL の Reson Code
DLL_PROCESS_ATTACH
は、DllMain 関数の引数 fdwReason として渡される Reson Code(DLL のエントリポイントが呼び出された理由を示す値) に該当します。
BOOL WINAPI DllMain(
_In_ HINSTANCE hinstDLL,
_In_ DWORD fdwReason,
_In_ LPVOID lpvReserved
);
DLL_PROCESS_ATTACH
が使用される場合は、以下の通り LoadLibrary によって DLL がプロセスにロードされた場合などに該当するようです。
DLLPROCESSATTACH 1: プロセスが起動した結果、または LoadLibrary の呼び出しの結果として、DLL が現在のプロセスの仮想アドレス空間に読み込まれます。 DLL では、この機会を使用してインスタンス データを初期化したり、 TlsAlloc 関数を使用してスレッド ローカル ストレージ (TLS) インデックスを割り当てたりできます。 lpvReserved パラメーターは、DLL が静的または動的に読み込まれているかどうかを示します。
参考:DllMain エントリ ポイント (Process.h) - Win32 apps | Microsoft Learn
ちなみに、Binary Ninja でこの DLL を解析してみると、以下のようなデコンパイル結果を得ることができます。
DLLMain のベストプラクティス
DLLMain 関数は、以下の図のようにローダーロックが保持されている間に実行されるため、利用にいくつかの制限があるようです。
DllMain に関するベストプラクティスについてまとめられた以下の公開情報に記載の通り、DLLMain の動作によってデッドロックやクラッシュが発生しないように注意する必要があるようです。
例えば、DLLMain のスレッドはローダーロックを保持するため、追加の DLL を動的に読み込むことはできません。
参考:ダイナミック リンク ライブラリのベスト プラクティス - Win32 apps | Microsoft Learn
プロセスに DLL をロードする
プロセスは LoadLibrary 関数を使用することで、指定したディスク上のモジュールファイル名のファイルをロードすることができます。
HMODULE LoadLibraryA(
[in] LPCSTR lpLibFileName
);
参考:LoadLibraryA 関数 (libloaderapi.h) - Win32 apps | Microsoft Learn
この時、lpLibFileName には完全パスもしくはファイル名のみを指定できるようですが、ファイル名のみを指定した場合は標準の検索順序に従って DLL がロードされます。
DLL の検索順序
標準の検索順序については色々と条件によって変わるようではありますが、とりあえず既定で有効な「安全な DLL 検索モード」が有効化されており、かつアプリケーションが代替検索順序を使用していない場合には以下の順序で DLL の検索が行われるようです。
- The directory from which the application loaded.
- The system directory.
- The 16-bit system directory.
- The Windows directory.
- The current directory.
- The directories that are listed in the PATH environment variable.
悪用が比較的容易なカレントディレクトリが下にある点がポイントです。
参考:Dynamic-link library search order - Win32 apps | Microsoft Learn
参考:ダイナミック リンク ライブラリのセキュリティ - Win32 apps | Microsoft Learn
実際、手元の Windows 10 環境で試してみたところ、確かに上記の順序の通りアプリケーションの実行ファイルが配置されたフォルダから順に DLL の検索を行っているとみられるイベントを確認することができました。
rundll32.exe からプロセスのエクスポート関数を実行する
作成した DLL がエクスポートしている関数については、rundll32.exe を使用することでも呼び出すことができます。
以下は、作成した TESTDLL.dll の ShowMessage2 関数を実行するコマンドです。
# rundll32.exe <DLL file>,<Export function>
rundll32.exe TESTDLL.dll,ShowMessage2
実際に上記のコマンドを実行すると、定義したエクスポート関数が呼び出されることを確認できます。
この rundll32.exe は、LoadLibraryExW 関数を使用して DLL の読み込みを行うようです。
HMODULE LoadLibraryExW(
[in] LPCWSTR lpLibFileName,
HANDLE hFile,
[in] DWORD dwFlags
);
参考:Rundll32:悪意あるコードを実行する悪名高きプロキシ | BLOG | サイバーリーズン | EDR(次世代エンドポイントセキュリティ)
参考:LoadLibraryExW 関数 (libloaderapi.h) - Win32 apps | Microsoft Learn
リモートプロセスで DLL をロードする
ローカルプロセス内で DLL をロードする方法の次は、リモートプロセスに特定の DLL をロードする方法を試していきます。
なお、DLL Injection のテクニック自体は Malware などに特別に悪用されているものではなく、セキュリティソフトウェアなどの商用ソフトウェアでも利用されるテクニックです。
例えば SEP では sysfer.dll をシステム内の他のプロセスにインジェクションすることに依存していることが公式のドキュメントで案内されています。
SEP が DLL Injection を行う際に使用するテクニックについては公式の情報には記載がありませんでしたが、以下の記事によると KeInitializeApc と KeInsertQueueApc を使用しているらしい記載が確認できます。
参考:Security Software and Undocumented API Usage - NTDEV - OSR Developer Community
VirtualAllocEx と WriteProessMemory でリモートプロセスに DLL をロードする
今回は、リモートプロセスのメモリ領域を確保する VirtualAllocEx 関数を使用して割り当てたメモリに WriteProessMemory 関数でロードする DLL のパスを書き込み、そのコードを CreateRemoteThread 関数経由で実行した LoadLibrary 関数でロードすることによる DLL Injection のテクニックを試していきます。
なお、今回試した方法は Microsoft が用意している正規の API を使用してリモートプロセスに DLL をロードするものです。
前述の通り DLL Injection のテクニック自体は正規のツールでも利用するものであり、この操作自体が何らかの不正な攻撃を行うものではありません。
※ ただし、念のため本記事ではコピペ可能なコードは記載せず、一般的な Windows の API 関数の呼び出しコードのみを記載するようにしています。
リモートプロセスに DLL をロードするため、まずは scanf_s
関数で受け取ったターゲットプロセスの PID を使用し、OpenProcess 関数でターゲットプロセスのハンドルを取得します。
HANDLE OpenProcess(
[in] DWORD dwDesiredAccess,
[in] BOOL bInheritHandle,
[in] DWORD dwProcessId
);
参考:OpenProcess 関数 (processthreadsapi.h) - Win32 apps | Microsoft Learn
今回は PID を手動で入力することを想定していますが、プログラム内でシステム内のプロセス列挙と検索を行う方法でもターゲットプロセスのハンドルを取得することができます。
参考:Win32 API でシステム内のプロセス情報を列挙してみるやつ - かえるのひみつきち
プロセスのハンドルを取得したら、そのプロセスハンドルを引数として VirtualAllocEx 関数を実行し、リモートプロセスにロードする DLL のファイル名を書き込むメモリ領域を確保します。
pAddress = VirtualAllocEx(hProcess, NULL, dwSizeToWrite, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
参考:VirtualAllocEx 関数 (memoryapi.h) - Win32 apps | Microsoft Learn
続けて、リモートプロセスで確保したメモリ領域に WriteProcessMemory 関数で DLL ファイル名を書き込みます。
WriteProcessMemory(hProcess, pAddress, DLLFileName, dwSizeToWrite, &lpNumberOfBytesWritten) || lpNumberOfBytesWritten != dwSizeToWrite)
書き込み用のデータは WriteProcessMemory 関数の第 3 引数で指定しています。
BOOL WriteProcessMemory(
[in] HANDLE hProcess,
[in] LPVOID lpBaseAddress,
[in] LPCVOID lpBuffer,
[in] SIZE_T nSize,
[out] SIZE_T *lpNumberOfBytesWritten
);
参考:WriteProcessMemory 関数 (memoryapi.h) - Win32 apps | Microsoft Learn
最後に、GetProcAddress 関数で kernel32.dll でエクスポートされている LoadLibraryW 関数のアドレスを取得し、CreateRemoteThread 関数を使用してターゲットプロセスのスレッドを起動します。
LPVOID pLoadLibraryW = GetProcAddress(GetModuleHandle(L"kernel32.dll"), "LoadLibraryW");
HANDLE hThread = CreateRemoteThread(hProcess, NULL, NULL, pLoadLibraryW, pAddress, NULL, NULL);
参考:GetProcAddress 関数 (libloaderapi.h) - Win32 apps | Microsoft Learn
CreateRemoteThread 関数は、別のプロセスの仮想アドレス空間で実行されるスレッドを作成できる関数です。
lpStartAddress には取得した LoadLibraryW 関数のアドレスを、またスレッド関数に渡す引数である lpParameter には、ターゲットプロセスのメモリ領域内に書き込んだロードする DLL のファイル名へのポインタを指定します。
HANDLE CreateRemoteThread(
[in] HANDLE hProcess,
[in] LPSECURITY_ATTRIBUTES lpThreadAttributes,
[in] SIZE_T dwStackSize,
[in] LPTHREAD_START_ROUTINE lpStartAddress,
[in] LPVOID lpParameter,
[in] DWORD dwCreationFlags,
[out] LPDWORD lpThreadId
);
参考:CreateRemoteThread 関数 (processthreadsapi.h) - Win32 apps | Microsoft Learn
ちなみに、この時ローカルプロセスで取得した LoadLibraryW 関数のアドレスをリモートプロセスで実行する CreateRemoteThread 関数の引数にそのまま転用できるのは、kernel32.dll のアドレスがプロセス間で共有されているためです。(そのため、ローカルプロセスとターゲットプロセスの bit 数が違う場合にはこの方法による DLL のロードは利用できないそうです)
この一連の操作を実行することで、PID で指定したターゲットプロセスに任意の DLL をロードすることができました。
なお、この時ロードする DLL を探索する順序は、デフォルトの場合既定の検索順序と同じでした。
ただ、ロードするプロセスはあくまでリモートプロセスとなるので、優先される参照元は DLL Injection を行うプログラムの実行フォルダではなく、ターゲットプロセスの実行ファイルが存在するフォルダになっていました。
そのため、この方法で DLL Injection を行う際には、ロード対象の DLL ファイルのフルパスを指定するか、アプリケーションフォルダやシステムフォルダ、または PATH の通ったフォルダなどに事前にロードする DLL ファイルを配置しておく必要があります。
※ 以下は Notepad++ に DLL Injection を試みた場合の TESTDLL.dll の検索順序。
Reflective DLL Injection について
上記の方法で行う DLL Injection は、ファイルとして存在する DLL を単にロードするだけのものです。
しかし、悪用目的で DLL Injection のテクニックを使用する場合には、このような古典的な手法ではなく、Reflective DLL Injection と呼ばれる手法がよく悪用されるそうです。
参考:Reflective DLL Injection | すなのかたまり
参考:Reflective DLL Injection で調べる不正コード - サイバーフォートレス
詳しい実装については若干怒られが発生する可能性がありそうなので記載しませんが、上記の記事に非常に詳しく書いてあるので参考になりました。
まとめ
今回は Windows で DLL のロードを行ういくつかの方法についてまとめてみました。