Partilhar via


Acessores de modo utilizador

Os acessadores de modo de usuário (UMA) são um conjunto de DDIs projetados para acessar e manipular com segurança a memória de modo de usuário a partir do código de modo kernel. Essas DDIs abordam vulnerabilidades de segurança comuns e erros de programação que podem ocorrer quando drivers de modo kernel acessam a memória do modo de usuário.

O código de modo kernel que acessa/manipula a memória de modo de usuário em breve será necessário para usar o UMA.

Possíveis problemas ao acessar a memória do modo de usuário a partir do modo kernel

Quando o código de modo kernel precisa acessar a memória de modo de usuário, vários desafios surgem:

  • Aplicativos de modo de usuário podem passar ponteiros maliciosos ou inválidos para o código de modo kernel. A falta de validação adequada pode levar a corrupção de memória, falhas ou vulnerabilidades de segurança.

  • O código de modo de utilizador é multiencadeado. Como resultado, diferentes threads podem modificar a mesma memória do modo de utilizador entre acessos separados do modo kernel a ela, possivelmente levando a uma corrupção da memória do kernel.

  • Os desenvolvedores de modo kernel muitas vezes esquecem de sondar a memória do modo de usuário antes de acessá-la, o que é um problema de segurança.

  • Os compiladores assumem a execução em um único thread e podem otimizar o que parecem ser acessos redundantes à memória. Programadores que desconhecem tais otimizações podem escrever código inseguro.

Os trechos de código a seguir ilustram esses problemas.

Exemplo 1: Possível corrupção de memória devido a multithreading no modo de usuário

O código de modo kernel que precisa acessar a memória de modo de usuário deve fazê-lo dentro de um __try/__except bloco para garantir que a memória seja válida. O trecho de código a seguir mostra um padrão típico para acessar a memória do modo de usuário:

// User-mode structure definition
typedef struct _StructWithData {
    ULONG Size;
    CHAR* Data[1];
} StructWithData;

// Kernel-mode call that accesses user-mode memory
void MySysCall(StructWithData* Ptr) {
    __try {
        // Probe user-mode memory to ensure it's valid
        ProbeForRead(Ptr, sizeof(StructWithData), 1);

        // Allocate memory in the kernel
        PVOID LocalData = ExAllocatePool2(POOL_FLAG_PAGED, Ptr->Size);
        
        // Copy user-mode data into the heap allocation
        RtlCopyMemory(LocalData, Ptr->Data, Ptr->Size);
    } __except (…) {
        // Handle exceptions
    }
}

Este trecho sonda a memória primeiro, o que é um primeiro passo importante, mas frequentemente negligenciado.

No entanto, um problema que pode ocorrer neste código é devido ao multithreading no modo de usuário. Especificamente, Ptr->Size pode mudar após a chamada para ExAllocatePool2 , mas antes da chamada para RtlCopyMemory, potencialmente levando a corrupção de memória no kernel.

Exemplo 2: Possíveis problemas devido a otimizações do compilador

Uma tentativa de resolver o problema de multithreading no Exemplo 1 pode ser copiar Ptr->Size para uma variável local antes da alocação e copiar:

void MySysCall(StructWithData* Ptr) {
    __try {
        // Probe user-mode memory to ensure it's valid
        ProbeForRead(Ptr, sizeof(StructWithData), 1);
        
        // Read Ptr->Size once to avoid possible memory change in user mode
        ULONG LocalSize = Ptr->Size;
        
        // Allocate memory in the kernel
        PVOID LocalData = ExAllocatePool2(POOL_FLAG_PAGED, LocalSize);
        
        //Copy user-mode data into the heap allocation
        RtlCopyMemory(LocalData, Ptr, LocalSize);
    } __except (…) {}
}

Embora essa abordagem atenue o problema causado pelo multithreading, ela ainda não é segura porque o compilador não está ciente de vários threads e, portanto, assume um único thread de execução. Como uma otimização, o compilador pode ver que ele já tem uma cópia do valor que Ptr->Size aponta para em sua pilha e, portanto, não fazer a cópia para LocalSize.

Solução de acessores em modo de usuário

A interface UMA resolve os problemas encontrados ao acessar a memória do modo de usuário a partir do modo kernel. A UMA fornece:

  • Sondagem automática: A sondagem explícita (ProbeForRead/ProbeForWrite) não é mais necessária, pois todas as funções da UMA garantem a segurança do endereço.

  • Acesso volátil: Todos os DDIs da UMA usam semântica volátil para evitar otimizações do compilador.

  • Facilidade de portabilidade: O conjunto abrangente de DDIs de UMA torna mais fácil para os clientes portarem seu código existente para usar DDIs de UMA, garantindo que a memória de modo de usuário seja acessada de forma segura e correta.

Exemplo usando UMA DDI

Usando a estrutura de modo de usuário definida anteriormente, o trecho de código a seguir demonstra como usar o UMA para acessar com segurança a memória do modo de usuário.

void MySysCall(StructWithData* Ptr) {
    __try {

        // This UMA call probes the passed user-mode memory and does a
        // volatile read of Ptr->Size to ensure it isn't optimized away by the compiler.
        ULONG LocalSize = ReadULongFromUser(&Ptr->Size);
        
        // Allocate memory in the kernel.
        PVOID LocalData = ExAllocatePool2(POOL_FLAG_PAGED, LocalSize);
        
        //This UMA call safely copies UM data into the KM heap allocation.
        CopyFromUser(&LocalData, Ptr, LocalSize);
        
        // To be safe, set LocalData->Size to be LocalSize, which was the value used
        // to make the pool allocation just in case LocalData->Size was changed.
        ((StructWithData*)LocalData)->Size = LocalSize;

    } __except (…) {}
}

Implementação e utilização de UMA

A interface UMA é fornecida como parte do Kit de Driver do Windows (WDK):

  • As declarações de função são encontradas no arquivo de cabeçalho usermode_accessors.h .
  • As implementações de função são encontradas em uma biblioteca estática chamada umaccess.lib.

O UMA funciona em todas as versões do Windows, não apenas na mais recente. Você precisa consumir o WDK mais recente para obter as declarações de função e implementações de usermode_accessors.h e umaccess.lib, respectivamente. O driver resultante será executado bem em versões mais antigas do Windows.

Umaccess.lib fornece uma implementação segura e de nível inferior para todos os DDIs. Nas versões com reconhecimento de UMA do kernel do Windows, os drivers terão todas as suas funções redirecionadas para uma versão mais segura implementada no ntoskrnl.exe.

Todas as funções de acessador de modo de usuário devem ser executadas dentro de um manipulador de exceção estruturado (SEH) devido a possíveis exceções ao acessar a memória de modo de usuário.

Tipos de acessadores DDIs em modo de utilizador

A UMA fornece vários DDIs para diferentes tipos de acesso à memória no modo de usuário. A maioria desses DDIs são para tipos de dados fundamentais, como BOOLEAN, ULONG e ponteiros. Além disso, a UMA fornece DDIs para acesso à memória em massa, recuperação de comprimento de cadeia de caracteres e operações intertravadas.

DDIs genéricas para tipos de dados fundamentais

O UMA fornece seis variantes de função para ler e escrever tipos de dados simples. Por exemplo, as seguintes funções estão disponíveis para valores BOOLEANOS:

Nome da Função Description
ReadBooleanFromUser Leia um valor da memória do modo de usuário.
ReadBooleanFromUserAcquire Leia um valor da memória em modo de utilizador com a semântica de aquisição para ordenação de memória.
ReadBooleanFromMode Leia a partir da memória de modo de utilizador ou de modo núcleo com base num parâmetro de modo.
WriteBooleanToUser Escreva um valor na memória do modo de usuário.
WriteBooleanToUserRelease Escreva um valor na memória de modo de usuário com semântica de liberação para ordenação de memória.
WriteBooleanToMode Escreva na memória de modo utilizador ou de modo núcleo com base num parâmetro de modo.

Para funções ReadXxxFromUser , o parâmetro Source deve apontar para o espaço de endereço virtual (VAS) de modo de usuário. O mesmo é verdade nas versões ReadXxxFromMode quando Mode == UserMode.

Para ReadXxxFromMode, quando Mode == KernelMode, o parâmetro Source deve apontar para o VAS no modo kernel. Se a definição do pré-processador DBG estiver definida, a operação rapidamente falhará com o código FAST_FAIL_KERNEL_POINTER_EXPECTED.

Nas funções WriteXxxToUser , o parâmetro Destination deve apontar para o VAS de modo de usuário. O mesmo é verdadeiro nas versões WriteXxxToMode quando Mode == UserMode.

DDIs de cópia e manipulação de memória

O UMA fornece funções para copiar e mover memória entre os modos de usuário e kernel, incluindo variantes para cópias não temporais e alinhadas. Essas funções são marcadas com anotações indicando possíveis exceções SEH e requisitos IRQL (máx. APC_LEVEL).

Os exemplos incluem CopyFromUser, CopyToMode e CopyFromUserToMode.

Macros como CopyFromModeAligned e CopyFromUserAligned incluem sondagem de alinhamento para segurança antes de executar a operação de cópia.

Macros como CopyFromUserNonTemporal e CopyToModeNonTemporal fornecem cópias não temporais que evitam a poluição do cache.

Estruturar macros de leitura/gravação

Macros para estruturas de leitura e gravação entre modos garantem compatibilidade e alinhamento de tipos, chamando funções auxiliares com parâmetros de tamanho e modo. Os exemplos incluem WriteStructToMode, ReadStructFromUser e suas variantes alinhadas.

Funções de preenchimento e memória zero

DDIs são fornecidos para preencher ou zerar memória em espaços de endereço de usuário ou modo, com parâmetros especificando destino, comprimento, valor de preenchimento e modo. Essas funções também carregam anotações SEH e IRQL.

Os exemplos incluem FillUserMemory e ZeroModeMemory.

Operações interligadas

O UMA inclui operações interligadas para acesso à memória atômica, que são essenciais para manipulações de memória seguras para threads em ambientes simultâneos. As DDIs são fornecidas para valores de 32 bits e 64 bits, com versões direcionadas à memória do usuário ou do modo.

Os exemplos incluem InterlockedCompareExchangeToUser, InterlockedOr64ToMode e InterlockedAndToUser.

DDIs de comprimento de cadeia de caracteres

Funções para determinar comprimentos de cadeia de caracteres com segurança a partir da memória do usuário ou modo estão incluídas, suportando ANSI e cadeias de caracteres largos. Essas funções são projetadas para gerar exceções no acesso inseguro à memória e são restritas ao IRQL.

Os exemplos incluem StringLengthFromUser e WideStringLengthFromMode.

Funções de acesso para inteiros grandes e cadeias de caracteres Unicode

UMA fornece DDIs para ler e gravar tipos LARGE_INTEGER, ULARGE_INTEGER, e UNICODE_STRING entre a memória de utilizador e a memória de modo. As variantes têm semântica de aquisição e libertação com parâmetros de modo para garantir segurança e correção.

Os exemplos incluem ReadLargeIntegerFromUser, WriteUnicodeStringToMode e WriteULargeIntegerToUser.

Adquirir e liberar semântica

Em algumas arquiteturas, como ARM, a CPU pode reordenar os acessos à memória. Todos os DDIs genéricos têm uma implementação Acquire/Release se você precisar de uma garantia de que os acessos à memória não sejam reordenados para o acesso no modo de usuário.

  • As semânticas de aquisição impedem a reordenação da carga em relação a outras operações de memória.
  • A semântica de liberação impede a reordenação do armazenamento em relação a outras operações de memória.

Exemplos de semântica de aquisição e liberação na UMA incluem ReadULongFromUserAcquire e WriteULongToUserRelease.

Para obter mais informações, consulte Adquirir e liberar semântica.

Melhores práticas

  • Sempre use UMA DDIs ao aceder à memória do modo de utilizador a partir do código do kernel.
  • Manipule exceções com blocos apropriados __try/__except .
  • Use DDIs baseadas em modo quando o seu código pode lidar com a memória de modo de utilizador e de modo de núcleo.
  • Considere a semântica de aquisição/liberação quando a ordem de memória for importante para seu caso de uso.
  • Valide os dados copiados depois de copiá-los para a memória do kernel para garantir a consistência.

Suporte de hardware futuro

Os acessadores de modo de usuário são projetados para suportar futuros recursos de segurança de hardware, como:

  • SMAP (Supervisor Mode Access Prevention): Impede que o código do kernel acesse a memória do modo de usuário, exceto por meio de funções designadas, como DDIs UMA.
  • ARM PAN (Privileged Access Never): Proteção semelhante em arquiteturas ARM.

Usando DDIs UMA de forma consistente, os drivers serão compatíveis com esses aprimoramentos de segurança quando forem habilitados em versões futuras do Windows.