
CVE-2022-37969のチュートリアル。CVEの内部原因ではなく、カーネルエクスプロイトの方法論に焦点を当てています。
これは Windows エクスプロイトに関する一般的な側面を明確にするために作られました。CVE-2022-37969 に適用される基本概念を説明します。最終結果は動作する PoC です。CVE のすべての側面を明確にするものではありませんが、再利用可能なコード片を提供し、多くの一般的なエクスプロイトに見られるメカニズムを説明します。
対象ユーザーは、テストして Windows 内部の基本を理解するための動作する概念実証ソースコードを求める、初心者のリバースエンジニア、エクスプロイト開発者です。さらなる学習のための参照点を提供します。
要件: 基本的なカーネルデバッグ、基本的なリバースエンジニアリング、基本的な Windows 内部構造、c/c++ プログラミングスキル
プログラムとは、マシン上で実行されるコードの断片です。一般にプログラムはデータ(入力)を受け取り、入力を使って計算を行い、データ(出力)を生成します。ほとんどのプログラムは人間によって書かれるため、バグが存在します。バグは、正しく書かれなかったソースコード(プログラマが入力に対して何かをしようとしたが、結果のコードが意図した結果と異なる)によって発生します。ほとんどのバグは製品リリース前に修正されますが、中には残ってしまうものもあります。これは、バグにはさまざまな種類があり、発見が難しいものもあるためです。
Windows もコンピュータープログラムであり、人間によって書かれたため、バグが存在します。なぜそれが重要なのでしょうか? Windows システムは、銀行口座、医療データベースなどの機密データを扱うプログラムを実行できるからです。一部のバグは、制限されたデータへの不正アクセスに利用される可能性があります(これはエクスプロイトの良いユースケースです)。
バグにはさまざまな種類があり、役に立つものもあれば、そうでないものもあります。一般に、バグはプログラムへの入力によって引き起こされます。その入力が、誤って書かれたコード行と組み合わさることで、不正な出力や動作を生成します。その入力を発見するのがセキュリティ専門家(またはハッカー)の仕事です。次のステップは、発生した不正な出力/動作を評価し、「これは有用に使えるか?」という問いに答えることです。ここでバグはさまざまなカテゴリに分類されます。例えば、あるバグはデータ構造を破壊し、対象のコンピューターを再起動させるような動作を引き起こすかもしれません。その有用性は限られています。 別のバグは、制限されたファイルへのアクセス許可を制御するメモリゾーンに入力を書き込ませるかもしれません。この種のバグはより有用です。
したがって、ハッカーは考えられるすべてのバグの中から、自分の目的に最も有用なサブセットを探します。一般的に言えば、問題は「システムを壊さずに、標的のプログラムに特別に細工した入力を与えて、アクセスレベルを昇格させて利益を得ることはできるか?」です。
この技術的でない導入の後、チュートリアルの範囲を定式化できます: 不正な入力を受け入れ、開発者の誤ったコードの結果として、通常ユーザーから管理者への権限を不正に昇格させることができる Windows プログラムを見つけられるか?
対象プログラム: Windows CLFS(Common Log File System Driver)
エクスプロイト名: CVE-2022-37969
種類: ローカル権限昇格
脆弱な ISO のダウンロード: ここからダウンロード
Windows のアドレス空間は、ユーザー空間(一般的なプログラムを実行)とカーネル空間(オペレーティングシステム自体とハードウェアコンポーネントソフトウェア、つまりドライバーを実行)に大まかに分割されています。通常ユーザーはカーネル空間にアクセスしてはなりません。ただし、通常ユーザーのプログラムがカーネルコードの一部(システムコール、ドライバープロシージャ)にアクセスできるメカニズムは存在します。なぜアクセスが必要なのでしょうか? OS 設計者が提供する、安全で制御された方法で OS と対話するためです。
一部のドライバーは、ユーザーが提供するデータ入力を使用して、カーネル空間のデータ構造を操作します。入力がバグを引き起こす場合、カーネルが破損する可能性があります。その一例が Common Log File System Driver です。特別な入力を使うことで、ユーザーの特権アクセスレベルを保持するカーネルデータ構造をドライバーに変更させ、通常ユーザー を 管理者 で上書きさせることができます。
管理者に特権を昇格させるためには何を変更する必要があるか?
まず最終目標を念頭に置きます。Windows は、システム上で実行中の各プロセスの情報を、_EPROCESS という名前のカーネルデータ構造内に格納します。_EPROCESS の例
重要なフィールドの1つは struct _EX_FAST_REF Token です。これは別のデータ構造であり、そのプロセスの特権レベルを参照するデータをさらに指し示します。次の図では、System プロセスがシステムトークンを持ち、Explorer プロセスが通常ユーザートークンを持っています。

したがって、Explorer.exe の特権を昇格させるには、System の _EPROCESS-->Token の値を Explorer の _EPROCESS-->Token にコピーする必要があります。System トークンを自分たちのプログラムのトークンにコピーし、昇格したプロセスからコマンドプロンプトを起動することで(子プロセスは親プロセスのトークンを継承します)、同様のことを達成します。
これらのアクションを完了するには、以下のメカニズムが必要です:
はじめに: 長年にわたる Windows の性質: 新しい脆弱性が発見されるにつれて、Windows はそれらを緩和するためのパッチが必要でした。また、新しいテクノロジーの出現に伴い、Windows は競争力を維持するためにアップデートが必要でした。重要な要件の1つは、以前のバージョンとの後方互換性でした。そして時には、セキュリティは隠蔽によって達成されました。データ構造や関数定義はマニュアルから削除されましたが、機能は残っていました。リバースエンジニアリングにより、研究者はそれらの機能をさまざまな目的に利用できました。
_EPROCESS のカーネルアドレスを見つけるために、非公開関数 NtQuerySystemInformation を使用します(パラメーターはリンク参照)。SystemInformationClass パラメーターを使用することで、取得したい情報の種類を指定できます。SystemExtendedHandleInformation 値(#define SystemExtendedHandleInformation 0x40)を指定して、一般的なプロセス情報を取得します。
NtQuerySystemInformation を使用する際の注意点は、返されるデータの長さを事前に知ることができないことですが、NtQuerySystemInformation にはそれを助けるメカニズムがあります。必要なデータに対して誤ったサイズの配列で呼び出すと、ERROR と、要求すべき正しいデータサイズを返します。これを使って、次のようにしてプロセス情報を正しく読み取ることができます。
SystemInformationLength パラメーターを指定して NtQuerySystemInformation を呼び出すReturnLength パラメーターの値を読み取るSystemInformationLength 値を指定して、再度 NtQuerySystemInformation を呼び出す返されるデータ構造は PSYSTEM_HANDLE_INFORMATION_EX 型です。これは非公開のデータ構造です(リンク参照)。これは SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX データ構造につながり、その Object フィールドには対応するプロセスの _Eprocess データ構造のカーネルアドレスが保持されます。
したがって、ロジックは次のようになります: すべての PSYSTEM_HANDLE_INFORMATION_EX 要素を反復処理し、SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX の UniqueProcessId フィールドを対象のプロセス PID と比較し、対応する Object フィールドを選択して、その _Eprocess アドレスをカーネル内で見つけます。
コードの一部:

NtQuerySystemInformation は関数へのポインターとして宣言され、そのアドレスは実行時に loadlibrary と getprocaddress を介して動的に取得されます。
typedef NTSTATUS(WINAPI* ptr_NtQuerySystemInformation)(int, PVOID, ULONG, PULONG);ntdll=LoadLibrary(L"ntdll.dll");MyNtQuerySystemInformation = (ptr_NtQuerySystemInformation)GetProcAddress(ntdll, "NtQuerySystemInformation");トークンの読み取りと書き込みには、clfsw32.sys と NamedPipes 内部の脆弱性に依存する必要があります
これはややブラックボックスであり、他の記事で詳述されていますが、エクスプロイトのプロセスを基本的に理解するために、この機能の最低限の知識を説明します。 パイプはプロセス間通信メカニズムです。プロセスはパイプを使用してお互いに情報を渡すことができます。パイプはカーネルデータ構造として表され、ユーザー空間から値を設定できるフィールドがあります。その例がパイプ属性(Pipe Attributes)です。
(後でこれを CLSF の脆弱性と結び付けて、ユーザー空間からカーネル空間への任意の読み取り/書き込みを実現します)
さらに読むには、fengshui-spraying-big-kids-pool を参照してください。メカニズムの簡略化した説明は次のとおりです:
カーネルには、割り当てたいサイズに応じてメモリを割り当てる2つの方法があります: オブジェクト <4KB+ヘッダーの場合はスモールプール、オブジェクト >4KB+ヘッダーの場合はビッグプールです。 ビッグプールページは、ユーザー空間から列挙できるため重要です。これは、通常ユーザーがビッグプールページを含むすべてのカーネル開始アドレスを見つけられることを意味します。
どうやって? 各ビッグプールページには Tag という名前のフィールドがあり(そこに保存されているデータの種類に関する情報を取得するために使用できます)、システム内のすべてのビッグプールページは、SystemInformationClass パラメーターの値として SystemBigPoolInformation を指定した NtQuerySystemInformation を使用して列挙できます。次に、すべてのページから Tag でフィルタリングして、関心のあるビッグプールページのアドレスを取得できます。例えば、CLFS は 'Clfs' タグを持つビッグプールページを使用します。カーネル内で CLFS オブジェクトが割り当てられているすべてのページのアドレスを取得できます。

パイプに戻ると、同じ方法で操作できます。ビッグプールメカニズムを使用するのに十分な大きさのパイプを割り当て、ビッグプールページを列挙し、パイプ固有の Tag を検索してそれらのページをフィルタリングします。
これにより、カーネルが私たちのパイプを割り当てた場所をユーザー空間にリークできます。カーネルに入るデータを制御して読み取ることはどうでしょうか?
このためにパイプ属性(Pipe Attributes)に依存します。ビッグプールタグと同様に、パイプ属性はパイプを説明する情報を含む配列です(ユーザーが入力します)。非公開関数 NtFsControlFile を使用して、パイプ属性データ構造に任意の読み取り、書き込みを行うことができます。非公開であるため、PipeAttribute ベクトルを設定してから読み取る概念実証のみが提供されます。読み取り、書き込みのプリミティブに対して許可されている変更は、入出力バッファーの内容とそのサイズを制御することだけです。
ここでは、ビッグプールページ(0x2000)を使用するのに十分な大きさのパイプを割り当て、入力バッファーと出力バッファーを制御された値に設定します。 注意: 入力値の最初の2バイトは、機能するために 0x5a 0x00 でなければなりません。

ここでは、先ほどカーネルに書き込んだものを読み戻します。ここでも出力バッファーのみを変更します。

そして結果:

カーネル内のパイプのビッグプールページの内容は次のとおりです: ビッグプールページをクエリすることで、カーネル内のパイプデータ構造の先頭を見つけました。アドレス pipe_begin+0x20 には、入力バッファー+0x2 へのポインターがあります。

パイプ属性読み取り関数を呼び出すと、OS は次のことを行います:
なぜこれが有用なのでしょうか? pipe_begin+0x20 のポインターを System プロセスのセキュリティトークンの場所に置き換えて、パイプ属性読み取りを呼び出すことができれば、その値をユーザー空間にリークできます。この置き換えは、CLFS.SYS の脆弱性を介して達成されます。
この技術を理解することは、エクスプロイトを理解するために不可欠です。
ほとんどのエクスプロイトは本質的に決定的ではなく、確率的です。脆弱性を利用するコードが正しくても、エクスプロイトが機能しない可能性があります。標的のプログラムを不安定な状態に置くことで、エクスプロイト開発者は、エクスプロイト実行後にシステムがクラッシュしないことを確認する必要があります。エクスプロイトされると、プログラム内の変数値に依存するアドレスから読み取る能力を与えるプログラムを想像してください。
例:
target=0x1000000+ var_1&0xff+ var_2&0xff00 とします。
ハッカーは target、var_1、var_2 を制御できません。
しかし、エクスプロイトはアドレス target から読み取る機能を与えます。
0x1000000 から 0x100FFFF までの任意の場所から読み取ることができ、これは場合によっては有用かもしれませんし、そうでないかもしれません。
例2:
エクスプロイトが、特定のパターンに一致するアドレスから QWORD を読み取り、その内容をセキュリティトークンの値に入れる機能を与えるとします。以前に System.exe のセキュリティトークンの値を取得しているとします。 エクスプロイトをどのように利用して特権を昇格させることができるでしょうか?
read_addr=0x1000000+alfa&0xFFFF00
alfa パラメーターを制御することはできません。
本題から外れますが非常に重要: カーネルは、その時点でカーネルコードを実行しているプロセスに対応するユーザー空間にアクセスできます。
どのアドレスから読み取れるでしょうか? 0x1000000、0x1000100(alfa=1)、0x1000200(alfa=2)、....、0x1FFFF00(alfa=ffff00) です。
エクスプロイトが確実に成功するように、プログラマーは次のことを行う必要があります:
memory=virtualalloc(dest=0x1000000,size=0x1000000,....)for (i=0x1000000;i<0x2000000;i+=0x100) ((QWORD*)i)[0]=system_token_value;これは実際にはメモリスプレーです。エクスプロイトの結果である確率式の可能なすべての値に一致する特定のパターンに従ったメモリ操作です。
私たちの場合にこれが機能するための要件は、アドレス 0x1000000 にメモリを割り当てられることです。これは制限であり、実際のエクスプロイトの場合でも対応する必要があります。
いくつかの技術的側面が明確になったところで、CLFS の基本的な理解が必要です。Microsoft のリンク。CLFS は、アプリケーションログ、データベースログ、トランザクションなどに使用されます。
エクスプロイトは一般に、機能するために特定のメモリレイアウトを必要とします。レイアウトの形式は、エクスプロイト実行の正確な時点でのプログラム内の変数値によって決まります。
このチュートリアルでは、脆弱なコードやログファイルシステムの形式を詳細に説明しません。エクスプロイトを生成するプロセスの基本的な理解を提供します。
ログファイルは、特定の形式を持つ特別な種類のファイルであり、CLSF ドライバーと DLL API によって操作できます。 このシステムには、ログコンテナーという概念もあります。ログコンテナーもログファイルですが、メインのログファイル(それらが追加されるログファイル)にメモリ上でリンクされています。
この構造は次のように機能します:
この操作により、メインのログファイル内に新しいメモリ空間の割り当てが強制され、メインのログファイルのメモリレイアウト内のさまざまなオブジェクトが変更されます。メモリレイアウトの変更は、概念的には次のようになります:

重要なデータ構造/ファイルを使用する前と同様に、CLFS ドライバーはログファイルの形式に対して整合性チェックを行います:
2つ目のポイントが脆弱性の起点です。攻撃者は、以前に作成したログファイルを変更し、そのハッシュを再計算して、ハッシュフィールドを編集することで、ドライバーに整合性テストを通過させることができます。 変更はファイルヘッダーの長さに関連しています。慎重に細工された一連の値により、そうでなければ失敗してエラーがスローされる範囲長チェックを通過させます。ドライバーは偽の長さを有効なものとして受け入れ、通常どおりコードの実行を続行します。偽の長さは、ハードコードされたアドレスが書き込まれるファイル内のオフセットを計算するために使用されます。これにより、攻撃者は以前の書き込み操作が発生するアドレスを制御できます。
この脆弱性は、エクスプロイトを完成させるために、パイプによるカーネルの読み書きと連鎖させる必要があります。 視覚的な証拠:

前の図では、偽の範囲チェックを担当する CLFS.sys 内の関数 AllocSymbol が見えます。脆弱な長さチェックは、0xC0000023 エラーコードを返すものです。(BaseLogRecord + v9 + Size_1 + 0x1338 > v8 + *(0x68))。その条件への入力は、一部攻撃者によって制御されます。通常は失敗するチェックを通過させる値を数式に注入することが可能です。v8 と v9 を制御することで、条件の真理値を FALSE に操作し、エラー返却を防ぎ、攻撃者が制御する v9 値を使用して v10 変数の値を任意に計算させることができます。
V10 はさらに、設定する値として 0 を使う memset に使用されます。したがって、攻撃者はメモリを任意に 0 に設定する能力を得ます。これだけではありません。最終リターンの前に *a3=v10 があります。A3 はアドレスによって AllocSymbol 関数に送られるパラメーターです。したがって、AllocSymbol の呼び出し元は、AllocSymbol から戻った後、V10 の値を受け取ります。
AllocSymbol の呼び出し元は FindSymbol です。

FindSymbol 内では、V33 は AllocSymbol で a3 という名前のパラメーターです。したがって、v33 は v10 の値を取得します。次に、v33 アドレスの値が定数(0xc1fdf006)に設定され、さらに 0x30 という値が前置されます。

したがって、攻撃者は任意のメモリ位置に定数を設定する能力を得ます。
なぜこれが重要なのでしょうか? 関数ポインターのデフォルト値を、ユーザー空間とカーネル空間の両方からアクセス可能な定数アドレスで上書きでき、そのポインターが呼び出されることが保証されれば、コード実行制御を得ることができます。これが一般的な考え方ですが、さらに説明される技術的な障害があります。
CClfsContainer* pContainer; という名前のポインタがあります。該当するコンテナが解放されると、親ファイル内でクリーンアップ処理が実行され、pContainer をデリファレンスし、[[pContainer]+0x18] と [[pContainer]+0x8] の値を関数ポインタとして使用します。したがって、pContainer を制御し、追加および削除用のコンテナ固有APIを実行できると、制御フローのリダイレクトが保証されます。
pContainer はどこにあるのでしょうか?
CLFSファイル構造は未公開ですが、ファイル構造をリバースエンジニアリングしようとする個人的な試みはいくつか存在します。
ログファイル構造の概観:

ベースブロックの詳細:

構造とメモリビューの違い:
CLFSファイルは、Clfs というタグ値を持つビッグプールページに割り当てられます。ユーザースペースでCLFSログアドレスを取得する場合(パイプオブジェクトのカーネルスペースアドレスを取得するのと同じ方法)、カーネルはベースブロックが始まるアドレスを返します(前の図のオフセット0x800)。オフセット0xb98には、regContainers というデータ構造があります。これは32ビット値の配列です。各値は1つのコンテナに関連し、コンテナの内部データ構造がベースブロックファイル内のどこにあるかのオフセット(0x870から数えて)を表します。
コンテナファイルのメモリレイアウトは次のとおりです:
CLFS_CONTAINER_CONTEXT structure
CLFS_CONTAINER_CONTEXT 構造体のオフセット0x18に、ターゲットである pContainer があります。
どうやら pContainer へのオフセットは一定で、regContainers 配列の最初の要素です。その値は 0x1468 です。
pContainer をデリファレンスして関数ポインタとしてアクセスすることが、このエクスプロイトの核心です。これはCLFS.SYSドライバの Remove container 関数内で発生します。したがって、コンテナが削除されるとエクスプロイトがトリガーされます。
証明:

コンテナの削除手順を確認して、実際に pContainer ポインタが関数へのポインタとしてアクセスされることを示しましょう。
GetBaseLogRecord APIを呼び出し、ベースブロックのカーネルアドレス + 0x70 を返します(ファイルポインタをヘッダを越えて移動させます)。BaseRecord_1)が初期化されます。a4 の別のコピーが v10 として初期化されます。LODWORD(a4) = *( (_DWORD*)BaseLogRecord_1 + StartingIndex_1 + 0xCA); ファイルにコンテナが1つだけ追加されている場合、StartingIndex_1 はゼロです。GetBaseLogRecord が返す値はDWORD(32ビット)ベクトルにキャストされ、0xCA要素でインデックスされます。(_DWORD*)BaseLogRecord_1 + 0 + 0xCA は BaseLogRecord_1 に単純に0xCA値を加算するわけではないことに注意してください。キャストのため、これは BaseLogRecord_1[0xCA] と同等です。これは対応する逆アセンブルコード mov eax, [rdi+r12*4+328h] に反映されています。ここで rdi はベース、r12 は StartingIndex_1 です(4、つまり32ビット要素(DWORD)の長さが乗算され、0xCA*0x4 である0x328が加算されることに注意)。a4 の値は最終的に BaseBlock+0x70+0+328=(BaseBlock+0x398) となり、RgConrainers の最初の値(0x398+0x800=0xB98)に到達します。
pContainer に対応する構造体の3番目のフィールドです。jmp eax 命令への呼び出しです)。これにより、pConainter の値を書き換えることで、ドライバの制御フローを変更し、攻撃者が制御するアドレスを指すようにできることが証明されます。
以前、エクスプロイトの実行を成功させるために、メモリの一部を事前に定義された値のパターンで埋める必要がある場合があることを説明しました。これは、攻撃者がエクスプロイト実行時のプログラムの状態を構成する変数の一部しか制御できないためです。先ほどの単純なメモリスプレーの例では、特定の条件を満たすためにベクトルに書き込まれた値のみを使用しました。
実際のケースでは、条件はより複雑です。カーネルメモリ内のログファイルを、既知のオフセットを保ちながら特定の順序で配置する必要があります。
まず、ループ内で多数のログファイルを作成し、OSがそれらにメモリをどのように割り当てるかを調べましょう。実験では次のパターンを使用します:
次のコードでこれを実現します:

そして結果:

アドレスを並べてみましょう:

アドレス間のオフセットを調べると、割り当てに関する擬似的なパターンが見えます: 互いに一定のオフセットを持つ連続した割り当てが存在します。例えば ffffd80faf444000 から ffffd80faf4ee000 の間では、連続する2つの割り当て間のオフセットは 0x11000 です。
この仮定はエクスプロイトの動作にとって極めて重要です。これに依存することになります。
もう1つの仮定: 0x11000 離れたページをいくつか取り上げます。例: ffffd80faf444000,ffffd80faf455000,ffffd80faf466000,ffffd80faf477000,ffffd80faf488000,ffffd80faf499000。これらのページはすべて、開かれたCLFSファイルに対応しています。1つのファイルを閉じると、ページは解放され、該当アドレスのメモリは空きになります。これはメモリ上では次のようになります(ffffd80faf466000 のページに対応するファイルを閉じるとします):
ffffd80faf444000,ffffd80faf455000,xxxxxxxxxxxxxxxx,ffffd80faf477000,ffffd80faf488000,ffffd80faf499000
もしファイルを再度開けば、OSは高い確率で同じアドレス(ffffd80faf466000)にページを割り当て、穴を埋めてメモリを連続させるでしょう。--> これもエクスプロイトの実行にとって極めて重要です。
ここで、ユーザースペース内の制御下にあるアドレスで *pConainter ポインタを上書きするために使用する戦略の概略図を紹介します。


前の図では start(aux1)+0x11000=start(A),start(A)+0x11000=start(B),start(B)+0x11000=start(aux2) です。
Logfile A を使用して Logfile Bの *pcontainer ポインタ を上書きし、特別な close 後削除コードを使用して Logfile B を閉じることでエクスプロイトをトリガーします。
Bが正しいコンテナファイルを持つことを示すフィールドを割り当てて更新するために、Logfile Bにログコンテナを追加します。aux2 をBのコンテナとして追加できます。
ディスク上で編集できるようにLogfileAを閉じます。OSが優先的に埋めるメモリの穴を作るために、AとBを0x11000間隔のファイルの最大シーケンスの中央に選ぶことが重要です。そうしないとエクスプロイトは失敗します。
Aのハッシュを再計算し、整合性を保つためにハッシュフィールドを編集してAを再度開き、カーネルが同じアドレスに配置することを期待します。そうでなければエクスプロイトは失敗します。
通常のログファイルを指定してAでAddLogContainerを呼び出し、Bの *pContainer ポインタの上書きをトリガーします。
RemoveConainterが呼び出されるようにBファイルを削除し、実行が、現在破損していてユーザー割り当てコードを指すBの *pContainer に転送されるようにします。
次の図は前述の手順を示しています。

まず最初に50個のCLFSファイルを割り当てます。各割り当ての後、カーネルメモリ内のすべてのCLFSページを照会し、各ファイルに割り当てられたすべてのアドレスのリストを作成します。次に、互いに0x11000離れた2つのファイル(スキーム上のAとB)を特定します(そして first、second 変数に格納します)。

アドレスを特定した後、0x11000離れた2つのファイルを検索します A->first、B->second

次に、A(first)を閉じてディスク上で編集し(ヘッダを壊し)、再度開きます。

この手順はさらに説明が必要です。なぜなら、AllocSymbol内の悪用可能なコードをトリガーしたときに、Bの *pConainter ポインタの上書きにつながる正確な値を計算する必要があるからです。
AllocSymbol と FindSymbol 関数に関する以前の分析から、Aファイルを、IF条件の論理値をFALSEに上書きし、Bの *pContainer 位置ポインタへの書き込みにつながる値を v9 変数に注入するように変更する必要があることが分かっています。
まず、v9 にはどの値を設定すべきでしょうか?
AllocSymbol は v10(書き込みのターゲットアドレス)を次のように計算します:
v10=BaseLogRecord + v9 + 0x1338。これはAのアドレス空間のコンテキストで計算されます。したがって、BaseLogRecord は: Aのカーネルページアドレス + 0x70 です。v9 は攻撃者が制御し、0x1338 は定数です。
Bの *pContainer は、そのカーネルページアドレスから見てどこにあるのでしょうか?
これは前述のとおりです。Bのカーネルページアドレスから0x398を加算してBの regContainter ベクトルに到達し、最初の要素でインデックスして最初の CONTEINER_CONTEXT 構造体へのオフセットを取得します。前述のように、最初のコンテナの場合、インデックスは0x1468(Bの BaseBlock +0x70 から数えて)と決定されました。
したがって、Bの *pContainer の位置は Bの Kernel_Address + 0x70 + 0x1468 + 0x18(CONTEINER_CONTEXT構造体の3番目の要素) です。
Bの Kernel_Address=Aの Kernel Address+0x11000(このようにメモリを構築したため)
Aの BaseRecordAddress=Aの Kernel Address+0x70
V10=Aの Kernel Address+0x70+v9+0x1338
v10 は Bの *pContainer を上書きする必要があります
V10 は次のようである必要があります: v10=Bの Kernel_Address + 0x70 + 0x1468 + 0x18(CONTEINER_CONTEXT構造体の3番目の要素)
Bの Kernel_Address を代入: v10=Aの Kernel Address+0x110000+x70 + 0x1468 + 0x18
v10を簡約: Aの Kernel Address+0x70+v9+0x1338=Aの Kernel Address+0x11000+x70 + 0x1468 + 0x18
v9 について解くと v9 = 0x11000+0x70+0x1468+0x18-0x1338-0x70=0x11148
では、A内のどのフィールドを上書きする必要があるのでしょうか? --> フィールドには2つのカテゴリがあります:
AllocSymbol 内の IF 条件を FALSE と評価させるためのフィールドカテゴリ1のフィールドについては詳しく説明しません。V9 は、Aファイル内のディスク上のオフセット0x1b98にあるフィールドに対応します。これらのフィールドがなぜエクスプロイトをトリガーするのかについての詳細はこれ以上説明しません。興味があれば、読者が各自で調査してください。
変更する必要がある値は次のとおりです:

リトルエンディアンでは、使用する値は0x11148ではなく0x11149であることに注意してください。これは *pContainer の上位オクテットを制御する必要があるためです。最下位オクテットは X0(10、20、30...)の形式になります。この条件に対応するため、各値にメモリスプレーを行うことができます。
v10 は、長さ0xa0のメモリ領域を00でmemsetするためにも使用されることに注意してください。

次に、同じアドレスで定数による上書きが行われます。

0x30c1fdf006X0 という形式の定数値に注意してください。
これはAファイルにログコンテナを追加するときに発生しますが、Aのフィールドを変更した後のハッシュ再計算プロセスについてはまだ説明していません。
ディスク上のログファイルのビューは、メモリ内のビューとは多少異なります。変更するのは ベースブロック 上のコンテンツのみで、その長さは 0x7a00 で、ディスク上のファイルオフセット 0x800 から始まります。ベースブロックのハッシュに使用されるアルゴリズムはCRC32です。ハッシュ値を保持するフィールドもベースブロック内のオフセット 0x80c に格納されています。
ハッシュを再計算する手順:
0x80c にある古いCRC32値をゼロにする0x800 から始まる長さ 0x7a00 のベースブロックのCRC32を計算するコード実行をユーザースペースにルーティングするには、以下が必要です:
*pConainter 値が 0x30c1fdf006X0 形式の定数値で上書きされます。RemoveContainer APIはトリガーされないためです。
*pContainer を使用して実行フローをリダイレクトするターゲットとなるユーザーメモリには、いくつかの条件があります。プログラムがカーネルモードのときに、ユーザースペースからランダムに命令を実行し始めることはできません。
このセクションの目的は、システムクラッシュ(BSOD)を引き起こさずに、SystemTokenの値をユーザースペースで漏洩させることです。
カーネル内のアドレスから読み取り、結果をユーザースペースに保存するにはどうすればよいでしょうか? チュートリアルのパイプセクションについて簡単に思い出してください:

ここでは次のことを行います:
パイプオブジェクトとシステムトークン値を取得するという目的を結びつけるアイデアは、NtFsControlFile PipeReadAttributeを使用して、PipeWrite Attributeでカーネルに渡した情報を保持するバッファの先頭ではなく、システムトークンのアドレスから読み取ることです。
つまり、アドレス PIPE_BEGIN+=0x20 にあるポインタの値を、システムトークンが配置されているアドレスを保持するように変更する必要があります。
そのアドレスは既知であり、チュートリアルの初期段階でEPROCESS構造体を特定して解析したときに取得しました。
ここでは、パイプ構造体の固定位置に書き込むことを可能にするメカニズムを見つける必要があります。
このために、CLFS AddLogConainer 関数の悪用によって得られるコードリダイレクションを使用します。生の置換を行うシェルコードを書くだけで十分だと思うかもしれませんが、(テストはされていませんが)おそらく機能しないでしょう。これは、ドライバがカーネルコンテキストで実行され、ユーザープロセス領域からコードを実行するためです。
この制限を回避するには、任意の値への書き込みを実現するカーネルROPを見つける必要があります。つまり、関数の末尾にあり ret 命令で終わるカーネルコードの断片を見つけ、そのアドレスをリダイレクションのターゲットとして提供します。これにより、コードは依然としてカーネルによって実行されます。
これが主なアイデアですが、CLFSドライバが *pConainer 内のコードを呼び出す方法によって制限が生じます:

使用可能なメモリパターンを作成するには、RemoveContiner関数が *pContainer コードにアクセスする方法を調べる必要があります。
前の図では、*pContainer をデリファレンスして関数ポインタとして使用するコードの部分を強調表示しています。
RDI は、AllocSymbol を悪用して *pcontainer 内に書き込まれた定数です。ご覧のとおり、定数は値が完全に固定されているわけではなく、最初のバイトが変化します(形式は 6X0 で、X は任意の値です)。
mov rax, [rdi] は定数値をデリファレンスします。無効な読み取り(およびBSOD)を引き起こさないようにするには、rdi が有効なアドレスへのポインタを保持していることを確認する必要があります。これを行うには、0x30C1FDF00000 から少なくとも 0x30C1FDF006FF までユーザーメモリをスプレーする必要があります。これは VirtualAlloc でメモリのチャンクを割り当てることで行われます。もちろん、何らかの理由でシステムがメモリを割り当てられない場合、エクスプロイトは失敗します。
LPVOID dirtySpray = VirtualAlloc((LPVOID)0x000030c1fdf006d0, 0x1000000, 0x3000, 0x4);
次に、X の任意の可能な値に対して、0x30C1FDF000X0 から 0x30C1FDF00FX0 に、有効なユーザーメモリに対応する別の値(任意の値、例えば 0x5000000)を格納します。これにより、0x30C1FDF006X0 形式の任意の rdi に対して rax が 0x5000000 になることを確認します。
このステップの後、さらに2つのデリファレンス操作が見られます: mov rax, [rax+0x18] と mov rax ,[rax+0x8]
これらのアドレスでメモリがまだ一貫していることを保証するには、0x5000008 と 0x5000018 にカーネル関数へのポインタ(ROP1 と ROP2、後で計算されます)を格納する必要があります。
これがスプレーパターンを生成するアルゴリズムです。

注: このパターンはROPを導入すると進化します。ROPにも考慮すべきパラメータがあるためです。ただし、これまでのところ、無効なメモリアドレス指定を防ぐための最小限のものです。
スプレーされたメモリの最初のステージがデバッガでどのように見えるかを次に示します(値 0x5000000 がどこに配置されているか観察してください-->X0アライメント):

0x5000000 のメモリがどのように見えるかを次に示します(詳細は後述のROP)

そして、カーネルデバッガでのコードリダイレクションのプロセス:

デバッガ内の RDI レジスタと命令ポインタの値に注目してください。RDI を1回デリファレンスすると 0x5000000 になり、RAX に格納されます。[RAX+0x18] は2番目のROPで、さらに下の [RAX+0x8] は最初のROPのアドレスになります。
この段階で、リンクさせる必要がある次のエクスプロイトの構成要素があります:
この時点での範囲は、コードリダイレクションを、パイプの属性バッファへのポインタをシステムトークンのアドレスで上書きするコードとリンクさせ、Pipe read attributeを使用してユーザースペースで情報を取り戻すことです。既に述べたように、再ルーティングされるコードもカーネル内に存在しなければなりません。さらに、使用できる関数は2つだけです。このチュートリアルでは、これらの2つの特定の関数がどのように見つかったのかは扱いませんが、おそらく頻繁に使用されるROP候補のリストが存在します。
2つの関数を調べます:
ntoskrnl.exe 内の SeSetAccessStateGenericMapping 2番目に呼び出される ([rax+0x8])CLFS.SYS 内の ClfsEarlierLsn 最初に呼び出される ([rax+0x18])ClfsEarlierLSn の分析:

この関数の唯一の役割は、誤ったパラメータで呼び出されたときに、EDX を 0xFFFFFFFF に設定して戻ることです。これが必要な理由は、2つ目のROPを分析すると明らかになります。
SeSetAccessStateGenericMapping の分析:

これは、1行ずつ詳細に説明する必要があります。
入力: この関数に入るとき:
0x30C1FDF006X0 定数です。0x30C1FDF00YX8 の形式で、常に 0x8 でアラインされており、0 でアラインされ 0x5000000 を含む 0x30C1FDF006X0 と競合しません。関数コードの実行:
mov rax, [rcx+48h] は 0x30C1FDF00YX8 から逆参照し、その値を RAX に移動します。movups xmm0, xmmword ptr [rdx] は16バイトを RDX から XMM0 レジスタに移動します。ここでは ClfsEarlierLSn から RDX は 0xFFFFFFFF であることに注意してください。movdqu xmmword ptr [rax+8], xmm0 は XMMO の値を [RAX+0x8] に移動します。基本的にこれは、アドレスから読み取り、カーネル内に保存します。次のように解釈できます。

したがって、パイプの属性バッファポインタをシステムトークンへのポインタで上書きするには、0xFFFFFFFF に16バイトを割り当て、そこにシステムトークンのアドレスを保存する必要があります。次に、0x30C1FDF00YX8 のメモリを、宛先アドレス -0x8 の値で繰り返しスプレーする必要があります。これは、パイプ属性バッファへのオフセット -0x8 の値です。すなわち、PIPE_KERNEL_PAGE_ADDRESS+0x20-0x8 です。
実際のコードでは、次のように実現されます:

説明されていないもう1つの側面: ユーザー空間でROPのカーネルアドレスをどのように取得するのでしょうか?
これはトリックです。まず、次のパラメータで NtQuerySystemInformation を使用することで、任意のカーネルモジュールのベースアドレスを取得できます: NtQuerySystemInformation(SystemModuleInformation, (HANDLE)ModuleInfo, ModuleInfoSize, &retlen)
次に、ModuleInfo->Modules[i].Name の値でフィルタリングして、必要なライブラリと一致させます。これにより、カーネル内のベースアドレスが取得されます。
次に、ベースアドレスとエクスポートアドレスの間のオフセットは、実行可能ファイルがロードされるアドレス空間に関係なく一定であるという事実を利用します。
モジュールをユーザー空間でロードし(それが可能です)、LoadLibrary を使用し、Getprocaddress を使用してエクスポートのアドレスを取得します。エクスポートとユーザーベースの間のデルタを計算し、それをカーネルベース(NtQuerySystemInformation を使用して見つけたもの)に加算します。これにより、ユーザー空間で、カーネル空間にロードされているエクスポートのアドレスを取得できます。
属性へのポインタが破損し、システムトークンの場所を指すように設定されると、適切なパラメータを持つ MyNtFsControlFile への呼び出しがアドレスから読み取り、システムトークンの値をユーザー空間に漏えいさせます。

システムトークンの正しい値を取得したら、エクスプロイトを完了するために、それを自分自身のプロセスの代わりに書き込む必要があります。つまり、エクスプロイトを実行するプロセスのトークンをシステムトークンの値で上書きします。
この変更を行うために必要な手順には、新しい技術や方法論は含まれていませんが、以前のコードの再利用に依存しています。自分自身のトークン値を上書きするアルゴリズムは、カーネル内の特定のアドレスへの書き込み操作を意味します。これはROP関数 SeSetAccessStateGenericMapping と ClfsEarlierLSn によって実現されます。つまり、CLFSエクスプロイトを2回目にトリガーする必要があります。その通りです。コンテナの割り当てとメモリスプレーを2回目に行い、OSがクラッシュしないことを願う必要があります。
自分自身のトークン値を上書きする手順:
2回目はカーネルからの読み取りが不要なため、今回はパイプを使用しません。
手順に追加の知識は必要ないため、チュートリアルは、nt authority\system を持つ cmd.exe の最終的な証明でここで終了できます。

このチュートリアルの主な焦点は、CVE自体の内部構造ではなく、Windowsの権限昇格の例を作成するプロセスのユーザーフレンドリーな説明です。多くのエクスプロイトは、メモリスプレーや特定のWindowsデータ構造の操作など、同じ方法と構成要素を共有しています。そしてほとんどの場合、理論上のWindows内部知識と、脆弱性を活用するアルゴリズムを効果的に書くこととの間には大きなギャップがあります。