IAT書き換えによるAPIフックをやってみる

「CreateRemoteThread関数によるDLLインジェクションをやってみる」では、DLLを他のプロセスに読み込ませることにより他プロセス内でのコード実行を行った。 DLLインジェクションの応用のひとつに、他プロセスにおけるAPIフックがある。 これは、プロセスとそのプロセスがロードするモジュールすべてについて特定のAPI関数のIATエントリを書き換え、読み込ませたDLL内の関数を指すようにするものである。 ここでは、DLLインジェクションからのIAT書き換えによるAPIフックをやってみる。

環境

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

APIフックを行うDLLを書いてみる

読み込ませるDLLとして、shell32.dllのShellAboutW関数をフックするプログラムコードを書くと次のようになる。 ShellAboutW関数は、メモ帳や電卓などでバージョン情報のダイアログボックスを表示する際に使われる関数である。

/* hook.c */
#include <windows.h>
#include <tlhelp32.h>              /* CreateToolhelp32Snapshot */
#include <Dbghelp.h>               /* ImageDirectoryEntryToData */
#pragma comment(lib, "Dbghelp")

void modifyIAT(char *modname, void *origaddr, void *newaddr);
void modifyIATonemod(char *modname, void *origaddr, void *newaddr, HMODULE hModule);

int newShellAboutW(HWND hWnd, LPCTSTR szApp, LPCTSTR szOtherStuff, HICON hIcon);
FARPROC origShellAboutW;

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
    HMODULE shell32 = GetModuleHandle("shell32");
    origShellAboutW = GetProcAddress(shell32, "ShellAboutW");

    switch (fdwReason) {
      case DLL_PROCESS_ATTACH:
        modifyIAT("shell32.dll", origShellAboutW, newShellAboutW);
        break;
      case DLL_PROCESS_DETACH:
        modifyIAT("shell32.dll", newShellAboutW, origShellAboutW);
        break;
    }

    return TRUE;
}

void modifyIAT(char *modname, void *origaddr, void *newaddr)
{
    HANDLE hModuleSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, GetCurrentProcessId());

    MODULEENTRY32 me;
    me.dwSize = sizeof(me);

    /* modify IAT in all loaded modules */
    BOOL bModuleResult = Module32First(hModuleSnap, &me);
    while (bModuleResult) {
        modifyIATonemod(modname, origaddr, newaddr, me.hModule);
        bModuleResult = Module32Next(hModuleSnap, &me);
    }

    CloseHandle(hModuleSnap);
}

void modifyIATonemod(char *modname, void *origaddr, void *newaddr, HMODULE hModule)
{
    ULONG ulSize;
    PIMAGE_IMPORT_DESCRIPTOR pImportDesc;
    pImportDesc = ImageDirectoryEntryToData(hModule, TRUE, IMAGE_DIRECTORY_ENTRY_IMPORT, &ulSize);
    if (pImportDesc == NULL) {
        return;
    }

    /* seek the target DLL */
    while (pImportDesc->Name) {
        char *name = (char*)hModule + pImportDesc->Name;
        if (lstrcmpi(name, modname) == 0) {
            break;
        }
        pImportDesc++;
    }
    if (pImportDesc->Name == 0) {
        return;
    }

    /* modify corrensponding IAT entry */
    PIMAGE_THUNK_DATA pThunk = (PIMAGE_THUNK_DATA)((char *)hModule + pImportDesc->FirstThunk);
    while (pThunk->u1.Function) {
        PROC *paddr = (PROC*)&pThunk->u1.Function;
        if (*paddr == origaddr) {
            DWORD flOldProtect;
            VirtualProtect(paddr, sizeof(paddr), PAGE_EXECUTE_READWRITE, &flOldProtect);
            *paddr = newaddr;
            VirtualProtect(paddr, sizeof(paddr), flOldProtect, &flOldProtect);
        }
        pThunk++;
    }
}

int newShellAboutW(HWND hWnd, LPCTSTR szApp, LPCTSTR szOtherStuff, HICON hIcon)
{
    return origShellAboutW(hWnd, L"ALL YOUR BASE ARE BELONG TO US", szOtherStuff, hIcon);
}

上のコードは、ShellAboutW関数をフックし、タイトル文字列の改変を行う。

コードの内容を簡単に説明すると次のようになる。

  1. LoadLibrary関数およびGetProcAddress関数で、ShellAboutW関数のアドレスを取得する
  2. modifyIAT関数を使い、DLLのロード時に改変、アンロード時に復帰を行う
  3. modifyIAT関数では、CreateToolhelp32Snapshot関数で実行状態のスナップショットを取り、すべてのモジュールに対して4を行う
  4. 各モジュールについて、インポートしているDLLからshell32.dllを探し、ShellAboutW関数に対応するアドレスが格納されているIATエントリを書き換える
  5. 置き換える関数(newShellAboutW)では、引数を一部変えた上で本来の関数(origShellAboutW)を呼ぶ

通常IATが置かれているメモリ領域は書き込み不可となっているので、書き換えの前後でVirtualProtect関数を使ってメモリ属性を変更する必要がある。

なお、LoadLibrary関数とGetProcAddress関数で直接ShellAboutW関数のアドレスが取得されている場合、上のコードではフックできないことに注意する。 このような場合にも対応するには、GetProcAddress関数も合わせてフックし、引数に応じた処理を行う必要がある。

DLLインジェクションで読み込ませてみる

「CreateRemoteThread関数によるDLLインジェクションをやってみる」で作成したものと同じコードを用い、上のDLLを読み込ませてみる。

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

int main(int argc, char *argv[])
{
    char dllpath[] = "C:\\Users\\user\\Desktop\\test\\hook.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インジェクションを行うプログラムおよびDLLをそれぞれ64ビットコンパイラコンパイルする。

>cl injector.c

>cl hook.c /LD

メモ帳を起動し、tasklistコマンドでpidを調べる。 このpidを使い、DLLインジェクションを行うプログラムを実行する。

>notepad

>tasklist

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

>injector.exe 5136

「ヘルプ」「バージョン情報」からダイアログボックスを開くと、タイトル文字列が書き換わっていることが確認できる。

f:id:inaz2:20150810123213p:plain

メモリパッチによるAPIフック

ここでは書き換える対象としてIATエントリを選んだが、関数そのものの書き換えによりAPIフックを行うことも可能である。 汎用APIフックライブラリであるDetoursでは、関数の先頭5バイトをjmp命令(e9 xx xx xx xx)に書き換え、目的の関数に飛ばすことでAPIフックを行っている。 また、32ビット環境では各API関数の先頭に2バイトNOP(mov edi, edi)があり、これは直前にある5バイトの空きスペースと合わせて次のように書き換えてフックに用いることが想定されている(hot patching)。

  cc cc cc cc cc    int3 (x5)
func:
  8b ff             mov edi, edi

↓

  e9 xx xx xx xx    jmp 0xXXXXXXXX
func:
  eb f9             jmp func-5

関連リンク