LoginSignup
5
1

More than 1 year has passed since last update.

x64インラインフッキング(Detour, CodeCaves, Trampoline)

Last updated at Posted at 2021-06-08

環境

windows10 で VisualStudio 使ってる。x64用でDLL作ってる。
DLL Injection 必要。

目的

以下の一秒間隔で "hello" と表示し続けるプログラムを sample.exe としてgccでコンパイルしたものに対し、Function Hook をかける。具体的には printf("hello\n") の部分を別の自分が書いたアセンブラの方に飛ばして、次の Sleep(1000) に戻してくる。

sample.c
#include <stdio.h>
#include <windows.h>

int main(void) {
    while(1) {
        printf("hello\n");
        Sleep(1000);
    }
    return 0;
}

※ ちなみになぜかコンパイルすると printf ではなく puts が呼ばれているので注意。


IAT Hook と Inline Hook の違い

IAT Hook の利点

  • PIEとか関係ない(たぶん)
  • どこで何やってるかをアセンブリで解読しなくても、IATだけ書き換えればいい

IAT Hook の欠点

  • IATエントリのアドレスを対応するモジュールのアドレスの範囲内にあるかのチェックですぐ検出される
  • ランタイムリンクでモジュールが利用される時に使えない(IAT再構築が必要)
  • 関数が序数としてインポートされてるとできないらしい
  • そもそもAPIしかフックできない
  • WindowsのPatchGuard により弾かれるらしい

Inline Hook の利点

  • APIだけでなく任意のコードをフックできる
  • モジュールがランタイムリンクされていようが関係ない

Inline Hook の欠点

  • アセンブリのどの部分で何をしているのかを把握しないといけない
  • (この方法では)フックした後の処理を機械語で書かないといけない…
  • PIE有効の場合アドレスが変わるので、これの対策しないといけない

__asm を使ってる記事が多いですが、x64の場合はなんか __asm 使えないっぽいので機械語です
※ 今回はPIE無しの場合


Inline Hook の仕組み

まず、Inline Hook、Inline Patch、Function Hook、Detour はどれも同じような意味。
大まかな流れは以下のような感じで、全て textセクション の中で起こる。

image.png

まず、左の図が sample.exe の textセクション の様子で、textセクションには実際のプログラムのアセンブリと、その下にパディングのような用途で0で埋められた部分がある。この0で埋められた部分を "Code Cave" というらしい。

Inline Hook を行った場合、フックしたい部分、すなわち今回だと puts の部分を jmp 命令に置き換えて、自分が実行させたいアセンブラを書き込んだ Code Cave に処理を移す。
最後に、元の処理に戻れるように、フックした部分(call puts)の下の命令に飛ばす。

jmp で置換したコードや、Code Cave に書いた最後の元の処理に戻す命令は "Trampoline" と呼ばれる。


Inline Hook やり方

DLL Injection を結局使うので、Visual Studio で新規DLLを作り、以下のようにコードを書く。

dllmain.cpp
#include "pch.h"
#include <Windows.h>


// jmpに置き換える
bool Hook(void* target, void* myfunc, int len) {
    if (len < 5)
        return false;

    DWORD curProtection;
    VirtualProtect(target, len, PAGE_EXECUTE_READWRITE, &curProtection);
    memset(target, 0x90, len);

    DWORD relativeAddr = ((DWORD)myfunc - (DWORD)target) - 5;
    *(BYTE*)target = 0xe8;
    *(DWORD*)((DWORD)target + 1) = relativeAddr;

    DWORD temp;
    VirtualProtect(target, len, curProtection, &temp);
    return true;
}


// CodeCaveに機械語書き込む
bool Inject(void *addr, void *retaddr, int len) {
    DWORD curProtection;
    VirtualProtect(addr, len, PAGE_EXECUTE_READWRITE, &curProtection);
    memset(addr, 0x90, len);
    DWORD putsAddr = 0x00402a60;

    // inject code
    *(BYTE*)((DWORD)addr)      = 0x68;
    *(DWORD*)((DWORD)addr + 1) = (DWORD)addr+11 ; // push return address
    *(BYTE*)((DWORD)addr + 5)  = 0x68;
    *(DWORD*)((DWORD)addr + 6) = (DWORD)putsAddr; // push <&puts>
    *(BYTE*)((DWORD)addr + 10) = 0xc3;            // ret

    // jump back to original flow
    *(BYTE*)((DWORD)addr + len - 6)  = 0x68;
    *(DWORD*)((DWORD)addr + len - 5) = (DWORD)retaddr; // push imm32
    *(BYTE*)((DWORD)addr + len - 1)  = 0xc3;           // ret

    DWORD temp;
    VirtualProtect(addr, len, curProtection, &temp);
    return true;
}


DWORD WINAPI dothread(LPVOID param) {
    int hookLength = 5;
    DWORD hookAddr = 0x00401564;
    int codecaveLength = 100;
    DWORD codecaveAddr = 0x00402cc3;
    DWORD jmpBackAddy = hookAddr + hookLength;

    Inject((void*)codecaveAddr, (void*)jmpBackAddy, codecaveLength);
    if (Hook((void*)hookAddr, (void*)codecaveAddr, hookLength))
        MessageBox(NULL, TEXT("hooked"), TEXT("info"), MB_OK);

    while (1) {
        if (GetAsyncKeyState(VK_ESCAPE))
            break;
        Sleep(50);
    }

    FreeLibraryAndExitThread((HMODULE)param, 0);
    return 0;
}


BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {
    if (ul_reason_for_call == DLL_PROCESS_ATTACH)
        CreateThread(0, 0, dothread, hModule, 0, 0);
    return TRUE;
}

大まかな手順としては、

  1. Inject関数で、CodeCaveに push returnAddr; push puts; ret を書き込む
  2. Hook関数で、元のコードの方の call putsjmp <書き込んだCodeCaveへのアドレス> に書き換える

という流れで、call puts がうまく行かなかったので push <&puts>; ret にしてる。
注意としては push returnAddr; push puts; ret これの最初の push returnAddr をしている理由は、puts を呼び出した後、CodeCaveの方のコードにちゃんと戻ってくるようにするためである。
実際にどうなるかをデバッガで確認しながらコードを理解するのが一番わかりやすい。


x64dbgで挙動を確認

実際に上記のDLLをビルドして、DLL Injectionしてデバッガでどうなるか見てみる。
実行の手順は以下。

  1. ./sample.exe でプログラム起動
  2. x64dbgを起動して、上記プロセスにアタッチする
  3. x64dbgで call puts の所にブレークポイント打ってそこで止めておく
  4. DLL Injection を実行して先ほどのDLLをInject
  5. x64dbgでブレークポイント残したままとりあえずループ一回分実行させる
  6. 5の後コードが書き換わっているので、そこからステップ実行とかして挙動確かめる

それなので、まず3までやってみると以下のようになる。

image.png

上記のコードで hookAddr = 0x00401564 としていたが、これはこの call <JMP.&puts> の部分のアドレスである。ここを書き換えたい。
ちなみに機械語を挿入するCodeCaveの部分も見てみる。codecaveAddr = 0x00402cc3 なので、0x402cc3 を見てみると、

image.png

たしかに0で埋められていることがわかる。
次に、手順4の通りDLL Injectionをすると、デバッガで止まっているので、DLLがアタッチされた際の MessageBox(NULL, TEXT("hooked"), TEXT("info"), MB_OK); はまだ出てこないはず。
ここで、手順5の通り一回実行させると、

image.png

このようにポップは最前面に出てこないものの、一応ちゃんと "hooked" となっていることがわかる。
またよくアセンブラを見てみると、

image.png

先ほどと違って、 call <JMP.&puts> の所が call sample.402cc3 になっていることがわかる。この402cc3codecaveAddr = 0x00402cc3 の通り、CodeCaveに挿入した部分の先頭アドレスなので、手順6の通りステップ実行して、CodeCaveの 0x402cc3 に飛んでみる。

image.png

これが、 push returnAddr; push puts; ret の部分で、実際に挿入した機械語の通りになっていることがわかる。
コードの方で、実際に元の処理に戻る際のものが以下だったが、

// jump back to original flow
*(BYTE*)((DWORD)addr + len - 6)  = 0x68;
*(DWORD*)((DWORD)addr + len - 5) = (DWORD)retaddr; // push imm32
*(BYTE*)((DWORD)addr + len - 1)  = 0xc3;           // ret

ここで、len は今回は大きく codecaveLength = 100 のように100取っているので、100Byteほどしたに行くと、
image.png
ちゃんと 0x401569 に戻っていることがわかる。実際にここに処理が来ているのかを確認するため、0x402D21 にブレークポイントを打ち、実行を再開してみると、
image.png
ちゃんと来る。
このままステップ実行すると、
image.png
このように call sample.402cc3 の直後に戻ってきていることがわかる。


普通に動かしてみて挙動を確認

今回は本来の call puts をCodeCaveにジャンプするように書き換え、CodeCaveの方で出力する文字とかを変えるとかもしないでただ同じように puts を呼び出しただけなので、ぶっちゃけ実行しても結果は変わらないが、まぁ一応やってみる。

image.png

こんな感じでInjectした後も、helloをと表示され続ければOK。
もっとちゃんと確認したければ、

bool Inject(void *addr, void *retaddr, int len) {
    DWORD curProtection;
    VirtualProtect(addr, len, PAGE_EXECUTE_READWRITE, &curProtection);
    memset(addr, 0x90, len);
    DWORD putsAddr = 0x00402a60;

    // inject code
    // *(BYTE*)((DWORD)addr)      = 0x68;
    // *(DWORD*)((DWORD)addr + 1) = (DWORD)addr+11 ; // push return address
    // *(BYTE*)((DWORD)addr + 5)  = 0x68;
    // *(DWORD*)((DWORD)addr + 6) = (DWORD)putsAddr; // push <&puts>
    // *(BYTE*)((DWORD)addr + 10) = 0xc3;            // ret

    // jump back to original flow
    *(BYTE*)((DWORD)addr + len - 6)  = 0x68;
    *(DWORD*)((DWORD)addr + len - 5) = (DWORD)retaddr; // push imm32
    *(BYTE*)((DWORD)addr + len - 1)  = 0xc3;           // ret

    DWORD temp;
    VirtualProtect(addr, len, curProtection, &temp);
    return true;
}

このようにCodeCaveの方で puts を呼び出す処理をコメントアウトしてビルドした後、このDLLをInjectしたときに "hello" という表示が止まればOK。


ゲームチートにおける IAT Hook と Inline Hook

IAT Hook を習得した後、実際に自分で作ったUnityのゲームで当たり判定を無効化するために、InterSectRectみたいなAPIをフックしようとしたが、どうやら動的にこのAPIのDLLがリンクされているのか、パッキングされているのかわからないが、このAPIのアドレスがわからずフックできなかった。

どっかの記事でUnityのゲームはパッキングされているという話を聞いたので、IAT再構築を習得して、アンパックをしようと思ったが、そもそもゲームのエントリーポイントとかよくわからないしメンドクサイ。

IAT系は無理だと思い、今回のInline Hookを習得してみた。CheatEngineには変数に値を書き込んでる処理のアセンブリとかそういうのが見れたりするので、こっちの方が使いやすいと思った。
機械語で書かないといけないのがかなり痛いが、全部 nop(0x90) に置き換えるのは簡単そう。


参考

5
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1