
驱动程序逆向与利用
⚠️ 警告: このプロジェクトは厳密に教育的・デモンストレーション目的です。悪意のある目的で使用することを意図していません。目的は、リバースエンジニアリングの手法とWindowsドライバーのエクスプロイト手順を学ぶことです。
ここでは、d1rk(SaadAhla) https://github.com/SaadAhla が提供する演習を解くために私が取ったアプローチを説明します。この演習は、正規に署名され、ブロックリスト(HVCI、LOLBIN...)に存在しないドライバーに対してリバースエンジニアリングとエクスプロイトを行うというものです。 このカーネルモードドライバーを介してシステム上の任意のアクティブなプロセスを終了できるCプログラムも用意しています。その動作については、この後詳しく説明します。

📃 使用方法: DriverKiller.exe <プロセス名.exe> [-d]
オプション -d : エクスプロイト後にサービスとドライバーをシステムから削除します。
ドライバーの証明書が期限切れのため、ターゲットマシンでテスト署名モードを有効にする必要があります。
パート1 - リバースエンジニアリング:
演習では、SHA-256ハッシュを名前とする.sysファイルが提供されます。
最初のステップは、このファイルをIDAで開くことです。
IDAは無料で利用できます。Hex-Raysのサイトにアクセスしてライセンスを生成し、ソフトウェアをダウンロードするだけです。
まず、ドライバーのIAT(Import Address Table)を一覧表示し、目的のAPIへの呼び出しを探します: ZwTerminateProcess。
ZwTerminateProcess をダブルクリックすると、IDAはこの関数のコンパイル済みコードに移動します。エントリを選択してクロスリファレンスを表示すると、それを呼び出すドライバーの関数のリストが得られます。
ZwTerminateProcess を使用しているのは、オフセット 1CE の関数 sub_12EF4 であることがわかります。ダブルクリックすると、IDAはそのコンパイル済みコードを表示します。
逆コンパイルされたコードには、ZwOpenProcess(ターゲットプロセスへのハンドルを開く)と ZwTerminateProcess(そのハンドルを介してプロセスを終了する)への呼び出しが示されています。
ZwOpenProcess のドキュメント(https://learn.microsoft.com/fr-fr/windows-hardware/drivers/ddi/ntddk/nf-ntddk-zwopenprocess)を参照すると、ClientID パラメータが対象プロセスのPIDを示すポインタであることがわかります。
その上の行では、ClientId.UniqueProcess が変数 v22 で初期化されています。これはその直前に定義されています:
v22 = (void )((_QWORD *)i + 10);
この代入を理解するには、変数 i と +10 フィールドを特定する必要があります。
この関数のさらに上では、SYSTEM_PROCESS_INFORMATION パラメータを指定した ZwQuerySystemInformation への呼び出しがあります。また、i が変数 v6 とともにこの構造体のエントリを反復するイテレータであることもわかります。
ZwQuerySystemInformation のドキュメント(https://learn.microsoft.com/en-us/windows/win32/sysinfo/zwquerysysteminformation)によると、この関数はシステム上のアクティブなプロセスごとに1エントリを含む配列を返します。
SYSTEM_PROCESS_INFORMATION 構造体は、https://learn.microsoft.com/en-us/windows/win32/api/winternl/nf-winternl-ntquerysysteminformation で説明されています。
typedef struct _SYSTEM_PROCESS_INFORMATION {
ULONG NextEntryOffset;
ULONG NumberOfThreads;
BYTE Reserved1[48];
UNICODE_STRING ImageName;
KPRIORITY BasePriority;
HANDLE UniqueProcessId;
PVOID Reserved2;
ULONG HandleCount;
ULONG SessionId;
PVOID Reserved3;
SIZE_T PeakVirtualSize;
SIZE_T VirtualSize;
ULONG Reserved4;
SIZE_T PeakWorkingSetSize;
SIZE_T WorkingSetSize;
PVOID Reserved5;
SIZE_T QuotaPagedPoolUsage;
PVOID Reserved6;
SIZE_T QuotaNonPagedPoolUsage;
SIZE_T PagefileUsage;
SIZE_T PeakPagefileUsage;
SIZE_T PrivatePageCount;
LARGE_INTEGER Reserved7[6];
} SYSTEM_PROCESS_INFORMATION;
参考: Windows x64 におけるいくつかの型のサイズ
typedef struct _UNICODE_STRING {
USHORT Length; -> 2
USHORT MaximumLength; -> + 2 = 4
PWSTR Buffer; -> + 8 = 12 (12 n'est pas un multiple de 8 donc padding de 4 ajouté en amont de Buffer) = 16
} UNICODE_STRING;UniqueProcessId のオフセットの計算:
ULONG NextEntryOffset; -> 4
ULONG NumberOfThreads; -> + 4 = 8
BYTE Reserved1[48]; -> + 48 = 56
UNICODE_STRING ImageName; -> + 16 = 72
KPRIORITY BasePriority; -> + 4 = 76 (76 n'est pas un multiple de 8 donc padding de 4 ajouté) = 80
HANDLE UniqueProcessId; -> + 8 = 88
したがって、UniqueProcessId メンバーはオフセット 0x50(10進数の80)にあります。
変数 v22 の代入を見ると、i が QWORD(8オクテット)ポインタにキャストされていることがわかります:
v22 = (void )((_QWORD *)i + 10);
v22 は i + 10 * 8 = 80オクテットのアドレスに対応します。したがって、この変数には SYSTEM_PROCESS_INFORMATION 構造体から取得したPIDが含まれています。
どのPIDが ZwTerminateProcess に渡されるかを知るには、この代入を取り巻く条件を分析する必要があります。
最初にプロセスイメージ名が取得されていることがわかります:
v9 = (wchar_t )((_QWORD *)i + 8);
v9 = i + 8 × 8 = 64オクテットのアドレスだからです。これは、ImageName メンバーの Buffer に対応します。このメンバーはオフセット 56 + 2 (USHORT) + 2 (USHORT) + 4 (padding) = 64 にあるためです。
以下の操作とループを考慮すると、引数として渡されたプロセス名(a2)とシステム上のアクティブなプロセス v9/String の間で比較が行われているという仮説を立てることができます。
sub_1C078(String, v9, (int)v13); v17 = strupr(a2); v18 = strupr(String);
つまり、ZwTerminateProcess で終了するプロセス名を保持することになっているのはパラメータ a2 です。
a2 は関数 sub_12EF4 のパラメータであることに注目してください。さらに進めるには、この関数の参照を調べる必要があります(読みやすくするために ZwTerminateProcessCaller と改名しました)。
ZwTerminateProcessCaller はオフセット 61A の関数 sub_13624 によって呼び出されていることがわかります。
この逆コンパイル済みコードを分析する前に、関数 sub_13624(ZwTerminateProcessCallerCaller と改名)の参照を調べます。このコードがUserModeからの DeviceIoControl API呼び出しの後に実際に使用されていることを確認するためです。
ZwTerminateProcessCallerCaller は関数 sub_14130(ZwTerminateProcessCallerCallerCaller と改名... 幸いなことに、これはエントリポイントの前の最後の関数です 😅)によって呼び出されていることがわかります。
ZwTerminateProcessCallerCallerCaller はオフセット 306 の関数 sub_1A4A8 によって呼び出されていることがわかります。
ZwTerminateProcessCallerCallerCaller の代入が見つかります:
memset64(DriverObject->MajorFunction, (unsigned __int64)ZwTerminateProcessCallerCallerCaller, 0x1Cu);
関数 sub_13624(別名 ZwTerminateProcessCallerCaller)に戻る前に、Symbolic Name と Device Name(ここでは同一)を取得します: Viragtlt。
ZwTerminateProcessCallerCaller に戻ると、その2番目のパラメータ(つまり a2)が MasterIrp->AssociatedIrp.SystemBuffer に対応していることがわかります。
ZwTerminateProcessCaller への呼び出しのすぐ上に、IOCTL コード -2106392528(16進数: 0x82730030)があります。
これらの情報から、このドライバーを悪用するには、終了するプロセス名を SystemBuffer に入れて DeviceIoControl API呼び出しをドライバーに送信する必要があると推測できます。
🔷 リバースエンジニアリングで得られた情報:
0x82730030ViragtltViragtltパート2 - エクスプロイト
このドライバーを悪用するには(ターゲットマシンにインストールされアクティブな場合)、まずドライバーへのハンドルを開き、終了したいプロセス名を含むバッファを指定して DeviceIoControl API呼び出しを行う必要があります。
この演習では、Cプロジェクトを開発しました。その機能は次のとおりです:
また、エクスプロイト後にサービスとドライバーをシステムから削除できる -d オプションも追加しました。
実行サイクル全体におけるCプログラムの動作は次のとおりです:
AV/EDR回避
この場合、DriverKiller.exe は Microsoft Defender によって静的にも動的にも検出されません。 ここでの回避はあまり意味がありません。悪用されるドライバーは期限切れの証明書を持っているため、実際の環境での使用は考えにくいからです。 ただし、よりステルス性を高めるために、次のものを実装することもできました:
2025年8月29日時点のドライバー検出(既存の結果であり、明白な理由からVirusTotalには何も提出していません):
⚠️ このプロジェクトは学習目的で作成されています。不正確な点やエラーが含まれる可能性があります。提案、修正、議論はいつでも歓迎します! 😃 d1rk(SaadAhla) に感謝します: https://github.com/SaadAhla !