6: DLL Injectionで任意のプログラムを埋め込む

概要

ゲームやその他実行ファイルに自分で書いたプログラムを読み込ませて実行させたい場合、おそらく一番使われる手法がこの DLL Injection という手法。
今回は、メッセージボックスを出すプログラムをDLL Injectionしてゲームに実行させることを目標とする。
Unityで作ったゲームを対象にやっていき、ゲームのサンプルはココからダウンロードできる。 また、Windows対象で、C++を言語として用いる。ソースコードはまとめてページの一番下にある。

DLLとは

DLL(Dynamic Link Library)とは、動的にEXEファイルなどにリンクされて使用される実行ファイル形式のライブラリです。
例えば、普通にC/C++でハローワールドのプログラムを書いて、標準出力としてprintfを使用した際、実際のprintfの処理はコンパイルした実行ファイルの中にあるわけではなく、 その実行ファイルが読み込むDLLの中に処理があり、printfを呼び出す際にDLL内の方の該当アドレスにジャンプします。

メッセージボックスを表示するDLLの作成と単体実行

ここでは Visual Studio 2019 を使って作っていく。
まずは、VisualStudioを起動後、以下のようにしてDLL用のプロジェクトを作成する。 バージョンが違うとレイアウトも違うと思うが、DLL & C++ & Windowsの条件がそろってればよさげ。
この後、適当に名前や保存先フォルダを指定して作成すると、DLLのテンプレのコードと共にC++コードが開かれるはずである。
最後に、デフォルトでx86用でビルドするようになっているので、下図の部分を変えてx64用にビルドするようにしましょう。
どのDLLにもDllMainというメイン関数があり、これはDLLがプロセスに読み込まれた時(DLL_PROCESS_ATTACH)や、 DLLがスレッドからアンロードされる時(DLL_PROCESS_DETACH)に、この関数が呼ばれる。
今回は、DLLが読み込まれた瞬間にメッセージボックスを出したいだけなので、下図のように一行追加して終了で、書けたらCtrl-bを押してビルドしましょう。 C++なのになんか見慣れないコードだなと思った方もいるかもですが、Windowsプログラミングはこんな感じでマクロが多く、WindowsAPIを主に使用してプログラミングしていきます。 これでDLLを作れましたが、DLLはライブラリなので普通は単体で実行する事ができません。 しかし、rundll32.exeというWindowsに標準で入ってる実行ファイルを使って、x86やx64のDLLを実行する事ができます。
使い方は、rundll32.exe DLLファイル名 DLLのエクスポート関数名となっているが、今回のDLLはライブラリとして何か関数を提供(エクスポート)しているわけではないので、 DLLのエクスポート関数名は適当で良いです。下図のように実行してメッセージボックスが無事出てきたら終了です。

DLL Injectionで上記のDLLをゲームに読み込ませる

ゲームのプロセスに、自分で書いた(チートの)DLLを読み込ませて、そのDLL処理を実行させる手法です。
どのDLLをそのEXEファイルがインポートするかというのは予めEXEファイルのインポートセクションという場所に記載されているのですが、ここに書かれてないDLLでも、 その実行ファイルがLoadLibrary("/path/to/hoge.dll")というWindowsAPIを実行して呼び出す事により、DLLを動的にインポートすることができます。 DLL Injectionの中でもやり方は様々にあるのですが、今回はこのLoadLibrary("/path/to/上記で作ったDLL")関数をゲームのプロセスに実行させることにより、DLLをInjectします。
他のプロセスにどうやってこれを実行させるんだという点については、CreateRemoteThreadという、他のプロセスで走らせるスレッドを作成するWindowsAPIを使用して、 LoadLibraryを呼び出すスレッドを登録することで実行させます。

では早速、DLL Injectionを行うEXEを作るための、VisualStudioプロジェクトを作成しましょう。
基本的な流れは上記DLLの時と同じですが、DLLではなく今回はEXEなので、下図を選びましょう。 今回は先ほどと違って、作成した後にテンプレファイルなどは現れず、まっさらだと思います。 とりあえず、x86からx64に変更した後、以下のようにソリューション名の所で右クリックしてC++コードを追加しましょう。 この後、何を追加するかが聞かれるので、C++コードを選びましょう。 これで、何も書かれてないC++ファイルが出てきたと思います。
最後に、空のプロジェクトを選択する際にConsoleと書いてあったから分かると思いますが、メッセージボックスとかのGUI系がデバッグ用に使えなくなっているので、 下図のようにソリューション名を右クリックしてプロパティを開き、サブシステムの所を下図のように変えましょう。 また、Windowsでは文字コードが普通のUTF-8ではなく、ワイド文字列のUTF-16なので、そのままプロパティ画面の詳細を開き、下図のように設定しましょう。
Windowsのプログラムの場合、一番最初に実行されるメイン関数はWinMainと言って、以下のようになっているので、まずは下図を映しちゃいましょう。 このWinMainにDLL Injectionの処理を書いていきます。
まずはゲームのプロセスのポインタ(Windows風ではハンドルという)を下図のように、プロセスIDを用いて取得します(#include <stdio.h>を追加しているので忘れずに…)。 これで、hprocにゲームのプロセスのハンドルが入るので、これに対してLoadLibraryを呼び出させる処理を CreateRemoteThreadで登録すればいいのですが、その前に、メッセージボックスを表示するDLLへのパスをゲームのプロセスのメモリ内に書き込んだり、 その書き込みを行うためのメモリスペースをアロケートしないといけません。
まずは、DLLへのパスを書き込むためのメモリスペースを下図のようにVirtualAllocExで確保しましょう。 これで、DLLへのパスを格納できる領域を確保し、そのアドレスをdllPathAddrに取得したので、実際にWriteProcessMemory でパスを書き込んでいきます。 これで、準備が整ったので、CreateRemoteThreadを使って、ゲームプロセスにLoadLibrary("/path/to/上記で作ったDLL")を呼ばせるスレッドを登録します。 これで完成です! Ctrl-bでビルドしちゃいましょう。

実際に試してみましょう。
まずはゲームを起動した後、powershellを起動して以下のコマンドでゲームのプロセスIDを調べます。 PIDがわかったら、以下のようにDLL Injectionの実行ファイルを実行しましょう。 メッセージボックスが出てくるはずです!
もしメッセージボックスが出てきた後、ゲームがクラッシュしたりすぐ終了してしまう場合、アンチウイルスソフトに検知されて消されてる可能性があります。 これは、DLL Injectionというのはマルウェアもよく用いる手法だからです。それなので、アンチウイルスソフトにこの実行ファイルを除外するよう登録しましょう。

DLL Injectionのツールに関して

DLL Injectionは結構多様するので、一々自分でプログラムを書かずにツールを使うことが多いです。
ググれば色々出てくるので何かしら一つ入れておきましょう。自分は、自分で作ったこのツールを使っていきます。

ソースコード

メッセージボックスを表示するDLL

// dllmain.cpp : DLL アプリケーションのエントリ ポイントを定義します。
#include "pch.h"

BOOL APIENTRY DllMain( HMODULE hModule, DWORD  ul_reason_for_call, LPVOID lpReserved) {
	switch (ul_reason_for_call) {
		case DLL_PROCESS_ATTACH:
			MessageBox(NULL, TEXT("Hello world"), TEXT("hoge"), MB_OK);
		case DLL_THREAD_ATTACH:
		case DLL_THREAD_DETACH:
		case DLL_PROCESS_DETACH:
			break;
	}
	return TRUE;
}

DLL Injectionの実行ファイル

#include <windows.h>
#include <stdio.h>
	
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
	int argc;
	LPWSTR* argv;
	int pid;
	HANDLE hproc;
	LPSTR dllPathAddr;
	TCHAR dllPath[MAX_PATH] = "C:\\Users\\hoge\\Desktop\\dlli\\msgbox_dll\\x64\\Debug\\msgbox_dll.dll";

	// dllinjector.exe <ゲームのプロセスID> で実行する
	argv = CommandLineToArgvW(GetCommandLineW(), &argc);

	pid = _wtoi(argv[1]);
	hproc = OpenProcess(PROCESS_CREATE_THREAD | PROCESS_VM_OPERATION | PROCESS_VM_WRITE, FALSE, pid);

	dllPathAddr = (LPSTR)VirtualAllocEx(hproc, NULL, strlen(dllPath), MEM_COMMIT, PAGE_READWRITE);
	WriteProcessMemory(hproc, dllPathAddr, dllPath, strlen(dllPath), NULL);
	CreateRemoteThread(hproc, NULL, 0, (LPTHREAD_START_ROUTINE)LoadLibrary, dllPathAddr, 0, NULL);

	CloseHandle(hproc);
	return 0;
}