- 概要
- 攻撃フロー全体像
- コア技術:Windows Bind Filter(bindfltapi.dll)
- DLL の「改ざん」戦略(CopyAndPatchFile)
- サービス登録とブート順序の制御
- Session 0 検出によるモードの切り替え
- EDR 監視ループ:タイミング設計の詳細
- コマンドラインインターフェース:2つのモード
- ディレクトリ確保とログ出力
- 防御側からの検知・対策ポイント
- 制約・限界
- リファレンス一覧
- 1. Windows Bind Filter (bindflt.sys) と Bind Link API (bindfltapi.dll)
- 2. PE ファイルフォーマットと DOS スタブ
- 3. Authenticode 署名と署名検証の仕組み
- 4. Windows Resource Protection (WRP) と System32 保護
- 5. Windows サービス起動順序・Load Order Group・SCM
- 6. Session 0 Isolation
- 7. Code Integrity (CI) と署名検証失敗時の挙動
- 8. Protected Process Light (PPL) と ELAM
- 9. プロセス列挙 API
- 10. 検知・フォレンジック関連
- 11. その他の Win32 API(本文中で言及されるもの)
概要
EDRStartupHinder は、Windows の Bind Filter ドライバ(bindflt) を悪用し、EDR/AV 製品が依存する DLL のファイルシステムパスを改ざん済みコピーへリダイレクトすることで、EDR プロセスの正常起動を妨害するツールである。攻撃は OS 起動シーケンスの中で、EDR サービスよりも先にロードされる Windows サービスとして実行される点が設計上の核心となっている。
リポジトリ:https://github.com/TwoSevenOneT/EDR-Redir
作者ブログ:https://www.zerosalarium.com/2026/01/edrstartuphinder-edr-startup-process-blocker.html
デモ動画:https://youtu.be/mSywzuGsirU
攻撃フロー全体像
[初回セットアップ(管理者権限で手動実行)] 1. 対象 DLL をコピーし、1バイト改ざん → "壊れた DLL" を生成 2. Windows サービスを登録(EDR より先にロードされるグループを指定) [OS 再起動後(サービスとして自動実行)] 3. サービスが Session 0 で起動、EDR プロセスの出現をポーリング監視 4. EDR プロセスが出現した瞬間に Bind Filter で DLL パスをリダイレクト 5. EDR が壊れた DLL をロード → 署名検証失敗 → 起動失敗 6. EDR 終了を検知 → リダイレクト解除 → ステップ3に戻る
コア技術:Windows Bind Filter(bindfltapi.dll)
Bind Filter とは何か
Bind Filter(bindflt.sys)は、Windows のファイルシステムスタックに挿入される ミニフィルタドライバ の一種である [Ref-03]。Windows のファイル I/O は、アプリケーションが CreateFile 等の API を呼ぶと、最終的にカーネル内で IRP(I/O Request Packet) というデータ構造に変換され、ファイルシステムドライバ(NTFS 等)へ送られる。ミニフィルタドライバはこの IRP がファイルシステムに到達する前の段階で介入し、リクエストを検査・変更・リダイレクトできる仕組みである [Ref-05]。
Bind Filter はこのミニフィルタの仕組みを使い、特定のパスへのファイルアクセス要求を受け取ると、IRP 内のパス情報を別のパスに書き換えてからファイルシステムに渡す。つまり、アプリケーションが C:\Windows\System32\msvcp_win.dll を開こうとしても、Bind Filter が IRP を横取りして C:\TMP\msvcp_win.dll への要求に差し替える。アプリケーション側はこの差し替えを一切認識できない。ファイルシステム上のディスク実体には何の変更も加えないため、ファイルの直接的なハッシュ検証(ディスク上のファイルを読み取って検証する場合)では異常を検出できない。
bindflt.sys の Altitude は 409800 であり、これはミニフィルタの中でも高い位置(FSFilter Virtualization グループ)に割り当てられている [Ref-06]。この位置は Windows Sandbox や MSIX パッケージの仮想化など、ファイルシステムの仮想化を行うフィルタに適した範囲である。Anti-Virus ミニフィルタの Altitude 範囲(320000–329999)よりも上位に位置するため、bindflt による IRP のリダイレクトは多くの EDR のミニフィルタよりも先に処理される。
本来この機構は、Windows Sandbox のファイルシステム分離(ホスト OS のファイルをサンドボックス内に仮想的にマッピングする)や MSIX パッケージの仮想化(アプリごとにファイルシステムビューを分離する)に使われる正規の Windows 機能である [Ref-01]。
なぜ Bind Filter が EDR 妨害に有効なのか
従来の EDR 妨害手法と比較すると、Bind Filter を使うアプローチが各種防御を回避できる理由が明確になる。
手法1:DLL の直接上書き(C:\Windows\System32\*.dll を改ざん済みファイルで上書き)の場合、まず Windows の WRP(Windows Resource Protection) が System32 配下の重要ファイルの書き換えを TrustedInstaller 以外に許可しない [Ref-09]。仮に書き換えに成功しても、EDR のファイル整合性監視(FIM)がファイル変更イベントを即座に検知する。さらに、ディスク上のファイル実体が変わるため、SFC(System File Checker)やオフラインフォレンジックでも容易に発見される。
手法2:レジストリの ImagePath 改ざん(EDR サービスのバイナリパスを変更)の場合、EDR は自身のレジストリキーに対してアクセス制御を設定していることが多く、改ざんが阻止される。また、改ざんに成功してもレジストリの変更はイベントログに記録される。
手法3:サービスの無効化(EDR サービスの StartType を DISABLED に変更)の場合、EDR の改ざん防止機能(Tamper Protection)がサービスの無効化操作を拒否する。
Bind Filter によるリダイレクトは、これらの防御をすべて迂回する。ディスク上の元ファイルは一切変更されないため WRP は反応せず、ファイルの作成・変更・削除イベントも発生しない。レジストリの EDR サービス設定にも触れないため、レジストリ監視も反応しない。さらに、Bind Filter のマッピングはカーネルメモリ上にのみ存在する一時的な状態であり、永続的なディスク上の変更を伴わないため、リブート後やマッピング解除後にはフォレンジック痕跡が残りにくい。
加えて、EDR 自身もファイル読み込みの際にカーネルの I/O スタックを経由するため、Bind Filter によるリダイレクトの影響を受ける。EDR がリダイレクトを検知するには、bindflt ドライバの状態を直接照会するか、自身のミニフィルタでより高い Altitude(優先度)で IRP を監視する必要があるが [Ref-04] [Ref-05]、多くの EDR はこのケースを想定した実装を持っていない。特に bindflt.sys の Altitude 409800 は Anti-Virus グループ(320000–329999)よりも高いため、通常の EDR ミニフィルタでは bindflt によるリダイレクト後の IRP しか観測できない。
コード上の実装(EDRStartupHinder.cpp 13–18行目)
HMODULE hBindflt = LoadLibraryW(L"bindfltapi.dll"); if (hBindflt) { MyCreateBindLink = (PtrCreateBindLink)GetProcAddress(hBindflt, "BfSetupFilter"); MyRemoveBindLink = (PtrRemoveBindLink)GetProcAddress(hBindflt, "BfRemoveMapping"); }
bindfltapi.dll は、カーネルモードの bindflt.sys ドライバに対するユーザーモード API のラッパーである [Ref-01]。この DLL は Windows SDK にインポートライブラリ(.lib)やヘッダが提供されていない(あるいはごく最近まで提供されていなかった)非公開 API である [Ref-02]。そのためコンパイル時に静的リンクすることができず、LoadLibraryW [Ref-25] + GetProcAddress [Ref-26] による実行時の動的解決が必須となる。
エクスポート関数名 BfSetupFilter と BfRemoveMapping は、公式ドキュメントに記載されている CreateBindLink / RemoveBindLink [Ref-01] とは異なる内部名称である。これらの名前は bindfltapi.dll のエクスポートテーブルを逆アセンブルして特定されたもので [Ref-02]、公式の高レベル API(CreateBindLink)が利用可能になる以前から使用されていた低レベルエントリポイントである。
この動的ロード方式にはもう一つの実用的な利点がある。bindfltapi.dll は Windows 10 1809 以降でのみ存在するため、古いバージョンの Windows で実行した場合は LoadLibraryW が NULL を返す [Ref-25]。21–24行目の分岐で "OS NOT SUPPORT" と表示して安全に終了できるため、単一のバイナリで複数の Windows バージョンに対応できる。静的リンクの場合、DLL が存在しない環境ではプロセスの起動自体が失敗し、エラーメッセージを表示する余地すらなくなる。
BindLnk.h の型定義と引数の意味
typedef HRESULT(__stdcall* PtrCreateBindLink)( PVOID jobHandle, CREATE_BIND_LINK_FLAGS createBindLinkFlags, PCWSTR virtualPath, // リダイレクト元(本物の DLL パス) PCWSTR backingPath, // リダイレクト先(改ざん済み DLL パス) UINT32 exceptionCount, PCWSTR* const exceptionPaths);
各引数の役割を補足する [Ref-01] [Ref-02]。
jobHandle:Job オブジェクトのハンドル。特定の Job 内のプロセスにのみリダイレクトを適用する場合に使用する。0(NULL)を渡すとシステム全体に対してグローバルにマッピングが適用される。このツールでは EDR がどの Job に属しているかを事前に知る必要をなくすため、また EDR が再起動して新しいプロセスになっても確実にリダイレクトが効くようにするため、グローバルマッピング(0)を使用している。createBindLinkFlags:CREATE_BIND_LINK_FLAG_NONE(0x0)は通常のリダイレクト。READ_ONLYフラグを立てると読み取り専用になり、MERGEDフラグではリダイレクト先と元のパスの内容をマージできる。このツールでは単純な完全リダイレクトが目的なのでNONEを使用している。virtualPathとbackingPath:名前が直感に反するが、virtualPathが「リダイレクト元」(つまり本来のパス)で、backingPathが「リダイレクト先」(改ざん済みファイルのパス)である。アプリケーションがvirtualPathにアクセスすると、実際にはbackingPathのファイルが返される [Ref-01]。
DLL の「改ざん」戦略(CopyAndPatchFile)
PE ファイルと Authenticode 署名の構造
この攻撃が成立する仕組みを理解するには、Windows の PE(Portable Executable)ファイルフォーマットと Authenticode デジタル署名の関係を知る必要がある [Ref-07] [Ref-08]。
PE ファイルは先頭から順に、DOS ヘッダ、DOS スタブ、PE ヘッダ(COFF ヘッダ + Optional ヘッダ)、セクションテーブル、各セクション(.text, .data, .rsrc 等)で構成される [Ref-07]。DOS スタブは、PE ファイルを DOS 環境で実行した際に「This program cannot be run in DOS mode」と表示して終了するための小さなプログラムで、Windows 環境での実行には一切使用されない。
Authenticode 署名は、PE ファイルの特定領域(署名自身の格納位置とチェックサムフィールドを除くファイル全体)のハッシュを計算し、そのハッシュに対して発行元の秘密鍵で署名したものである [Ref-08]。重要なのは、DOS スタブもハッシュ計算の対象範囲に含まれる という点である。Authenticode 仕様では、ハッシュ計算から除外される領域は明示的に3箇所だけと規定されている:(1) PE ヘッダ内の CheckSum フィールド、(2) Optional ヘッダ内の Certificate Table エントリ、(3) 属性証明書テーブル自体 [Ref-08]。DOS スタブはこのいずれにも該当しないため、ハッシュ計算の対象となる。したがって、DOS スタブ内の 1 バイトでも変更すれば、計算されるハッシュ値が変わり、署名に埋め込まれたハッシュ値と一致しなくなるため、署名検証は失敗する。
なぜ DOS スタブの 1 バイトだけを変更するのか
改ざんの程度と効果のバランスがこの設計の鍵となっている。
改ざんが大きすぎる場合の問題:PE ヘッダ自体(マジックナンバー MZ や PE シグネチャ、エントリポイント、セクション情報など)を破壊すると、Windows の PE ローダー(ntdll の LdrLoadDll)がファイルを有効な PE として認識できなくなる。この場合、ローダーは STATUS_INVALID_IMAGE_FORMAT を返す。EDR 製品によっては、この種のエラーを受けてフォールバックロジック(別のパスからのロード試行、SxS マニフェストに基づく代替 DLL の検索など)を実行する可能性がある。
改ざんが適切な場合の効果:DOS スタブのみを 1 バイト変更した場合、PE ヘッダの構造は完全に正常なため、ローダーはファイルを有効な PE として認識する [Ref-07]。ファイルサイズ、エクスポートテーブル、インポートテーブルなどの構造情報も本物と同一である。しかし、ファイル全体のハッシュが変わっているため Authenticode 署名の検証は失敗する [Ref-08]。
Windows カーネルの コード整合性(CI: Code Integrity) サブシステムは、DLL のロード時に署名検証を行い、失敗した場合は STATUS_INVALID_IMAGE_HASH を返してロードを拒否する [Ref-16]。このエラーは PE 構造の破損(STATUS_INVALID_IMAGE_FORMAT)とは異なり、「ファイルとしては正しいが信頼できない」という意味であるため、ローダーが代替パスを試す余地が少ない。CI サブシステムの内部では ci.dll が署名チェーンの検証、ハッシュの照合、証明書の信頼性評価を一括して行っており、DOS スタブの変更によるハッシュ不一致はこの検証の最初期段階で検出される [Ref-16]。
実装コード(Utils.cpp 35–43行目)
const char* target = "This program cannot be run in DOS mode"; for (DWORD i = 0; i < fileSize - strlen(target); ++i) { if (memcmp(buffer + i, target, strlen(target)) == 0) { buffer[i] = 'H'; // Replace 'T' with 'H' break; } }
ファイル全体をバッファに読み込み、DOS スタブに含まれる既知の文字列 "This program cannot be run in DOS mode" をバイト列検索で特定し、先頭の T を H に置換している。この文字列は事実上すべての PE ファイルの DOS スタブに存在するため [Ref-07]、対象 DLL に依存しない汎用的な改ざんポイントとして機能する。
break で最初のマッチ時点で検索を停止しているのは、1 バイトの変更で署名を無効化するには十分であり [Ref-08]、複数箇所を変更する必要がないためである。
CopyAndPatchFile の前半部分:ファイルコピー(Utils.cpp 3–9行目)
BOOL CopyAndPatchFile(std::wstring srcPath, std::wstring dstPath) { if (!CopyFileW(srcPath.c_str(), dstPath.c_str(), FALSE)) { std::wcerr << L"CopyFileW failed with error: " << GetLastError() << std::endl; return false; }
CopyFileW の第3引数 FALSE(bFailIfExists)は「コピー先にファイルが既に存在する場合は上書きする」を意味する [Ref-27]。これにより、ツールを複数回実行した場合でもエラーにならず、冪等(idempotent)な動作となる。コピー元は C:\Windows\System32\msvcp_win.dll のような正規の DLL パスで、コピー先は C:\TMP\msvcp_win.dll のようなユーザーが指定した任意のパスである。正規の DLL を WRP で保護された System32 から [Ref-09]、保護されていない別のディレクトリにコピーすることで、改ざんが可能になる。
サービス登録とブート順序の制御
なぜ Windows サービスとして登録するのか
このツールの目的は「EDR が起動する前に Bind Filter のリダイレクトを設定する」ことだが、ここで「EDR より前」とは具体的にどういう意味かを理解する必要がある [Ref-10]。
Windows の起動シーケンスでは、カーネルが初期化された後、Session Manager(smss.exe)、Windows Subsystem(csrss.exe)、ログオンプロセス(winlogon.exe)が順に起動する。この過程で SCM(Service Control Manager: services.exe)が開始し、登録されたサービスを順次起動する。サービスの起動順序は以下の要素で決定される [Ref-10]。
- Start Type:
SERVICE_BOOT_START(0)→SERVICE_SYSTEM_START(1)→SERVICE_AUTO_START(2)→SERVICE_DEMAND_START(3)の順。番号が小さいほど早い。 - Load Order Group:同じ Start Type 内では、
HKLM\SYSTEM\CurrentControlSet\Control\ServiceGroupOrderレジストリキーに列挙されたグループの順序に従う [Ref-13]。 - 依存関係:
DependOnService/DependOnGroupで指定された依存先が起動してから起動する。
EDR のカーネルドライバ部分は通常 SERVICE_BOOT_START または SERVICE_SYSTEM_START で登録され、OS 起動の非常に早い段階でロードされる。特に Microsoft Defender のような主要 EDR は ELAM(Early Launch Anti-Malware)ドライバ を持ち、これは SERVICE_BOOT_START かつ Early-Launch グループに属する最も早い段階で起動されるドライバである [Ref-18] [Ref-19]。ELAM ドライバは後続のブートドライバの署名を評価し、不正なドライバのロードを阻止する役割を持つ。
しかし、EDR の ユーザーモードサービス(例:Windows Defender の MsMpEng.exe)は SERVICE_AUTO_START で登録されていることが多い。このユーザーモードプロセスが実際にファイルシステムから DLL をロードするタイミングが攻撃のターゲットとなる。
Load Order Group の選択が攻撃成否を分ける理由
このツールは SERVICE_AUTO_START で登録されるため、EDR のカーネルドライバ(SERVICE_BOOT_START)や ELAM ドライバ [Ref-18] より後に起動する。しかし、攻撃対象は EDR のカーネルドライバではなく、同じく SERVICE_AUTO_START で起動する EDR のユーザーモードサービスプロセス である。
同じ SERVICE_AUTO_START のサービス間では Load Order Group の順序が起動タイミングを決定する [Ref-10]。コマンドライン例にある TDI(Transport Driver Interface)グループは、ServiceGroupOrder リストにおいてネットワークスタック初期化の非常に初期段階に位置する。一方、多くの EDR のユーザーモードサービスは後段のグループに属するか、グループが指定されていない(グループなしのサービスは全グループのサービスの後に起動する)[Ref-10]。
したがって、TDI グループに登録されたこのツールのサービスは、EDR のユーザーモードサービスよりも先に起動し、EDR プロセスの出現を待機する状態に入れる。
CreateServiceW の各引数(Utils.cpp 94–108行目)
SC_HANDLE hService = CreateServiceW( hSCM, serviceName.c_str(), // 内部サービス名(レジストリキー名) displayName.c_str(), // services.msc に表示される名前 SERVICE_ALL_ACCESS, SERVICE_WIN32_OWN_PROCESS, // 独自プロセスとして実行 SERVICE_AUTO_START, // OS 起動時に自動開始 SERVICE_ERROR_NORMAL, // 起動失敗時にイベントログに記録するが OS 起動は続行 imagePath.c_str(), // 実行するバイナリのパス(cmd.exe 経由) groupName.c_str(), // Load Order Group nullptr, // TagId(不要) nullptr, // 依存関係なし L"LocalSystem", // 実行アカウント nullptr // パスワードなし(LocalSystem は不要) );
各引数の意味は CreateServiceW API リファレンス [Ref-11] に基づく。
L"LocalSystem" を実行アカウントに指定する理由:LocalSystem は Windows 上で最も高い権限を持つビルトインアカウントであり、カーネルオブジェクトや特権 API へのアクセスが可能である。Bind Filter のマッピング操作(BfSetupFilter)はカーネルドライバとの通信を伴うため、通常のユーザーアカウントや NetworkService では権限が不足する。また、CreateToolhelp32Snapshot で全プロセスを列挙するためにも SeDebugPrivilege 等の特権が必要であり、LocalSystem はこれを暗黙的に保持している [Ref-11]。
SERVICE_ERROR_NORMAL の意味:このサービスの起動に失敗した場合でも、OS の起動処理は続行される [Ref-11]。SERVICE_ERROR_CRITICAL を指定すると、起動失敗時にシステムが「前回正常起動時の構成」で再起動を試みるが、攻撃ツールがそのような副作用を持つのは望ましくないため NORMAL が選択されている。
cmd.exe /k 経由の起動が必要な理由(EDRStartupHinder.cpp 110行目)
std::wstring imagePath = L"%SystemRoot%\\System32\\cmd.exe /k \"" + GetCurrentProcessPath() + L" " + fakeLib + L" " + originalLib + L" " + edrProcess + L"\"";
Windows サービスとして登録されたプロセスは、起動後一定時間内(デフォルト30秒)に SCM に対して RegisterServiceCtrlHandlerEx [Ref-29] でコントロールハンドラを登録し、SetServiceStatus [Ref-30] で SERVICE_RUNNING 状態を報告しなければならない。この報告がないと、SCM はサービスの起動に失敗したと判断し、プロセスを強制終了する。
EDRStartupHinder のコードには RegisterServiceCtrlHandlerEx や SetServiceStatus の呼び出しが一切存在しない。正式なサービスハンドラを実装するには、サービスのメインエントリポイント(ServiceMain)を定義し、SCM との通信プロトコルに従う必要があり、コードの複雑さが増す。
cmd.exe /k を介することでこの問題を回避している。仕組みは以下の通りである。
- SCM が
cmd.exeをサービスプロセスとして起動する cmd.exe自体は特にサービスハンドラを登録しないが、SCM のタイムアウトが発生してもプロセスが即座に強制終了されるわけではない。SCM はサービスの状態をSERVICE_START_PENDINGのままタイムアウトとして記録するが、プロセス自体は動作を続けるcmd.exe /kの引数として EDRStartupHinder が子プロセスとして起動され、Session 0 で無限ループに入る
この方法は「正規のサービスプロセス」としては不完全であり、sc query でサービスの状態を見ると START_PENDING や異常状態として表示される場合がある。しかし、ツールの目的(EDR 妨害のために永続的にバックグラウンドで動作する)には十分機能する。
/k フラグの意味:cmd.exe /c は指定コマンドの実行後に cmd.exe 自体が終了するが、/k はコマンド実行後もコマンドプロンプトを開いたまま維持する。EDRStartupHinder は無限ループするため実際には cmd.exe に制御が戻ることはないが、万が一 EDRStartupHinder がクラッシュ等で終了した場合にも cmd.exe プロセスは存続し、サービスプロセスとして残り続ける。
Session 0 検出によるモードの切り替え
なぜモード切り替えが必要なのか
EDRStartupHinder は単一のバイナリでありながら、2つの異なるコンテキストで実行される。
- 初回セットアップ時:管理者が対話的にコマンドプロンプトから実行する。この場合はユーザーのログオンセッション(Session 1 以降)で動作し、DLL のコピー・改ざんとサービスの登録を行う。
- サービスとして自動実行時:OS 起動時に SCM がサービスとして起動する。この場合は Session 0 で動作し、EDR プロセスの監視と Bind Filter の制御を行う。
同じバイナリがこの2つのモードのどちらで動作しているかを実行時に判定する必要があり、その判定に Session ID を使用している。
実装の詳細(Utils.cpp 122–133行目)
BOOL IsRunningAsService() { DWORD sessionId = 0; if (ProcessIdToSessionId(GetCurrentProcessId(), &sessionId)) { if (sessionId == 0) { return true; } } return false; }
Windows Vista 以降、Session 0 分離(Session 0 Isolation) が導入され、すべてのサービスは Session 0 で実行される [Ref-14]。対話的にログオンしたユーザーのプロセスは Session 1 以降で実行される。この分離は、サービスとユーザープロセスが同一セッションで動作することによるセキュリティリスク(Shatter Attack 等)を防ぐために導入された [Ref-14]。
ProcessIdToSessionId API [Ref-15] を使って現在のプロセスのセッション ID を取得し、0 であればサービスとして動作していると判定する。
厳密には、Session 0 で動作するプロセスにはサービス以外のもの(例えば Session 0 で起動されたドライバーのユーザーモードコンポーネント)も含まれるが、このツールの場合、Session 0 で動作している=サービスとして起動された、という前提は正しい。管理者が対話的にツールを実行する場合は必ず Session 1 以降になるため、この判定で十分に機能する。
サービスコントロールハンドラ未実装の副次的効果
RegisterServiceCtrlHandlerEx [Ref-29] を実装しないことで、管理者が sc stop <ServiceName> や services.msc からサービスを停止しようとしても、SCM が停止コマンドを送る先のハンドラが存在しないため停止命令は無視される。結果として、サービスの停止には taskkill /F /IM cmd.exe(ただしこれは他の cmd.exe プロセスにも影響する)や、正確な PID を指定した taskkill /F /PID <PID> が必要になる。これは EDR 管理者が容易にツールを無効化できないようにする防御回避効果を持つ。
EDR 監視ループ:タイミング設計の詳細
「EDR プロセス出現を待つ」設計の意図
一見すると、「EDR より先に起動するのだから、最初から Bind Link を掛けっぱなしにすればいいのでは?」という疑問が生じる。EDR プロセスの出現を待ってからリダイレクトを開始する設計には、以下の技術的・運用的理由がある。
理由1:他プロセスへの副作用の最小化。Bind Filter のリダイレクトはグローバル(jobHandle = 0)であるため、EDR だけでなくシステム上のすべてのプロセスに影響する。リダイレクトをかけっぱなしにすると、対象 DLL(例:msvcp_win.dll)に依存する他のプロセス(通常のアプリケーションや Windows のサービス)もすべて改ざん済み DLL をロードすることになる。msvcp_win.dll は Microsoft の C++ ランタイムの一部であり多数のプロセスが依存しているため、常時リダイレクトするとシステム全体が不安定化する可能性が高い。EDR の存在期間だけにリダイレクトを限定することで、この副作用を最小限に抑えている。
理由2:EDR の DLL ロードタイミングとの競合。EDR のユーザーモードプロセスは起動直後にプロセスの初期化処理を行い、その一部として必要な DLL をロードする。Windows の PE ローダーは、EXE の起動時にインポートテーブルに記載された DLL を自動的にロードする(暗黙的リンク)ため、DLL のロードは プロセスが作成されてからメインの処理が開始する前の非常に早い段階 で行われる [Ref-07]。つまり、Bind Filter のリダイレクトが有効になるのが、EDR プロセスの作成と DLL ロードの間の極めて短い時間窓に間に合う必要がある。
ここで「EDR プロセスが IsProcessRunning で検出された時点では、既に DLL をロード済みでは?」という疑問が自然に生じる。この点について、考えられるシナリオは以下の通りである。
- 初回起動時:このツールのサービスは EDR より先に起動するため、EDR プロセスが出現する前に監視ループに入っている。EDR プロセスが
CreateProcessで作成された直後にスナップショットで検出し、即座に Bind Link を作成する。プロセス作成から DLL ロード完了までの間に 10ms ポーリングがリダイレクトを間に合わせる可能性がある。ただし、これが常に間に合う保証はなく、初回の起動では EDR が正常に起動してしまう場合もあり得る。 - EDR の再起動時(主要なターゲット):初回起動で EDR が正常に動作を開始しても、何らかの理由で EDR プロセスが終了し再起動する際には、リダイレクトが確実に間に合う。なぜなら、EDR プロセスが終了した時点でツールはリダイレクトを解除し、次の出現を待機する状態に遷移する。EDR の SCM による自動再起動(
SERVICE_FAILURE_ACTIONS[Ref-12])では、通常数秒〜数十秒の遅延が設定されているため、その間にツールは再びポーリング待機状態に入っており、プロセス出現から DLL ロードまでの時間窓に十分間に合う。 - 初回起動でも間に合う可能性:Windows サービスの初期化には
RegisterServiceCtrlHandlerの呼び出し [Ref-29]、設定ファイルの読み込み、ドライバとの通信チャネル確立など、複数のステップが含まれる。対象の DLL がインポートテーブルではなくLoadLibraryによる明示的ロード(遅延ロード)で読み込まれる場合、プロセス作成から DLL ロードまでの間隔はさらに広がる。
監視ループの構造(EDRStartupHinder.cpp 33–68行目)
do { // フェーズ1:EDR プロセスの出現を待機 while (IsProcessRunning(edrProcess) == FALSE) { Sleep(10); // 10ms 間隔でポーリング } // フェーズ2:EDR が起動した → Bind Link を作成 if (FAILED(MyCreateBindLink(0, CREATE_BIND_LINK_FLAG_NONE, originalLib.c_str(), fakeLib.c_str(), 0, NULL))) { // エラー処理・終了 } // フェーズ3:EDR が終了するのを待機 while (IsProcessRunning(edrProcess) == TRUE) { Sleep(10); } // フェーズ4:EDR が終了した → Bind Link を解除 if (FAILED(MyRemoveBindLink(0, originalLib.c_str()))) { // エラー処理・終了 } } while (TRUE); // 無限ループ:EDR の再起動に対応
このループには4つのフェーズがあり、外側の do-while(TRUE) によって EDR が何度再起動しても対応し続ける。多くの EDR は、ウォッチドッグプロセスや SCM の SERVICE_FAILURE_ACTIONS(RestartService アクション)[Ref-12] によって、クラッシュ後に自動的に再起動する仕組みを備えている。単発のリダイレクトではこの自動再起動を阻止できないが、無限ループにより「再起動 → 再度リダイレクト → 再度失敗」のサイクルを永続的に維持できる。
プロセス検出の実装と 10ms ポーリングの技術的根拠
BOOL IsProcessRunning(std::wstring processName) { HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); ... }
CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) は、呼び出し時点のシステム全プロセスのスナップショットを作成する [Ref-20] [Ref-21]。プロセスの作成通知コールバック(PsSetCreateProcessNotifyRoutine をカーネルドライバで使用する方法)と比較すると、ポーリング方式には「プロセス作成から検出までに最大 10ms の遅延がある」というデメリットがある。しかし、カーネルドライバの開発・署名・ロードが不要であり、ユーザーモードだけで完結するという大きなメリットがある。
10ms という間隔は、CPU 負荷とタイミング精度のバランスを取った値である。Sleep(10) は実際には Windows のスレッドスケジューラの分解能(通常 15.6ms)に丸められるため、実効的なポーリング間隔は約 15–16ms 程度になる。この間隔は、EDR のプロセス作成から暗黙的 DLL ロード完了までの時間窓(通常数十〜数百ミリ秒)に対して十分に短い。
コマンドラインインターフェース:2つのモード
セットアップモード(5引数: EDRStartupHinder.cpp 91–117行目)
EDRStartupHinder.exe <FakeLib> <OriginalLib> <EDRProcess> <ServiceName> <ServiceGroup>
例:EDRStartupHinder.exe C:\TMP\msvcp_win.dll C:\Windows\System32\msvcp_win.dll MsMpEng.exe DusmSvc-01 TDI
このモードでは以下を順に実行する。
EnsureDirectoryExists(fakeLib):改ざん済み DLL の保存先ディレクトリが存在しない場合に作成する。SHCreateDirectoryExW[Ref-28] を使用しており、中間ディレクトリも再帰的に作成される(mkdir -p相当の動作)。CopyAndPatchFile(originalLib, fakeLib):前述の DLL コピー+1バイト改ざん処理。CreateNewService(...):サービスを登録し、次回の OS 起動から自動的にサービスモードで動作するようにする [Ref-11]。
サービス名に DusmSvc-01 のような既存サービスに似た名前を使うことで、services.msc やイベントログでの目立ちにくさを意図している(DusmSvc は実在する Windows サービス "Data Usage Monitor" の内部名)。
リンク解除モード(2引数: EDRStartupHinder.cpp 118–130行目)
EDRStartupHinder.exe <VirtualPath>
このモードは、以前に作成された Bind Link を手動で解除するために使用する。MyRemoveBindLink(0, fakeLib.c_str()) を呼び出し、指定パスに設定されたリダイレクトを削除する。テスト後のクリーンアップや、攻撃終了後の痕跡除去に使用される。
ディレクトリ確保とログ出力
EnsureDirectoryExists(Utils.cpp 135–157行目)
size_t pos = fullFilePath.find_last_of(L"\\/"); std::wstring dirPath = fullFilePath.substr(0, pos); int result = SHCreateDirectoryExW(nullptr, dirPath.c_str(), nullptr);
ファイルパスからディレクトリ部分を抽出し、SHCreateDirectoryExW [Ref-28] で再帰的にディレクトリを作成する。CreateDirectoryW は中間ディレクトリを自動作成しないが、SHCreateDirectoryExW は C:\TMP\sub1\sub2\ のような深い階層も一括で作成できる。改ざん済み DLL の保存先として、既存でない任意のパスを指定できるようにするための処理である。
AppendLog(Utils.cpp 191–201行目)
VOID AppendLog(std::wstring logEntry) { std::wofstream logFile; logFile.open(GetCurrentProcessDirectoryOnly() + L"\\RunLog.txt", std::ios::app); ... }
サービスモードでの動作状況を実行ファイルと同じディレクトリの RunLog.txt に追記する。サービスは Session 0 で動作し [Ref-14]、デスクトップを持たないため、std::wcout による画面出力はユーザーに表示されない。ファイルへのログ出力がサービスモードでの唯一のデバッグ・監査手段となる。std::ios::app フラグにより追記モードで開くため、サービスの再起動をまたいでログが蓄積される。
防御側からの検知・対策ポイント
| 観点 | 検知手法 | 詳細 | 関連リファレンス |
|---|---|---|---|
| Bind Filter の使用 | Sysmon Event ID 7(Image Loaded) | bindfltapi.dll がサービスプロセスからロードされるイベントを監視。正規の使用(Windows Sandbox 等)以外でのロードは高い異常指標 |
[Ref-22] |
| 不審なサービス登録 | System Event ID 7045 | 新規サービスの登録。特に TDI や NetworkProvider 等の早期ロードグループへの登録、cmd.exe /k を含む ImagePath は極めて不審 |
[Ref-10] [Ref-11] |
| Session 0 の cmd.exe | プロセス監視 | cmd.exe が Session 0 で長時間動作し、子プロセスとして不明なバイナリを持つパターン |
[Ref-14] |
| DLL の整合性 | ファイルハッシュ比較 | System32 配下の正規 DLL とリダイレクト先ファイルのハッシュが異なる(1バイト違い) | [Ref-08] |
| 高頻度プロセス列挙 | ETW(Event Tracing for Windows) | 10ms 間隔の CreateToolhelp32Snapshot は異常な頻度。ETW の Process / Thread プロバイダで検知可能 |
[Ref-23] [Ref-20] |
| Bind Filter の状態確認 | fltmc filters / fltmc instances |
bindflt のインスタンス数が通常より多い場合、不正なマッピングが存在する可能性 | [Ref-24] [Ref-06] |
| レジストリ痕跡 | レジストリ監視 | HKLM\SYSTEM\CurrentControlSet\Services\<ServiceName> に攻撃ツールのパスを含むエントリ |
[Ref-13] |
| ログファイル | ファイルシステム監視 | ツールと同じディレクトリに RunLog.txt が生成される |
— |
| bindflt ドライバ通信の監視 | カーネルモード ETW / ミニフィルタ | bindflt.sys への IOCTL を監視するカスタムミニフィルタドライバの導入。EDR ベンダーが独自に bindflt の状態を照会する API を実装することで検知が可能 | [Ref-03] [Ref-05] |
制約・限界
管理者権限が前提:
CreateServiceW(SC_MANAGER_CREATE_SERVICE権限)[Ref-11] と Bind Filter の操作には管理者権限が必要。ペネトレーションテストにおいては、初期アクセス+権限昇格が完了した後の Post-Exploitation フェーズで使用されるツールである。bindflt 依存:
bindfltapi.dllは Windows 10 1809 以降でのみ存在する [Ref-01]。Windows Server 2016 以前、または Windows 10 の古いバージョンでは動作しない。コード上ではLoadLibraryWの失敗で "OS NOT SUPPORT" と表示して終了する。タイミング依存性:EDR のプロセスが作成されてから暗黙的リンクの DLL がロードされるまでの時間窓に、ポーリングがリダイレクトの設定を間に合わせる必要がある。この時間窓が非常に短い場合(インポートテーブルによる暗黙的リンクで初期化が極めて高速な場合)、初回起動時にはリダイレクトが間に合わない可能性がある。ただし、EDR の再起動時には SCM の再起動遅延 [Ref-12] により時間窓が広がるため、成功確率が高まる。
対象 DLL の選定が攻撃成否を左右する:すべての DLL がこの攻撃に適しているわけではない。EDR プロセスが起動に不可欠な DLL(ロード失敗でプロセスが完全に停止する DLL)を選ぶ必要がある。
msvcp_win.dll(C++ ランタイム)は多くの C++ アプリケーションが暗黙的にリンクするため、有効な選択肢となる。PPL(Protected Process Light)との関係:一部の EDR は PPL として保護されており [Ref-17]、未署名の DLL のロードが拒否される。しかし、この攻撃では未署名の DLL を新たに読み込ませるのではなく、「署名検証に失敗する(元は署名されていた)DLL」にリダイレクトするため、PPL の保護とは異なるレイヤーで作用する。結果として、CI(Code Integrity)の署名検証で拒否されるという同じ効果が得られるが [Ref-16]、PPL の追加的な保護機構がこのリダイレクトをブロックする可能性は環境依存である。なお、ELAM ドライバ [Ref-18] を持つ EDR は PPL として起動するための前提条件(Microsoft による署名)を満たしている場合が多く、PPL 保護が有効な環境では攻撃の効果が限定される可能性がある。
フォレンジック痕跡:Bind Filter のマッピング自体は一時的だが、サービスの登録はレジストリに永続的に記録される [Ref-13]。改ざん済み DLL ファイルもディスク上に残存する。
RunLog.txtログファイルも攻撃の証拠となる。事後のフォレンジック調査では発見可能であるが、リアルタイムでの検知を回避することが主目的のツール設計となっている。サービスコントロールハンドラ未実装:
sc stopでは停止できないが、taskkill /Fやプロセスマネージャからの強制終了は可能。また、セーフモードで起動すればSERVICE_AUTO_STARTのサービスはロードされないため [Ref-10]、攻撃の影響を回避してサービスを削除(sc delete)できる。ELAM によるブートドライバ検証との関係:ELAM ドライバ [Ref-18] はブートプロセス中に他のブートドライバの署名を検証する機構であるが、このツールはブートドライバではなく
SERVICE_AUTO_STARTのユーザーモードサービスであるため、ELAM の検証対象には含まれない。したがって、ELAM が有効な環境でもツール自体のロードは阻止されない。
リファレンス一覧
本文中の各技術要素を正確に理解するために必要なリファレンスを以下にまとめる。Microsoft 公式ドキュメントを中心に構成し、非公式資料は補完的に最小限とした。本文中で [Ref-XX] として参照する。
1. Windows Bind Filter (bindflt.sys) と Bind Link API (bindfltapi.dll)
| ID | タイトル | URL | 説明 |
|---|---|---|---|
| Ref-01 | Bindlink API 公式ドキュメント | https://learn.microsoft.com/en-us/windows/win32/bindlink/ | 仮想パスとバッキングパスのバインド機構の全体像。CreateBindLink / RemoveBindLink の公式 API 定義 |
| Ref-02 | Undocumented BindFlt API (Nukem9) | https://github.com/Nukem9/BindFltAPI | BfSetupFilter / BfRemoveMapping 等、Windows SDK に公式ヘッダが存在しない内部 API の逆アセンブル定義。本ツールが使用する関数名の出典 |
| Ref-03 | ミニフィルタドライバの基本 | https://learn.microsoft.com/en-us/windows-hardware/drivers/ifs/about-file-system-filter-drivers | IRP 介入の仕組み、ファイルシステムフィルタドライバの全体アーキテクチャ |
| Ref-04 | ミニフィルタの Altitude とロード順序 | https://learn.microsoft.com/en-us/windows-hardware/drivers/ifs/load-order-groups-and-altitudes-for-minifilter-drivers | ミニフィルタの Altitude(優先度)による I/O スタック上の位置決定の仕組み。EDR のミニフィルタと bindflt の相対位置関係を理解するのに必須 |
| Ref-05 | Filter Manager Concepts | https://learn.microsoft.com/en-us/windows-hardware/drivers/ifs/filter-manager-concepts | FltMgr が IRP を Altitude 順に各ミニフィルタのコールバックに渡す仕組みの詳細 |
| Ref-06 | Allocated Filter Altitudes | https://learn.microsoft.com/en-us/windows-hardware/drivers/ifs/allocated-altitudes | Microsoft が割り当てた全ミニフィルタの Altitude 一覧。bindflt.sys は Altitude 409800 に割り当てられていることを確認可能 |
2. PE ファイルフォーマットと DOS スタブ
| ID | タイトル | URL | 説明 |
|---|---|---|---|
| Ref-07 | PE/COFF フォーマット公式仕様 | https://learn.microsoft.com/en-us/windows/win32/debug/pe-format | DOS ヘッダ、DOS スタブ、PE ヘッダ、セクションテーブルの構造と各フィールドの定義。改ざん対象の位置を理解するのに必須 |
3. Authenticode 署名と署名検証の仕組み
| ID | タイトル | URL | 説明 |
|---|---|---|---|
| Ref-08 | Authenticode PE 署名フォーマット公式ドキュメント | https://download.microsoft.com/download/9/c/5/9c5b2167-8017-4bae-9fde-d599bac8184a/Authenticode_PE.docx | ハッシュ計算の対象範囲が詳述されており、DOS スタブがハッシュ対象に含まれることを確認可能。署名の除外領域(チェックサムフィールドと証明書テーブルエントリ)の明示的な定義 |
4. Windows Resource Protection (WRP) と System32 保護
| ID | タイトル | URL | 説明 |
|---|---|---|---|
| Ref-09 | WRP 公式説明 | https://learn.microsoft.com/en-us/windows/win32/wfp/about-windows-file-protection | TrustedInstaller 以外のプロセスによる保護ファイルの書き換えを阻止する仕組み。DLL 直接上書きが困難な理由の根拠 |
5. Windows サービス起動順序・Load Order Group・SCM
| ID | タイトル | URL | 説明 |
|---|---|---|---|
| Ref-10 | 自動起動サービスのロード順序 | https://learn.microsoft.com/en-us/windows/win32/services/automatically-starting-services | ServiceGroupOrder の仕組み、Start Type と Load Order Group による起動順序の決定ロジック |
| Ref-11 | CreateServiceW API リファレンス | https://learn.microsoft.com/en-us/windows/win32/api/winsvc/nf-winsvc-createservicew | dwStartType、lpLoadOrderGroup、lpServiceStartName 等の全引数の公式定義。本ツールのサービス登録コードの各引数を理解するのに必須 |
| Ref-12 | SERVICE_FAILURE_ACTIONS 構造体 | https://learn.microsoft.com/en-us/windows/win32/api/winsvc/ns-winsvc-service_failure_actionsw | EDR の自動再起動メカニズム(SCM の障害回復アクション)の公式定義。無限ループ設計の必要性を理解するのに参照 |
| Ref-13 | ServiceGroupOrder レジストリキー | https://learn.microsoft.com/en-us/windows/win32/services/service-database | サービスデータベースの構成。HKLM\SYSTEM\CurrentControlSet\Control\ServiceGroupOrder の List 値にグループ名が起動順に列挙される |
6. Session 0 Isolation
| ID | タイトル | URL | 説明 |
|---|---|---|---|
| Ref-14 | Session 0 分離の公式解説 | https://techcommunity.microsoft.com/blog/askperf/application-compatibility---session-0-isolation/372361 | Windows Vista 以降のサービスが Session 0 で動作する理由、Shatter Attack 対策としての設計背景 |
| Ref-15 | ProcessIdToSessionId API | https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-processidtosessionid | 本ツールがモード判定に使用する API の公式定義 |
7. Code Integrity (CI) と署名検証失敗時の挙動
| ID | タイトル | URL | 説明 |
|---|---|---|---|
| Ref-16 | Code Integrity の内部構造(ci.dll) | https://www.cybereason.com/blog/code-integrity-in-the-kernel-a-look-into-cidll | カーネルモードでの署名検証の内部実装、STATUS_INVALID_IMAGE_HASH が返される条件の詳細分析 |
8. Protected Process Light (PPL) と ELAM
| ID | タイトル | URL | 説明 |
|---|---|---|---|
| Ref-17 | PPL 公式ドキュメント | https://learn.microsoft.com/en-us/windows/win32/services/protecting-anti-malware-services- | EDR/AM サービスを PPL として保護する仕組み。未署名 DLL のロード拒否、コードインジェクション防止の詳細 |
| Ref-18 | ELAM(Early Launch Anti-Malware)概要 | https://learn.microsoft.com/en-us/windows-hardware/drivers/install/early-launch-antimalware | ELAM ドライバの起動タイミング(SERVICE_BOOT_START の最初期)と、ブートプロセス中の他ドライバの評価・分類メカニズム。EDR カーネルドライバの起動順序を理解するのに重要 |
| Ref-19 | ELAM と Microsoft Defender の連携 | https://learn.microsoft.com/en-us/defender-endpoint/elam-on-mdav | Wdboot.sys による実装例と、ELAM がどのタイミングで動作するかの具体例 |
9. プロセス列挙 API
| ID | タイトル | URL | 説明 |
|---|---|---|---|
| Ref-20 | CreateToolhelp32Snapshot API | https://learn.microsoft.com/en-us/windows/win32/api/tlhelp32/nf-tlhelp32-createtoolhelp32snapshot | TH32CS_SNAPPROCESS フラグによるプロセススナップショット取得の公式定義。本ツールの EDR プロセス検出メカニズムの根拠 |
| Ref-21 | Taking a Snapshot and Viewing Processes | https://learn.microsoft.com/en-us/windows/win32/toolhelp/taking-a-snapshot-and-viewing-processes | Process32First / Process32Next による列挙パターンの公式サンプルコード |
10. 検知・フォレンジック関連
| ID | タイトル | URL | 説明 |
|---|---|---|---|
| Ref-22 | Sysmon 公式ダウンロード・ドキュメント | https://learn.microsoft.com/en-us/sysinternals/downloads/sysmon | Event ID 7(Image Loaded)で bindfltapi.dll のロードを監視する手段。Event ID 1(Process Create)でプロセス作成の検知も可能 |
| Ref-23 | ETW(Event Tracing for Windows)概要 | https://learn.microsoft.com/en-us/windows/win32/etw/about-event-tracing | 高頻度の CreateToolhelp32Snapshot 呼び出しを検知するための ETW プロバイダの仕組み |
| Ref-24 | fltmc コマンドリファレンス | https://learn.microsoft.com/en-us/windows-hardware/drivers/ifs/development-and-testing-tools#fltmc-exe-control-program | fltmc filters / fltmc instances による bindflt の状態確認 |
11. その他の Win32 API(本文中で言及されるもの)
| ID | タイトル | URL | 説明 |
|---|---|---|---|
| Ref-25 | LoadLibraryW API | https://learn.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-loadlibraryw | DLL の動的ロード。存在しない DLL に対して NULL を返す挙動の公式定義 |
| Ref-26 | GetProcAddress API | https://learn.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-getprocaddress | エクスポート関数のアドレス解決。非公開 API を名前で取得する手法の根拠 |
| Ref-27 | CopyFileW API | https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-copyfilew | 第3引数 bFailIfExists = FALSE による上書きコピーの動作定義 |
| Ref-28 | SHCreateDirectoryExW API | https://learn.microsoft.com/en-us/windows/win32/api/shlobj_core/nf-shlobj_core-shcreatedirectoryexw | 中間ディレクトリを含む再帰的ディレクトリ作成の公式定義 |
| Ref-29 | RegisterServiceCtrlHandlerEx API | https://learn.microsoft.com/en-us/windows/win32/api/winsvc/nf-winsvc-registerservicectrlhandlerexw | サービスコントロールハンドラの登録。本ツールがこれを意図的に省略する理由の理解に必要 |
| Ref-30 | SetServiceStatus API | https://learn.microsoft.com/en-us/windows/win32/api/winsvc/nf-winsvc-setservicestatus | サービスの状態報告。SCM のタイムアウト動作との関連 |