CreateRemoteThread関数によるDLLインジェクションをやってみる

Windowsにおいて、キーロガーなどが自身のプロセスを隠蔽する手法にDLLインジェクションがある。 これは、プログラムをDLLとして作成し、他のプロセスに読み込ませることで実行するというものである。 ここでは、CreateRemoteThread関数を使ったDLLインジェクションをやってみる。

環境

Windows 8.1 Pro 64 bit版、Visual Studio Community 2013 with Update 4

>systeminfo
OS 名:                  Microsoft Windows 8.1 Pro
OS バージョン:          6.3.9600 N/A ビルド 9600
OS 製造元:              Microsoft Corporation
システムの種類:         x64-based PC
プロセッサ:             1 プロセッサインストール済みです。
                        [01]: Intel64 Family 6 Model 69 Stepping 1 GenuineIntel ~758 Mhz

>cl
Microsoft (R) C/C++ Optimizing Compiler Version 18.00.31101 for x64

プログラムコードを書いてみる

DLLインジェクションは、動作中の他のプロセスのスレッドとしてLoadLibrary関数を実行させることによって行われる。 実際にコードを書くと次のようになる。

/* injector.c */
#include <windows.h>
#include <stdio.h>

int main(int argc, char *argv[])
{
    char dllpath[] = "C:\\Users\\user\\Desktop\\test\\spy.dll";

    int pid = atoi(argv[1]);

    HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
    void *datamemory = VirtualAllocEx(hProcess, NULL, sizeof(dllpath), MEM_COMMIT, PAGE_READWRITE);
    WriteProcessMemory(hProcess, datamemory, (void *)dllpath, sizeof(dllpath), NULL);

    HMODULE kernel32 = GetModuleHandle("kernel32");
    FARPROC loadlibrary = GetProcAddress(kernel32, "LoadLibraryA");
    HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)loadlibrary, datamemory, 0, NULL);
    if (!hThread) {
        /* 32 bit (WOW64) -> 64 bit (Native) won't work */
        char errmsg[512];
        FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, NULL, GetLastError(), 0, errmsg, sizeof(errmsg), NULL);
        printf("%hs", errmsg);
        return 1;
    }
    WaitForSingleObject(hThread, INFINITE);

    CloseHandle(hThread);
    VirtualFreeEx(hProcess, datamemory, sizeof(dllpath), MEM_RELEASE);

    return 0;
}

上のコードは、第一引数にDLLを読み込ませるプロセス(以下リモートプロセス)のpidをとる。 コードの内容を簡単に説明すると次のようになる。

  1. OpenProcess関数でリモートプロセスのハンドルを取得する
  2. VirtualAllocEx関数でリモートプロセス上のメモリ領域を確保する
  3. 確保したメモリ領域にWriteProcessMemory関数でDLLのパス文字列を書き込む
  4. GetModuleHandle関数とGetProcAddress関数でkernel32.dll内にあるLoadLibrary関数のアドレスを取得する
  5. CreateRemoteThread関数でリモートプロセス上でLoadLibrary関数を実行し、指定したDLLを読み込ませる
  6. WaitForSingleObject関数でLoadLibrary関数の終了を待つ
  7. スレッドハンドルおよび確保したメモリ領域を解放する

プログラムをコンパイルする際は、リモートプロセスのビット数(32ビットまたは64ビット)に合わせておく必要がある。 このときkernel32.dllはすべてのプロセスで同じアドレスにあるため、取得したLoadLibrary関数のアドレスをリモートプロセスでも用いることができる。

読み込ませるDLLを書いてみる

次に、読み込ませるDLLとして、動作しているプロセスのイメージパスをダイアログボックスで表示するコードを書いてみる。

/* spy.c */
#include <windows.h>
#pragma comment(lib, "user32")

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
    char filename[MAX_PATH];

    switch (fdwReason) {
      case DLL_PROCESS_ATTACH:
        GetModuleFileName(NULL, filename, sizeof(filename));
        MessageBox(NULL, filename, "Hello from", MB_SYSTEMMODAL);
        break;
    }
    return TRUE;
}

DLLインジェクションをやってみる

まず、DLLインジェクションを行うプログラムおよびDLLをそれぞれ64ビットコンパイラコンパイルする。

>cl injector.c

>cl spy.c /LD

適当な64ビットアプリケーション(たとえばメモ帳)を起動し、tasklistコマンドでpidを調べる。 このpidを使い、DLLインジェクションを行うプログラムを実行する。

>notepad

>tasklist

イメージ名                     PID セッション名     セッション# メモリ使用量
========================= ======== ================ =========== ============
(snip)
notepad.exe                   6464 Console                    1      8,288 K
(snip)

>injector.exe 6464

ダイアログボックスが表示され、リモートプロセスからコードが実行されていることが確認できる。

f:id:inaz2:20150808225526p:plain

DLLを使わないコードインジェクション

ここではRemoteCreateThread関数でLoadLibrary関数を呼び出したが、WriteProcessMemory関数で実行可能メモリを確保し、そこにコードをコピーして直接実行させることも可能である。 この場合DLLが不要となる一方、利用するAPI関数一式のアドレスを事前に取得し、構造体などの形でコピーしたコードに渡す必要があるため、複雑なコードの実行には適さない。

関連リンク