Gabriel Landau

Die unveränderliche Illusion: Ihren Kernel mit Cloud-Dateien knacken

Angreifer können eine Klasse von Schwachstellen ausnutzen, um Sicherheitsbeschränkungen zu umgehen und Vertrauensketten zu durchbrechen.

15 Minuten LesezeitInterna
Die unveränderliche Illusion: Ihren Kernel mit Cloud-Dateien knacken

Im Jahr 2024 haben wir eine neue Windows-Schwachstellenklasse, False File Immutability (FFI), offengelegt, die zuvor gezeigt hat, wie Netzwerk-Redirectoren ausgenutzt werden können, um falsche Annahmen im Design der Windows-Codeintegrität zu verletzen, was zu zwei Kernel-Exploits führte. Diese Exploits nutzten Windows-Netzlaufwerke aus, was die Komplexität erhöhte und einen Engpass in der Angriffskette schuf, der eine einfachere Erkennung und Abschwächung ermöglichte.

Diese Forschung stellt einen Fortschritt dar, indem sie eine effizientere und in sich geschlossene Methode der Nutzung vorstellt. Der neuartige Ansatz nutzt eine in Windows integrierte Funktion, um die gleiche Dateimodifikationsumgehung zu erreichen, ohne die Komplexität von SMB-Setups. Durch die Analyse, wie der Kernel-Treiber für diese Funktion Dateidaten verarbeitet, entdecken wir eine Sicherheitslücke, die es einem Angreifer ermöglicht, Dateien zu modifizieren, die Windows fälschlicherweise als unveränderlich annimmt, was zu einem Proof-of-Concept-Kernel-Exploit führt.

Kernerkenntnisse:

  • Kein Netzwerk-Redirector erforderlich: Im Gegensatz zu früheren Exploits nutzt die neue Exploitation-Methode die falsche Dateiunveränderlichkeit aus, ohne dass die Windows-Dateifreigabe benötigt wird.
  • Ausgenutzte integrierte Funktion: Der Exploit nutzt eine Sicherheitslücke in einer integrierten Windows-Funktion aus, die für die Cloud-Dateisynchronisierung zuständig ist.
  • Unveränderlichkeit verletzt: Dies ermöglicht die Modifizierung von Dateien, die der Windows-Kernel und der Speichermanager fälschlicherweise als unveränderlich annehmen, was zu einer Kernel-Schwachstelle führt.
  • Schutzmaßnahmen umgangen: Es ermöglicht Angreifern, eine von Microsoft speziell für eine frühere FFI-Schwachstelle entwickelte Schutzmaßnahme zu umgehen.
  • Forever-Day: Microsoft hat sich entschieden, diese Sicherheitslücke nur in einigen Windows-Versionen zu beheben, sodass sie auch im Februar 2026 noch auf mehreren vollständig gepatchten Windows-Versionen mit Mainstream-Support funktionsfähig ist.

Falsche Dateiunveränderlichkeit

Vielleicht erinnern Sie sich an False File Immutability aus unserem kürzlich erschienenen Artikel und dem BlueHat IL 2024 Vortrag, aber falls nicht, sollte dieser Abschnitt Ihnen helfen, Ihr Gedächtnis aufzufrischen. Wenn Sie damit bereits vertraut sind, können Sie getrost zum nächsten Abschnitt springen.

Wenn eine Anwendung unter Windows eine Datei öffnet, verwendet sie typischerweise eine Form der 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
);

Die Aufrufer von CreateFile geben in dwDesiredAccess an, welche Zugriffsrechte sie wünschen. Ein Aufrufer würde beispielsweise FILE_READ_DATA übergeben, um Daten lesen zu können, oder FILE_WRITE_DATA um Daten schreiben zu können. Die vollständigen Zugriffsrechte sind auf der Microsoft Learn-Website dokumentiert .

Zusätzlich zur Übergabe dwDesiredAccess müssen Aufrufer einen „Sharing-Modus“ in dwShareMode übergeben, der aus null oder mehr der Elemente FILE_SHARE_READ, FILE_SHARE_WRITE und FILE_SHARE_DELETE besteht. Man kann sich einen Freigabemodus so vorstellen, dass der Aufrufer erklärt: „Ich bin damit einverstanden, dass andere X mit dieser Datei machen, während ich sie benutze“, wobei X Lesen, Schreiben oder Umbenennen sein kann. Ein Aufrufer, der beispielsweise FILE_SHARE_WRITE übergibt, erlaubt anderen, die Datei zu beschreiben, während sie damit arbeiten.

Beim Öffnen einer Datei wird der dwDesiredAccess des Aufrufers mit dem dwShareMode aller vorhandenen Dateihandles verglichen. Gleichzeitig wird der dwShareMode des Aufrufers mit dem zuvor gewährten dwDesiredAccess aller vorhandenen Handles zu dieser Datei verglichen. Wenn einer dieser Tests fehlschlägt, schlägt CreateFile aufgrund einer Freigabeverletzung fehl.

Teilen ist nicht verpflichtend. Anrufer können den Freigabemodus Null angeben, um exklusiven Zugriff zu erhalten. Laut Microsoft -Dokumentation:

Eine geöffnete Datei, die nicht gemeinsam genutzt wird (dwShareMode auf Null gesetzt), kann weder von der Anwendung, die sie geöffnet hat, noch von einer anderen Anwendung erneut geöffnet werden, bis ihr Handle geschlossen wurde. Dies wird auch als exklusiver Zugang bezeichnet.

Die Freigabe wird durch das Dateisystem erzwungen, typischerweise NTFS, aber Windows unterstützt auch andere Dateisysteme wie FAT32. Windows selbst lässt FILE_SHARE_WRITE beim Öffnen bestimmter Dateitypen weg, um eine Änderung während der Nutzung zu verhindern. Solche unveränderlichen Dateien können als unveränderlich betrachtet werden.

In manchen Situationen ist der Speichermanager auf diese Unveränderlichkeit angewiesen. Tritt ein Seitenfehler in einer unveränderlichen, speicherabgebildeten Datei auf und wurde diese Seite nicht verändert, so kann der Speichermanager den Inhalt dieser Seite direkt aus der ursprünglichen Datei lesen. Es ist nicht nötig, eine zweite Kopie des Dateiinhalts in der Auslagerungsdatei zu speichern, da die Unveränderlichkeit sicherstellt, dass die Datei auf der Festplatte nicht verändert werden kann. Ausführbare Dateien, die im Speicher laufen, wie z. B. EXEs und DLLs, sind unveränderlich, sodass der Speichermanager diese Optimierung auf sie anwenden kann.

Netzwerk-Redirectoren ermöglichen die Verwendung von Netzwerkpfaden mit jeder API, die Dateipfade akzeptiert. Das ist sehr praktisch, da es Benutzern und Anwendungen ermöglicht, problemlos mit Dateien zu arbeiten und Programme von Netzlaufwerken auszuführen. Der Kernel leitet jegliche E/A transparent an den Remote-Rechner um. Wird ein Programm von einem Netzlaufwerk gestartet, werden alle EXE-Dateien und die dazugehörigen DLL-Dateien bei Bedarf transparent aus dem Netzwerk abgerufen.

Wenn ein Netzwerk-Redirector verwendet wird, muss der Server am anderen Ende der Leitung kein Windows-Rechner sein. Es könnte sich um eine Linux-Maschine handeln, auf der Samba läuft, oder sogar um ein Python- Impacket-Skript , das das SMB-Netzwerkprotokoll „spricht“. Das bedeutet, dass der Server die Semantik der Windows-Dateisystemfreigabe nicht beachten muss. Ein Angreifer kann einen Netzwerk-Redirector einsetzen, um „unveränderliche“ Dateien serverseitig zu modifizieren und so die Freigabebeschränkungen zu umgehen. Dies bedeutet, dass fälschlicherweise angenommen wird, diese Dateien seien unveränderlich. Dies ist eine Schwachstellenklasse, die wir als falsche Dateiunveränderlichkeit (FFI) bezeichnen.

Cloud-Dateien

Stellen Sie sich vor, Sie verlassen das Haus, um in den Tag zu starten, und vor Ihrer Haustür liegt ein Paket. Das ist doch das tolle Surface Book, das du letzte Woche bestellt hast. Voller Vorfreude, aber unter Zeitdruck, wirfst du es in deine Tasche und gehst ins Fitnessstudio. Nach einem Workout mit fetten Beats auf deinem Zune gehst du ins örtliche Café in Redmond, um dich mit einem Freund zu treffen, den du über Xbox Live kennengelernt hast. Leider sind sie in Verzug, also klappen Sie Ihr brandneues Surface Book auf und melden sich bei Windows an, voller Vorfreude darauf, Recall einzurichten. Trotz des nur mittelmäßigen WLANs im Café erscheint auf wundersame Weise sofort Ihr gesamtes 1 TB großes OneDrive vor Ihnen. Unmöglich, dass du 1 TB so schnell heruntergeladen hast, da muss irgendein Zauber im Spiel sein. Diese Hexerei ist Cloud Files.

Die in Windows 10 Version 1709 eingeführte Funktion Cloud Files ermöglicht es Benutzermodusanwendungen wie OneDrive, sich als Cloud-Synchronisierungsanbieter zu registrieren und leere „Platzhalter“-Dateien auf dem System zu erstellen. Anfangs sind diese Platzhalter dehydriert (leer). Beim Zugriff auf die Dateien wird die Ein-/Ausgabe vom CloudFiles-Kerneltreiber (cldflt.sys) abgefangen, der den Prozess des Anbieters aufruft. Der Anbieter kann dann den Inhalt der Datei aus der Cloud abrufen. Es ist nicht nötig, die gesamte Datei auf einmal herunterzuladen. Wenn Sie nur 1 MB benötigen, kann es genau diese 1 MB abrufen. Wenn Sie mehr von der Datei anfordern, kann sie den Dateiinhalt nach Bedarf wiederherstellen (auffüllen).

Wenn der Treiber eine Datei wiederherstellen muss, ruft er eine Wiederherstellungs-Callback-Funktion im Prozess des Anbieters auf (d. h. OneDrive.exe). Dieser Rückruf ruft den Inhalt der Datei ab (möglicherweise aus der Cloud) und ruft CfExecute auf, um diesen Inhalt an den Treiber zu übergeben, der ihn dann in die Datei schreibt. CloudFiles fordert nur dann eine Rehydratisierung von Dateibereichen an, die derzeit nicht hydratisiert sind. Es ist jedoch möglich, Dateien zu dehydratisieren , um Speicherplatz auf dem aktuellen System freizugeben.

Entwicklung von Ausbeutungsanlagen

Standardmäßig ermöglicht Windows die gemeinsame Nutzung von Dateien und Ordnern über das Netzwerk mithilfe des Server Message Block (SMB)-Protokolls. Wenn Sie jemals eine Verbindung zu einem freigegebenen Netzlaufwerk in einem Firmennetzwerk hergestellt haben, ist die Wahrscheinlichkeit groß, dass dabei SMB zum Einsatz kam. Windows enthält standardmäßig sowohl einen SMB-Client als auch einen Server. Die Clientkomponente stellt einen Netzwerk-Redirector bereit, wie oben beschrieben, der einen transparenten SMB-Zugriff auf Dateien über jede API ermöglicht, die Dateipfade akzeptiert. Beispielsweise können Sie Process Monitor jetzt über das Internet starten, indem Sie \\live.sysinternals.com\Procmon.exe eingeben.

Wir haben den PPLFault-Exploit im Mai 2023 zusammen mit unserem Vortrag auf der Black Hat Asia veröffentlicht. PPLFault nutzt einen Netzwerk-Redirector, um FFI in DLLs auszunutzen, die in Protected Process Light (PPL)-Prozesse geladen werden. Für den ersten Prototyp war eine zweite, vom Angreifer kontrollierte Maschine erforderlich, auf der ein bösartiger SMB-Server lief. Durch Deaktivierung des in Windows integrierten SMB-Servers konnten wir den bösartigen SMB-Server auf den lokalen Rechner verlagern, wodurch die Notwendigkeit eines zweiten Rechners entfiel (Prototyp).

Das war allerdings noch komplizierter, als wir es uns gewünscht hätten, denn wir gingen damals fälschlicherweise davon aus, dass zum Stoppen des in Windows integrierten SMB-Servers ein Neustart erforderlich sei. Glücklicherweise entdeckten wir James Forshaws Technik , den CloudFiles-Provider mit dem Loopback-SMB-Adapter (localhost) zu kombinieren, wodurch wir den finalen Exploit ohne Neustart erstellen konnten. Neben der optimierten Funktionalität unterscheidet sich die CloudFiles/SMB-Kombination von den beiden vorherigen Exploit-Versionen dadurch, dass sie den regulären Windows-SMB-Server verwendet, der die Dateifreigabe unterstützen sollte (d. h. FILE_SHARE_*) Semantik. Wenn beispielsweise ein SMB-Client eine Datei auf einem Server ohne FILE_SHARE_WRITE geöffnet hat, sollte der Server einem anderen Client nicht erlauben, diese Datei zum Schreiben zu öffnen. Ebenso sollte der Server keinen Schreibzugriff auf lokal auf dem Server laufende ausführbare Dateien zulassen.

Es scheint ein Widerspruch vorzuliegen. Wenn PPLFault den Beschränkungen der Dateifreigabe unterliegen muss, wie kann es dann Code in eine laufende DLL einschleusen? Schauen wir mal, was der Prozessmonitor uns dazu sagen kann. Die Ausführung von PPLFault unter Process Monitor zeigt die folgenden drei Operationen an (zur Veranschaulichung gefiltert). Diese Analyse wurde mit Version 10.0.22621.2861 von cldflt.sys unter Windows 11 22631.2861 durchgeführt.

Die Operationen sind der Reihe nach:

  1. Der Opferprozess services.exe lädt eine DLL als ausführbares Image.
  2. Nach dem Laden PPLFault.exe öffnen.
  3. Nach dem Öffnen schreibt PPLFault.exe hinein.

Hierzu sind einige wichtige Beobachtungen anzustellen:

Verletzung der Unveränderlichkeit
Wir beobachten einen erfolgreichen Schreibvorgang in eine Datei, während diese als ausführbares Image geladen ist. In unserer früheren FFI-Forschung haben wir die MmFlushImageSection -Prüfung im Dateisystem besprochen, die genau dazu dient, vor solchen Situationen zu schützen. Wie konnte diese Überprüfung umgangen werden?

Verletzung des Dateizugriffsmodells
Wir können sehen, dass PPLFault die Datei erfolgreich überschrieben hat. Laut Microsoft-Dokumentation zu WriteFile sollte die Datei mit Schreibzugriff geöffnet werden, also FILE_WRITE_DATA. Die Ausgabe zeigt jedoch, dass sie für „Attribute lesen, Attribute schreiben, Synchronisieren“ geöffnet wurde, also FILE_READ_ATTRIBUTES, FILE_WRITE_ATTRIBUTES und SYNCHRONIZE. Wie konnte diese Datei ohne FILE_WRITE_DATA überschrieben werden?

Versuchen wir, diese beiden Fragen im nächsten Abschnitt zu beantworten.

📘 Nerd-Bonus -

Process Monitor installiert einen Dateisystem-Minifiltertreiber , um die E/A-Aktivität des Systems abzufangen und zu protokollieren. Windows kapselt E/A-Aktionen in Strukturen, die als E/A-Anforderungspakete (IRPs) bezeichnet werden. Jedem Minifilter ist eine „Höhe“ zugeordnet , die man sich wie die Stockwerke in einem Gebäude vorstellen kann. Die meisten IRPs beginnen im obersten Stockwerk und verlaufen die Hierarchie nach unten. Wenn ein Minifilter seine eigenen Ein-/Ausgaben erzeugt, beginnt das IRP bei seiner Höhe und wandert von dort nach unten. Mit anderen Worten: Ein Minifilter im sechsten Stock wird niemals Ein-/Ausgabesignale vom fünften Stock empfangen. Der Minifiltertreiber von Process Monitor läuft in der Höhe 385200. Normalerweise wird es niemals die Aktivität von cldflt.sys sehen, das in der Höhe 180451 läuft. Glücklicherweise können wir die Höhe des Prozessmonitors mit dem Schalter /altitude anpassen und ihn unterhalb von CloudFiles auf Höhe 180450 platzieren.

Für dich gelten Regeln, aber nicht für mich

Wie bereits erwähnt, unterliegen Anwendungen Beschränkungen hinsichtlich der Dateifreigabe, der Kernel selbst ist jedoch nicht immer in gleicher Weise eingeschränkt. Kernel-Treiber können beispielsweise IoCreateFileEx verwenden, um Dateien zu öffnen oder zu erstellen.

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 Sieht der benutzerseitigen Funktion NtCreateFile sehr ähnlich, aber ihre Dokumentation beschreibt einige wichtige zusätzliche Funktionen, darunter den Parameter Options , der ein Flag unterstützt:

IO_IGNORE_SHARE_ACCESS_CHECK
Der E/A-Manager sollte nach der Erstellung des Dateiobjekts keine Freigabezugriffsprüfungen mehr durchführen. Das Dateisystem könnte diese Prüfungen jedoch weiterhin durchführen.

Ist es wirklich so einfach? Kann ein Kernel-Treiber IoCreateFileEx(IO_IGNORE_SHARE_ACCESS_CHECK) verwenden, um eine in Verwendung befindliche DLL für Schreibzugriffe zu öffnen? Lasst uns einen Kernel-Treiber schreiben, um es auszuprobieren. Der Code in diesem Artikel ist als Visual Studio-Projekt auf GitHub hier verfügbar.

/*
* 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);
}

Das Laden in einer VM mit aktivierter Testsignatur führt zu folgender Ausgabe:

ExperimentOne complete. IoCreateFileEx(IO_IGNORE_SHARE_ACCESS_CHECK) CAN open a write-sharing-denied file for FILE_WRITE_DATA. Status: 0x00000000

Haben wir gerade eine plausible Erklärung dafür gefunden, wie PPLFault „unveränderliche“ Dateien modifizieren kann? Nicht ganz. Dieses Experiment war zwar etwas vereinfacht, aber es zeigt IO_IGNORE_SHARE_ACCESS_CHECK in Aktion und beweist, dass Kernel-APIs mehr Freiheit bieten können als ihre Pendants im Benutzermodus.

In PPLFault geht es bei CloudFiles nicht nur darum, eine Datei mit Schreibzugriffssperren zu verändern. Es geht vielmehr darum, eine DLL zu modifizieren, während sie als ausführbares Image im Speicher abgebildet ist. Versuchen wir ein anderes Experiment, das dem PPLFault-Szenario etwas näher kommt. Im zweiten Experiment werden wir LoadLibrary emulieren, indem wir eine DLL öffnen, einen SEC_IMAGE -Abschnitt erstellen und dann eine Ansicht dieses Abschnitts in den Speicher abbilden. Sobald die Ansicht zugeordnet ist, schließen wir die Handles und testen, ob IoCreateFileEx(IO_IGNORE_SHARE_ACCESS_CHECK) einen beschreibbaren Handle erhalten kann.

Beginnen wir mit einer Hilfsfunktion, die ein PE als Bildabschnitt abbildet, ähnlich wie LoadLibrary. Wir werden dies im Kernel durchführen, um das Experiment in einem einzigen Treiber zu halten. Beachten Sie jedoch, dass dies für unsere Zwecke funktional äquivalent zu LoadLibrary ist.

// 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;
}

Jetzt verwenden wir diesen Helfer, um eine DLL abzubilden, und sehen dann, ob wir mit IO_IGNORE_SHARE_ACCESS_CHECK darauf schreiben können:

/*
* 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));
    }
}

Die Ausführung dieses Experiments liefert folgende Ausgabe:

ExperimentTwo complete. IoCreateFileEx(IO_IGNORE_SHARE_ACCESS_CHECK) CANNOT open a file backing a local SEC_IMAGE section for FILE_WRITE_DATA. Status: 0xc0000043

In diesem Fall schlug IoCreateFileEx mit 0xC0000043 (STATUS_SHARING_VIOLATION) fehl, da Dateien, die als ausführbare Images gemappt sind, über zusätzliche Schutzmechanismen verfügen, um sicherzustellen, dass sie unveränderlich bleiben, selbst wenn keine Handles geöffnet sind. Diese Überprüfung können Sie mithilfe der MmFlushImageSection -API im Microsoft FastFat-Beispieltreibercode sehen, sie existiert aber auch in anderen Dateisystemen, einschließlich 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 );
    }
}

Das Flag IO_IGNORE_SHARE_ACCESS_CHECK umgeht die Prüfungen des E/A-Managers, nicht aber die Prüfung MmFlushImageSection im Dateisystem. Beim erneuten Lesen der Beschreibung von IO_IGNORE_SHARE_ACCESS_CHECK wird es im Nachhinein deutlich:

IO_IGNORE_SHARE_ACCESS_CHECK
Der E/A-Manager sollte nach der Erstellung des Dateiobjekts keine Freigabezugriffsprüfungen mehr durchführen. Das Dateisystem könnte diese Prüfungen jedoch weiterhin durchführen.

ExperimentTwo ist keine wirklich faire Darstellung von PPLFault, das die DLL von einem Netzlaufwerk lädt. Wenn ein Netzwerkclient eine Datei auf einem Server öffnet, weist der SMB-Clienttreiber eine Dateikontrollblockstruktur (FCB) zu, die diese logische Datei repräsentiert. Entsprechend öffnet der Server die Datei mit den angeforderten Freigabemodi und weist ihm seinen eigenen FCB zu. Dies bedeutet, dass zwei unterschiedliche FCBs mit unterschiedlicher Semantik im Spiel sind. Wenn der Client eine DLL als ausführbare Datei in den Speicher einbindet, wird die resultierende SEC_IMAGE Dateizuordnung (auch Abschnitt genannt) mit ihrem FCB verknüpft, sodass sie den Schutz von MmFlushImageSection erhält. Der Server erstellt keinen entsprechenden Bildbereich, sodass sein FCB keinen solchen Schutz genießt. PPLFault nutzt diesen Unterschied aus, indem es die Schreibvorgänge auf den FCB des Servers durchführt und dabei die MmFlushImageSection -Prüfung umgeht.

Probieren wir das in ExperimentThree aus:

/*
* 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 erzeugt folgende Ausgabe:

ExperimentThree complete. IoCreateFileEx(IO_IGNORE_SHARE_ACCESS_CHECK) CAN open a file backing a remote SEC_IMAGE view for FILE_WRITE_DATA. Status: 0x00000000

Das obige ExperimentThree zeigt, wie Kernel-Treiber DLLs, die von SMB-Clients gemappt werden, modifizieren können, indem sie das IO_IGNORE_SHARE_ACCESS_CHECK -Flag in der Serverversion dieser Datei verwenden.

Ärmel hochkrempeln

Wir haben gerade gezeigt, was möglich ist, aber wir wissen immer noch nicht, was Cloud Files eigentlich tut. Um die zuvor aufgeworfenen Fragen zu beantworten, wollen wir die Ausgabe des Prozessmonitors genauer analysieren.

Zuvor hatten wir zwei Fragen gestellt:

Verletzung der Unveränderlichkeit
Wir beobachten einen erfolgreichen Schreibvorgang in eine Datei, während diese als ausführbares Image geladen ist. In unserer früheren FFI-Forschung haben wir die MmFlushImageSection -Prüfung im Dateisystem besprochen, die genau dazu dient, vor solchen Situationen zu schützen. Wie konnte diese Überprüfung umgangen werden?

Verletzung des Dateizugriffsmodells
Wir können sehen, dass PPLFault die Datei erfolgreich überschrieben hat. Laut Microsoft-Dokumentation zu WriteFile sollte die Datei mit Schreibzugriff geöffnet werden, also FILE_WRITE_DATA. Die Ausgabe zeigt jedoch, dass sie für „Attribute lesen, Attribute schreiben, Synchronisieren“ geöffnet wurde, also FILE_READ_ATTRIBUTES, FILE_WRITE_ATTRIBUTES und SYNCHRONIZE. Wie konnte diese Datei ohne FILE_WRITE_DATA überschrieben werden?

Den MmFlushImageSection -Bypass können wir leicht erklären. Bei dieser Prüfung wird nach FILE_WRITE_DATA gesucht, das hier nicht verwendet wurde. Die Datei wurde nur für „Attribute lesen, Attribute schreiben, Synchronisieren“ geöffnet. Die Verletzung des Dateizugriffsmodells können wir jedoch nicht erklären. Wie konnte eine nicht beschreibbare Datei überschrieben werden? Schauen wir uns den Aufrufstapel für die Operation WriteFile genauer an, um das herauszufinden.

Im Aufrufstapel können wir sehen, dass Zeile 176 von PPLFault.cpp aus dem Benutzermodus cldapi.dll!CfExecute (Zeilen 24-25) aufruft. Dies führt schließlich dazu, dass cldflt.sys!HsmiRecallWriteFileNoLock FltWriteFileEx aufruft. FltWriteFileEx ist irgendwie in der Lage, in eine Datei zu schreiben, die nicht zum Schreiben geöffnet ist. Schließen wir einen Kernel-Debugger an und sehen wir uns das genauer an.

Durch Setzen eines Haltepunkts bei FltWriteFileEx und erneutes Ausführen des Exploits können wir beim Aufruf von HsmiRecallWriteFileNoLock anhalten:

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]

Schauen wir uns an, welche Art von Zugriff dem Handle (~= FILE_OBJECT) gewährt wurde, das sich im zweiten Parameter von FltWriteFileEx befindet. Auf x64 ist dies 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.

Wir können sehen, dass die Datei nicht zum Schreiben geöffnet wurde, und „Freigabezugriffsprüfungen ignorieren“ klingt sehr nach IO_IGNORE_SHARE_ACCESS_CHECK. Lassen Sie uns die Parameter ByteOffset und Length auf Plausibilität prüfen. Es handelt sich dabei um den dritten und vierten Parameter von FltWriteFileEx, die in r8 bzw. r9 gespeichert sind.

0: kd> dx ((PLARGE_INTEGER)@r8)->QuadPart
((PLARGE_INTEGER)@r8)->QuadPart : 0 [Type: __int64]
0: kd> dx (int)@r9
(int)@r9         : 90112 [Type: int]

Ein Schreibvorgang von 90,112 Bytes an Offset 0 - das stimmt mit der ProcMon-Ausgabe überein. Und was ist mit Flags, dem 6. Parameter?

0: kd> dx *(PULONG)(@rsp+(8*6))
*(PULONG)(@rsp+(8*6)) : 0xa [Type: unsigned long]

0xA ist 0x2 | 0x8, was FLTFL_IO_OPERATION_PAGING | FLTFL_IO_OPERATION_SYNCHRONOUS_PAGING entspricht. Dies deckt sich mit „Paging I/O, Synchronous Paging I/O“, das wir in ProcMon gesehen haben.

Mal sehen, ob wir das in einem Treiber reproduzieren können. Wir werden eine lokal zugeordnete DLL öffnen, wie wir es in ExperimentTwo getan haben, aber anstatt nach FILE_WRITE_DATA zu fragen, werden wir uns an die gleichen Berechtigungen wie CloudFiles halten: SYNCHRONIZE | FILE_READ_ATTRIBUTES | FILE_WRITE_ATTRIBUTES. Dies löst zwar nicht die MmFlushImageSection -Prüfung aus, die nach FILE_WRITE_DATA sucht, aber wir fügen trotzdem IO_IGNORE_SHARE_ACCESS_CHECK hinzu, um das Verhalten von CloudFiles genauer nachzubilden. Als nächstes verwenden wir FltWriteFileEx , um einen synchronen Paging-Schreibvorgang in das nicht beschreibbare FILE_OBJECT durchzuführen.

Aus Gründen der Übersichtlichkeit lassen wir einige Hilfscodes weg. Der gesamte Beispielcode in diesem Artikel ist auf unserem GitHub-Repository verfügbar.

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);
}

Dieses Experiment liefert folgendes Ergebnis:

ExperimentFour complete. FltWriteFileEx CAN be used to write to a non-writable FILE_OBJECT Status: 0x00000000

Dies beweist, dass FltWriteFileEx dazu verwendet werden kann, mehrere Regeln zu brechen. Es gibt einen entscheidenden Unterschied zwischen PPLFault und diesem Experiment: Das Experiment war ohne Netzwerk-Redirectoren erfolgreich und bewies damit, dass CloudFiles allein in der Lage ist, in Gebrauch befindliche ausführbare Dateien zu modifizieren, unabhängig davon, ob diese lokal oder über SMB gemappt sind. Abstrakter betrachtet beweist es, dass die Ausnutzung von FFI über CloudFiles auch ohne Netzwerk-Redirectoren möglich sein kann.

Eine neue Ausbeutung

Die PPLFault-Schutzmaßnahmen von Microsoft zielen speziell auf ausführbare Dateien ab, die über Netzwerkumleitungen geladen werden. Können wir das hier Erkannte anwenden, um denselben Effekt ohne Netzwerk-Redirector zu erzielen?

Wenn CI die DLL zur Signaturprüfung anfordert, verwendet PPLFault CfExecute , um den Platzhalter aus seinem Fetch-Data-Callback zu beschreiben (zu rehydrieren). Sobald die Originaldatei zur Signaturprüfung bereitgestellt wurde, wird auf die Nutzdaten umgeschaltet. Dabei wird CfExecute während desselben Rückrufs ein zweites Mal aufgerufen, um einen Teil der Datei mit den Nutzdaten zu überschreiben. Durch die Anpassung von PPLFault, sodass das Opfer die DLL lokal anstatt über Loopback-SMB lädt, schlägt der zweite Aufruf von CfExecute mit der Meldung „Der Cloud-Vorgang wurde vom Benutzer abgebrochen“ fehl. Wir brauchten einen anderen Ansatz.

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

Nach einigen Reverse-Engineering-Untersuchungen stellten wir fest, dass der Fehler auf Prüfungen innerhalb von CloudFilter selbst zurückzuführen war und nicht auf die Interaktion mit dem I/O-Manager oder dem Dateisystem. Wir haben festgestellt, dass der Aufruf CfDehydratePlaceholder und anschließend der Aufruf von CfHydratePlaceholder aus einem anderen Thread (außerhalb des Rehydrations-Callbacks) den Zustand unserer Datei im CloudFilter-Treiber zurücksetzt und dadurch unseren Rehydrations-Callback erneut aufruft. Dies ermöglichte es uns, die aktuell verwendete DLL mit unserer Nutzlast zu überschreiben und als WinTcb-Light beliebigen Code auszuführen. Diese kleine Codeänderung hat PPLFault wieder zum Leben erweckt, deshalb haben wir die Variante Redux genannt.

Wir haben GodFault auf ähnliche Weise wiederbelebt, indem wir unseren hochprivilegierten PPL-Zugriff nutzten, um den Kernel-Speicher zu kompromittieren und die Prozessschutzmechanismen von Windows Defender zu umgehen, wodurch ein normalerweise nicht beendebarer Prozess beendet wurde.

Unsere Proof-of-Concepts für Redux und GodFault-Redux finden Sie auf GitHub.

Das untenstehende Video zeigt Folgendes auf einem vollständig aktualisierten Windows Server 2022 (Februar 2026 Version 20348.4773).

  1. PPLFault konnte nicht gesichert werden lsass
  2. Redux erfolgreich gespeichert lsass
  3. Ein Administrator kann MsMpEng.exe nicht beenden, da es sich um PPL handelt.
  4. GodFault-Redux wurde erfolgreich beendet MsMpEng.exe

Milderung

In unserem Bericht an MSRC haben wir einen Dateisystem-Minifilter bereitgestellt, der Redux entschärft, indem er IRP_MJ_ACQUIRE_FOR_SECTION_SYNCHRONIZATION -Operationen blockiert, die alle folgenden Kriterien erfüllen:

  • Der Antragsteller ist ein PPL.
  • Der Anforderer hat den Wert PreviousMode = UserMode.
  • Der Seitenschutz ist ausführbar (z. B. PAGE_EXECUTE_READ) oder die Zuordnungsattribute enthalten SEC_IMAGE.
  • Die Datei enthält ein Cloud Filter-Reparse-Tag wie IO_REPARSE_TAG_CLOUD.

In Elastic Defend Version 8.14 und neueren Versionen ist eine Schutzmaßnahme integriert. Falls auf Ihrer Flotte betroffene Betriebssysteme ausgeführt werden, können Sie dies in Defend Advanced Policy aktivieren, indem Sie die folgenden Einstellungen vornehmen.

windows.advanced.flags: e931849d52535955fcaa3847dd17947b

Durch diese Schutzmaßnahme wird die Sicherheitslücke geschlossen:

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

Gleichzeitig zeigt Windows ein Popup mit dem Statuscode STATUS_ACCESS_DENIED (0xC0000022) an.

Unseren Proof of Concept (PoC) zur Risikominderung finden Sie auf GitHub.

Offenlegung und Sanierung

Der Offenlegungszeitplan sieht wie folgt aus:

  • 2024-02-14 Wir haben Redux an MSRC gemeldet.
  • 29.02.2024 Das Windows Defender-Team hat sich gemeldet, um die Offenlegung zu koordinieren.
  • 2024-10-01 Windows 11 24H2 erreichte GA mit der Abschwächung.

Als wir Redux an MSRC weitergaben, funktionierte es mit vollständig gepatchten Versionen von Windows 11, jedoch nicht mit dem experimentellen Insider Canary Build 25936. Während wir das Problem mit dem Windows Defender-Team besprachen, erfuhren wir, dass der (ehemalige) leitende Sicherheitsforscher von Microsoft, Philip Tsukerman, es bei der Suche nach Varianten von PPLFault entdeckt hatte; die Behebung befindet sich noch in der Vorab-Testphase.

Die folgende Tabelle zeigt die betroffenen und behobenen Windows-Versionen zum Zeitpunkt der Veröffentlichung.

BetriebssystemLebenszyklusFix-Status
Windows 11 24H2Mainstream-Unterstützung✔ Behoben
Windows 10 Enterprise LTSC 2021Mainstream-Unterstützung❌ Noch funktionsfähig im Februar 2026 (19044.6937)
Windows Server 2025Mainstream-Unterstützung✔ Behoben
Windows Server 2022Mainstream-Unterstützung❌ Noch funktionsfähig am 2026 Februar (20348.4773)
Windows Server 2019Erweiterter Support❌ Noch funktionsfähig im Februar 2026 (17763.8389)

Fazit

Im Jahr 2024 haben wir eine neue Windows-Schwachstellenklasse, False File Immutability (FFI), aufgedeckt und sie mit der Veröffentlichung zweier unterschiedlicher Kernel-Exploits demonstriert: PPLFault und ItsNotASecurityBoundary. Beide Exploits nutzen Netzwerk-Redirectoren, um Designfehler in der Windows-Codeintegrität auszunutzen. In dieser Forschungsarbeit haben wir einen weiteren Exploit vorgestellt und veröffentlicht , der zeigt, wie man FFI ohne Netzwerk-Redirectoren ausnutzen kann. Wir gehen davon aus, dass dies der dritte FFI-Exploit war, als er im Februar 2024 gemeldet wurde; seither gab es mindestens zwei weitere.

Redux ist nicht das Ende von FFI; es gibt noch weitere ausnutzbare FFI-Schwachstellen.