
驱动程序逆向与利用
⚠️ 警告: このプロジェクトは厳密に教育的・デモンストレーション目的です。悪意のある目的で使用することを意図していません。目的は、リバースエンジニアリングの手法と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);これは、この関数が MajorFunction テーブルのすべてのエントリ(0x1B = 27、および28個の主要IRPが存在)に割り当てられていることを意味します。
