オルトプラスエンジニアの日常をお伝えします!

APIフックに負けない強い体づくり

この記事は Altplus Advent Calendar 2017 の19日目のエントリです。

※Qiitaに書いた記事を転載しています。

こんにちわ。id:takuya-kikuchiです。

前回notepad.exeにAPIフックをしかけてイタズラする手法を紹介しました。 今回は、そういったAPIフックによる攻撃からいかに身を守るか、という方法を紹介したいと思います。方法は色々とありますが、APIフックの脅威からはAPIフックで身を守ることにしましょう。俺はこの力を正しい方向に使うんだ。notepadは俺が守る。

とりあえずDLLインジェクション

APIフックしたいので、とりあえずDLLをインジェクションさせてもらいましょう。正義のためだからいいよね。手法は前回と同様、SetWindowsHookExを使います。

また、今回は守る側が先にDLLインジェクションを仕掛けることとします。一般的に、この種の戦いは先手を取られるとかなり不利です。

さて、では具体的にどのような方法でnotepad.exeを守るか考えましょう。

特定のAPIがフックされていることを検知する

実際に危害を加えてきた場合にそれを検知するパターンです。今回、敵はCreateFileWをフックして悪事に及ぶことはわかっているので、こちらもCreateFileWをフックして対処しましょう。

dllの初期化処理はこちらです。 フックと、後々行うStackWalk64のための初期化処理(SymInitialize)を行なっています。

void ShieldNotepad() {
    TCHAR fileNameBuf[MAX_PATH];

    // notepad.exeのみを対象にする
    if (GetModuleFileName(GetModuleHandle(NULL), fileNameBuf, MAX_PATH) > 0) {
        if (_tcsstr(fileNameBuf, TEXT("notepad.exe")) != NULL) {
            // StackWalk用の初期化処理
            SymInitialize(GetCurrentProcess(), NULL, TRUE);
            // フックする。戻り値は本来のCreateFileWのアドレス
            createFileWPtr = (fpCreateFileW)RewriteFunction("kernel32.dll", "CreateFileW", ShieldedCreateFileW);
        }
    }
}

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        ShieldNotepad();
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

フックした関数はこちら。フックされてたら死ぬ覚悟です。

HANDLE
WINAPI
ShieldedCreateFileW(
    _In_ LPCWSTR lpFileName,
    _In_ DWORD dwDesiredAccess,
    _In_ DWORD dwShareMode,
    _In_opt_ LPSECURITY_ATTRIBUTES lpSecurityAttributes,
    _In_ DWORD dwCreationDisposition,
    _In_ DWORD dwFlagsAndAttributes,
    _In_opt_ HANDLE hTemplateFile
)
{
    TerminateIfHooked();
    return createFileWPtr(lpFileName, dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile);
}

さて、TerminateIfHookedの実装はこちらです。

void TerminateIfHooked() {
    CONTEXT context;
    STACKFRAME64 stackFrame;

    // StackWalkのためのコンテキストの準備。
    RtlCaptureContext(&context);

    // StackWalkのために、現在のスタックフレームの状態を設定。
    ZeroMemory(&stackFrame, sizeof(STACKFRAME64));
    stackFrame.AddrPC.Offset = context.Rip;
    stackFrame.AddrPC.Mode = AddrModeFlat;
    stackFrame.AddrFrame.Offset = context.Rsp;
    stackFrame.AddrFrame.Mode = AddrModeFlat;
    stackFrame.AddrStack.Offset = context.Rsp;
    stackFrame.AddrStack.Mode = AddrModeFlat;

    auto frameCount = 0;

    // 深さ10くらいまで探索。特に意味はない
    while (frameCount < 10)
    {
        // 1個上のスタックフレームを取得
        if (!StackWalk64(IMAGE_FILE_MACHINE_AMD64, GetCurrentProcess(), GetCurrentThread(), &stackFrame, &context, NULL, SymFunctionTableAccess64, SymGetModuleBase64, NULL)) {
            break;
        }

        // ただしく取得できていたら、モジュール情報をチェック
        if (stackFrame.AddrPC.Offset != 0)
        {
            HMODULE mod;
            // アドレスから、属するモジュールベースを取得してくれるAPI
            if (GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, (LPCWSTR)stackFrame.AddrPC.Offset, &mod)) {
                TCHAR fileNameBuf[MAX_PATH];
                // モジュール名を取得
                if (GetModuleFileNameW(mod, fileNameBuf, sizeof(fileNameBuf)) > 0) {
                    if (wcsstr(fileNameBuf, TEXT("WaruiDll.dll")) != NULL) {
                        // アラートを表示して
                        MessageBoxW(NULL, TEXT("API Hooking Detected!"), TEXT("Good bye."), MB_OK);
                        // さようなら
                        TerminateProcess(GetCurrentProcess(), 0);
                    }
                }
            }
            frameCount++;
        }
        else {
            break;
        }
    }
    return;
}

少しわかりづらいのですが、やりたいことは、スタックトレースを1個ずつ上っていき、モジュール名を取得。気にくわない名前が見つかったら自害する、というかんじです。

もちろん、きちんと対策するのであれば、StackWalk64がフックされている可能性も考えなければなりません。

他にも方法はある

悪意あるDLLのロード自体を妨害することも有効でしょうし、わざわざCreateFileWが呼ばれなくとも、主要なAPIがフックされてる時点で自害するという手もあります。もうちょっと色々試したかったのですが、今回はここまで。

今回のコードはこちらにアップしてあります。くれぐれもご利用は慎重に、自己責任でよろしくお願いします。

おわりに

今回は、APIフックに対してAPIフックで応える形で、notepad.exeを守る方法について検討しました。

なお今回は守る側が先にフックを仕掛けている前提でしたが、逆の場合もあるでしょう。そうなると、また方針も変わってきます。DLLのロード順序の先取り合戦も、熾烈な争いが繰り広げられていることでしょう。さらには、CreateRemoteThread等による、DLL Injectionを伴わないような攻撃も当然考えられます。

また、今回はユーザ空間のみについて考えましたが、カーネルドライバが混ざってくるとより戦いは激化することになります。悪意あるハッカーにカーネルドライバを混入させられたら負けだ。気をつけましょう。

ともあれ、攻撃も防御も完璧などありえません。この戦いに終わりもありません。セキュリティいたちごっこは今日も続きます

あなたのアプリも狙われているかもしれませんよ。 それではまた。