#初めに
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のインストール
git clone https://github.com/microsoft/vcpkg
call .\vcpkg\bootstrap-vcpkg.bat
これでVcpkgのインストールが終わりました。
次に設定をします
今回はx64で実行するので、以下のコマンドを実行します。
.\vcpkg\vcpkg --triplet x64-windows-static integrate install
win32などの場合は、適材適所設定をしてください。
Vcpkgでライブラリをインストール
.\vcpkg\vcpkg --triplet x64-windows-static install polyhook2:x64-windows
インストールが終わるまで待ちます。
インストールが完了したら、後はコードを書くだけです。
Visual Studio でコードを書く
今回は手軽に解説する為、コンソールアプリケーションで関数フックをされる側のアプリケーションを作りたいと思います。
フックされる側
コンソールアプリケーションのプロジェクトを作成します。
また、アーキテクチャをx64に変更してください。
main関数を消して下記のコードを貼り付けてください。
#include <iostream>
void printHello()
{
std::cout << "Hello World!" << std::endl;
}
int main()
{
printHello();
}
シンプルに 'Hello world!'と表示されます。
フックをする側
次にフックをする側のアプリケーションを作成します。
C++のダイナミック リンク ライブラリのプロジェクトを作成してください。
また、アーキテクチャをx64に変更してください。
プロジェクトのプロパティからC++ 17に変更してください。
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 というファイルを作成してください。
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.exeの場所" exec "eatpdb.yaml"
いくつかのファイルが生成されます。
次に VisualStudioの x64 Native Tools Command Prompt を開きます
cd コマンドを使ってフックする側のexeがある階層に移動してください。
そして、コマンドを実行します。
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の数値を書き換えます。
// フックするアドレスのRVA
constexpr uint64_t mainAddress = 0x00012480;
constexpr uint64_t address = 0x00012500;
DLLインジェクション
最後に、Hookする側のDLLをされる側に注入します。
詳しい説明は割愛します。
(検索すると結構出てきます)
今回はC#でコードを書きます。
コンソールアプリケーションを作成します。
また、アーキテクチャをx64に変更してください。
DLLインジェクションのコード
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);
}
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フォルダに移動してください。
DLLインジェクションに成功している事が分かりました。
フックする側のDllMain.cppのupdateHookFuncのコードをここに(1)と(2)に
適当なコードを書いて実行してみてください。
サンプル
void updateHookFunc() {
// コードをここに(1)
std::cout << "Hacked Hello (1)" << std::endl;
//オリジナルの関数呼び出し
reinterpret_cast<tUpdateHook>(origFunc)();
// コードをここに(2)
std::cout << "Hacked Hello (2)" << std::endl;
}
再度ビルドして、DllやExeをコピーしてください。
まとめ
通常は、アプリケーション側でExportする宣言が必要なのですが、
こんな感じでEatPdbとPolyhook2を併用することでExportされていなくても、既存のアプリケーションを拡張したりすることが出来ます
また、複雑なプログラムで詳しく関数を解析する場合は、ghidraとプラグインを使いましょう
(UnrealEngine で作られたゲームの関数フックなど)