
CVE-2022-37969 の概念実証エクスプロイト。Windows Common Log File System ドライバーのローカル権限昇格の脆弱性です。ヒープスプレー、トークンスティーリング、任意カーネル書き込みを実証し、SYSTEM 権限を獲得します。
著者: Ricardo Narvaja & Daniel Kazimirow (Solid)
デモンストレーション目的のみ。完全なエクスプロイトは脆弱な Windows 11 21H2 システムで動作します。
以前にZscaler が公開した情報に基づく機能的な PoC
解説記事はこちら: CVE-2022-37969 Windows Common Log File System ドライバのローカル特権昇格を理解する
エクスプロイトのウォークスルー:
使用したシナリオは Windows 11 21H2 (OS ビルド 22000.918) clfs.sys v10.0.22000.918
最初のステップは、CreateLogFile() 関数を使用して、パブリックフォルダ (%public%) に MyLog.blf というファイルを作成することです:



次に、ループを使用してランダムな名前のログファイルを複数作成します。
そしてループ内で、getBigPoolInfo() 関数を呼び出します:

NtQuerySystemInformation() を、最初の引数 0x42 (10 進数 66) で呼び出すと、v5 に bigpool で行われたレイドに関する情報が返ります。その構造体は SYSTEM_BIGPOOL_INFORMATION 型です。

この関数は 2 回呼び出す必要があります。1 回目はエラーが返りますが、正しいバッファサイズが得られるため、2 回目で目的の情報を取得できます。

v5 は SYSTEM_BIG_POOL_INFORMATION 構造体の情報を受け取ります。

bigpool の割り当て数は、最初のフィールド Count に格納され、2 番目のフィールドには SYSTEM_BIGPOOL_ENTRY 構造体の配列があります。

次に、すべての構造体を検索して "Clfs" タグとサイズ 0x7a00 のものを探します。

CLFS タグかつサイズ 0x7a00 の各構造体の最初のフィールドである VirtualAddress を、kernelAddrArray という配列に格納します。以降、両方の条件を満たすプールを「適切なプール」と呼びます。

各適切なプールを配列に格納するだけでなく、最後に見つかった適切なプールを、関数の引数として使用される a2 変数の内容に格納します。

このようにして、a2 は常に最後に作成された CLFS タグかつサイズ 0x7a00 の適切なプールを指します。
変数 v26 は、常に前に見つかった適切なプールを格納します。getBigPoolinfo() を呼び出す前に v26=v24 と等しく設定されますが、この呼び出しを抜けるときに v24 が最後に見つかった適切なプールで更新され、v26 は前に見つかった適切なプールのままになります。

次に、両方のアドレスを減算し、結果が負の場合はオペランドを反転して常に正になるようにします。

このようにして、v32 には最後の 2 つの適切なプールの VirtualAddress の差が格納されます。
次に同様の処理を行います。この場合、v23 は最初は 0 なので、最初のループで v23 = v32 となります。

次のループでは v23 は同じ値のまま 0 ではないため、ループを抜けてここに来ます。

v32 は最新の差、v23 は前回の差を持っています。それらが等しい場合はループを抜けてカウンタを 1 増やしますが、等しくない場合はカウンタを 0 にリセットします。
目的は、CLFS タグかつサイズ 0x7a00 の 6 つの連続した比較を行い、それらの差が等しいこと、そしてその差が 0x11000 であることを確認することです。実行時に、6 つの連続した等間隔の適切なプールが見つかると、その間隔の値が得られることがわかります。


ここで、6 つの連続した適切なプールが見つかり、ログファイルを作成するループを抜けています。
"public" フォルダに作成されたファイルが確認できます。

craftFile() 関数は、元のファイル (MyLog.blf) を開いて変更し、バグをトリガーします。

ファイルを変更した後、CRC32 を変更する必要があります。そうしないとファイル破損エラーが発生します。
この値はファイルのオフセット 0x80C にあります。

次に、VirtualAlloc() 関数を使用してヒープスプレーを実行し、任意のアドレス 0x10000 と 0x5000000 にそれぞれメモリを割り当て、2 番目の割り当て (0x10000) に 0x10 バイトごとに値 0x5000000 を保存します。

CreatePipe() を使用して匿名パイプを作成し、NtFsControlFile() を引数 0x11003c で呼び出して属性を追加します。後で同じ関数を引数 0x110038 で呼び出して読み取ることができます。
この方法の詳細は こちら にあります。

そこには、追加している属性である入力バッファがあります。NtFsControlFile() を引数 0x11038 で再度呼び出すと、出力に同じ属性が返されるはずです。

作成された属性のタグ (NpAt) についてプールを検索します。


見つかったら、そのプールの VirtualAddress を v30.Pointer に保存します。
V30.pointer+24 はカーネルプール内の AttributeValueSize を指しており、それを以前に行ったヒープスプレーの 1 つに保存します。

目的は、そのカーネルアドレス+8 に書き込み、AttributeValue を上書きすることです。

