こんにちは!開発現場で日々、コードと低レイヤの挙動に向き合っているシニアエンジニアです。
今回は、多くの開発者が「なんだか黒魔術っぽくて難しそう……」と敬遠しがちな 「DLLインジェクションとAPIフック」 をテーマに選びました。
「えっ、いきなりそんなハッキングみたいな話?」と思いましたか?
でも安心してください。これはセキュリティを脅かすための技術ではなく、「自分が書いたプログラムや、動作しているプロセスが、裏でWindows OSとどう会話しているのか」を生々しく観察するための最強のデバッグ手法です。
これをマスターすると、例えば「サードパーティ製のアプリがどのファイルを読んでいるのか」「自作のアプリがなぜあのタイミングで固まるのか」といったブラックボックスを、レントゲン写真のように透視できるようになります。毎日のデバッグ作業が劇的に楽になり、OSの挙動に対する解像度が跳ね上がりますよ。
今回は、Windows上で最強のPOSIX互換・開発環境である MSYS2 / MinGW-w64 を使って、この低レイヤ実験場を構築する手順を、優しく丁寧に紐解いていきましょう。
—
1. なぜ MSYS2 / MinGW-w64 なのか?(ツールの役割と本質)
Windows上でネイティブなC/C++プログラムやDLL(Dynamic Link Library)をビルドしようとしたとき、真っ先に思い浮かぶのはVisual Studio(MSVC)かもしれません。もちろんMSVCは素晴らしいIDEですが、コンパイラの内部挙動や、Windows APIの低レイヤな仕組み(PEヘッダの構造やリンカの挙動)を肌で理解するには、GCC/Clang系ツールチェーンのほうが直感的で自由度が高いことが多いのです。
そこで登場するのが MSYS2 です。
MSYS2は、単なるコンパイラ集ではありません。Linuxでお馴染みの `pacman` パッケージ管理システムを備え、Windows上でUnixライクなシェル環境(Bash)や強力な開発ツール群を提供してくれる、いわば 「Windowsの中の小さなLinux(だけど中身は純粋なWindowsネイティブ)」 です。
その中核である MinGW-w64 を使うことで、Linuxを書くような軽快なコマンド操作で、完全にWindowsネイティブで動く `PE形式` の `.exe` や `.dll` を自由自在に作り出すことができます。今回のテーマである「プロセスへのコード注入」は、メモリレイアウトやポインタ操作の正確さが要求されるため、この軽量かつ強力なGCC環境が最高の相棒になるのです。
—
2. 基礎セットアップ:環境の構築と隠れた必須ツールの導入
まずは、実験場となるMSYS2環境を整えましょう。手順を間違えると後々パスの通し方でハマるため、ここだけは丁寧にいきます。
ステップ 1: MSYS2のインストール

2. インストール完了後、「MSYS2 MSYS」のシェルが起動します。
ステップ 2: ツールチェーンのアップデートとパッケージ導入
まずはパッケージデータベースを最新化します。シェル上で以下のコマンドを実行してください。
パッケージデータベースと基本システムの同期・更新
pacman -Syu
※途中で「ウィンドウを閉じろ」といった指示が出たら、指示に従って一度シェルを閉じ、再度「MSYS2 MSYS」を起動してもう一度 `pacman -Syu` を実行してください。
次に、Windowsネイティブ(64bit)のコンパイルに必要なツール群一式をインストールします。ここが一番重要なポイントです。
MinGW-w64 64bit用のGCCコンパイラ、Make、GDBデバッガーを一括インストール
pacman -S –needed mingw-w64-ucrt-x86_64-toolchain base-devel
インストールする際、確認プロンプトが出たらすべて Enter(デフォルト)で進めて問題ありません。
ステップ 3: パスの通し方(重要)
ここで注意が必要なのは、MSYS2には「MSYS環境(Linux風)」と「MinGW環境(Windowsネイティブ)」の2種類がある点です。
これから私たちがビルドするのは WindowsネイティブのDLL です。そのため、必ず 「MinGW UCRT 64」 というショートカットから起動したシェルを使用するか、環境変数にパスを通す必要があります。
スタートメニューから 「MSYS2 UCRT64」 を探して起動してください。これが、これからの作業場になります。
—
3. 実践!DLLインジェクションとAPIフックの仕組み
ここからが本番です。仕組みをざっくり説明しましょう。
1. ターゲット(被害者…ではなく被験者)プロセス:適当な常駐アプリ(今回は自作の簡単な無限ループプログラム)を動かします。
2. インジェクター(注射器):ターゲットのプロセスID(PID)を指定し、「このメモリ空間に、今から俺の作ったDLLを読み込め!」という命令(`CreateRemoteThread` と `LoadLibrary` の組み合わせ)を送り込みます。
3. ペイロード(DLLの中身):ターゲットのプロセス内に強制ロードされたDLLは、自身の初期化関数(`DllMain`)の中で、ターゲットが呼び出そうとしているWindows API(例: `MessageBoxW` や `CreateFileW` など)の機械語の先頭を書き換え、「自分の用意した別の関数にジャンプ(フック)させる」 ように細工します。
これによって、ターゲットアプリが「ファイルを保存しようとした瞬間」に、あなたの書いたコードを割り込ませて挙動を横取りすることができるのです。
—
4. 精度高い「HelloWorld」的動作確認(コード実装)
今回はシンプルに、「他のプログラムから呼び出されたときにメッセージボックスを表示するDLL」を作り、それを別のプログラムからインジェクトして動作確認する実験を行います。
ファイル1: 注入されるDLL (`my_hook.c`)
以下のコードを `my_hook.c` という名前で保存してください。
include
include
// DLLがプロセスにロードされたりアンロードされたりするときにOSから呼ばれるエントリポイント
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) {
switch (fdwReason) {
case DLL_PROCESS_ATTACH:
// プロセスにインジェクトされた瞬間に実行される
MessageBoxW(
NULL,
L”見知らぬプロセスからこんにちは!DLLインジェクション成功です。”,
L”低レイヤ実験室”,
MB_OK | MB_ICONINFORMATION
);
break;
case DLL_PROCESS_DETACH:
// プロセスからデタッチ(解放)されたとき
break;
}
return TRUE;
}
ファイル2: ターゲットとなるプログラム (`target.c`)
以下のコードを `target.c` として保存してください。これは、インジェクションされるのを待つだけの素朴なアプリです。
include
include
int main() {
printf(“ターゲットプロセスが起動しました。PID: %lu\n”, GetCurrentProcessId());
printf(“このウィンドウを開いたまま、別の端末からDLLをインジェクトしてください。\n”);
// 終了しないように無限ループで待機
while (1) {
Sleep(1000);
}
return 0;
}
ファイル3: インジェクター (`injector.c`)
以下のコードを `injector.c` として保存してください。これが先ほどのターゲットのPIDを指定して、DLLをねじ込むプログラムです。
include
include
int main(int argc, char argv[]) {
if (argc != 3) {
printf(“使い方: %s
return 1;
}
DWORD targetPid = atoi(argv[1]);
const char dllPath = argv[2];
// 1. ターゲットのプロセスハンドルを「フルアクセス」でオープン
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, targetPid);
if (!hProcess) {
printf(“プロセスを開けませんでした。Error: %lu\n”, GetLastError());
return 1;
}
// 2. ターゲットプロセス内に、DLLのパス文字列を格納するメモリ領域を確保
LPVOID pDllPathRemote = VirtualAllocEx(
hProcess, NULL, strlen(dllPath) + 1,
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE
);
if (!pDllPathRemote) {
printf(“リモートメモリの確保に失敗しました。Error: %lu\n”, GetLastError());
CloseHandle(hProcess);
return 1;
}
// 3. 確保したリモートメモリに、DLLのパス文字列を書き込む
if (!WriteProcessMemory(hProcess, pDllPathRemote, (LPVOID)dllPath, strlen(dllPath) + 1, NULL)) {
printf(“メモリへの書き込みに失敗しました。Error: %lu\n”, GetLastError());
VirtualFreeEx(hProcess, pDllPathRemote, 0, MEM_RELEASE);
CloseHandle(hProcess);
return 1;
}
// 4. Windowsの kernel32.dll 内にある LoadLibraryA の関数アドレスを取得
// (※すべてのプロセスで kernel32.dll のロードアドレスは基本的に同じ)
LPVOID pLoadLibraryA = (LPVOID)GetProcAddress(
GetModuleHandleA(“kernel32.dll”), “LoadLibraryA”
);
// 5. ターゲットプロセス上で新しいスレッドを強制起動し、エントリポイントに LoadLibraryA を指定、
// 引数としてリモートメモリ上の DLLパス を渡す
HANDLE hThread = CreateRemoteThread(
hProcess, NULL, 0,
(LPTHREAD_START_ROUTINE)pLoadLibraryA,
pDllPathRemote, 0, NULL
);
if (!hThread) {
printf(“リモートスレッドの作成に失敗しました。Error: %lu\n”, GetLastError());
} else {
printf(“大成功!ターゲットプロセスにDLLをインジェクトしました。\n”);
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
}
// クリーンアップ
VirtualFreeEx(hProcess, pDllPathRemote, 0, MEM_RELEASE);
CloseHandle(hProcess);
return 0;
}
—
5. ビルドと実行のコマンドログ
それでは、MSYS2 UCRT64のターミナルを開き、これら3つのファイルをコンパイルして実際に動かしてみましょう。
1. DLLのビルド (-shared と -o で共有ライブラリを指定)
gcc -shared -o my_hook.dll my_hook.c -luser32
2. ターゲットプログラムのビルド
gcc -o target.exe target.c
3. インジェクターのビルド
gcc -o injector.exe injector.c
エラーが出ずにコンパイルが通れば成功です。カレントディレクトリに `my_hook.dll`, `target.exe`, `injector.exe` が生成されているはずです。
動作テストの手順
1. ターミナルA(ターゲット実行)
./target.exe
実行すると、以下のようにPIDが表示されて待機状態になります。
ターゲットプロセスが起動しました。PID: 12345
このウィンドウを開いたまま、別の端末からDLLをインジェクトしてください。
2. ターミナルB(インジェクター実行)
もう一つMSYS2 UCRT64のウィンドウを開き、先ほどのPID(例: `12345`)を指定してインジェクターを実行します。
※DLLのパスは、絶対パスか、ターゲットから見た相対パスで指定するのが確実です。
# 例: カレントディレクトリにある my_hook.dll の絶対パスを渡す場合
./injector.exe 12345 C:/msys64/home/YourName/my_hook.dll
3. 結果の確認
インジェクターが成功すると、ターゲットプロセス側(ターミナルAの画面やデスクトップ)で、突然Windowsのメッセージボックスがポンとポップアップ表示されます!
これが「外部から別プロセスのメモリ空間に侵入し、コードを実行させた瞬間」です。
—
6. セキュリティ上のリスク回避に関する重要な注意点
今回紹介した技術は、ソフトウェアのデバッグやリバースエンジニアリング、ゲームのMOD開発、あるいはアンチウイルスソフトの研究など、低レイヤエンジニアにとっては必須の教養です。
しかし、その裏返しとして、マルウェア(ランサムウェアやスパイウェア)が他の正当なプロセス(explorer.exe やブラウザなど)に寄生して秘密裏にコードを実行する手法(プロセスインジェクション)そのものでもあります。
そのため、実務や学習において以下の点を必ず遵守してください:
- 自己責任の徹底: 必ず自分が所有・開発している安全なテストプロセスに対してのみ実行してください。
- セキュリティソフトの誤検知: 高度なヒューリスティック検知を持つアンチウイルスソフトやWindows Defenderは、`CreateRemoteThread` と `VirtualAllocEx` を組み合わせたコードを「トロイの木馬」として検知・削除することがあります。学習目的であっても、実環境でテストする際は除外設定を行うか、隔離された仮想環境(VMやサンドボックス)で行うのが鉄則です。
—
おわりに
いかがでしたでしょうか?
普段私たちが何気なく使っているWindowsというOSは、実はプロセスごとのメモリ境界がしっかりしている一方で、こうした「正規のAPI」を組み合わせることで、プロセス同士が互いの内側に深く干渉できる柔軟性を持っています。
MSYS2 / MinGW-w64という強力なライトウェイト環境を手に入れたあなたなら、C言語のポインタ操作やOSのAPI構造を武器に、どんなブラックボックスでも中身を暴くことができるはずです。
この技術を正しく理解し、日々のトラブルシューティングや高度なデバッグの引き出しとして役立ててください。あなたのエンジニアリングライフが、さらに深みのある楽しいものになることを応援しています!