polyhook2とeatpdbでexportされていない関数をフック

#初めに
[OIC ITCreate Club Advent Calendar 2020](https://adventar.org/calendars/5079)
12/6の記事です。

#利用する例
関数フックを利用することで、とあるゲームの公式サーバーソフトウェアを魔改造したり出来ます。
また、C++で作られたゲームのModなども開発できます。

#必要な環境
- Windows 10
- VisualStudio Community 2019
- Vcpkg

# 今回使うツール・ライブラリ
https://github.com/stevemk14ebr/PolyHook_2_0
https://github.com/codehz/EatPdb

# Vcpkgのインストール
``` vcpkg_install.bat
git clone https://github.com/microsoft/vcpkg
call .\vcpkg\bootstrap-vcpkg.bat
```
これでVcpkgのインストールが終わりました。
次に設定をします

今回はx64で実行するので、以下のコマンドを実行します。

``` vcpkg_setup.bat
.\vcpkg\vcpkg --triplet x64-windows-static integrate install
```

win32などの場合は、適材適所設定をしてください。

# Vcpkgでライブラリをインストール

``` vcpkg_pkg_install.bat
.\vcpkg\vcpkg --triplet x64-windows-static install polyhook2:x64-windows
```

インストールが終わるまで待ちます。
インストールが完了したら、後はコードを書くだけです。

# Visual Studio でコードを書く
今回は手軽に解説する為、コンソールアプリケーションで関数フックをされる側のアプリケーションを作りたいと思います。

## フックされる側
コンソールアプリケーションのプロジェクトを作成します。
また、アーキテクチャをx64に変更してください。
main関数を消して下記のコードを貼り付けてください。

``` プロジェクト名.cpp
#include <iostream>

void printHello() 
{
    std::cout << "Hello World!" << std::endl;
}

int main()
{
    printHello();
}
```
シンプルに 'Hello world!'と表示されます。

## フックをする側
次にフックをする側のアプリケーションを作成します。

C++のダイナミック リンク ライブラリのプロジェクトを作成してください。
また、アーキテクチャをx64に変更してください。
プロジェクトのプロパティからC++ 17に変更してください。

DllMain.cppを下記のコードに変更してください

``` DllMain.cpp
// dllmain.cpp : DLL アプリケーションのエントリ ポイントを定義します。
#include "pch.h"
#include <functional>
#include <polyhook2/Detour/x64Detour.hpp>
#include <polyhook2/CapstoneDisassembler.hpp>

// ベースアドレス
constexpr uint64_t base = 0x140000000;
// フックするアドレスのRVA
constexpr uint64_t mainAddress = 0x00000000;
constexpr uint64_t address = 0x00000000;

std::shared_ptr<PLH::x64Detour> hook;

uint64_t origFunc;

typedef void(_stdcall* tUpdateHook)();

// 関数フックの内容
void updateHookFunc() {
    // コードをここに(1)
    //オリジナルの関数呼び出し
    reinterpret_cast<tUpdateHook>(origFunc)();
    // コードをここに(2)
}

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    PLH::CapstoneDisassembler dis(PLH::Mode::x64);

    switch (ul_reason_for_call)
    {
    // Dllが読み込まれた時
    case DLL_PROCESS_ATTACH:
        std::cout << "inject dll" << std::endl;

        // フックされる側のExeを指定
        module = GetModuleHandle(L"フックされる側のExe_Hacked.exe");
        if (module == NULL)
        {
            std::cout << "error module" << std::endl;
            return FALSE;
        }

        // 絶対アドレスの計算
        baseAddress = reinterpret_cast<const uint64_t>(GetProcAddress(module, "main")) - (mainAddress + base);

        // ここでフックを作成する x86で使いたい場合は、x86Detour を使う
        hook = std::make_shared<PLH::x64Detour>(baseAddress + address + base, reinterpret_cast<uint64_t>(&updateHookFunc), &origFunc, dis);
        if (hook->hook()) {
            std::cout << "hook success" << std::endl;
        }
        else
        {
            std::cout << "hook fail" << std::endl;
        }
        break;

    // Dllがアンロードされた時又は、終了時
    case DLL_PROCESS_DETACH:
        hook->unHook();
        hook = nullptr;
        break;
    }
    return TRUE;
}


```

後でフックされる側のExeを指定の部分を置き換えるのを忘れないように
ファイル名の最後に_Hackedを付けて下さい。
後で生成されるファイルを利用するためです。

変更が出来たら、次にEatPdbというツールでフックされる側のExeを書き換えます
https://github.com/codehz/EatPdb/releases/tag/v0.0.5

ダウンロードをしたら、解凍して分かりやすい場所においてください。

フックする側のexeがある階層に eatpdb.yaml というファイルを作成してください。

``` eatpdb.yaml
in: (Exeの名前).exe
out: (Exeの名前)_Hacked.exe
filterdb: addition_symbols.db
filter: !blacklist
  - prefix: "_"
  - prefix: "?__"
  - prefix: "??_"
  - prefix: "??@"
  - prefix: "?$TSS"
  - regex: "std@@[QU]"
  - name: "atexit"
```
(Exeの名前)はいい感じに置き換えてください。

そして、以下のコマンドを実行

``` eatpdb.bat
"eatpdb.exeの場所" exec "eatpdb.yaml"
```

いくつかのファイルが生成されます。
次に VisualStudioの x64 Native Tools Command Prompt を開きます

cd コマンドを使ってフックする側のexeがある階層に移動してください。

そして、コマンドを実行します。

``` exports.bat
dumpbin /exports "(Exeの名前)_Hacked.exe" > dump.txt
```

dump.txtをテキストエディタで、printHelloとmain を検索してください。

```
6   10 00012480 ?printHello@@YAXXZ = ?printHello@@YAXXZ (void __cdecl printHello(void))
7   18 00012500 main = main
```

こんな感じの場所がヒットすると思います。
8桁の16進数をコピーして、(ここでは00012480)
DLLMain.cppの フックするアドレスのRVAの数値を書き換えます。

``` DLLMain.cpp
// フックするアドレスのRVA
constexpr uint64_t mainAddress = 0x00012480;
constexpr uint64_t address = 0x00012500;
```

## DLLインジェクション
最後に、Hookする側のDLLをされる側に注入します。

詳しい説明は割愛します。
(検索すると結構出てきます)

今回はC#でコードを書きます。
コンソールアプリケーションを作成します。
また、アーキテクチャをx64に変更してください。

<details><summary>DLLインジェクションのコード</summary><div>

``` Karnel.cs
    using System;
    using System.Runtime.InteropServices;
    using System.Text;

    public class Kernel32
    {
        [StructLayout(LayoutKind.Sequential)]
        public struct STARTUPINFO
        {
            public int cb;
            public string lpReserved;
            public string lpDesktop;
            public int lpTitle;
            public int dwX;
            public int dwY;
            public int dwXSize;
            public int dwYSize;
            public int dwXCountChars;
            public int dwYCountChars;
            public int dwFillAttribute;
            public int dwFlags;
            public int wShowWindow;
            public int cbReserved2;
            public byte lpReserved2;
            public IntPtr hStdInput;
            public IntPtr hStdOutput;
            public IntPtr hStdError;
        }

        [StructLayout(LayoutKind.Sequential)]
        public struct PROCESS_INFORMATION
        {
            public IntPtr hProcess;
            public IntPtr hThread;
            public int dwProcessId;
            public int dwThreadId;
        }

        [StructLayout(LayoutKind.Sequential)]
        public class SECURITY_ATTRIBUTES
        {
            public int nLength;
            public string lpSecurityDescriptor;
            public bool bInheritHandle;
        }

        [DllImport("kernel32.dll")]
        public static extern IntPtr LoadLibrary(string path);

        [DllImport("kernel32.dll")]
        public static extern bool FreeLibrary(IntPtr lib);

        [DllImport("kernel32.dll")] //声明API函数
        public static extern int VirtualAllocEx(IntPtr hwnd, int lpaddress, int size, int type, int tect);

        [DllImport("kernel32.dll")]
        public static extern bool VirtualFreeEx(IntPtr hProcess, IntPtr lpAddress, int dwSize, int dwFreeType);

        [DllImport("kernel32.dll")]
        public static extern bool GetExitCodeProcess(IntPtr hProcess, out int lpExitCode);

        [DllImport("kernel32.dll")]
        public static extern int WriteProcessMemory(IntPtr hwnd, int baseaddress, string buffer, int nsize,
            int filewriten);

        [DllImport("kernel32.dll")]
        public static extern int GetProcAddress(int hwnd, string lpname);

        [DllImport("kernel32.dll", SetLastError = true)]
        public static extern IntPtr GetProcAddress(IntPtr hModule, string lpProcName);

        [DllImport("kernel32.dll")]
        public static extern int GetModuleHandleA(string name);

        [DllImport("kernel32.dll")]
        public static extern int CreateRemoteThread(IntPtr hwnd, int attrib, int size, int address, int par, int flags,
            int threadid);

        [DllImport("kernel32.dll", CharSet = CharSet.Ansi)]
        public static extern bool CreateProcess(StringBuilder lpApplicationName, StringBuilder lpCommandLine,
            SECURITY_ATTRIBUTES lpProcessAttributes, SECURITY_ATTRIBUTES lpThreadAttributes, bool bInheritHandles,
            int dwCreationFlags, StringBuilder lpEnvironment, StringBuilder lpCurrentDirectory,
            ref STARTUPINFO lpStartupInfo, ref PROCESS_INFORMATION lpProcessInformation);

        [DllImport("kernel32.dll", SetLastError = true)]
        public static extern IntPtr GetModuleHandle(string lpModuleName);

        [DllImport("kernel32.dll", SetLastError = true)]
        public static extern IntPtr VirtualAllocEx(IntPtr hProcess, IntPtr lpAddress, IntPtr dwSize,
            uint flAllocationType, uint flProtect);

        [DllImport("kernel32.dll", SetLastError = true)]
        public static extern int WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, byte[] buffer, uint size,
            int lpNumberOfBytesWritten);

        [DllImport("kernel32.dll", SetLastError = true)]
        public static extern IntPtr CreateRemoteThread(IntPtr hProcess, IntPtr lpThreadAttribute, IntPtr dwStackSize,
            IntPtr lpStartAddress,
            IntPtr lpParameter, uint dwCreationFlags, IntPtr lpThreadId);

        [DllImport("kernel32.dll", SetLastError = true)]
        public static extern bool GetExitCodeThread(IntPtr hThread, out IntPtr hDll);

        [DllImport("kernel32.dll", CharSet = CharSet.Ansi)]
        public static extern uint WaitForSingleObject(IntPtr handle, uint dwMilliseconds);

        [DllImport("kernel32.dll", CharSet = CharSet.Ansi)]
        public static extern uint ResumeThread(IntPtr hThread);

        [DllImport("kernel32.dll", SetLastError = true)]
        public static extern int CloseHandle(IntPtr hObject);
    }
```

``` Program.cs
    using System;
    using System.IO;
    using System.Text;

    class Program
    {
        const int CREATE_SUSPENDED = 0x00000004;
        const int MEM_COMMIT = 0x00001000;
        const int MEM_RESERVE = 0x00002000;
        const int PAGE_READWRITE = 0x04;
        const uint INFINITE = 0xFFFFFFFF;
        const int MEM_RELEASE = 0x00008000;

        static void Main(string[] args)
        {
            // フックされる側のexe
            FileInfo fileInfo = new FileInfo("フックされる側のexe");
            Kernel32.STARTUPINFO sInfo = new Kernel32.STARTUPINFO();
            Kernel32.PROCESS_INFORMATION pInfo = new Kernel32.PROCESS_INFORMATION();
            // プロセス作成
            bool ret = Kernel32.CreateProcess(null, new StringBuilder(fileInfo.FullName),
                null, null, false,
                CREATE_SUSPENDED, null, null, ref sInfo, ref pInfo);
            if (!ret)
            {
                throw new Exception("Process Create Fail");
            }

            IntPtr proc = pInfo.hProcess;

            // Karnel32.dllのLoadLibraryのアドレス取得
            IntPtr addr = Kernel32.GetProcAddress(Kernel32.GetModuleHandle("kernel32.dll"), "LoadLibraryA");
            if (addr == IntPtr.Zero)
            {
                throw new Exception("LoadLibraryA Not Found");
            }

            // フックする側の Dll
            string sDllPath = new FileInfo("フックする側の Dll").FullName;
            IntPtr lpAddress = Kernel32.VirtualAllocEx(proc, (IntPtr) null, (IntPtr) sDllPath.Length,
                MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
            if (lpAddress == IntPtr.Zero)
            {
                throw new Exception("Alloc Error");
            }

            // DLLをメモリに書き込み
            byte[] bytes = Encoding.Default.GetBytes(sDllPath);
            if (Kernel32.WriteProcessMemory(proc, lpAddress, bytes, (uint) bytes.Length, 0) == 0)
            {
                Kernel32.VirtualFreeEx(proc, lpAddress, 0, MEM_RELEASE);

                throw new Exception("Write ProcessMemory");
            }

            var hRemoteThread = Kernel32.CreateRemoteThread(proc, (IntPtr) null,
                IntPtr.Zero, addr, lpAddress, 0, (IntPtr) null);
            if (hRemoteThread == IntPtr.Zero)
            {
                Kernel32.VirtualFreeEx(proc, lpAddress, 0, MEM_RELEASE);

                throw new Exception("Create RemoteThread");
            }

            if (!Kernel32.GetExitCodeThread(hRemoteThread, out IntPtr ptr))
            {
                throw new Exception("Injection Fail");
            }

            // プロセスが終了するまで待つ
            Kernel32.WaitForSingleObject(hRemoteThread, INFINITE);
            Kernel32.VirtualFreeEx(proc, lpAddress, 0, MEM_RELEASE);

            // プロセス終了後の後処理
            int exitcode = 0;
            Kernel32.ResumeThread(pInfo.hThread);
            Kernel32.CloseHandle(pInfo.hThread);
            Kernel32.WaitForSingleObject(proc, 0xFFFFFFFF);
            Kernel32.GetExitCodeProcess(proc, out exitcode);
            Kernel32.CloseHandle(proc);
        }
    }
```
</div></details>

次にビルドをしてエラーがないことを確認したら
フックする側のDllとフックされる側(Polyhook2.dllのDllなども一緒に)のExeを
C#のコンソールアプリケーションのbin/Debugフォルダに移動してください。

実行すると、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260041/06f66176-dc50-bc23-cff3-eb3c666449ab.png)

DLLインジェクションに成功している事が分かりました。

フックする側のDllMain.cppのupdateHookFuncのコードをここに(1)と(2)に
適当なコードを書いて実行してみてください。

サンプル

``` DllMain.cpp
void updateHookFunc() {
    // コードをここに(1)
    std::cout << "Hacked Hello (1)" << std::endl;
    //オリジナルの関数呼び出し
    reinterpret_cast<tUpdateHook>(origFunc)();
    // コードをここに(2)
    std::cout << "Hacked Hello (2)" << std::endl;
}
```

再度ビルドして、DllやExeをコピーしてください。

結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260041/edb4da2d-c3b1-e8d4-e0ba-0f4abc716889.png)

# まとめ
通常は、アプリケーション側でExportする宣言が必要なのですが、
こんな感じでEatPdbとPolyhook2を併用することでExportされていなくても、既存のアプリケーションを拡張したりすることが出来ます

また、複雑なプログラムで詳しく関数を解析する場合は、ghidraとプラグインを使いましょう
(UnrealEngine で作られたゲームの関数フックなど)

[ghidra](https://github.com/NationalSecurityAgency/ghidra)
[ghidraのC++プラグイン](https://github.com/astrelsky/Ghidra-Cpp-Class-Analyzer)