
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 を上書きすることです。


PipeAttribute 構造体は、最初のフィールドとして 16 バイトのサイズの LIST_ENTRY を持ち、次に属性の名前へのポインタ(8 バイト)、そしてオフセット 0x18 (24 進数) に AttributeValueSize フィールドがあります。これがヒープスプレーに保存しているものです。
その後、CLFS.sys と ntoskrnl をユーザーモードでロードし、GetProcAddress() を使用して ClfsEarlierLsn() と SeSetAccessStateGenericMapping() 関数のアドレスを見つけます。

次に、FindKernelModulesBase() 関数を呼び出して、NtquerySystemInformation() を使用し、今回は SystemModuleInformation 引数ですべてのモジュールに関する情報を返すことで、両方の同じモジュールのカーネルベースを見つけます。

このようにして、各関数のオフセットを計算し、それらをカーネルで取得できます。

pipeArbitraryWrite() 関数が 2 回呼び出されます。最初の呼び出しではフラグが 0、2 回目の呼び出しでは値が 1 になり、ヒープスプレーの値が変更されます。

最初の呼び出しでは、0x5000000 のメモリアドレスに次の値が配置されます。

この値は、そのアドレスへの割り当てに加えて、ヒープスプレーにも保存されることに注意してください。

最初の呼び出し後のメモリは次のようになります。前述の通り、0x5000000 付近のアドレスです。

そして、0x10000 からのメモリのヒープスプレーには、0x10 バイトごとに AttributeValueSize へのポインタと、0x5000000 へのポインタが格納されます。

このシーケンスがバグをトリガーします:

細工したファイルと、ランダムな名前の別のファイルに対して、再度 CreateLogFile() が呼び出されます。
次に、それらのファイルのハンドルを使用して AddLogContainer() が呼び出されます。

NtSetinformationFile() が呼び出され、ポインタが破壊されるハンドルが閉じられます(後で説明)。

ヒープスプレーは、この時点で BSOD が発生するのを防ぎます:

そこにブレークポイントを設定すると、ポインタが破壊されてヒープスプレーを指しており、vtable の次の 2 つの関数呼び出しを処理できることがわかります。


RAX は値 0x5000000 を取り、最初に 0x5000000+18 にある関数にジャンプし、次に 0x5000000+8 にジャンプします。


したがって、最初に fnClfsEarlierLsn() にジャンプし、次に fnSeSetAccessStateGenericMapping() にジャンプします。
ブレークポイントからトレースすると、CLFS!ClfsEarlierLsn() に到達することがわかります。

この関数は特に、戻るときに EDX を 0xFFFFFFFF に設定するために呼び出されています。

アドレス 0xFFFFFFFF には、SYSTEM _EPROCESS & 0xFFFFFFFFFFFFFFF000 の結果を保存していました。

前述の通り、CLFS!ClfsEarlierLsn() から戻ると、RDX の値は 0x00000000FFFFFFFF です。

次に、2 番目の関数 nt!SeSetAccessStateGenericMapping() に来ます。

この関数は有用です。なぜなら RCX はヒープスプレーを指し、RDX の値は 0xFFFFFFFF であり、その内容は制御可能だからです。


RCX+0x48 の内容は、v30.Pointer+24 に保存されていた AttributeValueSize へのポインタです。



その AttributeValueSize のポインタ値が RAX に移動され、次にアドレス 0xFFFFFFFF の内容(SYSTEM _EPROCESS & 0xFFFFFFFFFFFFFFF000 のアドレスを保存していた)を読み取ります。

次に、RAX+8 の次のフィールドである AttributeValue() を上書きします。


当然ながら、通常は AttributeValue はカーネル内で追加した属性を指しています。

そして今度は、SYSTEM _EPROCESS & 0xFFFFFFFFFFFFFFF00 の結果へのポインタで上書きされます。
これは、NtFsControlFile() 関数を再度、今度は引数 0x110038 で呼び出して属性を読み取ると、AttributeValue ポインタが指していた "A" の代わりに、_EPRROCESS & 0xFFFFFFFFFFFFFFFFF000 から要求されたバイト数を読み取り、出力バッファに返すことを意味します。これにより、最初の呼び出しで SYSTEM TOKEN の値を取得できます。

v9b は、System EPROCESS & 0xFFFFFFFFFFFFFFF000 の結果の内容がコピーされた Output Buffer の開始アドレスです。
それに、System EPROCESS の最後の 3 バイトである v14 を加え、さらにこのバージョンの Windows 11 の Token のオフセットである 0x4b8 を加えると、そのアドレスに System Token の値が保存されているのを見つけます。



最後の 4 ビットは変更されていますが、重要ではないため、値は依然として一致します。
2 回目の呼び出しでは、最初の呼び出しの最後にフラグが 1 に増加されているため、フラグの値は 1 です。

ここで値が格納される順序がわかります。

アドレス 0xFFFFFFFF には、見つけたばかりの System Process Token の値があります。


そしてヒープスプレーには、自プロセスの Token アドレスから 8 を引いた値があります。この値に 8 を加えたものがターゲットとして使用されます。書き込みは RAX+8 が指すアドレスに行われることに注意してください。


0x5000000 から始まるメモリアドレスでは:

また、別のコンテナ名を使用していることもわかります。前のコンテナはシステムプロセスによって使用されているため、再度開いたり削除したりできません。

次に、最初と同じ方法でバグが 2 回目にトリガーされます。

再び CLFS!ClfsEarlierLsn() に来ます。

RDX を 0xFFFFFFFF に設定:

次に nt!SeSetAccessStateGenericMapping() に来ます。

書き込む先の自プロセスの Token アドレスから 8 を引いたアドレスを読み取ります。

次に SYSTEM TOKEN を読み取ります。

そして、自プロセスの Token アドレス(8 を加える)に System Token を書き込みます。

これにより、自プロセスは System Token を持つことになります。

トークンが書き込まれたら、権限を確認するためのプロセスを開始します。この場合は Notepad.exe を起動します。



この PoC は Windows 11 でのみ動作することに注意してください。Windows 10 では BSOD が発生するため、正しく動作させるにはいくつかの修正が必要です。このブログ記事では説明しません。
構造体の解析
CLFS ファイル形式に関する構造体とほとんどのドキュメントは、IONESCU 氏の優れた CLFS Internals から引用しています。
ClfsBaseFilePersisted::LoadContainerQ 関数にチェックが追加されていることがわかります。

加算を行う値は、__CLFS_BASE_RECORD_HEADER 構造体に属します。

Base Block はファイルのオフセット 0x800 から始まり、オフセット 0x71FF で終了します。最初の 0x70 バイトは Log Block Header に対応します。
良い習慣として、IDA に _CLFS_LOG_BLOCK_HEADER 構造体を追加できます。
struct _CLFS_LOG_BLOCK_HEADER
{
UCHAR MajorVersion;
UCHAR MinorVersion;
UCHAR Usn;
char ClientId;
USHORT TotalSectorCount;
USHORT ValidSectorCount;
ULONG Padding;
ULONG Checksum;
ULONG Flags;
CLFS_LSN CurrentLsn;
CLFS_LSN NextLsn;
ULONG RecordOffsets[16];
ULONG SignaturesOffset;
};
次に、ファイルの先頭からオフセット 0x870 から始まり、長さ 0x1338 バイトの ベースレコードヘッダー (_CLFS_BASE_RECORD_HEADER) があります。

IDA にインポートする場合は、事前に次の型と不足している構造体を追加する必要があります。
typedef GUID CLFS_LOG_ID;
typedef UCHAR CLFS_LOG_STATE;
struct _CLFS_METADATA_RECORD_HEADER
{
ULONGLONG ullDumpCount;
};
これで追加する準備が整いました:
typedef struct _CLFS_BASE_RECORD_HEADER
{
CLFS_METADATA_RECORD_HEADER hdrBaseRecord;
CLFS_LOG_ID cidLog;
ULONGLONG rgClientSymTbl[0x0b];
ULONGLONG rgContainerSymTbl[0x0b];
ULONGLONG rgSecuritySymTbl[0x0b];
ULONG cNextContainer;
CLFS_CLIENT_ID cNextClient;
ULONG cFreeContainers;
ULONG cActiveContainers;
ULONG cbFreeContainers;
ULONG cbBusyContainers;
ULONG rgClients[0x7c];
ULONG rgContainers[0x400];
ULONG cbSymbolZone;
ULONG cbSector;
USHORT bUnused;
CLFS_LOG_STATE eLogState;
UCHAR cUsn;
UCHAR cClients;
} CLFS_BASE_RECORD_HEADER, *PCLFS_BASE_RECORD_HEADER;

構造体を含めた後、cbSymbolZone と _CLFS_BASE_RECORD_HEADER の終了アドレスとの間で加算が行われることに気付きます。 (start + 1338h)
cbSymbolZone は細工されたログファイルで 0x000000F8 から 0x0001114B に変更されたことを思い出してください。
(ファイルのオフセット 0x1b98)
0x800(ベースブロック開始オフセット) + 0x70 (logBlockHeader) + 0x1328 (cbsymbolZone)
0x800+0x70+0x1328 = 0x1b98
MyLog.blf ファイル上の細工された cbsymbolZone:


パッチが CClfsBaseFilePersisted::LoadContainerQ 関数にあるため、CClfsBaseFilePersisted オブジェクトを確認する必要があります。
CLFS!CClfsBaseFilePersisted::LoadContainerQ にブレークポイントを設定し、CreateLogFile が細工されたファイルのハンドルで呼び出されるとそこで停止します。

CClfsBaseFile::GetBaseLogRecord 関数を呼び出して、ベースログレコード (_CLFS_BASE_RECORD_HEADER) のアドレスを取得します。

RAX は _CLFS_BASE_RECORD_HEADER のアドレスを指します。

メモリ内の _CLFS_BASE_RECORD_HEADER 構造体と cbsymbolZone フィールド 0x1328 に注意してください。
バイト前方


r14 は "this" に対応する構造体を格納します。これは CClfsBaseFilePersisted であり、関数 CClfsBaseFilePersisted: :LoadContainerQ の this だからです。

メモリ内の CClfsBaseFilePersisted 構造体:

それでは、リバースしながらフィールドを完成させるために、長さ 0x21c0 の構造体を作成しましょう (これは未文書化構造体です)。これを struct_CClfsBaseFilePersisted と呼びます。

関数 CClfsBaseFile::GetBaseLogRecord() 内で _CLFS_BASE_RECORD_HEADER へのポインタを取得します。そして、その関数の "this" が構造体 struct_CClfsBaseFilePersisted であることがわかっています。

2 つのフィールドを読み取ります (オフセット 0x28 と 0x30)。

フィールド 0x28 はワードで値 6 を持っているため、構造体の型を word に変更します。



今のところ、定数 6 (const_6) に名前を変更します。


ドキュメントによると、6 はブロック数 CLFS_METADATA_BLOCK_COUNT です。このフィールドはこの値を指している可能性があります。
そして、そのポインタはオフセット 0x30 にあります。

そこに示されているサイズには、長さ 0x10 のヘッダーが含まれていることに注意してください。


ExAllocatePoolWithTag 関数が呼び出されるときに数バイトが要求されますが、ヘッダーは含まれないため、呼び出しでは 0x90 バイト (0xa0 – 0x10) が要求されます。
テキスト +30h] で検索すると、オフセット 0x30 に書き込む命令が多数見つかりますが、オブジェクトの型 CClfsBaseFilePersisted でリストをフィルタリングすると、結果は少なくなり、そのサイズが割り当てられている場所と、同じタグがすぐに見つかります。 (ヒント: Create や Initialize 関数名は、常に最初に確認する場所です。)


まだ名前がわからないので、pool_0x90 とします。これは別の未文書化構造体であり、そのサイズの構造体を作成します。


メモリ内の pool_0x90 は、自身のオフセット 0x30 に別のポインタを持っています。

この別のポインタは、ファイル内のベースブロックを指します (ベースブロックはオフセット 0x800 から始まります)。


Zscaler のブログ記事 から引用した画像:

割り当ては巨大です。これにはベースブロック全体が含まれているからです。



それでは、サイズ 0x7a00 の新しい構造体を作成し、BASE_BLOCK と呼びます。

最初の 70 バイトは _CLFS_LOG_BLOCK_HEADER に対応し、次の 0x1338 バイトは _CLFS_BASE_RECORD_HEADER に対応することが既にわかっています。

したがって、ベースブロックの先頭に次のレコードへのオフセット (0x70) を加算すると、_CLFS_BASE_RECORD_HEADER が得られます。

メモリ上の _CLFS_BASE_RECORD_HEADER。

同じ CClfsBaseFilePersisted オブジェクトの他のメソッドを見ると、CClfsBaseFilePersisted::AddContainer では CClfsBaseFile::GetBaseLogRecord を使用して _CLFS_BASE_RECORD_HEADER のアドレスも取得しています。

次に、cbOffset を使用して CClfsBaseFile::OffsetToAddr を呼び出し、_CLFS_CONTAINER_CONTEXT のアドレスを取得し、cboffset を _CLFS_BASE_RECORD_HEADER のオフセット 0x328 にある rgbcontainers 配列に格納します。

CClfsBaseFile::OffsetToAddr 関数は、オフセットから構造体のアドレスを見つけるために使用されます。

この時点で、0x328 に格納されるコンテナオフセットはまだ 0 です。まだコンテナを追加していないからです。

PoC は CreateLogFile を 2 回呼び出します。1 回目は不正なファイル MyLog.blf を使用し、2 回目は通常の MyLogxxx.blf ファイルを使用します。そのため、上記のすべての場所でデバッグを 2 回停止し、両方のファイルの上記構造体のアドレスをメモ帳にメモする必要があります。

少し先に進み、CLFS!CClfsLogFcbPhysical::AllocContainer にブレークポイントを設定し、そこまで実行しましょう。
POC で AddLogContainer() に到達すると、ブレークポイントで停止します。

また、既に確認した CClfsBaseFilePersisted::AddContainer+176 にブレークポイントを設定しましょう。ここでは、_CLFS_CONTAINER_CONTEXT 構造体へのオフセットとポインタが見つかります。


デバッガが停止すると、オフセットが 0x1468 であることがわかります。

RAX は _CLFS_CONTAINER_CONTEXT 構造体のアドレスを返します。

この構造体はまだ空です。コンテナがまだ追加されていないからです。

不正なファイルのオフセット 0x868 に書き込んだ SignatureOffset=0x50 の値が、ベースブロックの先頭 0x800 を差し引くと、_CLFS_LOG_BLOCK_HEADER 構造体のオフセット 0x68 にあることに注意してください。


PoC が不正なファイルを使用して AddLogContainer() 関数を呼び出すと、_CLFS_LOG_BLOCK_HEADER のオフセット 0x68 には、そこに書き込んだ 0x50 の値ではなく、メモリ上では現在 0xFFFF0050 が格納されています。

ある時点で、その値がプログラムによって変更されました。それがいつ発生したかを確認するために、次の実行で書き込みメモリブレークポイントを設定します。
オフセットは r15 + 0x328 に格納されています (r15 は _CLFS_BASE_RECORD_HEADER 構造体を指します)。


RBX はオフセット 0x1468 を格納します。

したがって、Base Block アドレス + 0x70 + 見つけたオフセット 0x1468 に、CLFS_CONTAINER_CONTEXT コンテナのアドレスが存在します。

CLFS_CONTAINER_CONTEXT 構造体のオフセット 0x18 には、そこに格納される pContainer ポインタがあります。書き込みブレークポイントを設定して、いつ書き込まれるかを確認できます。


これが、我々が破損させなければならないポインタです。脆弱性が存在する関数では、最初に CLFS_CONTAINER_CONTEXT を読み取り、次にそれを r15 に移動し、次に r15+18 の値を読み取ります。これが、ちょうど書き込みブレークポイントを設定したこのポインタです。


pContainer を struct_CClfsBaseFilePersisted 構造体のオフセット 0x1c0 に格納します。

何度か停止した後、破損が発生する瞬間に到達します。ポインタアドレスの上位が FF からゼロに変更されています。

これは、不正なファイルの 2 回目の AddLogContainer() が呼び出されたときに発生し、前の MyLogxxx のポインタが破損します。
問題は、SignaturesOffset が 0x50 であるべきところが、現在 0xFFFF0050 になっているために発生し、後続の memset で範囲外への書き込みが可能になります。


memset() 関数は、その下にある _CLFS_CONTAINER_CONTEXT 構造体を破損します。この構造体は MyLogxxx ファイルに対応します。作成時に互いに 0x11000 バイト離れて配置されたからです。
この方法で、次の構造体に書き込む正確な位置を計算し、ポインタの上位をゼロにして、HeapSpray が作成されたユーザーヒープを指すようにします。
不正なファイルのベースブロック構造体は、MyLogxxx ファイルのもののちょうど 0x11000 前にあります。
不正:

MyLogxxx


RCX は RDX より小さいです。0x50 (本来の値) ではなく、0xFFFF0050 が加算されたからです。

そして、memset() 関数に到達し、0xb0 バイトをゼロで設定します。RCX は MyLogxxx ファイルの CLFS_CONTAINER_CONTEXT 構造体、具体的には pContainer の上位 5 バイトを指しています。

このポインタは、最初のバイトを上書きすることで破損します:

残りは、以前 HeapSpray によって制御されたメモリアドレスを指すようになります。


次に、MyLogxxx ファイルのハンドルが閉じられ、CClfsBaseFilePersisted::RemoveContainer に到達し、脆弱性が最終的にトリガーされます。

より多くの情報が得られたので、ここで Base_Block.LOG_BLOCK_HEADER.SignaturesOffset と Base_Block. .LOG_BLOCK_HEADER.TotalSectorCount を読み取っていることに気付きます。
パッチの最初の部分では、SignaturesOffset が 0x7a00 より大きくてはなりません。私たちのものでは元々 0x50 でしたが、もし 0x7a00 より大きい値で到着すると、アウトになります。

パッチ適用済みのマシンで PoC を実行すると、0x50 と 0x7a00 を比較し、小さいので続行されます。

次のブロックでは、不正な cbSymbolZone が _CLFS_BASE_RECORD_HEADER の終了アドレスの値に加算され、この合計が result_1 に格納されます。

次に、Base_Block のアドレスに SignatureOffset の値が加算されます。通常のファイルではこれは 0x7980 です。

base_block の最大アドレスは 0x7a00 です。これで、SymbolZone は制限の 0x80 前まで許可されます。
これは result_2 に格納されます。つまり、base block 内の SymbolZone の最大制限になります。次に、両方の結果を比較し、最初の結果が 2 番目より大きい場合、範囲外になったことを意味します。


明らかに、最初のメンバーは 2 番目より大きくなり、続行されません。なぜなら、cbSymbolZone + _CLFS_BASE_RECORD_HEADER の終了アドレスの最初の合計が制限 (つまり result_2) を超え、「範囲外」になるからです。

最後に解明すべきことは、SignatureOffset の値 0x50 がどのようにして 0xFFFF0050 になるかということです。では、やり直して、再起動し、値がまだメモリ内で変更されておらず、依然として 0x50 である CLFS!CClfsBaseFilePersisted::LoadContainerQ で停止しましょう。
SignatureOffset のオフセット 0x68 にアクセスブレークポイントを設定します。

そして、数回の停止の後、ClfsEncodeBlockPrivate で値が変更される正しい瞬間を検出します。

この関数はパッチされていないため、0x50 の低い値と操作されている他の値によって引き起こされる動作である可能性があります。
細工された値の中に、_CLFS_BASE_RECORD_HEADER 構造体で rgClients という名前を持ち、Client Context オブジェクトを指すオフセットの配列を表す ccoffsetArray 値があります。
rgClients フィールドは、_CLFS_BASE_RECORD_HEADER 構造体のオフセット 0x138 (0x9a8-0x800-0x70) にあります。


PoC では、この値は FakeClientContext と呼ばれる偽のクライアントコンテキストオブジェクトを指すように破損されています。


これは Client Context 構造体 _CLFS_CLIENT_CONTEXT です。
struct _CLFS_CLIENT_CONTEXT
{
CLFS_NODE_ID cidNode;
CLFS_CLIENT_ID cidClient;
USHORT fAttributes;
ULONG cbFlushThreshold;
ULONG cShadowSectors;
ULONGLONG cbUndoCommitment;
LARGE_INTEGER llCreateTime;
LARGE_INTEGER llAccessTime;
LARGE_INTEGER llWriteTime;
CLFS_LSN lsnOwnerPage;
CLFS_LSN lsnArchiveTail;
CLFS_LSN lsnBase;
CLFS_LSN lsnLast;
CLFS_LSN lsnRestart;
CLFS_LSN lsnPhysicalBase;
CLFS_LSN lsnUnused1;
CLFS_LSN lsnUnused2;
CLFS_LOG_STATE eState;
union
{
HANDLE hSecurityContext;
ULONGLONG ullAlignment;
};
};
eState 値は構造体の先頭からオフセット 0x78 にあり、細工されたファイルでは 0x23a0+0x78 です。


この値はログの状態を示します。
typedef UCHAR CLFS_LOG_STATE, *PCLFS_LOG_STATE;
const CLFS_LOG_STATE CLFS_LOG_UNINITIALIZED = 0x01;
const CLFS_LOG_STATE CLFS_LOG_INITIALIZED = 0x02;
const CLFS_LOG_STATE CLFS_LOG_ACTIVE = 0x04;
const CLFS_LOG_STATE CLFS_LOG_PENDING_DELETE = 0x08;
const CLFS_LOG_STATE CLFS_LOG_PENDING_ARCHIVE = 0x10;
const CLFS_LOG_STATE CLFS_LOG_SHUTDOWN = 0x20;
const CLFS_LOG_STATE CLFS_LOG_MULTIPLEXED = 0x40;
const CLFS_LOG_STATE CLFS_LOG_SECURE = 0x80;
この値は CLFS_LOG_STATE CLFS_LOG_SHUTDOWN =0x20 に設定されています。
もう一つの破損した値は fAttributes で、これはベースログファイルに関連付けられた FILE_ATTRIBUTE フラグのセット(System や Hidden など)に対応します。


フィールドは 0xa で1バイト早く始まり、2バイトにまたがるため、fAttributes の値は 0x100 です。


最後に、オフセット 0x1bb8 を指す blocknameoffset 値があります。つまり、0x78 と 0x800 を加算するとファイルのオフセット 0x2428 を指します。


Client Context へのオフセットは 0x1b30 であることに注意してください。

したがって、Client Context はオフセット 0x23a0 にあります。


そして、そのちょうど 0x10 前に blocknameoffset に対応する値があります。


これは名前を持つ文字列を指すことになります。
最後のものは blockattributeoffset で、Client Context の 0xC 前の 0x2394 にあります。

これらの最後の2つの値は、Client Context の前にあり、長さが0x30バイトの構造体 _CLFSHASHSYM に属しています。
typedef struct _CLFSHASHSYM
{
CLFS_NODE_ID cidNode;
ULONG ulHash;
ULONG cbHash;
ULONGLONG ulBelow;
ULONGLONG ulAbove;
LONG cbSymName;
LONG cbOffset;
BOOLEAN fDeleted;
} CLFSHASHSYM, *PCLFSHASHSYM;


これらは _CLFSHASHSYM 構造体の先頭から0x20および0x24バイトの位置にあります。したがって、_CLFSHASHSYM 構造体において、POC で blockNameOffset と呼ばれる値は cbSymName フィールドであり、blockAttributteoffset は cbOffset フィールドです。


これらが破損した値です。次に、これらがどのように影響して SignaturesOffset を 0x50 の値から 0xFFFF0050 に変更するかを見る必要があります。
クライアントコンテキストを返すはずの CClfsBaseFile::AcquireClientContext() 関数を見てみましょう。

この関数は、Client Context へのポインタを格納する第4引数 _CLFS_CLIENT_CONTEXT ** を指定して CClfsBaseFile::GetSymbol を呼び出します。

CClfsBaseFile::GetSymbol 関数内で、破損した ccoffsetArray オフセットを CClfsBaseFile::OffsetToAddr に渡してクライアントコンテキストのアドレスを取得します。CreatelogFile で作成されたファイルを呼び出すときに停止するように、そこにブレークポイントを設定しましょう。

そこでは、細工された ccoffsetArray 引数で停止しています。


CClfsBaseFile::OffsetToAddr 関数は偽の Client Context を返します。

そして、RAX にある _CLFS_CLIENT_CONTEXT 構造体の前に 0xC が見つかるため、cbOffset の値がゼロでないことを確認します。


次に、cbOffset と ccoffsetArray(RSI にある)を比較し、等しくなければエラーになります。

また、cbSymName が cbOffset+0x88 に等しいことを確認し、そうでなければ同様にエラーになります。

最後に、cidClient バイトをゼロと比較します。

これらすべてのチェックが成功すると、client context が保存されます。

関数 r14 の出力は Client Context を指します。

CClfsLogFcbPhysical::Initialize から終了するとき、CLFS_CLIENT_CONTEXT のアドレスを取得します。

次に、fAttributes (0x100) の値を読み取ります。

この関数はクラス CClfsLogFcbPhysical に属しています。


これはここで割り当てられ、そのサイズは 0x15d0、タグは "ClfC" です。

リバースエンジニアリングしている内容を保存するための構造体を作成しましょう。名前は struct_CClfsLogFcbPhysical とします。

0x2b0 で CClfsBaseFilePersisted 構造体のアドレスを保存することに注意してください。

構造体に多くの値を保存した後、重要な部分に進みます。eState を 0x20 でテストします。


細工された値は 0x20 だったため、テストは1を返します。


コンストラクタの vtable に次のものがあることがわかります。

ファイルが multiplexed であるかどうかをチェックします。

したがって、目的のパスを通り、CClfsLogFcbPhysical::ResetLog に到達します。


いくつかのフィールドはゼロに初期化されますが、1つだけ 0xFFFFFFFF00000000 に初期化されます。

ここで Client Context を取得します。

値 0xFFFFFFFF00000000 を格納します。



0xFFFFFFFF をオフセット 0x5c に書き込みます。これは CLFS_LSN lsnRestart.ullOffset の上位部分です。



次に、ClfsEncodeBlockPrivate() 関数を実行します。この関数は、以前見たように 0x50 を 0xFFFF0050 で上書きする役割を担います。
そこで、SignatureOffset = 0x50 の値を読み取ります。これは破損ファイルに設定したままの値で、それを CLFS_LOG_BLOCK_HEADER の先頭に加算します。

これは2バイトを書き込むループです。SignatureOffset が正しい値(通常のファイルでは高い値、例えば 0x3f8 で先の方に書き込む)を指す代わりに、ここでは同じ CLFS_LOG_BLOCK_HEADER 内に書き込むことになります。
アイデアは、書き込み先を変更して SignatureOffset の値を破損しようとするものです。
通常のファイル

この時点で、ループが開始され、2バイトを書き込みます。

カウンタが値 0x3d に達するとループを終了します。

RCX は 0x200 から増加しており、すでに3サイクル目で、その値は 0x600 です。

イテレーション 0xe では、RCX は 0x1a00 です。


それが 0xFFFFFFFF000000 を書き込んだ場所でした。


最後の2バイト FFFF を読み取っています。

そしてそれを R8 にコピーします。


これまで見てきたように、この値は重要であり、チェックをバイパスして範囲外に書き込み、memset() の後に続くファイルの pContainer ポインタを破損し、先頭にゼロを書き込んで、制御下にあるメモリ(HeapSpray)を指すようにすることができます。
CClfsBaseFilePersisted::AllocSymbol では、memset の出力先を取得する同じ加算(cbSymbolZone + CLFS_BASE_RECORD_HEADER の最終アドレス)を、前に Base_block + 0xFFFF0050 と比較するため、方程式の両側で値が破損しています。
CbSymbolZone= 0x1114B
これは破損した値であり、CLFS_BASE_RECORD_HEADER の最終アドレスに加算されると範囲外への書き込みを引き起こします。比較のもう一方のメンバーは Base Block + SignatureOffset のアドレスであるべきですが、SignatureOffset =0xFFFF0050 のままであるため、このチェックを通過し、memset() で範囲外に書き込み、ポインタの先頭をゼロにして、HeapSpray を指したままにすることができます。

RCX が RDX よりも小さいため。

以前見たように(値は前の実行に属するため異なる場合があります)。
ポインタを破損し、最上位バイトを0に設定します。

HeapSpray を通じて制御するメモリ領域を指したままにします。


したがって、脆弱性がトリガーされると、CClfsBaseFilePersisted::RemoveContainer に到達します。

そこにはすでに破損したポインタがあり、以前見たように悪用可能です。

この時点でバグが悪用され、SYSTEM トークンを読み取り、自プロセスに書き込んでローカル権限昇格を達成する関数を制御できるようになります。
お役に立てれば幸いです。ご質問があれば、[email protected] または [email protected] までご連絡ください。
お楽しみください!