Gabriel Landau

La ilusión inmutable: Pwning tu kernel con archivos en la nube

Los actores maliciosos pueden abusar de una clase de vulnerabilidades para eludir restricciones de seguridad y romper cadenas de confianza.

15 min de lecturaFuncionamiento interno
La ilusión inmutable: Pwning tu kernel con archivos en la nube

En 2024, revelamos una nueva clase de vulnerabilidad de Windows, la Inmutabilidad de Archivos Falsos (FFI), que previamente demostró cómo los redireccionadores de red podían aprovechar para violar supuestos incorrectos en el diseño de la Integridad del Código de Windows, resultando en un par de exploits del núcleo. Estos exploits dependían de unidades de red Windows, lo que agregaba complejidad y creaba un punto de estrangulamiento en la cadena de apagadas que permitía una detección y mitigación más sencillas.

Esta investigación supone un avance al introducir un método de explotación más ágil y autosuficiente. El enfoque novedoso aprovecha una capacidad integrada de Windows para lograr el mismo bypass de modificación de archivos, sin las complejidades de las configuraciones SMB. Al analizar cómo el controlador del kernel para esta capacidad procesa los datos de archivos, descubrimos una emisión de seguridad que permite a un atacante modificar archivos que Windows asume erróneamente como inmutables, lo que lleva a una explotación de kernel de prueba de concepto.

Puntos clave:

  • No se necesita redirector de red: A diferencia de exploits anteriores, el nuevo método de explotación explota la Inmutabilidad de Archivos Falsos sin requerir el uso de compartición de archivos en Windows.
  • Capacidad incorporada explotada: El exploit aprovecha una emisión de seguridad dentro de una capacidad integrada de Windows que gestiona la sincronización de archivos en la nube.
  • Inmutabilidad violada: Permite modificar archivos que el kernel de Windows y el gestor de memoria asumen erróneamente como inmutables, lo que conduce a un exploit del kernel.
  • Mitigación eludida: Permite a los atacantes saltar una mitigación que Microsoft creó específicamente para un exploit FFI anterior.
  • Día para siempre: Microsoft decidió parchear este exploit solo en algunas versiones de Windows, por lo que sigue funcionando en varias versiones completamente parcheadas de Windows en Soporte Principal a fecha de febrero de 2026.

Inmutabilidad de archivos falsos

Quizá recuerdes la Inmutabilidad de Archivos Falsos por nuestro artículo reciente y la charla de BlueHat IL 2024 , pero si no, esta sección debería ayudarte a refrescar la memoria. Si ya lo conoces, siéntete libre de saltar a la siguiente sección.

Cuando una aplicación abre un archivo en Windows, normalmente emplea alguna forma de la API CreateFile de Win32.

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

Los llamantes de CreateFile especifican el acceso que desean en dwDesiredAccess. Por ejemplo, un llamador pasaría FILE_READ_DATA para poder leer datos, o FILE_WRITE_DATA para poder escribir datos. El conjunto completo de derechos de acceso está documentado en el sitio web de Microsoft Learn.

Además de pasar dwDesiredAccess, los llamantes deben pasar un "modo compartido" en dwShareMode, que consiste en cero o más de FILE_SHARE_READ, FILE_SHARE_WRITE, y FILE_SHARE_DELETE. Puedes pensar en un modo compartido como que el llamante declara "Estoy bien con que otros hagan X a este archivo mientras lo uso", donde X podría estar leyendo, escribiendo o cambiando de nombre. Por ejemplo, un llamador que pasa FILE_SHARE_WRITE permite que otros escriban el archivo mientras trabajan con él.

Al abrir un archivo, la dwDesiredAccess del llamante se prueba contra la dwShareMode de todos los handles de archivo existentes. Simultáneamente, la dwShareMode del llamante se prueba contra la dwDesiredAccess previamente concedida de todos los handleres existentes a ese archivo. Si alguna de estas pruebas falla, entonces CreateFile falla con una violación de compartición (shareshare).

No es obligatorio compartir. Los llamantes pueden pasar un modo de compartir cero para obtener acceso exclusivo. Según la documentación de Microsoft:

Un archivo abierto que no se comparte (dwShareMode puesto a cero) no puede volver a abrir, ni por la aplicación que lo abrió ni por otra aplicación, hasta que su handle se cerró. Esto también se denomina acceso exclusivo.

El sistema de archivos lo impone el compartir de archivos, normalmente NTFS, pero Windows soporta otros sistemas de archivos como FAT32. Windows en sí omite FILE_SHARE_WRITE al abrir ciertos tipos de archivos, impidiendo modificaciones mientras están en uso. Estos archivos no modificables pueden considerar inmutables.

En algunas situaciones, el gestor de memoria depende de esta inmutabilidad. Si ocurre un fallo de página dentro de un archivo de memoria inmutable y esa página no fue modificada, entonces el gestor de memoria puede leer el contenido de esa página directamente del archivo de respaldo original. No es necesario almacenar una segunda copia del contenido del archivo en el archivo de paginación porque la inmutabilidad cerciora que el archivo en disco no pueda cambiar. Los ejecutables que se ejecutan en memoria, como EXE y DLLs, son inmutables, por lo que el gestor de memoria puede aplicarles esta optimización.

Los redireccionadores de red permiten el uso de rutas de red con cualquier API que acepte rutas de archivo. Esto es muy cómodo, ya que permite a usuarios y aplicaciones trabajar fácilmente con archivos y ejecutar programas desde unidades de red. El kernel redirige de forma transparente cualquier E/S a la máquina remota. Si un programa se lanza desde una unidad de red, cualquier EXE y sus DLL serán extraídos de la red de forma transparente según sea necesario.

Cuando se emplea un redirector de red, el servidor al otro extremo de la tubería no tiene por qué ser un equipo con Windows. Podría ser una máquina Linux ejecutando Samba, o incluso un script Python Impacket que "habla" el protocolo de red SMB. Esto significa que el servidor no tiene que respetar la semántica de compartición del sistema de archivos de Windows. Un atacante puede emplear un redirector de red para modificar archivos "inmutables" del lado del servidor, saltar las restricciones de compartición de datos. Esto significa que se asume erróneamente que estos archivos son inmutables. Esta es una clase de vulnerabilidad que llamamos Inmutabilidad de Archivos Falsos (FFI).

Archivos en la nube

Imagina salir de casa para empezar el día y que tienes un paquete en la escalera. Es ese Surface Book tan chulo que pediste la semana pasada. Emocionado pero con poco tiempo, lo metes en la mochila y te diriges al gimnasio. Luego de capacitar con ritmos brutales en tu Zune, te diriges a la cafetería local de Redmond para encontrarte con un colega que conociste en Xbox Live. Por desgracia, van con retraso, así que abres tu Surface Book nuevo y entras en Windows, deseando configurar Recall. A pesar del Wi-Fi mediocre de la cafetería, de alguna manera todo tu OneDrive de 1TB aparece inmediatamente ante ti. No hay forma de que descargaste 1TB tan rápido, así que debe de haber algo de brujería de por medio. Esa brujería es Cloud Files.

Introducido en Windows 10 versión 1709, Cloud Files permite que aplicaciones en modo usuario como OneDrive se registren como Proveedores de Sincronización en la Nube y creen archivos vacíos "marcadores" en el sistema. Inicialmente, estos marcadores están deshidratados (vacíos). Al acceder a ellos, la E/S es interceptada por el controlador del kernel de CloudFiles (cldflt.sys), que se conecta con el proceso del proveedor. El proveedor puede entonces recuperar el contenido del archivo desde la nube. No necesita descargar todo el archivo de una vez. Si solo necesitas 1MB, puede recuperar solo ese 1MB. A medida que aplicar más del archivo, puede seguir rehidratando (rellenando) el contenido según sea necesario.

Cuando el conductor necesita rehidratar un archivo, se activa una llamada de rehidratación en el proceso del proveedor (es decir, OneDrive.exe). Esa callback recupera el contenido del archivo (potencialmente de la nube) y llama a CfExecute para entregar ese contenido al controlador, que luego escribe en el archivo. CloudFiles solo aplicar la rehidratación de regiones de archivos que no estén hidratadas en ese momento, pero es posible deshidratar archivos para liberar espacio en el sistema actual.

Desarrollo de explotaciones

Por defecto, Windows permite compartir archivos y carpetas a través de la red empleando el protocolo Server Message Block (SMB). Si alguna vez te conectaste a una unidad de red compartida en una red corporativa, hay muchas probabilidades de que usó pymes. Windows incluye por defecto tanto un cliente SMB como un servidor. El componente cliente proporciona un redireccionador de red, como se describió anteriormente, permitiendo el acceso transparente a los archivos de la SMB a través de cualquier API que acepte rutas de archivos. Por ejemplo, ahora mismo puedes ejecutar Process Monitor por Internet ejecutando \\live.sysinternals.com\Procmon.exe.

Publicamos el exploit PPLFault en mayo de 2023 junto con nuestra charla sobre Black Hat Asia. PPLFault aprovecha un redirector de red para explotar FFI en DLLs cargadas en procesos Protected Process Light (PPL). El prototipo inicial requería una segunda máquina controlada por el atacante que ejecutara un servidor SMB malicioso. Al desactivar el servidor SMB integrado de Windows, pudimos mover el servidor SMB malicioso a la máquina local, eliminando la necesidad de una segunda máquina (prototipo).

Sin embargo, esto fue más complicado de lo que nos gustaría, porque en ese momento creíamos erróneamente que detener el servidor SMB integrado de Windows requería resetear. Por suerte, descubrimos la técnica de James Forshaw de combinar el proveedor CloudFiles con el adaptador SMB de loopback (localhost), lo que nos permitió crear el exploit final sin resetear. Además de ser más eficiente, el emparejamiento CloudFiles/SMB se diferencia de las dos versiones anteriores de exploits en que emplea el servidor SMB normal de Windows, que debería respetar el intercambio de archivos (es decir, FILE_SHARE_*) semántica. Por ejemplo, mientras un cliente SMB tiene un archivo abierto en un servidor sin FILE_SHARE_WRITE, el servidor no debería permitir que otro cliente abra ese archivo para acceso de escritura. De manera similar, el servidor no debería permitir el acceso de escritura a ningún ejecutable que se ejecute localmente en el servidor.

Parece que tenemos una contradicción. Si PPLFault tiene que cumplir con las restricciones de intercambio de archivos, ¿cómo está inyectando código en una DLL en ejecución? Veamos qué puede decirnos Process Monitor . Ejecutar PPLFault bajo Monitor de Procesos muestra las siguientes tres operaciones (filtradas para fines ilustrativos). Este análisis se realizó con la versión 10.0.22621.2861 de cldflt.sys en Windows 11 22631.2861.

En orden, las operaciones son:

  1. El proceso víctima, services.exe, carga una DLL como imagen ejecutable.
  2. Luego de cargar, PPLFault.exe ábrelo.
  3. Luego de abrirlo, PPLFault.exe le escribe.

Hay algunas observaciones clave que hacer aquí:

Violación de la inmutabilidad
Vemos una operación de escritura exitosa en un archivo mientras se carga como imagen ejecutable. En nuestra investigación previa sobre FFI, hablamos de la verificación MmFlushImageSection en el sistema de archivos, que está diseñado para proteger contra esta misma situación. ¿Cómo se saltó esta comprobación?

Violación del Modelo de Acceso a Archivos
Podemos ver que PPLFault sobreescribir con éxito el archivo. La documentación de Microsoft para WriteFile indica que el archivo debería haber abierto con acceso de escritura, es decir ,FILE_WRITE_DATA, pero la salida muestra que fue abierto para "Atributos de lectura, Escritura de atributos, sincronizar", que es FILE_READ_ATTRIBUTES, FILE_WRITE_ATTRIBUTESy SYNCHRONIZE. Sin FILE_WRITE_DATA, ¿cómo sobreescribir este archivo?

Intentemos responder a estas dos preguntas en la siguiente sección.

📘 Bono de friki -

Process Monitor instala un controlador minifiltro para el sistema de archivos para interceptar y registrar la actividad de E/S en el sistema. Windows encapsula acciones de E/S en estructuras llamadas Paquetes de Solicitud de E/S (IRPs). A cada minifiltro se le asigna una "altitud", que puedes imaginar como plantas de un edificio. La mayoría de los IRP empiezan en la planta superior y bajan por la pila. Si un minifiltro emite su propia E/S, ese IRP comienza en su altitud y desde ahí descende. En otras palabras, un minifiltro en la sexta planta nunca verá la entrada/salida desde la quinta planta. El controlador de minifiltro de Process Monitor funciona a altitud 385200. Normalmente, nunca verá la actividad de cldflt.sys, que se mueve a altitud 180451. Afortunadamente, podemos ajustar la altitud de Process Monitor con el interruptor /altitude, situándolo por debajo de CloudFiles a altitud 180450.

Reglas para ti, pero no para mí

Como se comentó, las aplicaciones están sujetas a restricciones de intercambio de archivos, pero el kernel en sí no siempre está restringido de la misma manera. Por ejemplo, los controladores del kernel pueden usar IoCreateFileEx para abrir o crear archivos.

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 Se parece mucho a la función orientada al usuario NtCreateFile, pero su documentación describe algunas capacidades adicionales importantes, incluyendo su parámetro Options , que soporta una bandera:

IO_IGNORE_SHARE_ACCESS_CHECK
El gestor de E/S no debe realizar comprobaciones de acceso compartido al objeto de archivo una vez creado. Sin embargo, el sistema de archivos puede seguir realizando estas comprobaciones.

¿Es tan sencillo? ¿Puede un controlador del kernel usar IoCreateFileEx(IO_IGNORE_SHARE_ACCESS_CHECK) para abrir una DLL en uso para acceso de escritura? Vamos a escribir un driver de kernel para probarlo. El código de este artículo está disponible como proyecto Visual Studio en GitHub aquí.

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

Cargarlo en una máquina virtual con la firma de pruebas habilitada produce la siguiente salida:

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

¿Acabamos de encontrar una explicación plausible de cómo PPLFault puede modificar archivos "inmutables"? No del todo. Este experimento fue una simplificación excesiva, pero muestra IO_IGNORE_SHARE_ACCESS_CHECK en acción, demostrando que las APIs del kernel pueden ofrecer más libertad que sus equivalentes en modo usuario.

En PPLFault, CloudFiles no es simplemente modificar un archivo con handles write-sharing-denied. Más bien, está modificando una DLL mientras está mapeada en memoria como imagen ejecutable. Probemos otro experimento que se acerque un poco más al escenario PPLFault. En el experimento dos, emularemosLoadLibrary abriendo una DLL, creando una sección SEC_IMAGE y luego mapeando una vista de esa sección en memoria. Una vez que la vista esté mapeada, cerraremos las manillas y comprobaremos si IoCreateFileEx(IO_IGNORE_SHARE_ACCESS_CHECK) puede obtener una asa grabable.

Empecemos con una función auxiliar que mapea un PE como una sección de imagen, similar a LoadLibrary. Haremos esto en el kernel para mantener el experimento en un solo driver, pero ten en cuenta que es funcionalmente equivalente a LoadLibrary para nuestros propósitos.

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

Ahora usemos ese asistente para mapear una DLL y luego veamos si podemos escribir en ella con 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));
    }
}

Ejecutar este experimento produce el siguiente resultado:

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

En este caso, IoCreateFileEx falló con 0xC0000043 (STATUS_SHARING_VIOLATION) porque los archivos mapeados como imágenes ejecutables tienen protecciones adicionales para garantizar que permanezcan inmutables, incluso sin manos abiertos. Puedes ver esta comprobación usando la API MmFlushImageSection en el código de ejemplo del controlador Microsoft FastFat, pero también existe en otros sistemas de archivos, incluido 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 );
    }
}

La bandera de IO_IGNORE_SHARE_ACCESS_CHECK evita las comprobaciones del gestor de E/S, pero no la comprobación de MmFlushImageSection en el sistema de archivos. Releyendo la descripción de IO_IGNORE_SHARE_ACCESS_CHECK, es obvio en retrospectiva:

IO_IGNORE_SHARE_ACCESS_CHECK
El gestor de E/S no debe realizar comprobaciones de acceso compartido al objeto de archivo una vez creado. Sin embargo, el sistema de archivos puede seguir realizando estas comprobaciones.

ExperimentTwo no es exactamente una representación justa de PPLFault, que carga el DLL desde una unidad de red. Cuando un cliente de red abre un archivo en un servidor, el controlador del cliente SMB asigna una estructura de Bloque de Control de Archivos (FCB) que representa ese archivo lógico. Correspondientemente, el servidor abre el archivo con los modos de compartir aplicar y asigna su propio FCB. Esto significa que hay dos FCB distintas en juego con semánticas diferentes. Cuando el cliente mapea una DLL en memoria como ejecutable, el mapeo resultante de SEC_IMAGE archivo (también llamado sección) se asocia con su FCB, por lo que obtiene la protección de MmFlushImageSection. El servidor no crea correspondientemente una sección de imagen, por lo que su FCB no recibe tal protección. PPLFault aprovecha esta diferencia realizando las escrituras en el FCB del servidor, saltar la comprobación de MmFlushImageSection .

Probemos esto en el Experimento Tres:

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

Experimento Tres genera la siguiente salida:

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

El experimento tres anterior muestra cómo los controladores del kernel pueden modificar las DLLs mapeadas por los clientes SMB usando la bandera IO_IGNORE_SHARE_ACCESS_CHECK en la versión del servidor de ese archivo.

Arremanga

Acabamos de mostrar lo que es posible, pero aún no sabemos qué está haciendo realmente Cloud Files. Profundicemos en la salida del Monitor de Procesos para responder a las preguntas planteadas anteriormente.

Antes, hicimos dos preguntas:

Violación de la inmutabilidad
Vemos una operación de escritura exitosa en un archivo mientras se carga como imagen ejecutable. En nuestra investigación previa sobre FFI, hablamos de la verificación MmFlushImageSection en el sistema de archivos, que está diseñado para proteger contra esta misma situación. ¿Cómo se saltó esta comprobación?

Violación del Modelo de Acceso a Archivos
Podemos ver que PPLFault sobreescribir con éxito el archivo. La documentación de Microsoft para WriteFile indica que el archivo debería haber abierto con acceso de escritura, es decir ,FILE_WRITE_DATA, pero la salida muestra que fue abierto para "Atributos de lectura, Escritura de atributos, sincronizar", que es FILE_READ_ATTRIBUTES, FILE_WRITE_ATTRIBUTESy SYNCHRONIZE. Sin FILE_WRITE_DATA, ¿cómo sobreescribir este archivo?

Podemos explicar fácilmente el MmFlushImageSection bypass. Esa comprobación busca FILE_WRITE_DATA, que no se usó aquí. El archivo solo se abría para "Leer atributos, Escribir atributos, Sincronizar." Sin embargo, no podemos explicar la violación del modelo de acceso a archivos. ¿Cómo sobreescribir un archivo no grabable? Vamos a ampliar la pila de llamadas para esa operación WriteFile intentar averiguarlo.

En la pila de llamadas, podemos ver la 176 de línea de PPLFault.cpp llamandocldapi.dll!CfExecute (filas 24-25) desde el modo usuario. Esto finalmente hace que cldflt.sys!HsmiRecallWriteFileNoLock llame aFltWriteFileEx. FltWriteFileEx de alguna manera puede escribir en un archivo que no está abierto para acceso de escritura. Anexemos un depurador de kernel y echemos un vistazo más de cerca.

Estableciendo un punto de interrupción en FltWriteFileEx y volviendo a ejecutar el exploit, podemos romper la llamada desde 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]

Veamos qué tipo de acceso se concedió al handle (~= FILE_OBJECT) que reside en el segundo parámetro de FltWriteFileEx. En x64, esto es 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.

Podemos ver que el archivo no se abrió para acceso por escritura, y "Ignorar comprobaciones de acceso compartido" suena mucho a IO_IGNORE_SHARE_ACCESS_CHECK. Vamos a comprobar la cordura de los parámetros ByteOffset y Length , que son el tercer y cuarto parámetro de FltWriteFileEx, almacenados en r8 y r9 respectivamente.

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

Una escritura de 90,112 bytes a 0 desplazado - que coincide con la salida de ProcMon. ¿Y Flags, el sexto parámetro?

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

0xA es 0x2 | 0x8, que es FLTFL_IO_OPERATION_PAGING | FLTFL_IO_OPERATION_SYNCHRONOUS_PAGING. Esto coincide con "Paginación de Entrada/Salida, E/S de paginación síncrona" que vimos en ProcMon.

Veamos si podemos reproducir esto en un driver. Vamos a abrir una DLL mapeada localmente como hicimos en ExperimentTwo, pero en lugar de pedir FILE_WRITE_DATA, vamos a mantener las mismas licencias que CloudFiles: SYNCHRONIZE | FILE_READ_ATTRIBUTES | FILE_WRITE_ATTRIBUTES. Esto no activará la comprobación de MmFlushImageSection que busca FILE_WRITE_DATA, pero incluiremos IO_IGNORE_SHARE_ACCESS_CHECK igualmente para replicar más fielmente el comportamiento de CloudFiles. A continuación, usaremos FltWriteFileEx para realizar una escritura de paginación síncrona en la FILE_OBJECT no grabable.

Estamos omitiendo algo de código de ayuda para abreviar. Todo el código de ejemplo de este artículo está disponible en nuestro 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);
}

Este experimento produce el siguiente resultado:

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

Esto demuestra que FltWriteFileEx puede usar para romper varias reglas. Hay una diferencia clave entre PPLFault y este experimento: el experimento tuvo éxito sin ningún redireccionador de red, demostrando que CloudFiles por sí solo puede modificar ejecutables en uso, independientemente de si se mapean localmente o mediante SMB. De forma más abstracta, demuestra que la explotación de FFI a través de CloudFiles puede ser posible sin redirecciones de red.

Un nuevo exploit

La mitigación PPLFault de Microsoft apunta específicamente a ejecutables cargados sobre redireccionadores de red. ¿Podemos aplicar lo que descubrimos aquí para lograr el mismo efecto sin redireccionar la red?

Cuando el CI aplicar la verificación de la firma del DLL, PPLFault emplea CfExecute para escribir (rehidratar) el marcador de posición desde su callback de datos de obtención. Una vez que el archivo original fue servido para la verificación de la firma, cambia a la carga útil, llamando a CfExecute una segunda vez durante la misma llamada para sobreescribir una parte del archivo con una carga útil. Ajustando PPLFault para que la víctima cargue el DLL localmente en lugar de por SMB de bucle, la segunda llamada a CfExecute falla con "La operación en la nube fue cancelada por el usuario." Necesitábamos otro enfoque.

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

Tras algo de ingeniería inversa, descubrimos que el fallo se debía a comprobaciones dentro de CloudFilter en sí, no a sus interacciones con el gestor de E/S o el sistema de archivos. Descubrimos que llamar aCfDehydratePlaceholder luego llamar aCfHydratePlaceholder desde otro hilo (fuera del callback de rehidratación) resetear el estado de nuestro archivo dentro del controlador CloudFilter, haciendo que volviera a invocar nuestra callback de rehidratación. Esto nos permitió sobreescribir la DLL en uso con nuestra carga útil y lograr una ejecución arbitraria de código como WinTcb-Light. Este pequeño cambio de código resucitó PPLFault, así que llamamos a la variante Redux.

De manera similar, resucitamos GodFault, aprovechando nuestro acceso PPL altamente privilegiado para comprometer la memoria del núcleo y eludir las protecciones de procesos de Windows Defender, terminando un proceso normalmente imposible de matar.

Puedes encontrar nuestras PoCs para Redux y GodFault-Redux en GitHub.

El video de abajo muestra lo siguiente en Windows Server 2022 totalmente actualizado (versión 20348.4773 del 2026 de febrero).

  1. PPLFault fallando en el dump lsass
  2. Redux con éxito lsass
  3. Un administrador que no termina MsMpEng.exe porque es PPL
  4. GodFault-Redux terminando con éxito MsMpEng.exe

Mitigación

En nuestro reporte al MSRC, proporcionamos un minifiltro de sistema de archivos que mitiga Redux bloqueando IRP_MJ_ACQUIRE_FOR_SECTION_SYNCHRONIZATION operaciones que cumplen todos los siguientes criterios:

  • El solicitante es un PPL.
  • La PreviousMode del solicitante es UserMode.
  • La protección de la página es ejecutable (por ejemplo, PAGE_EXECUTE_READ) o los atributos de asignación contienen SEC_IMAGE.
  • El archivo tiene una etiqueta de reparso Cloud Filter , como IO_REPARSE_TAG_CLOUD.

Una mitigación está integrada en Elastic Defend versiones 8.14 y posteriores. Si tu flota ejecuta algún sistema operativo afectado, puedes configurar lo siguiente en Defender la Política Avanzada para habilitarlo.

windows.advanced.flags: e931849d52535955fcaa3847dd17947b

Con esta mitigación implementada, el exploit queda bloqueado:

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

Simultáneamente, Windows muestra una ventana emergente con el código de estado STATUS_ACCESS_DENIED (0xC0000022) .

Puedes encontrar nuestra opción de concepto para la mitigación en GitHub.

Divulgación y remediación

El plazo de divulgación es el siguiente:

  • 2024-02-14 Reportamos Redux a MSRC.
  • 2024-02-29 El equipo de Windows Defender se puso en contacto para coordinar la divulgación.
  • 2024-10-01 Windows 11 24H2 alcanzó GA con la mitigación.

Cuando comunicamos Redux a MSRC, funcionaba contra versiones totalmente parcheadas de Windows 11, pero no contra la versión experimental de Insider Canary 25936. Mientras discutíamos el problema con el equipo de Windows Defender, supimos que Philip Tsukerman , ahora exinvestigador senior de seguridad de Microsoft, lo descubrió mientras buscaba variantes de PPLFault, y la solución aún estaba en fase de pruebas previas.

La siguiente tabla muestra las versiones afectadas y corregidas de Windows a fecha de publicación.

Sistema operativoCiclo de vidaEstado de corrección
Windows 11 24H2Apoyo general✔ Fijado
Windows 10 Enterprise LTSC 2021Apoyo general❌ Aún funcionando a fecha de 2026 de febrero (19044.6937)
Windows Server 2025Apoyo general✔ Fijado
Windows Server 2022Apoyo general❌ Sigue funcionando a fecha de 2026 de febrero (20348.4773)
Windows Server 2019Soporte extendido❌ Aún funcional a fecha de febrero de 2026 (17763.8389)

Conclusión

En 2024, revelamos una nueva clase de vulnerabilidad de Windows, la Inmutabilidad de Archivos Falsos (FFI), demostrándola con la publicación de dos exploits de kernel distintos: PPLFault e ItsNotASecurityBoundary. Ambos exploits aprovechan los redireccionadores de red para explotar fallos de diseño en la integridad del código de Windows. En esta investigación, mostramos y publicamos otro exploit que demuestra cómo explotar FFI sin redirecciones de red. Creemos que este fue el tercer exploit FFI cuando se informó en febrero de 2024; Desde entonces hubo al menos dos más.

Redux no es el fin de FFI; hay más vulnerabilidades explotables en FFI.