All Articles

Windows で DLL ファイルを作成して様々な方法でプロセスにロードしてみる

今回は 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 がプロセスにロードされた際に実行されます。

image-20241231221242487

DLL のサンプルコードをビルドする際には、今回は特に必要のない Precompiled ヘッダを無効化しておきます。

image-20250102103357561

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 を解析してみると、以下のようなデコンパイル結果を得ることができます。

image-20241231221310375

DLLMain のベストプラクティス

DLLMain 関数は、以下の図のようにローダーロックが保持されている間に実行されるため、利用にいくつかの制限があるようです。

what happens when a library is loaded

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 の検索が行われるようです。

  1. The directory from which the application loaded.
  2. The system directory.
  3. The 16-bit system directory.
  4. The Windows directory.
  5. The current directory.
  6. 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 の検索を行っているとみられるイベントを確認することができました。

image-20250101104959674

rundll32.exe からプロセスのエクスポート関数を実行する

作成した DLL がエクスポートしている関数については、rundll32.exe を使用することでも呼び出すことができます。

以下は、作成した TESTDLL.dll の ShowMessage2 関数を実行するコマンドです。

# rundll32.exe <DLL file>,<Export function>
rundll32.exe TESTDLL.dll,ShowMessage2

実際に上記のコマンドを実行すると、定義したエクスポート関数が呼び出されることを確認できます。

image-20250101121830055

参考:rundll32 | Microsoft Learn

この 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 をシステム内の他のプロセスにインジェクションすることに依存していることが公式のドキュメントで案内されています。

参考:How to create an Application Control exception or stop sysfer.dll injection into a process with Endpoint Protection

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 をロードすることができました。

image-20250105203913817

image-20250105204019521

なお、この時ロードする DLL を探索する順序は、デフォルトの場合既定の検索順序と同じでした。

ただ、ロードするプロセスはあくまでリモートプロセスとなるので、優先される参照元は DLL Injection を行うプログラムの実行フォルダではなく、ターゲットプロセスの実行ファイルが存在するフォルダになっていました。

そのため、この方法で DLL Injection を行う際には、ロード対象の DLL ファイルのフルパスを指定するか、アプリケーションフォルダやシステムフォルダ、または PATH の通ったフォルダなどに事前にロードする DLL ファイルを配置しておく必要があります。

※ 以下は Notepad++ に DLL Injection を試みた場合の TESTDLL.dll の検索順序。

image-20250105204238809

Reflective DLL Injection について

上記の方法で行う DLL Injection は、ファイルとして存在する DLL を単にロードするだけのものです。

しかし、悪用目的で DLL Injection のテクニックを使用する場合には、このような古典的な手法ではなく、Reflective DLL Injection と呼ばれる手法がよく悪用されるそうです。

参考:Reflective DLL Injection | すなのかたまり

参考:Reflective DLL Injection で調べる不正コード - サイバーフォートレス

詳しい実装については若干怒られが発生する可能性がありそうなので記載しませんが、上記の記事に非常に詳しく書いてあるので参考になりました。

まとめ

今回は Windows で DLL のロードを行ういくつかの方法についてまとめてみました。