2024 年に、私たちは新しい Windows の脆弱性クラスであるFalse File Immutability (FFI) を公開しました。これは、ネットワーク リダイレクタを利用して Windows コード整合性の設計における誤った想定に違反し、2 つのカーネル エクスプロイトを引き起こす可能性があることを以前に実証しました。これらのエクスプロイトは Windows ネットワーク ドライブに依存していたため、複雑さが増し、キル チェーンにボトルネックが生じ、検出と緩和が容易になりました。
この研究は、より合理化され自己完結的な活用方法を導入することで進歩を示しています。この斬新なアプローチは、Windows の組み込み機能を活用して、SMB セットアップの複雑さなしで、同様のファイル変更バイパスを実現します。この機能のカーネル ドライバーがファイル データを処理する方法を分析することで、Windows が誤って不変であると想定しているファイルを攻撃者が変更し、概念実証のカーネル エクスプロイトにつながるセキュリティ バイパスを発見しました。
この記事のポイント:
- ネットワーク リダイレクタは不要:以前のエクスプロイトとは異なり、新しいエクスプロイト方法では、Windows ファイル共有を使用せずに False File Immutability を悪用します。
- 組み込み機能の悪用:このエクスプロイトは、クラウド ファイルの同期を処理する組み込みの Windows 機能内のセキュリティ バイパスを活用します。
- 不変性違反: Windows カーネルとメモリ マネージャーが不変であると誤って想定しているファイルの変更を可能にし、カーネルのエクスプロイトにつながります。
- 緩和策のバイパス:攻撃者は、以前の FFI エクスプロイト用に Microsoft が特別に作成した緩和策をバイパスできるようになります。
- 永久: Microsoft は、このエクスプロイトを Windows の一部のバージョンにのみパッチ適用することを選択したため、2026 年 2 月の時点で、メインストリーム サポート内の完全にパッチが適用されたいくつかのバージョンの Windows では引き続き機能します。
ファイルの不変性の偽り
False File Immutability については、最近の記事やBlueHat IL 2024 講演で紹介されましたが、覚えていない方もいるかもしれません。このセクションでは、その記憶を新たにするのに役立つはずです。すでにご存知の場合は、次のセクションに進んでください。
アプリケーションが Windows 上でファイルを開く場合、通常は何らかの形式の Win32 CreateFile API が使用されます。
HANDLE CreateFileW(
[in] LPCWSTR lpFileName,
[in] DWORD dwDesiredAccess,
[in] DWORD dwShareMode,
[in, optional] LPSECURITY_ATTRIBUTES lpSecurityAttributes,
[in] DWORD dwCreationDisposition,
[in] DWORD dwFlagsAndAttributes,
[in, optional] HANDLE hTemplateFile
);
CreateFileの呼び出し元は、 dwDesiredAccessで必要なアクセスを指定します。たとえば、呼び出し元は、データを読み取るためにはFILE_READ_DATA渡し、データを書き込むためにはFILE_WRITE_DATA渡します。アクセス権の全セットは、Microsoft Learn Web サイトに記載されています。
呼び出し元は、 dwDesiredAccessを渡すだけでなく、 dwShareModeで「共有モード」を渡す必要があります。これは、 FILE_SHARE_READ 、 FILE_SHARE_WRITE 、 FILE_SHARE_DELETEの 0 個以上で構成されます。共有モードは、呼び出し元が「私がこのファイルを使用している間、他のユーザーがこのファイルに対して X を実行しても構いません」と宣言するものと考えることができます。ここで、X は読み取り、書き込み、または名前の変更です。たとえば、 FILE_SHARE_WRITEを渡す呼び出し元は、そのファイルを操作している間に他のユーザーがファイルを書き込むことを許可します。
ファイルが開かれると、呼び出し元のdwDesiredAccessが既存のすべてのファイル ハンドルのdwShareModeに対してテストされます。同時に、呼び出し元のdwShareMode 、そのファイルへの既存のすべてのハンドルの以前に許可されたdwDesiredAccessに対してテストされます。これらのテストのいずれかが失敗した場合、CreateFile は共有違反で失敗します。
共有は必須ではありません。呼び出し元は、排他的アクセスを取得するために共有モード 0 を渡すことができます。Microsoft のドキュメントによると:
共有されていない開いているファイル (
dwShareModeゼロに設定されている) は、そのハンドルが閉じられるまで、そのファイルを開いたアプリケーションまたは別のアプリケーションによって再度開くことはできません。これは排他的アクセスとも呼ばれます。
共有はファイルシステム(通常は NTFS)によって強制されますが、Windows は FAT32 などの他のファイルシステムもサポートしています。Windows 自体は、特定の種類のファイルを開くときにFILE_SHARE_WRITEを省略し、使用中の変更を防止します。このような変更不可能なファイルは不変であるとみなすことができます。
状況によっては、メモリ マネージャーはこの不変性に依存します。不変のメモリマップ ファイル内でページ フォールトが発生し、そのページが変更されていない場合、メモリ マネージャーは元のバッキング ファイルからそのページの内容を直に読み取ることができます。不変性によりディスク上のファイルは変更できないため、ファイルの内容の 2 番目のコピーをページファイルに保存する必要はありません。EXE や DLL など、メモリ内で実行される実行可能ファイルは不変であるため、メモリ マネージャーはそれらにこの最適化を適用できます。
ネットワーク リダイレクタを使用すると、ファイル パスを受け入れる任意の API でネットワーク パスを使用できるようになります。これは非常に便利で、ユーザーとアプリケーションはネットワーク ドライブからファイルを簡単に操作し、プログラムを実行できるようになります。カーネルはすべての I/O をリモート マシンに透過的にリダイレクトします。プログラムがネットワーク ドライブから起動されると、必要に応じて EXE とその DLL がネットワークから透過的に取得されます。
ネットワーク リダイレクタが使用されている場合、パイプのもう一方の端にあるサーバーは Windows マシンである必要はありません。これは、 Samba を実行している Linux マシン、または SMB ネットワーク プロトコル を「話す」 Python Impacket スクリプト である可能性があります。つまり、サーバーは Windows ファイルシステムの共有セマンティクスを尊重する必要はありません。攻撃者はネットワーク リダイレクタを使用して、共有制限を回避し、サーバー側で「不変」のファイルを変更する可能性があります。つまり、これらのファイルは不変であると誤って想定されています。これは、False File Immutability (FFI) と呼んでいる脆弱性クラスです。
クラウドファイル
一日を始めるために家を出ようとした時、玄関先に荷物が置いてあるのを想像してみてください。それは先週注文したあの素敵な Surface Book です。ワクワクしながらも時間がないので、バッグに放り込んでジムに向かいます。Zune の最高のビートに合わせて運動した後、地元のレドモンドのコーヒー ショップに行き、Xbox Live で知り合った友人と会います。残念ながら、彼らは遅れているので、あなたは新しい Surface Book を開いて Windows にログインし、Recall をセットアップしようと意気込んでいます。平凡なコーヒーショップの Wi-Fi にもかかわらず、どういうわけか 1TB の OneDrive 全体がすぐに目の前に表示されます。1TB をそんなに早くダウンロードできるはずがないので、何か魔法が働いているに違いありません。その魔術とはクラウドファイルです。
Windows 10 バージョン 1709 で導入されたクラウド ファイルを使用すると、OneDrive などのユーザー モード アプリケーションをクラウド同期プロバイダーとして登録し、システムに空の「プレースホルダー」ファイルを作成できます。最初は、これらのプレースホルダーは脱水状態(空)です。アクセスすると、I/O は CloudFiles カーネル ドライバー ( cldflt.sys ) によってインターセプトされ、プロバイダーのプロセスが呼び出されます。プロバイダーはクラウドからファイルの内容を取得できます。ファイル全体を一度にダウンロードする必要はありません。1MB だけ必要な場合は、その 1MB だけを取得できます。ファイルのさらなる要求に応じて、必要に応じてファイルの内容の再水和 (入力) を継続できます。
ドライバがファイルをリハイドレートする必要がある場合、プロバイダのプロセスでリハイドレートコールバックを呼び出します(つまり、OneDrive.exe )。このコールバックはファイルの内容(クラウドから取得される可能性もあります)を取得し、 CfExecuteを呼び出してその内容をドライバーに渡します。ドライバーはそれをファイルに書き込みます。CloudFiles は、現在ハイドレートされていないファイル領域の再ハイドレートのみを要求しますが、ファイルをデハイドレートして現在のシステムのスペースを解放することも可能です。
エクスプロイト開発
デフォルトでは、Windows では、サーバー メッセージ ブロック (SMB) プロトコルを使用してネットワーク経由でファイルとフォルダーを共有できます。企業ネットワーク上の共有ネットワーク ドライブに接続したことがある場合は、SMB が使用されていた可能性が高いです。Windows には、デフォルトで SMB クライアントとサーバーの両方が含まれています。クライアント コンポーネントは、前述のようにネットワーク リダイレクタを提供し、ファイル パスを受け入れる任意の API を介してファイルへの透過的な SMB アクセスを可能にします。たとえば、 \\live.sysinternals.com\Procmon.exeを実行すると、今すぐインターネット経由で Process Monitor を実行できます。
私たちは 5 月に Black Hat Asia での講演 に合わせて PPLFault エクスプロイトを リリースしました 2023 。PPLFault は、ネットワーク リダイレクタを利用して、Protected Process Light (PPL) プロセスにロードされた DLL 内の FFI を悪用します。最初のプロトタイプでは、悪意のある SMB サーバーを実行する、攻撃者が制御する 2 台目のマシンが必要でした。Windows の組み込み SMB サーバーを無効にすることで、悪意のある SMB サーバーをローカル マシンに移動することができ、2 台目のマシン (プロトタイプ) が不要になりました。
しかし、当時は Windows に組み込まれている SMB サーバーを停止するには再起動が必要であると誤って認識していたため、これは私たちが望んでいたよりも面倒なことでした。幸運にも、James Forshaw の手法、つまり CloudFiles プロバイダーとループバック (ローカルホスト) SMB アダプターを組み合わせる手法を発見し、最終的な再起動不要のエクスプロイトを作成することができました。CloudFiles/SMBの組み合わせは、合理化されているだけでなく、ファイル共有(つまり、FILE_SHARE_* ) セマンティクス。たとえば、SMB クライアントがFILE_SHARE_WRITEなしでサーバー上でファイルを開いている場合、サーバーは別のクライアントがそのファイルを書き込みアクセス用に開くことを許可しません。同様に、サーバー上でローカルに実行されている実行可能ファイルへの書き込みアクセスも許可しないでください。
矛盾があるようです。PPLFault がファイル共有の制限に従わなければならないのであれば、実行中の DLL にコードを挿入するのはなぜでしょうか。Process Monitor で何がわかるか見てみましょう。プロセス モニターで PPLFault を実行すると、次の 3 つの操作が表示されます (説明のためにフィルターされています)。この分析は、Windows 11 22631.2861 上のcldflt.sysのバージョン 10.0.22621.2861を使用して実行されました。
順序としては、操作は次のようになります。
- 被害プロセス
services.exeは、DLL を実行可能イメージとして読み込みます。 - 読み込まれたら、
PPLFault.exe開きます。 - 開かれた後、
PPLFault.exeが書き込みます。
ここで注目すべき重要な点がいくつかあります。
不変性の侵害
ファイルが実行可能イメージとしてロードされている間に、ファイルへの書き込み操作が成功したことがわかります。以前の FFI 調査では、まさにこのような状況を防ぐために設計されたファイル システムのMmFlushImageSectionチェックについて説明しました。どうやってこのチェックを回避したのでしょうか?
ファイルアクセスモデルの違反
PPLFault がファイルを正常に上書きしたことがわかります。Microsoft の WriteFile ドキュメントでは、ファイルは書き込みアクセスFILE_WRITE_DATAで開かれる必要があると記載されていますが、出力には「属性の読み取り、属性の書き込み、同期」 ( FILE_READ_ATTRIBUTES 、 FILE_WRITE_ATTRIBUTES 、 SYNCHRONIZE ) で開かれたことが示されています。FILE_WRITE_DATAがなければ、どうやってこのファイルを上書きするのでしょうか?
次のセクションでは、これら 2 つの質問に答えてみましょう。
📘 オタクボーナス -
プロセス モニターは、システム上の I/O アクティビティをインターセプトしてログに記録するファイル システム ミニフィルター ドライバーをインストールします。Windows は、I/O アクションをI/O 要求パケット(IRP) と呼ばれる構造体にカプセル化します。各ミニフィルターには「高度」が割り当てられており、これは建物の階数に相当します。ほとんどの IRP は最上階から始まり、スタックを下っていきます。ミニフィルターが独自の I/O を発行する場合、その IRP はその高度から開始され、そこから下方向に移動します。つまり、6 階のミニフィルターは 5 階からの I/O を認識することはありません。プロセス モニターのミニフィルター ドライバーは高度
385200で実行されます。通常、高度180451で実行されるcldflt.sysのアクティビティは認識されません。幸いなことに、 /altitude スイッチを使用して Process Monitor の高度を調整し、高度180450で CloudFiles の下に配置することができます。
あなたにはルールがあるが、私にはない
すでに説明したように、アプリケーションはファイル共有の制限の対象となりますが、カーネル自体は必ずしも同じように制限されるわけではありません。たとえば、カーネル ドライバーはIoCreateFileExを使用してファイルを開いたり作成したりできます。
NTSTATUS IoCreateFileEx(
[out] PHANDLE FileHandle,
[in] ACCESS_MASK DesiredAccess,
[in] POBJECT_ATTRIBUTES ObjectAttributes,
[out] PIO_STATUS_BLOCK IoStatusBlock,
[in, optional] PLARGE_INTEGER AllocationSize,
[in] ULONG FileAttributes,
[in] ULONG ShareAccess,
[in] ULONG Disposition,
[in] ULONG CreateOptions,
[in, optional] PVOID EaBuffer,
[in] ULONG EaLength,
[in] CREATE_FILE_TYPE CreateFileType,
[in, optional] PVOID InternalParameters,
[in] ULONG Options,
[in, optional] PIO_DRIVER_CREATE_CONTEXT DriverContext
);
IoCreateFileEx ユーザー向け関数NtCreateFileと非常によく似ていますが、そのドキュメントには、フラグをサポートするOptionsパラメータなど、いくつかの重要な追加機能について説明されています。
IO_IGNORE_SHARE_ACCESS_CHECK
I/O マネージャーは、ファイル オブジェクトの作成後に、ファイル オブジェクトに対する共有アクセス チェックを実行しないでください。ただし、ファイル システムは引き続きこれらのチェックを実行する可能性があります。
それはそんなに単純なことですか?カーネル ドライバーはIoCreateFileEx(IO_IGNORE_SHARE_ACCESS_CHECK)を使用して、使用中の DLL を書き込みアクセス用に開くことができますか?試しにカーネル ドライバーを書いてみましょう。この記事のコードは、GitHub 上の Visual Studio プロジェクトとしてこちらから入手できます。
/*
* This experiment shows that a file opened without FILE_SHARE_WRITE
* can't be modified unless IO_IGNORE_SHARE_ACCESS_CHECK is used.
*/
VOID ExperimentOne()
{
DECLARE_CONST_UNICODE_STRING(filePath, L"\\??\\C:\\TestFile.bin");
NTSTATUS ntStatus = STATUS_SUCCESS;
HANDLE hFile = NULL;
HANDLE hFile2 = NULL;
OBJECT_ATTRIBUTES objAttr{};
IO_STATUS_BLOCK iosb{};
BOOLEAN bSuccessful = FALSE;
BOOLEAN bReportResults = FALSE;
InitializeObjectAttributes(&objAttr, (PUNICODE_STRING)&filePath,
OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE, NULL, NULL);
// Create a file without FILE_SHARE_WRITE
// This mimics ntdll!LdrpMapDllNtFileName
ntStatus = ZwCreateFile(
&hFile,
FILE_READ_DATA | FILE_EXECUTE | SYNCHRONIZE,
&objAttr, &iosb, NULL, FILE_ATTRIBUTE_NORMAL,
FILE_SHARE_READ | FILE_SHARE_DELETE,
FILE_OPEN_IF,
FILE_SYNCHRONOUS_IO_NONALERT | FILE_NON_DIRECTORY_FILE,
NULL, 0);
if (!NT_SUCCESS(ntStatus))
{
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL,
"ExperimentOne: ZwCreateFile %wZ failed with NTSTATUS 0x%08x\n",
&filePath, ntStatus);
goto Cleanup;
}
bReportResults = TRUE;
// IoCreateFileEx without IO_IGNORE_SHARE_ACCESS_CHECK should not be able to open the file
ntStatus = IoCreateFileEx(
&hFile2,
FILE_WRITE_DATA | SYNCHRONIZE,
&objAttr, &iosb, NULL, FILE_ATTRIBUTE_NORMAL,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
FILE_OPEN,
FILE_SYNCHRONOUS_IO_NONALERT | FILE_NON_DIRECTORY_FILE,
NULL, 0, CreateFileTypeNone, NULL,
0,
NULL);
if (NT_SUCCESS(ntStatus))
{
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL,
"ExperimentOne: IoCreateFileEx(FILE_WRITE_DATA) unexpectedly "
"succeeded on a write-sharing-denied file\n");
ntStatus = STATUS_UNSUCCESSFUL;
goto Cleanup;
}
// Can IoCreateFileEx(IO_IGNORE_SHARE_ACCESS_CHECK) open a
// write-sharing-denied file for write access?
ntStatus = IoCreateFileEx(
&hFile2,
FILE_WRITE_DATA | SYNCHRONIZE,
&objAttr, &iosb, NULL, FILE_ATTRIBUTE_NORMAL,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
FILE_OPEN,
FILE_SYNCHRONOUS_IO_NONALERT | FILE_NON_DIRECTORY_FILE,
NULL, 0, CreateFileTypeNone, NULL,
IO_IGNORE_SHARE_ACCESS_CHECK,
NULL);
bSuccessful = NT_SUCCESS(ntStatus);
Cleanup:
if (bReportResults)
{
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL,
"ExperimentOne complete. "
"IoCreateFileEx(IO_IGNORE_SHARE_ACCESS_CHECK) %s open a "
"write-sharing-denied file for FILE_WRITE_DATA. "
"Status: 0x%08x\n",
bSuccessful ? "CAN" : "CANNOT",
ntStatus);
}
HandleDelete(hFile);
HandleDelete(hFile2);
}
テスト署名を有効にした VM にロードすると、次の出力が生成されます。
ExperimentOne complete. IoCreateFileEx(IO_IGNORE_SHARE_ACCESS_CHECK) CAN open a write-sharing-denied file for FILE_WRITE_DATA. Status: 0x00000000
PPLFault が「不変」なファイルをどのように変更できるかについて、もっともらしい説明ができたでしょうか?そうでもないです。この実験は少し単純化しすぎていますが、 IO_IGNORE_SHARE_ACCESS_CHECK動作を示しており、カーネル API がユーザー モードの API よりも多くの自由度を提供できることを証明しています。
PPLFault では、CloudFiles は書き込み共有が拒否されたハンドルを持つファイルを変更するだけではありません。むしろ、実行可能イメージとしてメモリにマップされている間に DLL を変更します。PPLFault シナリオに少し近い別の実験を試してみましょう。実験 2 では、DLL を開いてSEC_IMAGEセクションを作成し、そのセクションのビューをメモリにマッピングすることでLoadLibraryエミュレートします。ビューがマップされたら、ハンドルを閉じて、 IoCreateFileEx(IO_IGNORE_SHARE_ACCESS_CHECK)書き込み可能なハンドルを取得できるかどうかをテストします。
まず、 LoadLibraryと同様に、PE をイメージ セクションとしてマッピングするヘルパー関数から始めましょう。実験を単一のドライバー内に維持するためにカーネル内でこれを実行しますが、ここでは機能的にはLoadLibraryと同等であることに注意してください。
// Emulate a portion of LoadLibrary
NTSTATUS MapFileAsImageSection(
PCUNICODE_STRING pPath,
HANDLE* phFile,
HANDLE* phSection,
PVOID* ppMappedBase
)
{
NTSTATUS ntStatus = STATUS_SUCCESS;
HANDLE hFile = NULL;
HANDLE hSection = NULL;
PVOID pMappedBase = NULL;
SIZE_T viewSize = 0;
OBJECT_ATTRIBUTES objAttr{};
IO_STATUS_BLOCK iosb{};
InitializeObjectAttributes(&objAttr, (PUNICODE_STRING)pPath,
OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE, NULL, NULL);
// From ntdll!LdrpMapDllNtFileName
// NtOpenFile(&FileHandle, 0x100021u, &ObjectAttributes, &IoStatusBlock, 5u, 0x60u);
ntStatus = ZwOpenFile(
&hFile,
FILE_READ_DATA | FILE_EXECUTE | SYNCHRONIZE,
&objAttr, &iosb,
FILE_SHARE_READ | FILE_SHARE_DELETE,
FILE_SYNCHRONOUS_IO_NONALERT | FILE_NON_DIRECTORY_FILE);
if (!NT_SUCCESS(ntStatus))
{
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL,
"MapFileAsImageSection: ZwCreateFile %wZ failed with NTSTATUS 0x%08x\n",
pPath, ntStatus);
goto Cleanup;
}
InitializeObjectAttributes(&objAttr, NULL, OBJ_KERNEL_HANDLE, NULL, NULL);
// From ntdll!LdrpMapDllNtFileName
// NtCreateSection(&Handle, 0xDu, 0LL, 0LL, 0x10u, v18, FileHandle);
ntStatus = ZwCreateSection(&hSection,
SECTION_QUERY | SECTION_MAP_READ | SECTION_MAP_EXECUTE,
&objAttr, NULL, PAGE_EXECUTE, SEC_IMAGE, hFile
);
if (!NT_SUCCESS(ntStatus))
{
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL,
"MapFileAsImageSection: ZwCreateSection %wZ failed with NTSTATUS 0x%08x\n",
pPath, ntStatus);
goto Cleanup;
}
// From ntdll!LdrpMinimalMapModule
// Map a view of this SEC_IMAGE section into lower half of the the System process address space
ntStatus = ZwMapViewOfSection(
hSection, ZwCurrentProcess(), &pMappedBase, 0, 0, NULL,
&viewSize, ViewShare, 0, PAGE_EXECUTE_WRITECOPY);
if (!NT_SUCCESS(ntStatus))
{
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL,
"MapFileAsImageSection: ZwMapViewOfSection %wZ failed with NTSTATUS 0x%08x\n",
pPath, ntStatus);
goto Cleanup;
}
// Move ownership to output parameters and prevent cleanup
*ppMappedBase = pMappedBase;
pMappedBase = NULL;
*phFile = hFile;
hFile = NULL;
*phSection = hSection;
hSection = NULL;
Cleanup:
HandleDelete(hFile);
HandleDelete(hSection);
if (pMappedBase)
{
NTSTATUS unmapStatus = ZwUnmapViewOfSection(ZwCurrentProcess(), pMappedBase);
NT_ASSERT(NT_SUCCESS(unmapStatus));
}
return ntStatus;
}
次に、そのヘルパーを使用して DLL をマップし、 IO_IGNORE_SHARE_ACCESS_CHECKを使用して書き込むことができるかどうかを確認しましょう。
/*
* This experiment shows that a file opened without FILE_SHARE_WRITE can't be
* modified even if IO_IGNORE_SHARE_ACCESS_CHECK is used because the file has
* an associated active SEC_IMAGE section.
*/
VOID ExperimentTwo()
{
DECLARE_CONST_UNICODE_STRING(filePath, L"\\SystemRoot\\System32\\TestDll.dll");
NTSTATUS ntStatus = STATUS_SUCCESS;
HANDLE hFile = NULL;
HANDLE hSection = NULL;
HANDLE hFile2 = NULL;
OBJECT_ATTRIBUTES fileObjAttr{};
OBJECT_ATTRIBUTES sectionObjAttr{};
IO_STATUS_BLOCK iosb{};
BOOLEAN bSuccessful = FALSE;
BOOLEAN bReportResults = FALSE;
PVOID pMappedBase = NULL;
PFILE_OBJECT pFileObject = NULL;
ntStatus = MapFileAsImageSection(
&filePath, &hFile, &hSection, &pMappedBase);
if (!NT_SUCCESS(ntStatus))
{
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL,
"ExperimentThree: MapFileAsImageSection %wZ failed with NTSTATUS 0x%08x\n",
&filePath, ntStatus);
goto Cleanup;
}
// MmFlushImageSection should return FALSE. This is what fails the FILE_WRITE_DATA request below.
// MmFlushImageSection requires SECTION_OBJECT_POINTERS, which we can get from the FILE_OBJECT.
ntStatus = ObReferenceObjectByHandle(hFile, 0, *IoFileObjectType, KernelMode, (PVOID*)&pFileObject, NULL);
if (!NT_SUCCESS(ntStatus))
{
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL,
"ExperimentThree: ObReferenceObjectByHandle %wZ failed with NTSTATUS 0x%08x\n",
&filePath, ntStatus);
goto Cleanup;
}
if (MmFlushImageSection(pFileObject->SectionObjectPointer, MmFlushForWrite))
{
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL,
"ExperimentThree: MmFlushImageSection unexpectedly succeeded %wZ\n",
&filePath);
goto Cleanup;
}
// Now that a view of the SEC_IMAGE mapping exists, close the file and section handles to remove them from the equation
// We're trying to test whether IO_IGNORE_SHARE_ACCESS_CHECK can bypass the MmFlushImageSection check here:
// https://github.com/Microsoft/Windows-driver-samples/blob/622212c3fff587f23f6490a9da939fb85968f651/filesys/fastfat/create.c#L3572-L3593
ReferenceDelete(pFileObject);
HandleDelete(hFile);
HandleDelete(hSection);
InitializeObjectAttributes(&fileObjAttr, (PUNICODE_STRING)&filePath, OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE, NULL, NULL);
// Can IoCreateFileEx(IO_IGNORE_SHARE_ACCESS_CHECK) open a file mapped as SEC_IMAGE for write access?
ntStatus = IoCreateFileEx(
&hFile2,
FILE_WRITE_DATA | SYNCHRONIZE,
&fileObjAttr, &iosb, NULL, FILE_ATTRIBUTE_NORMAL,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
FILE_OPEN,
FILE_SYNCHRONOUS_IO_NONALERT | FILE_NON_DIRECTORY_FILE,
NULL, 0, CreateFileTypeNone, NULL,
IO_IGNORE_SHARE_ACCESS_CHECK,
NULL);
bSuccessful = NT_SUCCESS(ntStatus);
bReportResults = TRUE;
Cleanup:
if (bReportResults)
{
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL,
"ExperimentTwo complete. "
"IoCreateFileEx(IO_IGNORE_SHARE_ACCESS_CHECK) %s open a "
"file backing a local SEC_IMAGE section for FILE_WRITE_DATA. "
"Status: 0x%08x\n",
bSuccessful ? "CAN" : "CANNOT",
ntStatus);
}
HandleDelete(hFile);
HandleDelete(hSection);
HandleDelete(hFile2);
ReferenceDelete(pFileObject);
if (pMappedBase)
{
NTSTATUS unmapStatus = ZwUnmapViewOfSection(ZwCurrentProcess(), pMappedBase);
NT_ASSERT(NT_SUCCESS(unmapStatus));
}
}
この実験を実行すると、次の出力が得られます。
ExperimentTwo complete. IoCreateFileEx(IO_IGNORE_SHARE_ACCESS_CHECK) CANNOT open a file backing a local SEC_IMAGE section for FILE_WRITE_DATA. Status: 0xc0000043
この場合、実行可能イメージとしてマップされたファイルには、開いているハンドルがなくても不変であることを保証するための追加の保護があるため、 IoCreateFileEx 0xC0000043 ( STATUS_SHARING_VIOLATION ) で失敗しました。このチェックは、Microsoft FastFat サンプル ドライバー コードのMmFlushImageSection API を使用して確認できますが、NTFS を含む他のファイル システムにも存在します。
//
// If the user wants write access access to the file make sure there
// is not a process mapping this file as an image. [ ... ]
//
if (FlagOn(*DesiredAccess, FILE_WRITE_DATA) || DeleteOnClose) {
[ ... ]
if (!MmFlushImageSection( &Fcb->NonPaged->SectionObjectPointers,
MmFlushForWrite )) {
Iosb.Status = DeleteOnClose ? STATUS_CANNOT_DELETE :
STATUS_SHARING_VIOLATION;
try_return( Iosb );
}
}
IO_IGNORE_SHARE_ACCESS_CHECKフラグは I/O マネージャー チェックをバイパスしますが、ファイル システム内のMmFlushImageSectionチェックはバイパスしません。IO_IGNORE_SHARE_ACCESS_CHECKの説明をもう一度読んでみると、後から考えれば明らかです。
IO_IGNORE_SHARE_ACCESS_CHECK
I/O マネージャーは、ファイル オブジェクトの作成後に、ファイル オブジェクトに対する共有アクセス チェックを実行しないでください。ただし、ファイル システムは引き続きこれらのチェックを実行する可能性があります。
ExperimentTwo は、ネットワーク ドライブから DLL をロードする PPLFault を正確に表現しているわけではありません。ネットワーク クライアントがサーバー上のファイルを開くと、SMB クライアント ドライバーはその論理ファイルを表すファイル制御ブロック ( FCB ) 構造を割り当てます。それに応じて、サーバーは要求された共有モードでファイルを開き、独自の FCB を割り当てます。これは、異なるセマンティクスを持つ 2 つの異なる FCB が機能していることを意味します。クライアントが DLL を実行可能ファイルとしてメモリにマップすると、結果のSEC_IMAGEファイル マッピング (セクションとも呼ばれます) はその FCB に関連付けられ、 MmFlushImageSectionの保護が得られます。サーバーはそれに応じてイメージ セクションを作成しないため、FCB はそのような保護を取得しません。PPLFault は、 MmFlushImageSectionチェックをバイパスしてサーバーの FCB への書き込みを実行することで、この違いを利用します。
これをExperimentThreeで試してみましょう:
/*
* This experiment shows that a file loaded as a DLL by an SMB client can't be modified
* server-side unless IO_IGNORE_SHARE_ACCESS_CHECK is used.
*/
VOID ExperimentThree()
{
DECLARE_CONST_UNICODE_STRING(filePathLocal,
L"\\SystemRoot\\System32\\TestDll.dll");
DECLARE_CONST_UNICODE_STRING(filePathSMB,
L"\\Device\\Mup\\127.0.0.1\\c$\\Windows\\System32\\TestDll.dll");
NTSTATUS ntStatus = STATUS_SUCCESS;
HANDLE hFile = NULL;
HANDLE hSection = NULL;
HANDLE hFile2 = NULL;
OBJECT_ATTRIBUTES fileObjAttr{};
OBJECT_ATTRIBUTES sectionObjAttr{};
IO_STATUS_BLOCK iosb{};
BOOLEAN bSuccessful = FALSE;
BOOLEAN bReportResults = FALSE;
PVOID pMappedBase = NULL;
ntStatus = MapFileAsImageSection(
&filePathSMB, &hFile, &hSection, &pMappedBase);
if (!NT_SUCCESS(ntStatus))
{
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL,
"ExperimentThree: MapFileAsImageSection %wZ failed with NTSTATUS 0x%08x\n",
&filePathSMB, ntStatus);
goto Cleanup;
}
// Now that a view of the SEC_IMAGE mapping exists,
// close the file and section handles to remove them from the equation.
// We're trying to test whether IO_IGNORE_SHARE_ACCESS_CHECK can bypass the
// MmFlushImageSection check here:
// https://github.com/Microsoft/Windows-driver-samples/blob/622212c3fff587f23f6490a9da939fb85968f651/filesys/fastfat/create.c#L3572-L3593
HandleDelete(hFile);
HandleDelete(hSection);
InitializeObjectAttributes(&fileObjAttr,
(PUNICODE_STRING)&filePathLocal, OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE, NULL, NULL);
bReportResults = TRUE;
// Can IoCreateFileEx() open a file mapped as SEC_IMAGE for write access?
ntStatus = IoCreateFileEx(
&hFile2,
FILE_WRITE_DATA | SYNCHRONIZE,
&fileObjAttr, &iosb, NULL, FILE_ATTRIBUTE_NORMAL,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
FILE_OPEN,
FILE_SYNCHRONOUS_IO_NONALERT | FILE_NON_DIRECTORY_FILE,
NULL, 0, CreateFileTypeNone, NULL,
0,
NULL);
if (NT_SUCCESS(ntStatus))
{
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL,
"ExperimentThree: IoCreateFileEx(FILE_WRITE_DATA) unexpectedly succeeded "
"on a file mapped as SEC_IMAGE remotely by an SMB client\n");
ntStatus = STATUS_UNSUCCESSFUL;
goto Cleanup;
}
// Can IoCreateFileEx(IO_IGNORE_SHARE_ACCESS_CHECK) open
// a file mapped as SEC_IMAGE for write access?
ntStatus = IoCreateFileEx(
&hFile2,
FILE_WRITE_DATA | SYNCHRONIZE,
&fileObjAttr, &iosb, NULL, FILE_ATTRIBUTE_NORMAL,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
FILE_OPEN,
FILE_SYNCHRONOUS_IO_NONALERT | FILE_NON_DIRECTORY_FILE,
NULL, 0, CreateFileTypeNone, NULL,
IO_IGNORE_SHARE_ACCESS_CHECK,
NULL);
bSuccessful = NT_SUCCESS(ntStatus);
bReportResults = TRUE;
Cleanup:
if (bReportResults)
{
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL,
"ExperimentThree complete. "
"IoCreateFileEx(IO_IGNORE_SHARE_ACCESS_CHECK) %s open a "
"file backing a remote SEC_IMAGE view for FILE_WRITE_DATA. "
"Status: 0x%08x\n",
bSuccessful ? "CAN" : "CANNOT",
ntStatus);
}
HandleDelete(hFile);
HandleDelete(hSection);
HandleDelete(hFile2);
if (pMappedBase)
{
NTSTATUS unmapStatus = ZwUnmapViewOfSection(ZwCurrentProcess(), pMappedBase);
NT_ASSERT(NT_SUCCESS(unmapStatus));
}
}
ExperimentThreeは次の出力を生成します。
ExperimentThree complete. IoCreateFileEx(IO_IGNORE_SHARE_ACCESS_CHECK) CAN open a file backing a remote SEC_IMAGE view for FILE_WRITE_DATA. Status: 0x00000000
上記の ExperimentThree は、カーネル ドライバーが、サーバーのバージョンのファイルのIO_IGNORE_SHARE_ACCESS_CHECKフラグを使用して、SMB クライアントによってマップされた DLL を変更する方法を示しています。
袖をまくって
何ができるかを示しましたが、Cloud Files が実際に何をするのかはまだわかりません。先ほど提起された質問に答えるために、プロセス モニターの出力を詳しく調べてみましょう。
先ほど、2つの質問をしました。
不変性の侵害
ファイルが実行可能イメージとしてロードされている間に、ファイルへの書き込み操作が成功したことがわかります。以前の FFI 調査では、まさにこのような状況を防ぐために設計されたファイル システムのMmFlushImageSectionチェックについて説明しました。どうやってこのチェックを回避したのでしょうか?ファイルアクセスモデルの違反
PPLFault がファイルを正常に上書きしたことがわかります。Microsoft の WriteFile ドキュメントでは、ファイルは書き込みアクセスFILE_WRITE_DATAで開かれる必要があると記載されていますが、出力には「属性の読み取り、属性の書き込み、同期」 (FILE_READ_ATTRIBUTES、FILE_WRITE_ATTRIBUTES、SYNCHRONIZE) で開かれたことが示されています。FILE_WRITE_DATAがなければ、どうやってこのファイルを上書きするのでしょうか?
MmFlushImageSectionバイパスについては簡単に説明できます。このチェックは、ここでは使用されていないFILE_WRITE_DATAを探します。ファイルは「属性の読み取り、属性の書き込み、同期」のみで開かれました。ただし、ファイル アクセス モデルの違反については説明できません。書き込み不可のファイルをどうやって上書きしたのでしょうか?WriteFile操作の呼び出しスタックを拡大して確認してみましょう。
呼び出しスタックでは、 PPLFault.cppの行 176 がユーザー モードからcldapi.dll!CfExecute (行 24 ~ 25) を呼び出していることがわかります。最終的にはcldflt.sys!HsmiRecallWriteFileNoLock FltWriteFileExを呼び出すことになります。FltWriteFileEx 、書き込みアクセス用に開かれていないファイルに何らかの方法で書き込むことができます。カーネルデバッガーを接続して詳しく見てみましょう。
FltWriteFileExにブレークポイントを設定し、エクスプロイトを再実行すると、 HsmiRecallWriteFileNoLockからの呼び出しで中断できます。
2: kd> bp fltmgr!FltWriteFileEx
2: kd> g
Breakpoint 0 hit
FLTMGR!FltWriteFileEx:
fffff800`425aad40 4055 push rbp
0: kd> k
# Child-SP RetAddr Call Site
00 ffffb90e`faa968e8 fffff800`5c2878d3 FLTMGR!FltWriteFileEx
01 ffffb90e`faa968f0 fffff800`5c2b2ccc cldflt!HsmiRecallWriteFileNoLock+0x2df
02 ffffb90e`faa969f0 fffff800`5c2b25f8 cldflt!HsmRecallTransferData+0x25c
03 ffffb90e`faa96aa0 fffff800`5c2b35d7 cldflt!CldStreamTransferData+0x65c
04 ffffb90e`faa96bd0 fffff800`5c27196c cldflt!CldiSyncTransferOrAckDataByObject+0x4c7
05 ffffb90e`faa96cb0 fffff800`5c2bb568 cldflt!CldiSyncTransferOrAckData+0xdc
06 ffffb90e`faa96d10 fffff800`5c2bafe1 cldflt!CldiPortProcessTransferData+0x46c
07 ffffb90e`faa96db0 fffff800`5c27895a cldflt!CldiPortProcessTransfer+0x291
08 ffffb90e`faa96e50 fffff800`4259530a cldflt!CldiPortNotifyMessage+0xd9a
09 ffffb90e`faa96f70 fffff800`425cf299 FLTMGR!FltpFilterMessage+0xda
0a ffffb90e`faa96fd0 fffff800`42597e60 FLTMGR!FltpMsgDispatch+0x179
0b ffffb90e`faa97040 fffff800`3eaebef5 FLTMGR!FltpDispatch+0xe0
0c ffffb90e`faa970a0 fffff800`3ef40060 nt!IofCallDriver+0x55
0d ffffb90e`faa970e0 fffff800`3ef41a90 nt!IopSynchronousServiceTail+0x1d0
0e ffffb90e`faa97190 fffff800`3ef41376 nt!IopXxxControlFile+0x700
0f ffffb90e`faa97380 fffff800`3ec2bbe8 nt!NtDeviceIoControlFile+0x56
10 ffffb90e`faa973f0 00007ffe`b074f454 nt!KiSystemServiceCopyEnd+0x28
11 000000dc`e7bff448 00007ffe`99383ca2 ntdll!NtDeviceIoControlFile+0x14
12 000000dc`e7bff450 00007ffe`99383251 FLTLIB!FilterpDeviceIoControl+0x136
13 000000dc`e7bff4c0 00007ffe`94f3b12b FLTLIB!FilterSendMessage+0x31
14 000000dc`e7bff510 00007ffe`94f36059 cldapi!CfpExecuteTransferData+0x103
15 000000dc`e7bff690 00007ff7`ac9216e0 cldapi!CfExecute+0x349
16 000000dc`e7bff730 00000029`8969cee4 PPLFault!FetchDataCallback+0x4b0 [C:\git\PPLFault\PPLFault\PPLFault.cpp @ 176]
FltWriteFileExの2 番目のパラメータにあるハンドル (~= FILE_OBJECT ) にどのようなアクセスが許可されたかを確認しましょう。x64 では、これはrdxです。
0: kd> dt _FILE_OBJECT @rdx ReadAccess WriteAccess DeleteAccess SharedRead SharedWrite SharedDelete Flags
ntdll!_FILE_OBJECT
+0x04a ReadAccess : 0 ''
+0x04b WriteAccess : 0 ''
+0x04c DeleteAccess : 0 ''
+0x04d SharedRead : 0 ''
+0x04e SharedWrite : 0 ''
+0x04f SharedDelete : 0x1 ''
+0x050 Flags : 0x4000a
0: kd> !fileobj @rdx
Device Object: 0xffffa909953848f0 \Driver\volmgr
Vpb: 0xffffa90995352ee0
Event signalled
Access: SharedDelete
Flags: 0x4000a
Synchronous IO
No Intermediate Buffering
Handle Created
FsContext: 0xffffcf04ac4c6170 FsContext2: 0xffffcf04a7d1cad0
CurrentByteOffset: 0
Cache Data:
Section Object Pointers: ffffa90999f44378
Shared Cache Map: 00000000
File object extension is at ffffa9099a4c5f40:
Flags: 00000001
Ignore share access checks.
ファイルは書き込みアクセス用に開かれていないことがわかります。また、「共有アクセス チェックを無視」はIO_IGNORE_SHARE_ACCESS_CHECKによく似ています。r8とr9にそれぞれ格納されている、 FltWriteFileExの 3 番目と 4 番目のパラメーターであるByteOffsetとLengthパラメーターの妥当性をチェックしてみましょう。
0: kd> dx ((PLARGE_INTEGER)@r8)->QuadPart
((PLARGE_INTEGER)@r8)->QuadPart : 0 [Type: __int64]
0: kd> dx (int)@r9
(int)@r9 : 90112 [Type: int]
オフセット0での90,112バイトの書き込み - これは ProcMon 出力と一致します。6番目のパラメータFlagsはどうでしょうか?
0: kd> dx *(PULONG)(@rsp+(8*6))
*(PULONG)(@rsp+(8*6)) : 0xa [Type: unsigned long]
0xA は0x2 | 0x8で、 FLTFL_IO_OPERATION_PAGING | FLTFL_IO_OPERATION_SYNCHRONOUS_PAGINGです。これは、ProcMon で見た「ページング I/O、同期ページング I/O」と一致します。
これをドライバーで再現できるかどうか確認してみましょう。実験 2 で行ったように、ローカルにマップされた DLL を開きますが、 FILE_WRITE_DATAを要求する代わりに、CloudFiles と同じ権限 ( SYNCHRONIZE | FILE_READ_ATTRIBUTES | FILE_WRITE_ATTRIBUTESを使用します。これにより、 FILE_WRITE_DATAを探すMmFlushImageSectionチェックは実行されませんが、CloudFiles の動作をより厳密に再現するために、とにかくIO_IGNORE_SHARE_ACCESS_CHECKが投入されます。次に、 FltWriteFileEx使用して、書き込み不可のFILE_OBJECTへの同期ページング書き込みを実行します。
簡潔にするために、一部のヘルパー コードを省略しています。この記事のすべてのサンプルコードは、 GitHubで入手できます。
VOID ExperimentFour()
{
DECLARE_CONST_UNICODE_STRING(filePath,
L"\\SystemRoot\\System32\\TestDll.dll");
NTSTATUS ntStatus = STATUS_SUCCESS;
HANDLE hFile = NULL;
HANDLE hSection = NULL;
HANDLE hFile2 = NULL;
OBJECT_ATTRIBUTES fileObjAttr{};
IO_STATUS_BLOCK iosb{};
BOOLEAN bSuccessful = FALSE;
BOOLEAN bReportResults = FALSE;
PVOID pMappedBase = NULL;
PFILE_OBJECT pFileObject = NULL;
PFLT_INSTANCE pInstance = NULL;
PFLT_VOLUME pVolume = NULL;
LARGE_INTEGER byteOffset{};
ULONG bytesWritten = 0;
ntStatus = MapFileAsImageSection(
&filePath, &hFile, &hSection, &pMappedBase);
if (!NT_SUCCESS(ntStatus))
{
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL,
"ExperimentFour: MapFileAsImageSection %wZ failed with NTSTATUS 0x%08x\n",
&filePath, ntStatus);
goto Cleanup;
}
// Find our own minifilter instance for the volume containing this file
// We'll need it later
ntStatus = GetMyInstanceForFile(hFile, &pVolume, &pInstance);
if (!NT_SUCCESS(ntStatus))
{
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL,
"ExperimentFour: GetMyInstanceForFile failed with NTSTATUS 0x%08x\n",
ntStatus);
goto Cleanup;
}
// Now that a view of the SEC_IMAGE mapping exists,
// close the file and section handles because that's what ntdll does
// https://github.com/Microsoft/Windows-driver-samples/blob/622212c3fff587f23f6490a9da939fb85968f651/filesys/fastfat/create.c#L3572-L3593
HandleDelete(hFile);
HandleDelete(hSection);
InitializeObjectAttributes(&fileObjAttr,
(PUNICODE_STRING)&filePath, OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE, NULL, NULL);
// Open the file without FILE_WRITE_DATA
// cldflt.sys!HsmiOpenFile uses this instead of IoCreateFileEx
ntStatus = FltCreateFileEx2(
gpFilter,
NULL,
&hFile2,
&pFileObject,
SYNCHRONIZE | FILE_READ_ATTRIBUTES | FILE_WRITE_ATTRIBUTES,
&fileObjAttr, &iosb, NULL, 0,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
FILE_OPEN,
FILE_NO_INTERMEDIATE_BUFFERING | FILE_SYNCHRONOUS_IO_NONALERT |
FILE_NON_DIRECTORY_FILE | FILE_OPEN_REPARSE_POINT,
NULL, 0,
IO_IGNORE_SHARE_ACCESS_CHECK,
NULL);
if (!NT_SUCCESS(ntStatus))
{
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL,
"ExperimentFour: IoCreateFileEx failed with NTSTATUS 0x%08x\n",
ntStatus);
goto Cleanup;
}
// cldflt.sys is using FltWriteFileEx with synchronous paging I/O
ntStatus = FltWriteFileEx(
pInstance, pFileObject, &byteOffset,
sizeof(gZeroBuf), gZeroBuf,
FLTFL_IO_OPERATION_PAGING | FLTFL_IO_OPERATION_SYNCHRONOUS_PAGING,
&bytesWritten, NULL, NULL, NULL, NULL);
// If FltWriteFileEx returns success without us passing FILE_WRITE_DATA,
// then we have succeeded
bSuccessful = NT_SUCCESS(ntStatus) && (sizeof(gZeroBuf) == bytesWritten);
bReportResults = TRUE;
Cleanup:
if (bReportResults)
{
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL,
"ExperimentFour complete. "
"FltWriteFileEx %s be used to write to a non-writable FILE_OBJECT "
"Status: 0x%08x\n",
bSuccessful ? "CAN" : "CANNOT",
ntStatus);
}
HandleDelete(hFile);
HandleDelete(hSection);
HandleDelete(hFile2);
if (pMappedBase)
{
NTSTATUS unmapStatus = ZwUnmapViewOfSection(ZwCurrentProcess(), pMappedBase);
NT_ASSERT(NT_SUCCESS(unmapStatus));
}
ReferenceDelete(pFileObject);
if (pInstance) FltObjectDereference(pInstance);
if (pVolume) FltObjectDereference(pVolume);
}
この実験では次の出力が得られます。
ExperimentFour complete. FltWriteFileEx CAN be used to write to a non-writable FILE_OBJECT Status: 0x00000000
これは、 FltWriteFileExを使用していくつかのルールを破ることができることを証明しています。PPLFault とこの実験には重要な違いがあります。実験はネットワーク リダイレクタなしで成功したため、ローカルにマップされているか SMB 経由でマップされているかに関係なく、CloudFiles だけで使用中の実行ファイルを変更できることが証明されました。より抽象的に言えば、ネットワーク リダイレクタなしでも CloudFiles 経由の FFI 攻撃が可能になる可能性があることを証明しています。
新たなエクスプロイト
Microsoft の PPLFault 軽減策は、ネットワーク リダイレクタ経由で読み込まれる実行可能ファイルを特に対象としています。ここで発見したことを応用して、ネットワーク リダイレクタなしで同じ効果を実現できますか?
CI が署名検証のために DLL を要求すると、PPLFault はCfExecute使用して、フェッチ データ コールバックからプレースホルダーに書き込み (再水和) します。元のファイルが署名検証のために提供されると、ペイロードに切り替え、同じコールバック中にCfExecute をもう一度呼び出して、ファイルの一部をペイロードで上書きします。PPLFault を調整して、被害者がループバック SMB 経由ではなくローカルで DLL をロードするようにすると、 CfExecuteへの 2 回目の呼び出しが「クラウド操作はユーザーによってキャンセルされました」というエラーで失敗します。別のアプローチが必要でした。
C:\Users\user\Desktop>PPLFault.exe 760 services.dmp
[+] Ready. Spawning WinTcb.
[+] SpawnPPL: Waiting for child process to finish.
[!] CfExecute #2 failed with HR 0x8007018e: The cloud operation was canceled by user.
[!] Did not find expected dump file: services.dmp
リバース エンジニアリングを行った結果、障害は I/O マネージャーやファイルシステムとのやり取りではなく、CloudFilter 自体の内部チェックによって発生したことが判明しました。CfDehydratePlaceholderを呼び出してから、別のスレッド (リハイドレーション コールバックの外部) からCfHydratePlaceholderを呼び出すと、CloudFilter ドライバー内のファイルの状態がリセットされ、リハイドレーション コールバックが再度呼び出されることが判明しました。これにより、使用中の DLL をペイロードで上書きし、WinTcb-Light として任意のコード実行を実現できるようになりました。この小さなコード変更により PPLFault が復活したため、このバリアントを Redux と名付けました。
同様に、私たちはGodFault を復活させ、高度な権限を持つ PPL アクセスを利用してカーネル メモリを侵害し、Windows Defender のプロセス保護を回避して、通常は終了できないプロセスを終了しました。
Redux と GodFault-Redux の PoC はGitHubで見つかります。
以下のビデオは、完全に更新された Windows Server 2022 (2 月 2026 バージョン 20348.4773) での次の内容を示しています。
- PPLFault がダンプに失敗する
lsass - Redux のダンプに成功
lsass - 管理者は PPL であるため
MsMpEng.exeを終了できませんでした - GodFault-Redux は正常に終了しました
MsMpEng.exe
緩和
MSRC へのレポートでは、次のすべての条件を満たすIRP_MJ_ACQUIRE_FOR_SECTION_SYNCHRONIZATION操作をブロックすることで Redux の影響を軽減するファイルシステム ミニフィルターを提供しました。
- 要求者は PPL です。
- 要求者の
PreviousModeはUserModeです。 - ページ保護は実行可能です(例:
PAGE_EXECUTE_READ) または割り当て属性にSEC_IMAGEが含まれています。 - ファイルには、
IO_REPARSE_TAG_CLOUDなどのCloud Filter 再解析タグがあります。
Elastic Defend バージョン 8.14 以降には緩和策が組み込まれています。影響を受けるオペレーティング システムを実行している場合は、 Defend Advanced Policyで以下を設定して有効にすることができます。
windows.advanced.flags: e931849d52535955fcaa3847dd17947b
この緩和策を実施することで、エクスプロイトはブロックされます。
C:\Users\user\Desktop>Redux 624 services.dmp
[+] Ready. Spawning WinTcb.
[+] SpawnPPL: Waiting for child process to finish.
[!] SpawnPPL: WaitForSingleObject returned 258. Expected WAIT_OBJECT_0. GLE: 2
[!] Did not find expected dump file: services.dmp
同時に、Windows はSTATUS_ACCESS_DENIED (0xC0000022)ステータス コードを含むポップアップを表示します。
緩和策に関する PoC はGitHubでご覧いただけます。
開示と是正
開示のタイムラインは次のとおりです。
- 2024-02-14 Redux を MSRC に報告しました。
- 2024-02-29 Windows Defender チームが情報開示の調整のために連絡を取りました。
- 2024-10-01 Windows 11 24H2 が緩和策を伴って GA に到達しました。
私たちが Redux を MSRC に公開したとき、Redux は完全にパッチを適用したバージョンの Windows 11 に対しては機能していましたが、実験的な Insider Canary ビルド 25936 に対しては機能していませんでした。Windows Defender チームとこの問題を議論しているときに、(現在は元) Microsoft シニア セキュリティ研究者のPhilip Tsukerman氏が PPLFault の亜種を探しているときにこの問題を発見し、修正プログラムはまだリリース前のテスト段階にあることを知りました。
以下の表は、発行日時点で影響を受ける Windows のバージョンと修正されたバージョンを示しています。
| オペレーティングシステム | ライフサイクル | ステータスの修正 |
|---|---|---|
| ウィンドウズ 11 24時間24時間 | メインストリームサポート | ✔ 修正済み |
| Windows 10 エンタープライズ LTSC 2021 | メインストリームサポート | ❌ 2月 2026 時点でまだ機能しています(19044.6937) |
| Windows Server 2025 | メインストリームサポート | ✔ 修正済み |
| Windows Server 2022 | メインストリームサポート | ❌ 2月 2026 時点でまだ機能しています(20348.4773) |
| Windows Server 2019 | 延長サポート | ❌ 2月 2026 時点でまだ機能しています(17763.8389) |
まとめ
2024 年に、私たちは新しい Windows の脆弱性クラスである False File Immutability (FFI) を公開し、 PPLFaultとItsNotASecurityBoundaryという 2 つの異なるカーネル エクスプロイトのリリースによってそれを実証しました。どちらのエクスプロイトも、ネットワーク リダイレクタを利用して、Windows コード整合性の設計上の欠陥を悪用します。この研究では、ネットワーク リダイレクタなしで FFI を悪用する方法を示す別のエクスプロイトを紹介し、公開しました。これは、2024 年 2 月に報告された時点では 3 番目の FFI エクスプロイトであったと考えられます。その後、少なくともさらに2 つのエクスプロイトが発生しています。
Redux は FFI の終わりではありません。悪用可能な FFI の脆弱性はまだまだあります。
