環境
windows10 で VisualStudio 使ってる。x64用でDLL作ってる。
DLL Injection 必要。
目的
以下の一秒間隔で "hello" と表示し続けるプログラムを sample.exe
としてgccでコンパイルしたものに対し、Function Hook をかける。具体的には printf("hello\n")
の部分を別の自分が書いたアセンブラの方に飛ばして、次の Sleep(1000)
に戻してくる。
#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セクション の中で起こる。
まず、左の図が 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を作り、以下のようにコードを書く。
#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;
}
大まかな手順としては、
-
Inject
関数で、CodeCaveにpush returnAddr; push puts; ret
を書き込む -
Hook
関数で、元のコードの方のcall puts
をjmp <書き込んだCodeCaveへのアドレス>
に書き換える
という流れで、call puts
がうまく行かなかったので push <&puts>; ret
にしてる。
注意としては push returnAddr; push puts; ret
これの最初の push returnAddr
をしている理由は、puts
を呼び出した後、CodeCaveの方のコードにちゃんと戻ってくるようにするためである。
実際にどうなるかをデバッガで確認しながらコードを理解するのが一番わかりやすい。
x64dbgで挙動を確認
実際に上記のDLLをビルドして、DLL Injectionしてデバッガでどうなるか見てみる。
実行の手順は以下。
-
./sample.exe
でプログラム起動 - x64dbgを起動して、上記プロセスにアタッチする
- x64dbgで
call puts
の所にブレークポイント打ってそこで止めておく - DLL Injection を実行して先ほどのDLLをInject
- x64dbgでブレークポイント残したままとりあえずループ一回分実行させる
- 5の後コードが書き換わっているので、そこからステップ実行とかして挙動確かめる
それなので、まず3までやってみると以下のようになる。
上記のコードで hookAddr = 0x00401564
としていたが、これはこの call <JMP.&puts>
の部分のアドレスである。ここを書き換えたい。
ちなみに機械語を挿入するCodeCaveの部分も見てみる。codecaveAddr = 0x00402cc3
なので、0x402cc3
を見てみると、
たしかに0で埋められていることがわかる。
次に、手順4の通りDLL Injectionをすると、デバッガで止まっているので、DLLがアタッチされた際の MessageBox(NULL, TEXT("hooked"), TEXT("info"), MB_OK);
はまだ出てこないはず。
ここで、手順5の通り一回実行させると、
このようにポップは最前面に出てこないものの、一応ちゃんと "hooked" となっていることがわかる。
またよくアセンブラを見てみると、
先ほどと違って、 call <JMP.&puts>
の所が call sample.402cc3
になっていることがわかる。この402cc3
は codecaveAddr = 0x00402cc3
の通り、CodeCaveに挿入した部分の先頭アドレスなので、手順6の通りステップ実行して、CodeCaveの 0x402cc3
に飛んでみる。
これが、 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ほどしたに行くと、
ちゃんと 0x401569
に戻っていることがわかる。実際にここに処理が来ているのかを確認するため、0x402D21
にブレークポイントを打ち、実行を再開してみると、
ちゃんと来る。
このままステップ実行すると、
このように call sample.402cc3
の直後に戻ってきていることがわかる。
普通に動かしてみて挙動を確認
今回は本来の call puts
をCodeCaveにジャンプするように書き換え、CodeCaveの方で出力する文字とかを変えるとかもしないでただ同じように puts
を呼び出しただけなので、ぶっちゃけ実行しても結果は変わらないが、まぁ一応やってみる。
こんな感じで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)
に置き換えるのは簡単そう。
参考