Em 2024, divulgamos uma nova classe de vulnerabilidade do Windows, a False File Immutability (FFI), que demonstrou anteriormente como os redirecionadores de rede poderiam ser explorados para violar suposições incorretas no projeto de Integridade de Código do Windows, resultando em dois exploits de kernel. Essas vulnerabilidades exploravam unidades de rede do Windows, adicionando complexidade e criando um ponto de estrangulamento na cadeia de ataque, o que facilitava a detecção e a mitigação.
Esta pesquisa apresenta um avanço ao introduzir um método de exploração mais simplificado e autossuficiente. A nova abordagem aproveita um recurso integrado do Windows para conseguir contornar a modificação de arquivos, sem as complexidades das configurações SMB. Ao analisar como o driver do kernel para essa funcionalidade processa os dados dos arquivos, descobrimos uma falha de segurança que permite a um invasor modificar arquivos que o Windows assume erroneamente como imutáveis, resultando em uma prova de conceito de exploração de vulnerabilidade no kernel.
Principais conclusões:
- Sem necessidade de redirecionador de rede: Ao contrário de exploits anteriores, o novo método de exploração explora a imutabilidade falsa de arquivos sem exigir o uso do compartilhamento de arquivos do Windows.
- Funcionalidade integrada explorada: A exploração aproveita uma falha de segurança em uma funcionalidade integrada do Windows que lida com a sincronização de arquivos na nuvem.
- Violação de imutabilidade: Permite a modificação de arquivos que o kernel do Windows e o gerenciador de memória assumem erroneamente como imutáveis, levando a uma exploração de vulnerabilidade no kernel.
- Mitigação contornada: Permite que os atacantes contornem uma mitigação criada pela Microsoft especificamente para uma vulnerabilidade FFI anterior.
- Forever-Day: A Microsoft optou por corrigir essa vulnerabilidade apenas em algumas versões do Windows, portanto, ela permanece funcional em diversas versões totalmente atualizadas do Windows com suporte principal até fevereiro de 2026.
Imutabilidade de arquivo falsa
Você pode se lembrar da Falsa Imutabilidade de Arquivos do nosso artigo recente e da palestra BlueHat IL 2024 , mas se não, esta seção deve ajudar a refrescar sua memória. Se você já está familiarizado com o assunto, fique à vontade para pular para a próxima seção.
Quando um aplicativo abre um arquivo no Windows, ele normalmente usa alguma forma da API CreateFile do 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
);
Os chamadores de CreateFile especificam o acesso que desejam em dwDesiredAccess. Por exemplo, um chamador passaria FILE_READ_DATA para poder ler dados ou FILE_WRITE_DATA para poder escrever dados. O conjunto completo de direitos de acesso está documentado no site Microsoft Learn.
Além de passar dwDesiredAccess, os chamadores devem passar um “modo de compartilhamento” em dwShareMode, que consiste em zero ou mais de FILE_SHARE_READ, FILE_SHARE_WRITE e FILE_SHARE_DELETE. Você pode pensar em um modo de compartilhamento como o usuário declarando "Não me importo que outros façam X com este arquivo enquanto eu o estiver usando", onde X pode ser ler, escrever ou renomear. Por exemplo, um chamador que passa FILE_SHARE_WRITE permite que outros escrevam no arquivo enquanto estão trabalhando nele.
Ao abrir um arquivo, o dwDesiredAccess do chamador é comparado com o dwShareMode de todos os identificadores de arquivo existentes. Simultaneamente, o dwShareMode do chamador é testado em relação ao dwDesiredAccess previamente concedido de todos os identificadores existentes para esse arquivo. Se algum desses testes falhar, a função CreateFile falhará com uma violação de compartilhamento.
Compartilhar não é obrigatório. Quem liga pode passar um código de compartilhamento igual a zero para obter acesso exclusivo. Conforme a documentação da Microsoft:
Um arquivo aberto que não é compartilhado (
dwShareModedefinido como zero) não pode ser aberto novamente, nem pelo aplicativo que o abriu nem por outro aplicativo, até que seu identificador tenha sido fechado. Isso também é conhecido como acesso exclusivo.
O compartilhamento é imposto pelo sistema de arquivos, normalmente NTFS, mas o Windows suporta outros sistemas de arquivos, como o FAT32. O próprio Windows omite FILE_SHARE_WRITE ao abrir certos tipos de arquivos, impedindo a modificação enquanto eles estão em uso. Esses arquivos que não podem ser modificados podem ser considerados imutáveis.
Em algumas situações, o gerenciador de memória depende dessa imutabilidade. Se ocorrer uma falha de página em um arquivo mapeado em memória imutável, e essa página não tiver sido modificada, o gerenciador de memória poderá ler o conteúdo dessa página diretamente do arquivo de suporte original. Não é necessário salvar uma segunda cópia do conteúdo do arquivo no arquivo de paginação , pois a imutabilidade garante que o arquivo no disco não possa ser alterado. Os executáveis que rodam na memória, como EXEs e DLLs, são imutáveis, portanto o gerenciador de memória pode aplicar essa otimização a eles.
Os redirecionadores de rede permitem o uso de caminhos de rede com qualquer API que aceite caminhos de arquivos. Isso é muito conveniente, permitindo que usuários e aplicativos trabalhem facilmente com arquivos e executem programas a partir de unidades de rede. O kernel redireciona de forma transparente qualquer operação de E/S para a máquina remota. Se um programa for executado a partir de uma unidade de rede, todos os arquivos EXE e suas DLLs serão obtidos da rede de forma transparente, conforme necessário.
Quando um redirecionador de rede está em uso, o servidor na outra extremidade da conexão não precisa ser uma máquina Windows. Pode ser uma máquina Linux executando o Samba, ou até mesmo um script Python Impacket que "fala" o protocolo de rede SMB. Isso significa que o servidor não precisa respeitar a semântica de compartilhamento do sistema de arquivos do Windows. Um atacante pode usar um redirecionador de rede para modificar arquivos "imutáveis" no servidor, burlando as restrições de compartilhamento. Isso significa que esses arquivos são erroneamente considerados imutáveis. Esta é uma classe de vulnerabilidade que denominamos Falsa Imutabilidade de Arquivos (FFI, na sigla em inglês).
Arquivos na nuvem
Imagine sair de casa para começar o seu dia e encontrar um pacote na sua porta. É aquele Surface Book incrível que você encomendou semana passada. Animado(a), mas com pouco tempo, você joga tudo na mochila e vai para a academia. Depois de malhar ao som de músicas incríveis no seu Zune, você vai até a cafeteria local em Redmond para encontrar um amigo que conheceu na Xbox Live. Infelizmente, eles estão atrasados, então você abre seu Surface Book novinho em folha e entra no Windows, ansioso para configurar o Recall. Apesar da conexão Wi-Fi mediana da cafeteria, de alguma forma todo o seu OneDrive de 1 TB aparece imediatamente diante de você. Não tem como você ter baixado 1 TB tão rápido, então deve haver alguma bruxaria envolvida. Essa bruxaria é o Cloud Files.
Introduzido no Windows 10 versão 1709, o Cloud Files permite que aplicativos em modo de usuário, como o OneDrive, se registrem como provedores de sincronização na nuvem e criem arquivos “espaços reservados” vazios no sistema. Inicialmente, esses espaços reservados estão desidratados (vazios). Ao acessá-los, a E/S é interceptada pelo driver de kernel CloudFiles (cldflt.sys), que chama o processo do provedor. O provedor pode então recuperar o conteúdo do arquivo da nuvem. Não é necessário baixar o arquivo inteiro de uma só vez. Se você precisar apenas de 1 MB, ele poderá recuperar somente esse 1 MB. À medida que você solicita mais conteúdo do arquivo, ele pode continuar a reidratá-lo (preenchê-lo) conforme necessário.
Quando o driver precisa reidratar um arquivo, ele invoca um retorno de chamada de reidratação no processo do provedor (ou seja, OneDrive.exe). Essa função de retorno recupera o conteúdo do arquivo (potencialmente da nuvem) e chama CfExecute para fornecer esse conteúdo ao driver, que então o grava no arquivo. O CloudFiles só solicitará a reidratação de regiões de arquivos que não estejam hidratadas no momento, mas é possível desidratar arquivos para liberar espaço no sistema atual.
Desenvolvimento de exploração
Por padrão, o Windows permite o compartilhamento de arquivos e pastas em rede usando o protocolo Server Message Block (SMB). Se você já se conectou a uma unidade de rede compartilhada em uma rede corporativa, há uma boa chance de que ela tenha usado o protocolo SMB. O Windows inclui, por padrão, um cliente e um servidor SMB. O componente cliente fornece um redirecionador de rede, conforme descrito acima, permitindo o acesso SMB transparente a arquivos por meio de qualquer API que aceite caminhos de arquivo. Por exemplo, você pode executar o Process Monitor pela internet agora mesmo executando \\live.sysinternals.com\Procmon.exe.
Lançamos o exploit PPLFault em maio 2023 juntamente com nossa palestra na Black Hat Asia. O PPLFault utiliza um redirecionador de rede para explorar a FFI em DLLs carregadas em processos Protected Process Light (PPL). O protótipo inicial exigia uma segunda máquina controlada pelo atacante, executando um servidor SMB malicioso. Ao desativar o servidor SMB integrado do Windows, conseguimos mover o servidor SMB malicioso para a máquina local, eliminando a necessidade de uma segunda máquina (protótipo).
No entanto, isso ainda era mais complicado do que gostaríamos, porque na época acreditávamos erroneamente que interromper o servidor SMB integrado do Windows exigia uma reinicialização. Felizmente, descobrimos a técnica de James Forshaw de combinar o provedor CloudFiles com o adaptador SMB de loopback (localhost), o que nos permitiu criar o exploit final sem necessidade de reinicialização. Além de ser simplificado, o par CloudFiles/SMB se distingue das duas versões anteriores do exploit por usar o servidor SMB padrão do Windows, que deve respeitar o compartilhamento de arquivos (ou seja, FILE_SHARE_*) semântica. Por exemplo, enquanto um cliente SMB tiver um arquivo aberto em um servidor sem FILE_SHARE_WRITE, o servidor não deve permitir que outro cliente abra esse arquivo para acesso de gravação. Da mesma forma, o servidor não deve permitir acesso de escrita a quaisquer executáveis em execução localmente no servidor.
Parece haver uma contradição. Se o PPLFault precisa obedecer às restrições de compartilhamento de arquivos, como ele está injetando código em uma DLL em execução? Vamos ver o que o Process Monitor pode nos dizer. Executar o PPLFault no Process Monitor mostra as três operações a seguir (filtradas para fins ilustrativos). Esta análise foi feita com a versão 10.0.22621.2861 de cldflt.sys no Windows 11 22631.2861.
Em ordem, as operações são:
- O processo vítima,
services.exe, carrega uma DLL como uma imagem executável. - Depois de carregado,
PPLFault.exe-o. - Depois de aberto,
PPLFault.exeescreve nele.
Há algumas observações importantes a serem feitas aqui:
Violação da Imutabilidade
Observamos uma operação de escrita bem-sucedida em um arquivo enquanto ele está carregado como uma imagem executável. Em nossa pesquisa anterior sobre FFI, discutimos a verificação MmFlushImageSection no sistema de arquivos, que foi projetada para proteger contra essa situação específica. Como conseguiu burlar essa verificação?
Violação do Modelo de Acesso a Arquivos
Podemos ver que o PPLFault sobrescreveu o arquivo com sucesso. A documentação da Microsoft para WriteFile afirma que o arquivo deveria ter sido aberto com acesso de gravação, ou seja, FILE_WRITE_DATA, mas a saída mostra que ele foi aberto para “Ler atributos, Gravar atributos, Sincronizar”, que é FILE_READ_ATTRIBUTES, FILE_WRITE_ATTRIBUTES e SYNCHRONIZE. Sem FILE_WRITE_DATA, como foi possível sobrescrever este arquivo?
Vamos tentar responder a essas duas perguntas na próxima seção.
📘 Bônus Nerd -
O Process Monitor instala um driver de minifiltro do sistema de arquivos para interceptar e registrar a atividade de E/S no sistema. O Windows encapsula ações de E/S em estruturas chamadas Pacotes de Solicitação de E/S (IRPs). A cada minifiltro é atribuída uma "altitude", que você pode imaginar como os andares de um prédio. A maioria dos IRPs começa no último andar e percorre a pilha de andares. Se um minifiltro emite sua própria E/S, esse IRP começa em sua altitude e se propaga para baixo a partir dali. Em outras palavras, um minifiltro no sexto andar nunca verá entradas e saídas do quinto andar. O driver minifilter do Process Monitor é executado na altitude
385200. Normalmente, nunca verá a atividade decldflt.sys, que ocorre na altitude180451. Felizmente, podemos ajustar a altitude do Process Monitor com a opção /altitude, colocando-o abaixo do CloudFiles na altitude180450.
Regras para ti, mas não para mim.
Conforme discutido, os aplicativos estão sujeitos a restrições de compartilhamento de arquivos, mas o próprio kernel nem sempre é restringido da mesma forma. Por exemplo, os drivers do kernel podem usar IoCreateFileEx para abrir ou criar arquivos.
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 Parece muito semelhante à função voltada para o usuário NtCreateFile, mas sua documentação descreve algumas funcionalidades adicionais importantes, incluindo seu parâmetro Options , que suporta um sinalizador:
IO_IGNORE_SHARE_ACCESS_CHECK
O gerenciador de E/S não deve realizar verificações de acesso compartilhado no objeto de arquivo após sua criação. No entanto, o sistema de arquivos ainda pode realizar essas verificações.
É tão simples assim? Um driver de kernel pode usar IoCreateFileEx(IO_IGNORE_SHARE_ACCESS_CHECK) para abrir uma DLL em uso para acesso de escrita? Vamos escrever um driver de kernel para testar. O código deste artigo está disponível como um projeto do Visual Studio no GitHub aqui.
/*
* 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);
}
Carregar o programa em uma máquina virtual com a assinatura de teste ativada produz a seguinte saída:
ExperimentOne complete. IoCreateFileEx(IO_IGNORE_SHARE_ACCESS_CHECK) CAN open a write-sharing-denied file for FILE_WRITE_DATA. Status: 0x00000000
Será que acabamos de encontrar uma explicação plausível para como o PPLFault consegue modificar arquivos "imutáveis"? Não exatamente. Este experimento foi um pouco simplificado demais, mas mostra IO_IGNORE_SHARE_ACCESS_CHECK em ação, provando que as APIs do kernel podem fornecer mais liberdade do que suas contrapartes do modo de usuário.
No PPLFault, o CloudFiles não está apenas modificando um arquivo com identificadores de compartilhamento de gravação negados. Na verdade, está modificando uma DLL enquanto ela está mapeada na memória como uma imagem executável. Vamos tentar outro experimento que seja um pouco mais próximo do cenário PPLFault. No experimento dois, emularemos LoadLibrary abrindo uma DLL, criando uma seção SEC_IMAGE e, em seguida, mapeando uma visualização dessa seção na memória. Assim que a visualização for mapeada, fecharemos os identificadores e testaremos se IoCreateFileEx(IO_IGNORE_SHARE_ACCESS_CHECK) pode obter um identificador gravável.
Vamos começar com uma função auxiliar que mapeia um PE como uma seção de imagem, semelhante a LoadLibrary. Faremos isso no kernel para manter o experimento em um único driver, mas observe que é funcionalmente equivalente a LoadLibrary para nossos 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;
}
Agora vamos usar esse auxiliar para mapear uma DLL e ver se conseguimos escrever nela com 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));
}
}
A execução deste experimento produz a seguinte saída:
ExperimentTwo complete. IoCreateFileEx(IO_IGNORE_SHARE_ACCESS_CHECK) CANNOT open a file backing a local SEC_IMAGE section for FILE_WRITE_DATA. Status: 0xc0000043
Neste caso, IoCreateFileEx falhou com 0xC0000043 (STATUS_SHARING_VIOLATION) porque os arquivos mapeados como imagens executáveis têm proteções adicionais para garantir que permaneçam imutáveis, mesmo sem nenhum identificador aberto. Você pode ver essa verificação usando a API MmFlushImageSection no código de exemplo do driver FastFat da Microsoft, mas ela também existe em outros sistemas de arquivos, incluindo o 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 );
}
}
O sinalizador IO_IGNORE_SHARE_ACCESS_CHECK ignora as verificações do gerenciador de E/S, mas não a verificação MmFlushImageSection no sistema de arquivos. Relendo a descrição de IO_IGNORE_SHARE_ACCESS_CHECK, fica óbvio em retrospectiva:
IO_IGNORE_SHARE_ACCESS_CHECK
O gerenciador de E/S não deve realizar verificações de acesso compartilhado no objeto de arquivo após sua criação. No entanto, o sistema de arquivos ainda pode realizar essas verificações.
O ExperimentTwo não representa exatamente de forma justa o PPLFault, que carrega a DLL de uma unidade de rede. Quando um cliente de rede abre um arquivo em um servidor, o driver do cliente SMB aloca uma estrutura de Bloco de Controle de Arquivo (FCB) que representa esse arquivo lógico. Consequentemente, o servidor abre o arquivo com os modos de compartilhamento solicitados e aloca seu próprio FCB. Isso significa que existem dois FCBs distintos em jogo, com semânticas diferentes. Quando o cliente mapeia uma DLL na memória como um executável, o mapeamento de arquivo SEC_IMAGE resultante (também conhecido como seção) é associado ao seu FCB, então ele ganha a proteção de MmFlushImageSection. O servidor não cria uma seção de imagem correspondente, portanto seu FCB não obtém essa proteção. O PPLFault explora essa diferença realizando as gravações no FCB do servidor, ignorando a verificação MmFlushImageSection .
Vamos testar isso no Experimento Três:
/*
* 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));
}
}
O ExperimentThree gera a seguinte saída:
ExperimentThree complete. IoCreateFileEx(IO_IGNORE_SHARE_ACCESS_CHECK) CAN open a file backing a remote SEC_IMAGE view for FILE_WRITE_DATA. Status: 0x00000000
O Experimento Três acima mostra como os drivers do kernel conseguem modificar DLLs mapeadas por clientes SMB usando o sinalizador IO_IGNORE_SHARE_ACCESS_CHECK na versão do servidor desse arquivo.
Arregace as mangas
Acabamos de mostrar o que é possível, mas ainda não sabemos o que o Cloud Files realmente faz. Vamos analisar mais detalhadamente a saída do Monitor de Processos para responder às perguntas levantadas anteriormente.
Anteriormente, fizemos duas perguntas:
Violação da Imutabilidade
Observamos uma operação de escrita bem-sucedida em um arquivo enquanto ele está carregado como uma imagem executável. Em nossa pesquisa anterior sobre FFI, discutimos a verificaçãoMmFlushImageSectionno sistema de arquivos, que foi projetada para proteger contra essa situação específica. Como conseguiu burlar essa verificação?Violação do Modelo de Acesso a Arquivos
Podemos ver que o PPLFault sobrescreveu o arquivo com sucesso. A documentação da Microsoft para WriteFile afirma que o arquivo deveria ter sido aberto com acesso de gravação, ou seja,FILE_WRITE_DATA, mas a saída mostra que ele foi aberto para “Ler atributos, Gravar atributos, Sincronizar”, que éFILE_READ_ATTRIBUTES,FILE_WRITE_ATTRIBUTESeSYNCHRONIZE. SemFILE_WRITE_DATA, como foi possível sobrescrever este arquivo?
Podemos explicar facilmente o bypass MmFlushImageSection . Essa verificação procura por FILE_WRITE_DATA, que não foi usado aqui. O arquivo foi aberto apenas para as operações “Ler atributos, Escrever atributos, Sincronizar”. Contudo, não conseguimos explicar a violação do modelo de acesso a arquivos. Como foi possível sobrescrever um arquivo que não podia ser gravado? Vamos ampliar a pilha de chamadas para essa operação WriteFile para tentar descobrir.
Na pilha de chamadas, podemos ver a linha 176 de PPLFault.cpp chamando cldapi.dll!CfExecute (linhas 24-25) do modo de usuário. Isso eventualmente resulta em cldflt.sys!HsmiRecallWriteFileNoLock chamando FltWriteFileEx. FltWriteFileEx consegue, de alguma forma, escrever em um arquivo que não está aberto para acesso de escrita. Vamos conectar um depurador de kernel e dar uma olhada mais de perto.
Ao definir um ponto de interrupção em FltWriteFileEx e executar o exploit novamente, podemos interromper a execução na chamada 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]
Vamos ver que tipo de acesso foi concedido ao identificador (~= FILE_OBJECT) que reside no segundo parâmetro de FltWriteFileEx. Em x64, isto é 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 o arquivo não foi aberto para acesso de escrita e “Ignorar verificações de acesso de compartilhamento” soa muito parecido com IO_IGNORE_SHARE_ACCESS_CHECK. Vamos verificar a sanidade dos parâmetros ByteOffset e Length , que são o terceiro e o quarto parâmetros de FltWriteFileEx, armazenados em r8 e 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]
Uma escrita de 90,112 bytes no deslocamento 0 - que se alinha com a saída do ProcMon. E quanto a Flags, o 6º parâmetro?
0: kd> dx *(PULONG)(@rsp+(8*6))
*(PULONG)(@rsp+(8*6)) : 0xa [Type: unsigned long]
0xA é 0x2 | 0x8, que é FLTFL_IO_OPERATION_PAGING | FLTFL_IO_OPERATION_SYNCHRONOUS_PAGING. Isso está de acordo com "E/S de paginação, E/S de paginação síncrona" que vimos no ProcMon.
Vamos ver se conseguimos reproduzir isso em um driver. Vamos abrir uma DLL mapeada localmente como fizemos no Experimento Dois, mas em vez de pedir FILE_WRITE_DATA, vamos manter as mesmas permissões do CloudFiles: SYNCHRONIZE | FILE_READ_ATTRIBUTES | FILE_WRITE_ATTRIBUTES. Isso não acionará a verificação MmFlushImageSection que procura por FILE_WRITE_DATA, mas vamos adicionar IO_IGNORE_SHARE_ACCESS_CHECK de qualquer maneira para replicar mais de perto o comportamento do CloudFiles. Em seguida, usaremos FltWriteFileEx para realizar uma gravação de paginação síncrona no FILE_OBJECT não gravável.
Para maior brevidade, estamos omitindo parte do código auxiliar. Todo o código de exemplo deste artigo está disponível em nosso 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 produz o seguinte resultado:
ExperimentFour complete. FltWriteFileEx CAN be used to write to a non-writable FILE_OBJECT Status: 0x00000000
Isso prova que FltWriteFileEx pode ser usado para quebrar várias regras. Existe uma diferença fundamental entre o PPLFault e este experimento: o experimento foi bem-sucedido sem nenhum redirecionador de rede, provando que o CloudFiles sozinho pode modificar executáveis em uso, independentemente de estarem mapeados localmente ou via SMB. De forma mais abstrata, isso prova que a exploração de FFI via CloudFiles pode ser possível sem redirecionadores de rede.
Uma nova vulnerabilidade
A mitigação de falhas PPLFault da Microsoft visa especificamente executáveis carregados por meio de redirecionadores de rede. Podemos aplicar o que descobrimos aqui para obter o mesmo efeito sem um redirecionador de rede?
Quando o CI solicita a DLL para verificação de assinatura, o PPLFault usa CfExecute para escrever (reidratar) o espaço reservado a partir de seu retorno de chamada de busca de dados. Após o arquivo original ser disponibilizado para verificação de assinatura, o processo passa a utilizar o conteúdo (payload), chamando CfExecute uma segunda vez durante a mesma função de retorno de chamada para sobrescrever uma parte do arquivo com o conteúdo. Ajustando o PPLFault para que a vítima carregue a DLL localmente em vez de usar o loopback SMB, a segunda chamada para CfExecute falha com “A operação na nuvem foi cancelada pelo usuário”. Precisávamos de uma abordagem diferente.
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
Após alguma engenharia reversa, descobrimos que a falha se devia a verificações internas do CloudFilter, e não às suas interações com o gerenciador de E/S ou o sistema de arquivos. Descobrimos que chamar CfDehydratePlaceholder e depois chamar CfHydratePlaceholder de uma thread diferente (fora do callback de reidratação) redefiniria o estado do nosso arquivo dentro do driver CloudFilter, fazendo com que ele invocasse novamente nosso callback de reidratação. Isso nos permitiu sobrescrever a DLL em uso com nosso payload e obter execução de código arbitrário como WinTcb-Light. Essa pequena alteração no código ressuscitou o PPLFault, então demos à variante o nome de Redux.
De forma semelhante, ressuscitamos o GodFault, aproveitando nosso acesso PPL altamente privilegiado para comprometer a memória do kernel e contornar as proteções de processo do Windows Defender, encerrando um processo normalmente indestrutível.
Você pode encontrar nossas provas de conceito (PoCs) para Redux e GodFault-Redux no GitHub.
O vídeo abaixo mostra o seguinte no Windows Server 2022 totalmente atualizado (versão de fevereiro 2026 20348.4773).
- Falha ao despejar o PPLFault
lsass - Redux executando o despejo com sucesso.
lsass - Um administrador não conseguiu encerrar
MsMpEng.exeporque é PPL - GodFault-Redux encerrado com sucesso
MsMpEng.exe
Mitigação
Em nosso relatório para o MSRC, fornecemos um minifiltro de sistema de arquivos que mitiga o Redux bloqueando operações IRP_MJ_ACQUIRE_FOR_SECTION_SYNCHRONIZATION que atendam a todos os seguintes critérios:
- O solicitante é um PPL (Licença de Piloto Privado).
- O
PreviousModedo solicitante éUserMode. - A proteção da página é executável (por exemplo,
PAGE_EXECUTE_READ) ou os atributos de alocação contêmSEC_IMAGE. - O arquivo possui uma tag de reparse do Cloud Filter, como
IO_REPARSE_TAG_CLOUD.
Uma medida de mitigação está integrada nas versões 8.14 e posteriores do Elastic Defend. Se a sua frota utiliza algum sistema operacional afetado, você pode configurar o seguinte na Política Avançada de Defesa para habilitá-lo.
windows.advanced.flags: e931849d52535955fcaa3847dd17947b
Com essa mitigação implementada, a exploração é bloqueada:
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
Simultaneamente, o Windows exibe uma janela pop-up com o código de status STATUS_ACCESS_DENIED (0xC0000022) .
Você pode encontrar nossa prova de conceito (PoC) para a mitigação no GitHub.
Divulgação e Remediação
O cronograma de divulgação é o seguinte:
- 14/02/2024 Reportamos o Redux ao MSRC.
- 29/02/2024 - A equipe do Windows Defender entrou em contato para coordenar a divulgação.
- 2024-10-01 Windows 11 24H2 alcançou GA com a mitigação.
Quando divulgamos o Redux para o MSRC, ele funcionava em versões totalmente atualizadas do Windows 11, mas não na versão experimental Insider Canary 25936. Ao discutirmos o problema com a equipe do Windows Defender, descobrimos que Philip Tsukerman, (agora ex-)Pesquisador Sênior de Segurança da Microsoft, havia descoberto a vulnerabilidade enquanto procurava por variantes do PPLFault, e que a correção ainda está em fase de testes pré-lançamento.
A tabela abaixo mostra as versões do Windows afetadas e corrigidas até a data de publicação.
| Sistema operacional | Vida útil | Status de correção |
|---|---|---|
| Windows 11 24H2 | Apoio convencional | ✔ Corrigido |
| Windows 10 Enterprise LTSC 2021 | Apoio convencional | ❌ Ainda funcional em fevereiro de 2026 (19044.6937) |
| Windows Server 2025 | Apoio convencional | ✔ Corrigido |
| Windows Server 2022 | Apoio convencional | ❌ Ainda funcional em fevereiro de 2026 (20348.4773) |
| Windows Server 2019 | Suporte Estendido | ❌ Ainda funcional em fevereiro de 2026 (17763.8389) |
Conclusão
Em 2024, divulgamos uma nova classe de vulnerabilidade do Windows, a False File Immutability (FFI), demonstrando-a com o lançamento de duas explorações distintas do kernel: PPLFault e ItsNotASecurityBoundary. Ambas as explorações utilizam redirecionadores de rede para explorar falhas de projeto na Integridade de Código do Windows. Nesta pesquisa, apresentamos e divulgamos mais um exploit que demonstra como explorar a FFI sem redirecionadores de rede. Acreditamos que este foi o terceiro ataque FFI quando foi relatado em fevereiro de 2024; desde então, houve pelo menos mais dois .
Redux não é o fim do FFI; existem mais vulnerabilidades exploráveis no FFI.
