Zscalerのブログ
Zscalerの最新ブログ情報を受信
購読するWindows CLFSのゼロデイ脆弱性(CVE-2022-37969)の技術分析パート1:根本原因分析
2022年9月2日、Zscaler ThreatLabzはWindows共通ログ ファイル システム ドライバー(CLFS.sys)におけるゼロデイ脆弱性の実際のエクスプロイトを確認し、Microsoftに報告しました。これを受けMicrosoftは、毎月第2火曜日にリリースするセキュリティ パッチの9月分でこの脆弱性を修正しています。この脆弱性は、Windows共通ログ ファイル システム ドライバーの権限昇格に関するもので、識別子はCVE-2022-37969です。攻撃者は、この脆弱性を悪用することでSYSTEM権限を取得できてしまいます。9月のパッチが適用される前の状態のWindows10およびWindows11では、このゼロデイ攻撃による権限昇格が起こり得るということです。この脆弱性の原因は、CLFS.sysのベース ログ ファイル(BLF)のBase Block中のSignaturesOffsetフィールドで厳密な境界チェックが行われていないことです。このため、特別な細工を施したClient Context配列とベース ログ ファイル内の偽のClient Contextによって、CLFSをエクスプロイトしてSignaturesOffsetフィールドを異常値で上書きできてしまいます。結果的に、Symbolが割り当てられる際のcbSymbolZoneフィールドの検証を回避でき、不正なcbSymbolZoneフィールドによって、任意のオフセットにおける境界外書き込みを行えます。このブログでは、この脆弱性とそれを利用したゼロデイ攻撃の実例について2部構成で解説していきます。パート1では根本原因に関する分析を、パート2では実際に確認されたエクスプロイトに関する分析を行っていきます。パート1となる今回の記事では、まずCVE-2022-37969の根本原因について詳しく見ていきます。
デバッグ環境
パート1、パート2を通じ、分析とデバッグは以下の環境で実行しています。
Windows 11 21H2 version 22000.918
CLFS.sys 10.0.22000.918
CLFS内部コードの概要
「共通ログ ファイル システム(CLFS)」は、汎用ログ サブシステムの1つです。カーネル モードで実行されているアプリケーションでも、ユーザー モードで実行されているアプリケーションでも使用でき、高パフォーマンスのトランザクション ログの構築に役立っているものです。CLFS.sysというドライバーに実装されています。共通ログ ファイル システムは、「ベース ログ ファイル(BLF)」にトランザクション ログを生成します。CLFSのコンセプトや関連する用語については、Microsoftの公式ドキュメントで説明されています。
CLFSを使用する前に、CreateLogFile APIを使ってログ ファイルが作成されます。ログ ファイルはメタデータ ブロックで構成されるベース ログ ファイルと、実際のデータを格納する複数のコンテナーで構成されます。AddLogContainer APIは、ログ ハンドルに関連付けられている物理ログにコンテナーを追加するために使用されます。
図1はBLFの構造を表しています。これは、CLFSに関する公式ドキュメントとAlex Ionescuの非公式ドキュメントを基にまとめたものです。
図1. ベース ログ ファイル(BLF)の構造
ベース ログ ファイルは、異なる6つのメタデータ ブロックで構成されています。具体的には、Control Block、Base Block、Truncate Blockと、この3つそれぞれに対応するシャドウです。これらのブロックには、3種類のレコード(Control Record、Base Record、Truncate Record)を格納することができますが、この記事では、今回発見された脆弱性と関連のあるBase Recordに焦点を絞って説明していきます。Base Recordを構成するシンボル テーブルには、ベース ログ ファイルに関連するClient Context、Container Context、Security Contextに関する情報が格納されます。
ログ ブロックはすべて、以下のように構造が定義されたLog Block Headerで始まります。
typedef struct _CLFS_LOG_BLOCK_HEADER
{
UCHAR MajorVersion;
UCHAR MinorVersion;
UCHAR Usn;
CLFS_CLIENT_ID ClientId;
USHORT TotalSectorCount;
USHORT ValidSectorCount;
ULONG Padding;
ULONG Checksum;
ULONG Flags;
CLFS_LSN CurrentLsn;
CLFS_LSN NextLsn;
ULONG RecordOffsets[16];
ULONG SignaturesOffset;
} CLFS_LOG_BLOCK_HEADER, *PCLFS_LOG_BLOCK_HEADER;
サイズが0x70バイトのCLFS_LOG_BLOCK_HEADER構造体のメモリー レイアウトは図1で示したとおりです。SignaturesOffsetフィールドは、すべてのセクターの署名の格納に使用するメモリー内配列のオフセットです。配列は各ブロックの最後のセクターに配置する必要があります。署名はすべてのセクターの末尾にあり(サイズ: 0x200)、Sector Block Type (1バイト)とUsn (1バイト)で構成されています。セクターの種類を列挙すると以下のようになります。
const UCHAR SECTOR_BLOCK_NONE = 0x00;
const UCHAR SECTOR_BLOCK_DATA = 0x04;
const UCHAR SECTOR_BLOCK_OWNER = 0x08;
const UCHAR SECTOR_BLOCK_BASE = 0x10;
const UCHAR SECTOR_BLOCK_END = 0x20;
const UCHAR SECTOR_BLOCK_BEGIN = 0x40;
図1で、Base Blockは.BLFファイルのオフセット0x800~0x71FFとなっており、最初にLog Block Header (0x70バイト)が、次にBase Record Headerが続いています。Base Record Headerは、図2に示すCLFS_BASE_RECORD_HEADER構造体で表されます。
図2. CLFS_BASE_RECORD_HEADER構造体の定義
CLFS_BASE_RECORD_HEADER構造体のレイアウトを図3に示します。
図3. BLFファイルのBase Record Headerの構造体レイアウト
Base Recordは、サイズが0x1338バイトのヘッダー(CLFS_BASE_RECORD_HEADER)で始まり、その後に関連するコンテキスト データが続きます。CLFS_BASE_RECORD_HEADER構造体には、今回の脆弱性に関連する重要なフィールドがいくつか含まれています。以下はこのフィールドに関する説明です。
- rgClients: Client Contextオブジェクトを指すオフセットの配列を表します。
- rgConteiners: Container Contextオブジェクトを指すオフセットの配列を表します。
- cbSymbolZone: シンボル ゾーン内の新しいシンボルに使用可能な次の空きオフセットを表します。
Base Recordでは、Client Context、Container Context、Shared Security Contextはシンボルで表され、以下のように定義されるCLFSHASHSYM構造体の後に続きます。
typedef struct _CLFS_NODE_ID {
ULONG cType;
ULONG cbNode;
} CLFS_NODE_ID, *PCLFS_NODE_ID;
typedef struct _CLFSHASHSYM
{
CLFS_NODE_ID cidNode;
ULONG ulHash;
ULONG cbHash;
ULONGLONG ulBelow;
ULONGLONG ulAbove;
LONG cbSymName;
LONG cbOffset;
BOOLEAN fDeleted;
} CLFSHASHSYM, *PCLFSHASHSYM;
CLFSHASHSYM構造体のメモリー レイアウトは以下のとおりです。CLFSHASHSYM構造体の後に、種々のコンテキスト オブジェクトが続いています(図4)。
図4. CLFSHASHSYM構造体(シンボル ヘッダー)
Base Recordでは、Client Contextを使用してログ ファイルのクライアントを識別します。ベース ログ ファイルには、Client Contextを少なくとも1つ作成できます。Client Contextは、以下に定義されるCLFS_CLIENT_CONTEXT構造体で表されます。
typedef 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; //+0x78
union
{
HANDLE hSecurityContext;
ULONGLONG ullAlignment;
};
} CLFS_CLIENT_CONTEXT, *PCLFS_CLIENT_CONTEXT;
eStateフィールドはCLFS_CLIENT_CONTEXT構造体のオフセット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;
Base Record内で、Container Contextは、ベース ログ ファイルで使用するコンテナー ファイルの追加に関連しており、以下に定義されるCLFS_CONTAINER_CONTEXT構造体で表されます。
typedef struct _CLFS_CONTAINER_CONTEXT
{
CLFS_NODE_ID cidNode; //8 bytes
ULONGLONG cbContainer; //8 bytes
CLFS_CONTAINER_ID cidContainer; // 4 bytes
CLFS_CONTAINER_ID cidQueue; // 4 bytes
union
{
CClfsContainer* pContainer; //8 bytes
ULONGLONG ullAlignment;
};
CLFS_USN usnCurrent;
CLFS_CONTAINER_STATE eState;
ULONG cbPrevOffset; //4 bytes
ULONG cbNextOffset; //4 bytes
} CLFS_CONTAINER_CONTEXT, *PCLFS_CONTAINER_CONTEXT;
pContainerフィールドは、実行時にコンテナーを表すCClfsContainerオブジェクトを指すカーネル ポインターであり、CLFS_CONTAINER_CONTEXT構造体のオフセット0x18に配置されます。図5は、CLFS_CONTAINER_CONTEXT構造体のメモリー レイアウトを示しています。
図5. CLFS_CONTAINER_CONTEXT構造体のメモリー レイアウト
ブルー スクリーン エラーの再現
CVE-2022-37969の根本原因を特定するために、ThreatLabzはブルー スクリーン エラーを安定して実行する概念実証(PoC)を行いました。図6は、脆弱性の実行後の詳細なクラッシュ情報です。
図6. CVE-2022-37969によるクラッシュに関するWinDbgの情報
図6に示されているように、レジスター「rdi」は無効なメモリー アドレスを指しています。レジスター「rdi」は、CClfsContainerオブジェクトを指すポインターを格納するものです。前述のCLFS_CONTAINER_CONTEXT構造体では、pContainerフィールドはCClfsContainerオブジェクトを指すポインターであり、メモリー レイアウト中のオフセット0x18に配置されます。クラッシュの場所に基づき、CLFS_CONTAINER_CONTEXT構造体のpContainerフィールドが破損しています。これにより、このポインターが逆参照された際にブルー スクリーン エラーが発生します。
図7は、適切に構造化されたベース ログ ファイル(.BLF)と、CVE-2022-37969を実行するために特別な細工が施されたベース ログ ファイルを比較したものです。
図7. CVE-2022-37969のエクスプロイトを目的として特別な細工が施されたベース ログ ファイル(BLF)
このベース ログ ファイルの作成後、SignatureOffsetフィールド、Client Contextのオフセット配列、cbSymbol、偽のClient Contextなどを含む特定のバイトが、これに合わせて変更されているはずです。図7のとおり、変更されたすべてのバイトはBase Log Record (オフセット:.BLFファイルの0x800~0x81FF)に存在しています。.BLFファイルに加えられた変更は図8のとおりです。
図8. CVE-2022-37969を実行するために.BLFファイルに加えられた変更
CVE-2022-37969を実行する概念実証コードは図9のとおりです。
図9. CVE-2022-37969の概念実証コード スニペット
CVE-2022-37969の概念実証の手順は以下のとおりです。
- CreateLogFile APIを使用して、「C:\ユーザー\Public\」フォルダーにベース ログ ファイル「MyLog.blf」を作成する。
- 「MyLog_xxx.blf」といった名前のベース ログ ファイルを数十個作成する。続いて作成される2つのメモリー領域は、Base Blockを表します。このメモリー領域間のオフセットは一定(0x11000バイト)とすることが非常に重要です。今回の脆弱性を狙ったゼロデイ攻撃の最初のサンプルでは、オフセット定数の生成に高度な手法が用いられていました。この手法の詳細については、パート2の記事で取り扱います。ThreatLabzの概念実証コードでは、定数カウントを使用してベース ログ ファイルを作成するため、その後に作成される2つのメモリー領域間のオフセットは一定とならない場合があります。そのような場合は定数を微調整する必要があります。
- MyLog.blfの特定のオフセットで数バイトに変更を加え、Base Blockの新しいCRC32チェックサムを再計算して、オフセット0x80Cに新しいチェックサム値を書き込む。その後、既存のベース ログ ファイルMyLog.blfを開きます。
- APIを呼び出し、「C:\ユーザー\Public\」フォルダーにベース ログ ファイル「MyLxg_xxx.blf」を作成する。
- 「AddLogContainer」APIを呼び出し、手順4で作成したベース ログ ファイル「MyLxg_xxx.blf」のログ コンテナーを追加する。
- GetProcAddress(LoadLibraryA("ntdll.dll"), "NtSetInformationFile")を呼び出し、NtSetInformationFile APIの関数アドレスを取得する。
- 「AddLogContainer」APIを呼び出し、手順3で開いたベース ログ ファイル「MyLog.blf」のログ コンテナーを追加する。
- 最後のパラメーターがFileInformationClassの型となるNtSetInformationFile(v55, (PIO_STATUS_BLOCK)v33, v28, 1, (FILE_INFORMATION_CLASS)13)を呼び出す。値がFileDispositionInformation (13)の場合、関数はファイルが閉じられたときにファイルを削除するか、以前にリクエストされた削除をキャンセルします。
- 「CloseHandle」APIを呼び出してベース ログ ファイル「MyLxg_xxx.blf」のハンドルを閉じ、この脆弱性を実行する。
根本原因分析
上記で紹介した概念実証コードを使って、根本原因を分析できます。図9の手順3は、5番目のパラメーターが4 (OPEN_ALWAYS)である「CreateLogFile」APIを呼び出し、既存のファイルを開くか、既存のものがない場合はファイルを作成するものです。今回のケースでは、既存のベース ログ ファイル「MyLog.blf」が開きます。手順4では、「CreateLogFile」APIを呼び出し、「MyLxg_xxx.blf」という名前の新しいベース ログ ファイルを作成しています。手順5と7では、それぞれ「AddLogContainer」を呼び出し、ログ ハンドルに関連付けられている物理ログにコンテナーを追加しています。
まず、AddLogContainer()関数がユーザー スペースで呼び出された際に、CLFSドライバーがログ コンテナーの追加リクエストをどのように処理しているかを詳しく見てみましょう。以下のブレイクポイントを設定することで、このリクエストの処理プロセスをトレースできます。
bu CLFS!CClfsRequest::AllocContainer
CLFS.sysでは、CClfsRequestクラスがユーザー スペースからのリクエストを処理します。CClfsRequest::AllocContainer関数は、物理ログにコンテナーを追加するリクエストの処理に使用されます。CClfsRequest::AllocContainer関数は、CClfsLogFcbPhysical::AllocContainerを呼び出します。CClfsLogFcbPhysical::AllocContainerの宣言は以下のとおりです。
CClfsLogFcbPhysical::AllocContainer(CClfsLogFcbPhysical *this, _FILE_OBJECT *,_UNICODE_STRING *,unsigned __int64 *)
次に、CClfsLogFcbPhysical::AllocContainerのブレイクポイントが以下のように設定されます。
bu CLFS!CClfsLogFcbPhysical::AllocContainer
手順5では、コードがAddLogContainer関数を呼び出すとCClfsLogFcbPhysical::AllocContainerのブレイクポイントが実行されます。ブレイクポイントに達したら、CClfsLogFcbPhysicalクラスのthisポインターを検査してみましょう。このthisポインターはCClfsLogFcbPhysicalオブジェクトを指しています。図10に表示されているように、レジスター「rcx」にはCClfsLogFcbPhysicalクラスのthisポインターが格納されます。
図10.CClfsLogFcbPhysical::AllocContainerにおけるCClfsLogFcbPhysicalクラスのthisポインターの検査
CClfsLogFcbPhysicalクラスのvftableのアドレスはオフセット0x00に格納されます。オフセットthis+0x30では、ログ名を指すポインターが格納されます。オフセットthis+0x2B0では、CClfsBaseFilePersistedクラスを指すthisポインターが格納されます。メモリーに格納されると、CLFSベース ログ ファイルはCClfsBaseFileクラスで表され、CClfsBaseFilePersistedクラスでさらに拡張できます。CClfsBaseFilePersistedクラスのthisポインターでは、オフセット0x30に0x90バイトのヒープ バッファーを指すポインターが格納されます。さらに、このヒープ バッファーではBase Blockを指すポインターがオフセット0x30に格納されます。また、CClfsBaseFilePersistedのthisポインターでは、CClfsContainerオブジェクトを指すポインターがオフセット0x1C0に格納されます。図11のとおり、コンテナーが正常に追加されると、図5で示したCLFS_CONTAINER_CONTEXT構造体をメモリー内で確認できます。
図11. コンテナーが正常に追加された後のCLFS_CONTAINER_CONTEXT構造体
CClfsBaseFilePersistedオブジェクトのオフセット0x1C0には、CLFS_CONTAINER_CONTEXT構造体のpContainerフィールドから取得される、CClfsContainerオブジェクトを指すポインターが格納されます。このとき、CLFS_CONTAINER_CONTEXT + 0x18のメモリー書き込みブレイクポイントを設定することで、CLFS_CONTAINER_CONTEXT構造体内のCClfsContainerオブジェクトを指すポインターがいつ破損するかをトレースできます。CClfsBaseFilePersistedオブジェクトのオフセット0x1C0にある別のメモリー書き込みブレイクポイントは、以下のように設定できます。
1: kd> ba w8 ffffc80c`cc86a4f0 //CLFS_CONTAINER_CONTEXT: +0x18
1: kd> ba w8 ffffb702`3cf251c0 //CClfsBaseFilePersisted: +0x1C0
手順7でコードがAddLogContainer関数を呼び出した際、CLFS!CClfsRequest::AllocContainerとCLFS!CClfsLogFcbPhysical::AllocContainerでは、再びブレイクポイントに達しています。ここで、CClfsLogFcbPhysicalクラスのthisポインター(図12参照)を検査してみましょう。細工されたMyLog.blfファイルでは0x00000050に設定されていたSignaturesOffsetフィールドが、メモリー内で0xFFFF0050に変更されていることに注目してください。
図12.CClfsLogFcbPhysicalクラスのthisポインターの検査
WinDbgで、引き続きコードを実行してみましょう。メモリー書き込みブレイクポイント「ba w8 ffffc80c'cc86a4f0」に達すると、Base RecordのCLFS_CONTAINER_CONTEXT構造体によって境界外書き込みが生成され、図13のとおり、CClfsContainerオブジェクトのポインターが破損します。
図13. CLFS_CONTAINER_CONTEXT構造体のCClfsContainerオブジェクトを指すポインターの破損
上記のバック スタック トレースに基づいてmemset関数が呼び出され、CClfsBaseFilePersisted::AllocSymbol関数でメモリー書き込みブレイクポイントが実行されました。図14はCClfsBaseFilePersisted::AllocSymbol関数の擬似コードです。
図14. CClfsBaseFilePersisted::AllocSymbol関数の擬似コード
この関数の詳細は次のとおりです。
- 変数v9はBase Record HeaderのcbSymbolZoneフィールドの値です。cbSymbolZoneフィールドは図8で示したような異常な値に設定されており、0x000000F8から0x0001114Bに変更されています。
- cbSymbolZoneフィールドを検証します。手順4で境界外書き込みを実行するためには、この検証を回避しなければなりません。図7の細工されたMyLog.blfファイルで0x00000050だったSignaturesOffsetフィールドは、メモリー内の巨大数(0xFFFF0050)で上書きされています。したがって、cbSymbolZoneフィールドが異常な値に設定されている場合でも、cbSymbolZoneフィールドの検証を回避できます。SignaturesOffsetフィールドが0x00000050から0xFFFF0050に変更される理由を特定すれば、CVE-2022-37969の根本原因が明らかになります。
- 変数v10はv9にBaseLogRecord、0x1338を加えた値に等しく、変数v9は0x0001114bに等しいためv10でのオフセットは無効です。
- この関数はmemset()を呼び出して無効なオフセットv10での境界外書き込みを引き起こします。これはMyLxg_xxx.blfのBase RecordのCLFS_CONTAINER_CONTEXT構造体の領域にあたり、同構造体のオフセット0x18におけるCClfsContainerポインターの破損を引き起こします。
図15は、どのように境界外書き込みが発生し、CClfsContainerオブジェクトのポインター破損につながるかを示したものです。
図15. CVE-2022-37969を悪用した境界外書き込みのプロセス
ここまでは、境界外書き込みが起こる理由と、CLFS_CONTAINER_CONEXT構造内のCClfsContainerオブジェクトを指すポインターが破損するメカニズムについて説明してきました。次は、破損したCClfsContainerポインターがどのようなタイミングで逆参照されるのかを見ていきましょう。その後、少し話を戻し、メモリー内のSignaturesOffsetフィールドが0x00000050から0xFFFF0050に変更される理由について見ていきます。
ユーザー スペースでCloseHandle関数が呼び出されると、CClfsRequest::Close(PIRP Irp)がこのリクエストを処理します。図16のとおり、カーネルではClfsBaseFilePersisted::WriteMetadataBlock関数内で別のメモリー ブレイクポイント(0x1c0 + CClfsBaseFilePersisted)に達します。
図16. ClfsBaseFilePersisted::WriteMetadataBlockでのメモリー ブレイクポイント(0x1c0+CClfsBaseFilePersisted)への到達
破損したポインターは、Base RecordのCLFS_CONTAINER_CONTEXT構造体のCClfsContainerオブジェクトからCClfsBaseFilePersistedオブジェクトのオフセット0x1c0にコピーされます。
図17は、CClfsContainerを指す破損したポインターがCClfsBaseFilePersistedオブジェクトのオフセット0x1c0に格納された後の、ClfsBaseFilePersisted::WriteMetadataBlock関数の擬似コードを示しています。このコードは、CLFS_CONTAINER_CONTEXT構造体のCClfsContainerオブジェクトを指すポインターのフィールドをゼロにします。ブロックのデコード後、CClfsContainerオブジェクトを指すポインターはCClfsBaseFilePersistedオブジェクトのオフセット0x1c0からCLFS_CONTAINER_CONTEXT構造体に復元されます。
図17. ClfsBaseFilePersisted::WriteMetadataBlock関数の擬似コード
最後に、CLFS!CClfsBaseFilePersisted::RemoveContainerのブレイクポイントを設定することで、Base RecordのCLFS_CONTAINER_CONTEXT構造内の、CClfsContainerオブジェクトを指す破損したポインターが逆参照された際にトレースできます。
図18は、CClfsBaseFilePersisted::RemoveContainer関数の擬似コードを示したものです。
図18. CClfsBaseFilePersisted::RemoveContainer関数の擬似コード
CClfsBaseFilePersisted::RemoveContainer関数では、以下の手順が実行されます。
- Base Recordのオフセット0x398にあるContainer Contextのオフセットを取得する。
- CClfsBaseFile::GetSymbolを呼び出してCLFS_CONTAINER_CONTEXT構造体を指すポインターを取得し、4番目のパラメーターに格納する。
- 手順2で取得したCLFS_CONTAINER_CONTEXT構造体のCClfsContainterオブジェクトを指すポインターの値をローカル変数v13に割り当てる。
- CLFS_CONTAINER_CONTEXT構造体内のCClfsContainterオブジェクトを指すポインターをゼロにする。
- 次にCClfsContainer::RemoveとCClfsContainer::Releaseを呼び出して、関連付けられているコンテナー ログ ファイルを削除してCClfsContainerオブジェクトを解放する。
CClfsContainterオブジェクトを指す破損したポインターを逆参照すると、メモリー違反につながります。図19はWinDbgのクラッシュ情報を示したものです。これがブルー スクリーン エラーを引き起こします。
図19. CClfsContainterオブジェクトを指す破損したポインターの逆参照
図14で見たとおり、CClfsBaseFilePersisted::AllocSymbol関数で境界外書き込みが発生しています。memset関数を呼び出す前に、メモリー内のSignaturesOffsetフィールドが0xFFFF0050で上書きされるため、cbSymbolZoneフィールドの検証が回避されています。Base Blockのメモリー内のSignaturesOffsetフィールドは、CreateLogFile APIを呼び出すリクエストを処理するプロセスにおいて0xFFFF0050で上書きでき、これにより図9の手順3の特別に細工されたベース ログ ファイル「MyLog.blf」が開かれます。
ユーザー スペースでCreateLogFile関数が呼び出されると、CLFS!CClfsRequest::Createがこのリクエストを処理します。CreateLogFile関数を使用して既存のベース ログ ファイルを開くと、CLFS.sysでCClfsLogFcbPhysical::Initialize関数が呼び出されます。図20は、CClfsLogFcbPhysical::Initialize関数の擬似コード スニペットを示しています。
図20. CClfsLogFcbPhysical::Initializeの擬似コード スニペット
この関数の詳細は次のとおりです。
1. CClfsBaseFilePersisted::OpenImage関数を呼び出して、ベース ログ ファイルにBase Blockのビッグプール(サイズ:0x7a00)を作成します。以下の順に関数呼び出しを行い、CClfsBaseFilePersisted::ReadMetadataBlock関数に入ります。
CClfsBaseFilePersisted::OpenImage -> CClfsBaseFilePersisted::ReadImage -> CClfsBaseFile::AcquireMetadataBlock -> CClfsBaseFilePersisted::ReadMetadataBlock
図21は、CClfsBaseFilePersisted::ReadMetadataBlock関数の擬似コード スニペットを示しています。この関数では ExAllocatePoolWithTag(PagedPoolCacheAligned, 0x7a00, 0x73666C43u)を呼び出してBase Blockを格納するビッグプールを作成します。その後、初期化を行い、ClfsDecodeBlock関数を呼び出してブロックをデコードします。ClfsDecodeBlock関数では、ClfsDecodeBlockPrivate関数を呼び出してBase Blockのオフセット0x50 (SignaturesOffsetの値)にあるセクター署名配列を解析します。各セクターのセクター署名の上書きに必要なのは2バイトです。Base Blockのオフセット0x68におけるこの2バイト(0x0050)は、13番目のセクションのセクター署名が格納されているオフセット0x19FE (0xC*0x200+0x1FE)で上書きされます。ここで、base_block+0x68とbase_block+0x200*0xE-0x8にある2つのメモリー書き込みブレイクポイントを設定します。この設定を行うのは、Base Blockの14番目のセクターのセクター署名が上書きされ、Base BlockのSignaturesOffsetフィールドが0xFFFF0050に書き換えられるタイミングをトレースするためです。
1: kd> ba w8 ffffd08b`51c03000+0x68 //base_block+0x68
1: kd> ba w8 ffffd08b`51c03000+0x200*0xE-0x8 // base_block+0x200*0xe-0x8
図21. CClfsBaseFilePersisted::ReadMetadataBlock関数の擬似コード スニペット
2. CClfsBaseFile::AcquireClientContext関数を呼び出して、Base BlockからClient Contextを取得します。図7で見たとおり、ベース ログ ファイルのオフセット0x9A8にあるClient Contextのオフセット配列の最初のオフセットには特別な細工が施されています。偽のClient Contextは、ベース ログ ファイルのオフセット0x23A0にあります。偽のClient Context構造体のオフセット0x78にあるeStateフィールドは、0x20 (CLFS_LOG_SHUTDOWN)に設定されます。
3. eStateフィールドがCLFS_LOG_SHUTDOWNではないこと、またはベース ログが多重化されたログであることを確認します。
4. 手順3における条件が偽であるため、CClfsLogFcbPhysical::ResetLog関数を呼び出します。図22はCClfsLogFcbPhysical::ResetLog関数の擬似コード スニペットを示したものです。base_block+0x1BF8にある8バイトは0xFFFFFFFF00000000に設定されており、セクター署名はオフセットbase_block+0x1BFCにあるため、セクター署名は0xFFFFで上書きされます。図23のWinDbgの画面は、上書きされたセクター署名を示したものです。
図22. CClfsLogFcbPhysical::ResetLog関数の擬似コード スニペット
図23. 0xFFFFで上書きされたセクター署名
5. CClfsLogFcbPhysical::FlushMetaData関数を呼び出します。 以下の順に関数呼び出しを行い、CLFS!ClfsEncodeBlockPrivate関数に入ります。
CLFS!CClfsLogFcbPhysical::FlushMetadata -> CLFS!CClfsBaseFilePersisted::FlushImage -> CLFS!CClfsBaseFilePersisted::WriteMetadataBlock -> CLFS!ClfsEncodeBlock -> CLFS!ClfsEncodeBlockPrivate
図24はCClfsLogFcbPhysical::ResetLog関数の擬似コード スニペットを示したものです。
図24. CLFS!ClfsEncodeBlockPrivate関数の擬似コード スニペット
上記のコードは、Base Block内の各セクターからセクター署名を取得し、セクター署名配列をセクター署名で上書きします。セクター署名配列はオフセット0x50にあり、Base BlockのSignaturesOffsetフィールドと重なっています。14番目のセクターのセクター署名は図23で見たとおり0xFFFFに設定されているため、2バイト(0xFFFF)がBase Blockのオフセット0x6C(0x50+0xE*2)で上書きされます。図25のとおり、このときSignaturesOffsetフィールドの値は0xFFFF0050となっています。
図25. 0xFFFF0050で上書きされたSignaturesOffsetフィールド
SignaturesOffsetフィールドの上書きプロセスをまとめると図26のようになります。
図26. SignaturesOffsetフィールドの上書きプロセス
まとめ
この記事では、CVE-2022-37969の根本原因に関するThreatLabzの詳細な分析をご紹介してきました。この脆弱性の原因は、CLFS.sysのベース ログ ファイル(BLF)におけるBase BlockSignaturesOffsetフィールドの不適切な境界チェックです。このため、特別な細工を施したClient Context配列とベース ログ ファイル内の偽のClient Contextによって、CLFSをエクスプロイトしてSignaturesOffsetフィールドを異常値で上書きできてしまいます。結果的に、Symbolが割り当てられる際のcbSymbolZoneフィールドの検証を回避でき、不正なcbSymbolZoneフィールドによって、任意のオフセットにおける境界外書き込みを行えます。これによりCClfsContainerオブジェクトを指すポインターの破損が起こり、このポインターが逆参照された際にメモリー違反が発生して、ブルー スクリーン エラーを引き起こします。パート2では、この脆弱性を権限昇格に利用するゼロデイ攻撃に関する分析をご紹介します。日本語版の公開まで今しばらくお待ちください。
リスク軽減策
Windowsユーザーは、必ず最新バージョンへのアップグレードを行ってください。Zscalerの高度な脅威対策やCloud Sandboxは、CVE-2022-37969を狙った実際のゼロデイ攻撃からお客様を保護できます。
Win32.GenExploit.LogFile
Win32.Exploit.CVE-2022-37969
Zscaler Cloud Sandboxによる検知
参考資料
https://msrc.microsoft.com/update-guide/vulnerability/CVE-2022-37969
https://github.com/ionescu007/clfs-docs/blob/main/README.md
https://i.blackhat.com/USA-22/Thursday/us-22-Jin-The-Journey-Of-Hunting-ITW-Windows-LPE-0day.pdf
https://www.slideshare.net/PeterHlavaty/deathnote-of-microsoft-windows-kernel
https://www.pixiepointsecurity.com/blog/nday-cve-2022-24521.html
https://learn.microsoft.com/ja-jp/previous-versions/windows/desktop/clfs/log-types
https://learn.microsoft.com/ja-jp/previous-versions/windows/desktop/clfs/creating-a-log-file
https://learn.microsoft.com/ja-jp/windows-hardware/drivers/kernel/clfs-terminology