6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-12-05

#初めに
OIC ITCreate Club Advent Calendar 2020
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に変更してください。

DLLインジェクションのコード
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);
        }
    }

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

実行すると、
image.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

まとめ

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

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

ghidra
ghidraのC++プラグイン

6
2
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
6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?