
CVE-2023-28252(Windows Common Log File System (CLFS) ドライバの特権昇格の脆弱性)の技術的分析と概念実証エクスプロイト。Nokoyawa ランサムウェア攻撃で使用されました。
2022年2月以降、Trend Microの調査によると、Windowsの0日脆弱性を悪用していると思われる新しいランサムウェアが報告されました。
このランサムウェアの詳細は、こちらのリンクでご覧いただけます。
Kasperskyの分析によると、Nokoyawaランサムウェアグループは2022年6月以降、Common Log File System (CLFS) ドライバーを標的とした他のエクスプロイトを使用しており、類似しているが異なる特徴を持ち、すべて単一のエクスプロイト開発者に関連しています。
2023年4月にMicrosoftがパッチをリリースした際に、CVE-2023-28252が割り当てられました。
以前、2022年には同じコンポーネントの類似のバグが私たちによって調査され、こちらのブログ記事に文書化されています。
解析に取り組むには、脆弱なCommon Log File System ドライバーであるCLFS.sysによって処理され、system32内のドライバーフォルダにある**.blf**ファイル形式を知る必要があります。
このファイルタイプの詳細については、以下のリンクを参照してください:
https://github.com/ionescu007/clfs-docs/blob/main/README.md
https://www.coresecurity.com/core-labs/articles/understanding-cve-2022-37969-windows-clfs-lpe
この解析は、Windows 11 21H2、clfs.sys バージョン 10.0.22000.1574 を対象としていますが、Windows 10 21H2、Windows 10 22H2、Windows 11 22H2、Windows Server 2022 でも動作します。
以前のWindowsバージョンでは、いくつかの値を調整する必要があります。そうしないと、BSODが発生します。
2023年4月のMicrosoft Patch Tuesday.
ドライバーバージョンは図のように確認できます
脆弱性が公開された2023年4月、私はEsteban Kazimirowと共にCLFS.sysドライバーのリバースエンジニアリングを開始しました。ただし、今回の場合、パッチを分析するだけではバグの場所やトリガー方法を推測するのは非常に困難でした。なぜなら、エクスプロイトは非常に複雑だからです。
その後、ブログ記事が公開され、その著者がマルウェアのサンプルからHexRaysによって逆コンパイルされたコードの一部と、エクスプロイトに取り組むための手がかりを提供しました。
明らかに提供された情報は完全ではありませんでしたが、この助けがなければPoCを構築し、後で機能するエクスプロイトを作成することは不可能だったでしょう。
理解を容易にするために、最初にPoCの構築方法を説明し、その後で脆弱性の分析を行います。
このブログ記事は2つのセクションで構成されています:
PoCの構築:
1-エクスプロイトに必要なカーネルアドレスを取得する
2-.blfファイルを作成するためのパスの準備
3-CreateLogFile()関数を使用して「トリガーblf」ファイルを作成する
4-「トリガーblf」ファイルを細工する
5-トリガーblfのBASE BLOCKのカーネルアドレスを取得する
6-トリガーblfのハンドルを使用してAddLogContainerを呼び出す
7-スプレーblfファイルを準備する
8-スプレーを実行するためのメモリを準備する
9-バグをトリガーする
デバッグ:
1-メモリスプレーを確認する
2-トリガーblfのRecordOffset[12]を見る
3-スプレーblfファイルのiFlushBlock値を見る
4-なぜBLOCK 0 CONTROLではなくBLOCK 1 SHADOWから読み取るのか?
5-なぜblfスプレーファイルのチェックサムがゼロなのか?
6-エクスプロイトを完了する。
7-実際のパッチ
InitEnvironmentという関数を作成し、必要なカーネルアドレスをいくつか取得します。
自分のプロセスのEPROCESSアドレスを取得し、g_EProcessAddress変数に格納します。次に、SYSTEMプロセスのEPROCESSアドレスを取得し、system_EPROCESSに格納します。次に、自分のプロセスのメインスレッドのEHTREADアドレスを取得し、g_EThreadAddressに格納します。最後に、このPoCのバージョンでは使用されないPREVIOUS MODEのアドレスを取得します。

この方法はよく知られています。GetObjectKernelAddress関数は、最初の引数SystemExtendedHandleInformationでNtQuerySystemInformationを2回呼び出します。最初の呼び出しは誤ったサイズで渡され、エラーを返しますが、正しいサイズも返します。そのサイズが2回目の呼び出しで使用され、すべてのハンドルの情報を取得します。その後、各ハンドルの情報をループで調べ、正しいhandleinfoのObjectフィールドからカーネル内の目的のアドレスを取得します。

また、CLFS.sysによってエクスポートされた以下の関数のカーネルアドレスも必要です:
• ClfsEarlierLsn
• ClfsMgmtDeregisterManagedClient
そして、NTOSKRNL.exeからエクスポートされた関数:
• RtlClearBit/PoFxProcessorNotification
• SeSetAccessStateGenericMapping
これらのアドレスを取得するには、両方のモジュールのカーネルベースを取得するために使用されるのと同様の方法を使用します。NtQuerySystemInformationを2回呼び出しますが、今回は最初の引数がSYSTEM_INFORMATION_CLASSになります(PoCではこの目的のためにFindKernelModulesBase関数を使用します)。
次に、CLFS.sysとNTOSKRNL.exeをLoadLibraryを呼び出して通常のモジュールとしてユーザーモードにロードし、GetProcAddressでユーザーモードのアドレスを取得し、それぞれからイメージベースを減算して関数のオフセットを取得します。最後に、各オフセットを対応するカーネルベースに加算し、必要なすべての関数のカーネルアドレスを取得します。

createInitialTriggerBlfFileという関数を作成し、.blfファイルを生成して書き込みます。
CreateLogFileで引数として使用されるパスは通常のパスとは異なります。たとえば、C:\Users\Publicフォルダにあるファイル1280.blfを開くには、パスLOG:C:\Users\Public\1280を設定する必要があります。これはstored_name_CreateLog変数に保存されます。
私はwsprintfW()を使用してこれを行います。stored_envには、以前に環境変数から取得したパスC:\Users\Publicが格納されています。この文字列の先頭に文字列LOG: を付け、末尾にランダムな名前を.blf拡張子なしで追加します。

これが、私が「トリガーblf」と呼ぶ初期ファイルへのパスになります。もちろん、同じファイルへの通常のパスも保存する必要があります。先頭のLOG: がなく、BLF拡張子が付いたもので、CreateFile()、**WriteFile()**を使用して他のファイルと同様に開いて変更するためのパスです。このパスは、たとえばC:\Users\Public\1280.blfとなり、stored_name_fopen変数に格納されます。

もちろん、両方のパスは同じファイルに対応しており、状況に応じて使い分ける必要があります。
CreateLogFile関数は、**CreateFile()**と非常によく似た機能を果たします(新しいファイルを作成したり、既存のファイルを開いてハンドルを取得したりします)。一部の引数も似ていますが、**CreateLogFile()**はblfファイルでのみ動作します。
さらに、既存のファイルを開く際には、各ブロックにチェックサムがあるかどうかなど、形式が正しいことを確認します。これが正しくない場合はエラーを返します。
私は2種類のBLFファイルを作成します:
トリガーblf
スプレーblf
どちらもblfファイルですが、異なる方法で変更されています。
このようにして、PoCはまず「トリガーblf」ファイルをCreateLogFileを使用して作成します。パスは、例えばLOG:C:\Users\Public\1280で、以前に設定し、stored_name_CreateLog変数に保存されたものです。
5番目の引数fCreateDispositionは、***CreateFileA()***と同様に、以下の値を取ることができます:

この場合、私はOPEN_ALWAYS引数を使用します。これにより、ファイルが存在しない場合は作成され、存在する場合は開かれます。ファイルはまだ存在しないため、ランダムな名前で作成されます。
logFile = CreateLogFile(stored_name_CreateLog, GENERIC_READ | GENERIC_WRITE, 1, 0, 4, 0);
CreateLogFile()は、6つのブロックとそれに対応するチェックサムを持つ「トリガーblf」ファイルを作成し、ハンドルを返します。このハンドルはlogFile変数に格納されます。

各ブロックは、左の列に示されたオフセットから始まり、サイズが0x70バイトのヘッダーを持ちます。
たとえば、CONTROL BLOCKのヘッダーはオフセット0x0から0x70までです。

すべてのブロックのヘッダーは同じ構造、_CLFS_LOG_BLOCK_HEADERです。
これがヘッダー構造です:

ヘッダーのオフセット0xCにchecksumがあります。したがって、CONTROL BLOCKはオフセット0から始まるため、チェックサムはファイルのオフセット0xCにあります。同様に、各ブロックはそのブロックの先頭からオフセット0xCにチェックサムを持ちます。

トリガーblfファイルを変更するには、CreateFileAまたはfopenを使用して通常のファイルとして開き、それぞれWriteFileまたはfwriteで変更する必要があります。これはPoCのfun_prepare関数の先頭で実行します。
通常のパスはstored_name_fopen変数に格納されているので、それを使ってwfopen_s(Unicode文字列をサポートするfopenの変種)でファイルを開きます。
ファイルはfun_prepareから呼び出されるcraftTriggerBlfFile関数で変更されます。

次に、fseekを呼び出して変更するオフセットを指し、fwriteでファイルを変更します。
**「トリガーblf」**ファイルに加える変更は以下の通りです:
これらの変更を行った後、FixCRCFileを呼び出して新しいチェックサムを計算し、最初の4つのブロックのチェックサムを修正します。次の2つのブロックには変更がないため、チェックサムを再計算する必要はありません。

CLFS.sysドライバーはファイルの6つのブロックを読み取り、その内容を保存するためにカーネルプールにメモリを割り当てます。

以前のCVE-2022-37969のブログ記事で、リバースエンジニアリングを通じて見つけた、サイズ0x90の非常に重要な構造があります。私はそれをpool_0x90と呼びました。さらにリバースエンジニアリングを行った結果、その実際の名前はm_rgBlocksであり、コントローラーがファイルから各ブロックの内容をコピーするためにメモリを割り当てる際、各ブロックのサイズ、開始オフセット、および保存されたカーネルアドレスをそこに保存することがわかりました。

これは6つのCLFS_METADATA_BLOCKを持ち、それぞれがブロック番号に対応しています。
各CLFS_METADATA_BLOCK構造体の長さは0x18バイトです。(0x18*6=0x90)
オフセット0にはユニオンがありますが、少なくともこのエクスプロイトではpbImageフィールドのみが使用されるため、簡略化すると次のようになります:
この構造体の割り当ては、新しいファイルの作成時か既存のファイルのオープン時に、CLFS.sysドライバーの2つの異なる場所から行われます。新しいファイルが作成される場合、ドライバーはCClfsBaseFilePersisted::CreateImage+28Aから0x90バイトを割り当てます。既存のファイルの場合は、CClfsBaseFilePersisted::ReadImage+6Eから割り当てます。
その後、トリガーblfファイルのブロック2の開始アドレスを取得します。これはBASE BLOCKに対応し、オフセット0x800から始まり、長さは0x7a00です。

fun_prepare関数内で、以下のコードを使用してこのアドレスをカーネル内で見つけます。

まず、getBigPoolInfo関数は、"Clfs"タグとサイズ0x7a00を持つプール内のすべての割り当てを見つけ、それらを配列に格納します。
その後、以前に変更したトリガーblfファイルをCreateLogFileでOPEN_EXISTING引数を使用して再度開きます。これにより、既存のファイルが開かれ、そのBASE BLOCKの割り当てが行われます。
getBigPoolInfoが再度呼び出されると、サイズ0x7a00の新しい"Clfs"プールが1つ追加され、そのアドレスはNtQuerySystemInformationを2回呼び出すことで取得されます。
トリガーblfファイルのBASE BLOCKのアドレスは、CLFS_kernelAddrArray変数に格納されます。

変更されたトリガーblfファイルのチェックサムが正しくない場合、**CreateLogFile()**関数は失敗することに注意してください。
fun_prepare関数の最後の部分では、トリガーblfファイルのハンドルを使用してAddLogContainer APIを呼び出します。

PoCの最後の関数to_triggerでは、2番目のタイプのblfファイルが作成されます。
これをスプレーblfと呼びます。
この種類のファイルはメモリ空間を埋める(スプレー)ために使用され、この種類のファイルが10個必要ですが、最初は1つだけ作成されます。
これらのファイルのランダムな名前を格納するために3つの配列が作成されます:
stored_log_arrays: CreateLogFileで使用される.blfファイルの10個の新しいランダムな名前を格納します。
stored_container_arrays: 10個の新しいコンテナファイルを作成するためのランダムな名前を格納します。
stored_fopen_arrays: 最初の配列(stored_log_arrays変数)のログファイル名を格納しますが、通常のパス(「LOG:」文字列なし)で.blf拡張子が付いたものです。

各反復で、CopyFileWを使用してblfファイルがコピーされ、配列に格納された名前が割り当てられます。
fun_trigger関数はcraftSprayBlfFileを呼び出し、そこで各ファイルに変更が加えられ、FixCRCFileがCRCを修正します。

まとめると、以下の変更を加えた10個の類似ファイル(スプレーblf)をランダムな名前で作成しました:

最後の変更は、ブロック0全体(CONTROL BLOCK)をブロック1(CONTROL BLOCK SHADOW)にコピーすることです。

これらの変更と、トリガーblfファイルに加えた変更の効果については、後でデバッグの章で説明します。
これらの変更の一部は脆弱性を引き起こすものであり、他のものはドライバーのチェックをバイパスするためにのみ必要です。
この時点でファイルは作成および変更され、スプレーを実行する準備が整いました。その後、CreateLogFileで開かれると、後で示すように、目的のメモリ領域に配置されます。

to_trigger関数では、12要素の配列が作成され、トリガーblfファイルのBASE BLOCKのアドレスに0x30を加えたものが含まれます。
次に、fun_pipeSpray関数で、パイプのスプレーでメモリが埋められます。内部にはCreatePipeを呼び出すループがあり、最初の引数として渡された数のパイプを作成します。2番目の引数は、作成されたすべてのパイプのハンドルを格納する配列です。

ループ内で、CreatePipeを呼び出して読み取り/書き込みパイプを作成します。
このようにして、最初に0x5000個のパイプが作成され、次に再度0x4000個のパイプを作成するために呼び出します。
次に、WriteFileを使用して最初の5000個のパイプに、トリガーblfファイルのBASE BLOCKのアドレス+ 0x30を含む最近作成された配列を書き込みます。

これでメモリ内にコンパクトなブロックが作成されました。次に、番号0x2000から0x2667までの0x667個のパイプを解放します。メモリ内ではパイプは作成された順序と同じではないため、このメモリブロック内に空きスペースができます。
パイプの割り当てはユーザーサイズが0x90バイトであるため、解放されると次のようになります。
パイプで満たされたメモリ間にサイズ0x90のメモリスペースを解放します。
次に、ループしてCreateLogFileを10個のスプレーblfファイルで呼び出します。
CreateLogFileが既存のファイルを開くために呼び出されると、各スプレーblfファイルに対してm_rgBlocksの0x90バイトの割り当てが行われます。これらの割り当ては、パイプが解放されたときに残された隙間を占有します。これは同じサイズだからです。

次に、最後の0x4000個のパイプに、トリガーblfのBASE BLOCK + 0x30のアドレスを持つ配列を書き込むプロセスを繰り返します。
これらの操作はすべて、制御されたメモリ空間を作成します。デバッグ中の様子を後で示しますが、アイデアとしては、各スプレーblfファイルのm_rgBlocksが解放された0x90バイトの隙間を占有することです。
そして、最終部分では、バグがwhile(1)ループ内でスプレーblfファイルへのAddLogContainerの呼び出しを使用してトリガーされます。
このwhile内でバグが発生します:

このwhileは、NtFsControlFile関数を使用してパイプの属性を読み取り、Systemトークンを見つけると終了します。

次にCreateLogFileを使用して、見つけたSystemトークンでプロセスのトークンを再度上書きし、これにより特権昇格を達成します。

その後、いくつかの値を復元し、パイプとblfファイルのハンドルを閉じて、Systemとしてメモ帳を実行し、正しく昇格したことを確認します。

PUBLICフォルダに作成されたblfファイルに注意してください。もう一度試行したい場合は、最初に作成されたファイルを削除する必要があります。一部のファイルはロックされて削除できませんが、PoCは引き続き動作します。

エクスプロイトを実行するためにtrigger blfとspray blfファイルに変更を加える効果について説明する前に、パイプスプレイとその後の固定数のパイプ解放後に、spray blfファイルのm_rgBlocksがメモリ分布に現れるホールに配置されることを確認する必要があります。
この手順が終了すると、m_rgBlocksの0x90バイトの下にパイプが配置されるはずです。そのため、m_rgBlocksが使用されるとOUT OF BOUNDSが発生し、その下にあるパイプから読み取ります。
PoCにはブレークポイントを設定する理想的なポイントがあります:

この時点で、spray blfファイルのオープンが完了し、AddLogContainer関数はまだ呼び出されていません。
ユーザーモードでデバッグするにはx64dbgを、カーネルモードではWindbgプラグインを備えたIDAを使用します。

この時点でメモリはすでに準備されているはずなので、分布を確認できます。
IDAを一時停止して、ブレークポイントを設定する興味深いポイントを見つけます。
AddLogContainerから呼び出されるCClfsBaseFilePersisted::AddContainerにブレークポイントを設定します。関数の先頭では、RCXレジスタがCClfsBaseFilePersisted構造体を指しており、オフセット0x30にm_rgBlocksへのポインタがあります。

ブレークポイントに到達したら、コールスタックでAddLogContainerがPoCから呼び出されていることを確認します。

RCXレジスタが指す先:

最初のフィールドはvtable(CLFS! CClfsBaseFilePersisted::'vftable')へのポインタで、オフセット0x30にm_rgBlocksへのポインタがあります。

ブロック0、1、4、5にはまだpbImageが保存されていませんが、ブロック2(BASE BLOCK)と3(SHADOW BLOCK)には保存されています。
m_rgBlocksテーブルの各ブロックには、cbOffset(ファイル内でのブロックの開始オフセット)、cbImage(ブロックサイズ)、eBlockType(ブロックタイプ)があります。
スプレイが正しければ、m_rgBlocksの下にはパイプがあり、その中にはtrigger blfのBASE BLOCK + 0x30へのポインタが含まれています。

Windbgの"!pool"コマンドでメモリ分布を表示します:

各m_rgBlocksには"Clfs"タグがあり、サイズは0xa0(ユーザーサイズ0x90 + ヘッダ0x10)で、その下には"NpFr"タグのパイプがあり、同じユーザーサイズ0x90 + ヘッダ0x10です。
分布は正確な科学ではないため、一部の"Clfs"は連続して配置されており、これは望ましくありませんが、現在扱っているものはパイプの後ろに正しく配置されています。
最初に影響を与える変更の1つは、trigger blfファイルのオフセット0x858に値0x369が格納されることです。

BASE BLOCKはファイルのオフセット0x800から始まります。

_CLFS_LOG_BLOCK_HEADER内のオフセット0x800+0x58(BASE BLOCKヘッダの先頭から0x58)に位置します。

オフセット0x28から配列RecordOffsets(DWORD)が始まります。
0x30バイト進むと、オフセット0x58(0x828+0x30=0x858)にRecordOffsetsのフィールド12があります。

下の画像のようにPoCを実行してCreateLogFileを呼び出します:


CreateLogFileに入る前に、値0x369がまだ使用されていない場所にブレークポイントを設定します。
CreateLogFileが既存のファイルを開く場合、m_rgBlocks構造体はここで割り当てられます:
CClfsBaseFilePersisted::ReadImage+6E
したがって、IDAでここにブレークポイントを設定します:

ブレークポイントがトリガーされたとき:

m_rgBlocksにはまだガベージがあります(初期化されていないため)が、ブロック2のpbImageが割り当てられるとすぐに、アドレスは先頭からオフセット0x30に保存されます。各CLFS_METADATA_BLOCKの最初のフィールドはpbImageだからです。


次に、書き込み用のハードウェアブレークポイントを設定します:ba w1 ffffd003'7f5bea30
ゼロに初期化された後、pbImageが保存されると停止します。

解析によると、これはblock0に対応しています。なぜなら、後でr14*8(これは0x30)という定数を考慮していないため、実際にはblock 2のpbImageを書き込んでいます。

CClfsBaseFilePersisted::ReadMetadataBlockは、引数として渡されたサイズを使用して、いずれかのブロックを割り当てるために使用されることに注意してください。

次に、ベースブロックの0x58に読み取り/書き込みブレークポイントを設定して、値0x369がいつ使用されるかを確認します。
ba r1 FFFF978A'16ECF000+0x58

ブレークポイントがヒットすると、RecordOffset[12]にある値0x369を読み取り、r14の奇妙なポインタに加算し、RAX+r14の内容をインクリメントします。
コードの数行上で、ESIは値0x13を持ち、0x18(各ブロックのサイズ)を乗算しています。
WINDBG>? 0x18*0x13
評価式: 456 = 00000000'000001c8
r8= 0x1c8(これは0x90より大きい)をm_rgBlocksの初期アドレスに加算すると、OUT OF BOUNDSの読み取りになります。


m_rgBlocksの下には、BASE BLOCK + 0x30へのポインタを持つパイプがあり、戦略的にパイプ内に配置されたこのポインタを読み取ります。
コード内の現在の位置は、メインモジュールの**while(1)**文から呼び出されました。
spray blfファイル内では、オフセット0x48a(iFlushBlock)に値0x13を戦略的に配置しました。

spray blfファイルのオフセット0x8aには、BLOCK 0のiFlushBlockがあり、その値は4です。一方、オフセット0x48aはBLOCK 1のiFlushBlockに属し、その値は0x13です。

ここで、なぜBLOCK 0のiFlushBlock = 4ではなく、BLOCK 1からiFlushBlock = 0x13を読み取るのかを調べる必要があります。
0x13がどこから来たのかを調べるために遡ると、コールスタックでWriteMetadataBlockがCClfsBaseFilePersisted::ExtendMetadataBlock+416から呼び出されていることがわかります。そこでは、2番目のiFlushBlock引数はEDX=0x13であり、r9wから来ています。


数行前では、CClfsBaseFile::GetControlRecordが呼び出されてBLOCK 0のアドレスを取得していました。おそらくここに問題があります。そのため、再起動してこの関数にブレークポイントを設定します。
GetControlRecordはCClfsBaseFile::AcquireMetadataBlockを呼び出し、m_rgBlocksテーブルをblock 0のアドレスで埋めるはずです。この関数をステップオーバーすると、block 1のアドレスが取得されます。したがって、問題はCClfsBaseFile::AcquireMetadataBlockの内部で発生します。
取得したアドレスに0x8Aを加算すると、BLOCK 1に属する0x13の値が存在することを確認できます。

再起動して、そこにブレークポイントを設定します:

CClfsBaseFile::GetControlRecord+27 call CClfsBaseFile::AcquireMetadataBlock

AcquireMetadataBlockに渡される2番目の引数はゼロで、これはblock 0に対応します。ファイルからコピーし、そのアドレスをm_rgBlocksに格納します。
_CLFS_METADATA_BLOCK_TYPEブロックタイプの列挙では、私が使用したものとは異なる名前ですが、同じ6つのブロックです。

ブロックタイプが最大m_cBlocks=6未満であることを確認した後、同じブロックを2回読み取らないように参照値を保存します。

ReadMetadataBlockが呼び出されます。block 0ではなくblock 1を読み取る問題はこの関数内にあるでしょう。

すべて問題なければ、cbImageをサイズとして割り当て、アドレスをm_rgBlocksのblock 0->pbImageフィールドに格納します。

!poolコマンドで割り当てられたタグとサイズを表示します。

これで、m_rgBlocksに格納されたblock 0のpbImageのアドレスがわかりました。次に、なぜblock 0のバイトではなくblock 1のバイトがそこにコピーされるのかを調べる必要があります。
pbImageを含む変数へのポインタが渡されるCClfsContainer::ReadSectorの呼び出しにたどり着き、バイトを書き込みます。

ReadSectorをステップオーバーしたときのpbimageの内容の変化に注意してください。

pbImageに0x8aを加算すると、値4(正しい値)が見つかります。0x13ではありません。したがって、問題は後で発生するはずです。
ClfsDecodeBlockを呼び出した後、エラー0x0C01A000Aが返されます。
CClfsBaseFilePersisted::ReadMetadataBlock+153はClfsDecodeBlockを呼び出します。
このエラーの後、タイプに1を加算し、タイプ1でCClfsBaseFilePersisted::ReadMetadataBlockを再度呼び出してblock 1を読み取ります。

CClfsBaseFilePersisted::ReadMetadataBlock内で、block 1用の新しいpbImageをm_rgBlocksに割り当てて格納します。

Blocks 0と1は異なるアドレスを持ちます。ここでblock 1のアドレスに0x8aを加算すると、その値は0x13です。
おそらく、block 0がエラーを返したため、block 1を使用し、それをControl BlockとしてGetControlRecordに返します。
前に示したように、4ではなく0x13の値を使用すると、m_rgBlocksの範囲外になり、制御されたパイプスプレイの値を読み取ります。
次に、block 0のpbImageを解放し、block 1のポインタをblock 0にコピーします。

ClfsDecodeBlock内でエラー0x0C01A000Aを引き起こす値を特定する必要があります。
ClfsDecodeBlock内で、最初のブロックのチェックサムはゼロです。これがエラー0xC01A000Aです。

AddLogContainerを呼び出す前に、任意のspray blfファイルを16進エディタで開くと、チェックサムがゼロに変更されていました。

これは、CreateLogFileで開かれたときに以前に変更されるべきでした。

何らかの理由で、spray blfファイルはCreateLogFileを終了した後、block 0のチェックサムが0になり、有効な ハンドルを返します。この理由を見てみましょう。
CreateLogFileで、いくつかのspray blfファイルを開く前に停止します。

CreateLogFileを呼び出す前に、sprayファイルのblock 0には正しいチェックサムがありますが、関数が完了した後、チェックサムの値がゼロに変わります。

そのため、CClfsBaseFile::GetControlRecordにブレークポイントを設定して、内部を調べます。
CClfsContainer::ReadSectorを通過した後、チェックサムはゼロではありません。
CRC32を計算する前に入る前に、メモリ内でチェックサムフィールドをゼロにしてCRCを計算し、結果は正しいです。


次に、eExtendState =2の値を確認し、WriteMetadataBlockに進みます。

ここではチェックサムはまだメモリ内でゼロであり、この値がいつファイルに書き込まれるかを確認する必要があります。

blf sprayファイルに細工されたいくつかの値を確認し、CClfsBaseFilePersisted::ExtendMetadataBlockに到達します。

まだ読み取られていないブロックを読み取るループの後、block 0はchecksum = 0のままです。

WriteMetadataBlockに到達します。

block 0を1に置き換える前に実行しているため、blf sprayファイルのiFlushBlock値はまだ4(正しい値)です。

現在はblock 4を処理しており、block 4をファイルに書き込みます。ここではまだ問題ではありません。
次に、CClfsBaseFilePersisted::FlushControlRecordに到達します。

内部でWriteMetadataBlockに到達しますが、引数0でblock 0をファイルに書き込みます。

次に、ClfsEncodeBlockがエラー0xC01A000Aを返しますが、その直下のCClfsContainer::WriteSectorで不良なblock 0でファイルに書き込みます。

変数var_54にはエラー値0xC01A000Aが格納され、関数を終了する前にチェックされます。

ただし、CClfsContainer::WriteSector(エラーを返さない)の呼び出し後、var_54の内容はゼロで上書きされます。
そのため、関数はエラーなしでゼロを返し、CreateLogFileがエラー値ではなくハンドルを返すため、処理を続行します。

iFlushBlockの値0x13により、範囲外になり、パイプ内のポインタ(trigger blfのBase Block +30を指す)が読み取られます。
そして、そのポインタに0x28を加算します(トリガーblfのベースブロックの先頭から0x58の位置)には、値0x369が格納されています。


INC命令は値0x14を1ずつ増加させ、4回繰り返すため、0x14は最終的に0x18になります。
WINDBG>db r14+369
ffffcb82'091e7397 14 00 00 00
その後、CreateLogFileが呼び出され、0x1858の値が読み取られます。

GetSymbolは、トリガーblf内に事前に作成された偽のブロックが、オフセット0x1858によって指されていることを確認し、正しい値を持っているかチェックします。

もしポインタが複数回インクリメントされていなければ、元の値0x1458を保持し、正しいブロックを指していたでしょう。
GetSymbolを抜けた後、ここでその偽のブロックを使用します。

次に、偽のブロックのオフセット0x18の値を読み取ります。ここには0x05000000を配置してあり、その場所にある内容にジャンプします。
WINDBG>dps 0x5000000
00000000'05000000 00000000'05001000

0x05000000の内容を読み取り、その内容は0x05001000であり、そこにはClfsEarlierLsnがあります。

この関数は、RDXに値0xFFFFFFFFを返すために使用されますが、この最初の呼び出しではその値は使用されません。
2回目の呼び出しはここで行われ、0x501000 +8にあったPoFxProcessorNotificationを呼び出します。

WINDBG>dps 00000000**'05001000**
00000000'05001000 fffff805'7ab13220 CLFS! ClfsEarlierLsn
00000000'05001008 fffff805'769dc3b0 nt! PoFxProcessorNotification

この関数ではRCX = 0x05000000であり、0x40バイト後ろがゼロ以外であることをチェックします。
WINDBG>dps rcx+40
00000000'05000040 00000000'05000000
ジャンプ先のアドレスは0x68後になります。
WINDBG>dps rcx+68
00000000'05000068 fffff805'7ab2bfb0 CLFS! ClfsMgmtDeregisterManagedClient
そして、引数は0x48バイト後になります。
WINDBG>dps rcx+48
00000000'05000048 00000000'05000400
ClfsMgmtDeregisterManagedClientは便利な関数です。なぜなら、引数を制御でき、さらに自分で制御する関数への2つのジャンプがあるからです。

最初の呼び出しは再びClfsEarlierLsnで、これはRDX=0xFFFFFFFFを返します。


書き込み元として、RDX=0xFFFFFFFFの内容が使用されます。
WINDBG>dps rdx
00000000'ffffffff ffff8005'3a4ee000
アドレス0xFFFFFFFFには、system_EPROCESS & 0xfffffffffffff000を保存していました。

書き込み先は、0x5000400 +0x48にあるポインタです。
*(UINT64*)0x5000448 = para_PipeAttributeobjInkernel + 0x18;

カーネル内のPipeAttributeポインタ("A"で埋められたバッファを指していた)は、SYSTEM EPROCESSポインタの上位部分で上書きされます。
このポインタは、以前に"A"で満たされたバッファを使って**_NtFsControlFile**を呼び出した際に作成されました。

その属性の内容は、NtFsControlFileを使用して読み取ることができます。

これで、パイプ属性は"A"を含むバッファを指すのではなく、system_EPROCESS & 0xffffffffffffff000を指すようになります。

このコードは、システムトークンが取得されるまで繰り返されます。


Windows 11では、システムトークンは、先ほど読み取ったEPROCESS構造体のオフセット0x4b8にあります。

CreateLogFileを呼び出して、そのシステムトークンを自分のプロセスに書き込むだけで済みます。

この作業を行うには、システムトークンの読み取りに使用した手順を繰り返すだけです。

2回の呼び出しでは、最初にClfsEarlierLsnを呼び出してRDXに0xFFFFFFFFを返させ、次にnt_SeSetAccessStateGenericMappingを呼び出します。

RDXが指す値がシステムトークンであることを確認します。

自分のプロセスのトークンは次のとおりです。

そこに書き込みが行われます。
WINDBG>dps rax+8
ffff9b8b'fc446578 ffffc402'f601c06c
WINDBG>dps rax+8
ffff9b8b'fc446578 ffffc402'ef841919
これで自分のプロセスはSystemになりました。メモ帳を実行して確認できます。



BINDIFFは、多くの変更された関数を示しています。

脆弱な関数はこちらです。


一次側がパッチ適用済みバージョン、二次側が脆弱なバージョンです。
パッチは、CflsEncodeBlockの戻り値(0xC01A000A)をテストし、それを変数var_54に格納します。値が負であるため、それをチェックし、WriteSectorを回避します。
パッチは、ファイルを書き込まないだけでなく、関数は正しく0xc01a000aを返します。これにより、CreateLogFileはハンドルを返さず、悪用は続行できなくなります。


ClfsDecodeBlockが負でない場合のみ、WriteSectorに進みますが、負の値0xC01A000Aを返したままになります。
これが実際のパッチであり、今添付したPoCを使用した悪用を実際に防止します。
ここまでで、バグがどのように悪用されたかを説明しました。これにより、SYSTEMトークンを読み取り、それを自身のプロセスに書き込んでローカル特権昇格を達成するための関数を制御できるようになります。動作するPoCはFortraのGitHubにあります。
お役に立てば幸いです。ご質問があれば、以下までご連絡ください。