APIフック (Cライブラリ関数やWindowsAPIの書き換え) と、その応用例

  • 54
    いいね
  • 2
    コメント
この記事は最終更新日から1年以上が経過しています。

概要

モジュールの実行可能領域のインポートセクションを書き替えることで、
Cライブラリ関数やWindowsAPI関数の呼び出しを別の関数呼び出しに差し替えます。

何故これが必要か

プロジェクト内のソースがすべて自前で管理されているなら、
置き換えたい関数のラッパー関数を作るとか、もしくは美醜はさておき define で置換もできますし、
今回みたいな大げさなことをする必要はあんまりないです。

問題はサードパーティ製ライブラリの挙動です。
製作元を介さずにこれを書き替える必要って多々あります。
以下、「使用例」の項にいくつか例を示します。

影響範囲など

DLLの関数置き換えなどが発生するわけですが、
「え、それって他アプリにも影響するんじゃないの?」って思う方もおられるかもしれませんが、
結論を言うと他アプリには影響しません。(影響を与える手法も存在しますが今回は触れません)
何故なら、書き替えるのは「実行プロセスの仮想アドレス空間上の」インポートセクションに過ぎないからです。

仮想アドレス空間について知りたい方はググりましょう。

サンプル

手っ取り早く動かしてみたい方は以下のサンプルをお使いください。
(Visual Studio 2010 プロジェクトになっています)

https://github.com/kobake/ApiHookSample

機能宣言

rewrite.h
/*! 関数コール書き換え。
    @param [in] szRewriteModuleName     書き替えたいモジュール名
    @param [in] szRewriteFunctionName   書き替えたい関数名
    @param [in] pRewriteFunctionAddress 書き替え後の関数アドレス
    @return 元の関数アドレス
*/
void* RewriteFunction(
    const char* szRewriteModuleName,
    const char* szRewriteFunctionName,
    void*       pRewriteFunctionAddress
);
void PrintFunctions();

機能実装はとりあえず

長いので後述。

使い方

機能実装部分はさておき、とりあえず先に使い方を。

(Step1)目的の関数の正確な名前とモジュール名を特定する

PrintFunctions を呼ぶと参照されているモジュールと関数の一覧が出力されます。
その中から書き替えたい関数の実際の名前とモジュール名を特定しましょう。

rewrite_usage_print.cpp
#include <stdio.h>
#include "rewrite.h"
int main()
{
    PrintFunctions();
    if(0){
        // ※実際にプログラム内で使われている関数が表示されるので
        //  書き替えたい関数が決まっていればダミーでも良いので
        //  どこかで呼んでおく。
        getchar();
    }
    return 0;
}
PrintFunctions実行結果
…
Module:KERNEL32.dll Hint:234, Name:EncodePointer
Module:KERNEL32.dll Hint:532, Name:GetModuleFileNameW
Module:USER32.dll Hint:526, Name:MessageBoxA
Module:imagehlp.dll Hint:18, Name:ImageDirectoryEntryToData
…
Module:MSVCR100D.dll Hint:289, Name:_CRT_RTC_INITW
Module:MSVCR100D.dll Hint:1289, Name:_wassert
Module:MSVCR100D.dll Hint:1499, Name:getchar ← 今回はこれを書き替えてみます
Module:MSVCR100D.dll Hint:1474, Name:fopen
Module:MSVCR100D.dll Hint:1560, Name:printf
…

(Step2)ヘッダから宣言を探す

Visual Studio なら関数名を右クリックで「宣言を探す」とかで。

stdio.h 抜粋
_Check_return_ _CRTIMP int __cdecl getchar(void);

(Step3)関数コールを書き替える

元の関数のシグニチャ通りに自前の関数を定義して、
その関数で元の関数を置き換えてみます。

関数は置き換えっぱなしでもあんまり問題起こらなかったりもしますが、
元に戻しておいたほうが無難でしょう。戻すか戻さないかは用途にもよりけりです。

rewrite_usage_rewrite.cpp
#include <stdio.h>
#include "rewrite.h"

int __cdecl mygetchar(void)
{
    return 'A';
}
int main()
{
    void* p = RewriteFunction("MSVCR100D.dll", "getchar", mygetchar);
    printf("%c\n", getchar());
    RewriteFunction("MSVCR100D.dll", "getchar", p); // 戻す
    return 0;
}

使用例

自分が実際に行ったことのある具体的な活用例を挙げてみます。

assertを無視してみる

サードパーティ製のライブラリ内に不要な assert が入っていることが原因で、デバッグ版ビルドでは正常に動かず、仕方なくリリース版ビルドでデバッグするという悲しい事態が発生していることがありました。

製作元の会社は音信どころかそもそも存在がなくなっている状態。さぁどうしよう。
そこでAPIフックの出番です。assert 呼び出しを別の関数呼び出しに差し替えてしまい、
チェック結果をスルーすることで
無事デバッグ版ビルドでのデバッグ環境が構築できました。(動作の保障はまた別の話)

rewrite_sample_assert.cpp
#include <assert.h>
#include <stdio.h>
#include "rewrite.h"
void __cdecl _wassert2(const wchar_t* expr, const wchar_t* filename, unsigned lineno)
{
    // ※ここで filename や lineno で条件チェックを行うほうがより丁寧ですが
    //  とりあえず今回は全スキップしてみました
}
int main()
{
    void* p = RewriteFunction("MSVCR100D.dll", "_wassert", _wassert2);
    printf("start\n");
    assert(0); // このassertはスルーされる
    printf("end\n");
    RewriteFunction("MSVCR100D.dll", "_wassert", p); // 戻す
    return 0;
}

timeを書き替えてみる

ある期間にだけ動作するアプリがあり、デバッグのためにマシンの時計をずらす必要がありました。
ただ、マシンの時計をずらすと多様なソフトウェアに影響が及び正直ダルい。
(たとえばスカイプの会話ログなんかグチャグチャになります)

じゃあ、アプリ内で呼ばれている time をすべて別の関数に置換 (define) すれば良いかというと、
やはりこれもうまく動く保障はありません。
何故ならアプリが依存しているサードパーティ製ライブラリ(Cライブラリ等も含め)が内部で time を呼んでいる可能性があるからです。それらも含めてまるごと time を置き換える必要がある。そこでやはりAPIフックです。

rewrite_sample_time.cpp
#include <stdio.h>
#include <time.h>
#include "rewrite.h"
typedef __time64_t (__cdecl *time64type)(__time64_t* t);
time64type orgtime;
__time64_t __cdecl mytime64(__time64_t* t)
{
    __time64_t value = orgtime(t);
    value += 60 * 60 * 24; // 1日進める
    if(t)*t = value;
    return value;
}
int main()
{
    printf("time: %d\n", time(NULL));
    orgtime = (time64type)RewriteFunction("MSVCR100D.dll", "_time64", mytime64);
    printf("time: %d\n", time(NULL));
    RewriteFunction("MSVCR100D.dll", "_time64", orgtime); // 戻す
    return 0;
}

ファイルアクセスをロギングする

既存関数の動作を変えずに、ログを注入する、などの用途にも使えます。
たとえばファイルアクセスをウォッチするために CreateFile を書き替えてログ関数を仕込むなど。

ちなみに Windows 2000 以降ではファイルアクセスは内部的には CreateFileW が最終的には呼ばれる(と記憶していますが間違ってたらごめんなさい)ので、CreateFileW だけAPIフックしておけばだいたい事が足ります。

rewrite_sample_file.cpp
#include <stdio.h>
#include <Windows.h>
#include "rewrite.h"

typedef HANDLE (WINAPI *CreateFileTypeW)(
    LPCWSTR name, DWORD dw1, DWORD dw2, LPSECURITY_ATTRIBUTES lps,
    DWORD dw3, DWORD dw4, HANDLE h);

CreateFileTypeW org;

HANDLE WINAPI MyCreateFileW(
    LPCWSTR name, DWORD dw1, DWORD dw2, LPSECURITY_ATTRIBUTES lps,
    DWORD dw3, DWORD dw4, HANDLE h)
{
    printf("CreateFileW: %ls\n", name);
    return org(name, dw1, dw2, lps, dw3, dw4, h);
}

int main()
{
    org = (CreateFileTypeW)RewriteFunction("KERNEL32.dll", "CreateFileW", MyCreateFileW);
    fopen("app.fopen", "rb");
    CreateFileW(L"app.createfileW", GENERIC_READ, 0, NULL,
        OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    CreateFileA("app.createfileA", GENERIC_READ, 0, NULL,
        OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    RewriteFunction("KERNEL32.dll", "CreateFileW", org); // 戻す
    return 0;
}

機能実装

ちょっと折り返しが気持ち悪くて申し訳ないですが、
ただ使うだけならコピれば使えます。

rewrite.cpp
#include <stdio.h>
#include <windows.h>
#include <imagehlp.h>
#pragma comment(lib,"imagehlp.lib")

void* RewriteFunctionImp(const char* szRewriteModuleName, const char* szRewriteFunctionName, void* pRewriteFunctionPointer)
{
    for(int i = 0; i < 2; i++){
        // ベースアドレス
        DWORD dwBase = 0;
        if(i == 0){
            if(szRewriteModuleName){
                dwBase = (DWORD)(intptr_t)::GetModuleHandleA(szRewriteModuleName);
            }
        }
        else if(i == 1){
            dwBase = (DWORD)(intptr_t)GetModuleHandle(NULL);
        }
        if(!dwBase)continue;

        // イメージ列挙
        ULONG ulSize;
        PIMAGE_IMPORT_DESCRIPTOR pImgDesc = (PIMAGE_IMPORT_DESCRIPTOR)ImageDirectoryEntryToData((HMODULE)(intptr_t)dwBase, TRUE, IMAGE_DIRECTORY_ENTRY_IMPORT, &ulSize);
        for(; pImgDesc->Name; pImgDesc++){
            const char* szModuleName = (char*)(intptr_t)(dwBase+pImgDesc->Name);
            // THUNK情報
            PIMAGE_THUNK_DATA pFirstThunk = (PIMAGE_THUNK_DATA)(intptr_t)(dwBase+pImgDesc->FirstThunk);
            PIMAGE_THUNK_DATA pOrgFirstThunk = (PIMAGE_THUNK_DATA)(intptr_t)(dwBase+pImgDesc->OriginalFirstThunk);
            // 関数列挙
            for(;pFirstThunk->u1.Function; pFirstThunk++, pOrgFirstThunk++){
                if(IMAGE_SNAP_BY_ORDINAL(pOrgFirstThunk->u1.Ordinal))continue;
                PIMAGE_IMPORT_BY_NAME pImportName = (PIMAGE_IMPORT_BY_NAME)(intptr_t)(dwBase+(DWORD)pOrgFirstThunk->u1.AddressOfData);
                if(!szRewriteFunctionName){
                    // 表示のみ
                    printf("Module:%s Hint:%d, Name:%s\n", szModuleName, pImportName->Hint, pImportName->Name);
                }
                else{
                    // 書き換え判定
                    if(stricmp((const char*)pImportName->Name, szRewriteFunctionName) != 0)continue;

                    // 保護状態変更
                    DWORD dwOldProtect;
                    if( !VirtualProtect(&pFirstThunk->u1.Function, sizeof(pFirstThunk->u1.Function), PAGE_READWRITE, &dwOldProtect) )
                        return NULL; // エラー

                    // 書き換え
                    void* pOrgFunc = (void*)(intptr_t)pFirstThunk->u1.Function; // 元のアドレスを保存しておく
                    WriteProcessMemory(GetCurrentProcess(), &pFirstThunk->u1.Function, &pRewriteFunctionPointer, sizeof(pFirstThunk->u1.Function), NULL);
                    pFirstThunk->u1.Function = (DWORD)(intptr_t)pRewriteFunctionPointer;

                    // 保護状態戻し
                    VirtualProtect(&pFirstThunk->u1.Function, sizeof(pFirstThunk->u1.Function), dwOldProtect, &dwOldProtect);
                    return pOrgFunc; // 元のアドレスを返す
                }
            }
        }
    }
    return NULL;
}

void* RewriteFunction(const char* szRewriteModuleName, const char* szRewriteFunctionName, void* pRewriteFunctionPointer)
{
    return RewriteFunctionImp(szRewriteModuleName, szRewriteFunctionName, pRewriteFunctionPointer);
}

void PrintFunctions()
{
    printf("----\n");
    RewriteFunctionImp(NULL, NULL, NULL);
    printf("----\n");
}