Observação
O acesso a essa página exige autorização. Você pode tentar entrar ou alterar diretórios.
O acesso a essa página exige autorização. Você pode tentar alterar os diretórios.
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 do modo de usuário do código do modo kernel. Esses DDIs abordam vulnerabilidades comuns de segurança 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 do modo kernel que acessa/manipula a memória do modo de usuário em breve será necessário para usar UMA.
Possíveis problemas ao acessar a memória do modo de usuário do modo kernel
Quando o código do modo kernel precisa acessar a memória do modo de usuário, vários desafios surgem:
Os aplicativos no modo de usuário podem passar ponteiros mal-intencionados ou inválidos para o código do modo kernel. A falta de validação adequada pode levar à corrupção de memória, falhas ou vulnerabilidades de segurança.
O código do modo de usuário é multiencadeado. Como resultado, diferentes threads podem modificar a mesma memória no modo de usuário entre acessos separados do modo kernel, o que pode levar à corrupção da memória de kernel.
Os desenvolvedores do modo kernel geralmente esquecem de investigar a memória do modo de usuário antes de acessá-la, o que é um problema de segurança.
Os compiladores assumem execução em um único thread e podem otimizar o que parecem ser acessos de memória redundantes. Os programadores que desconhecem essas otimizações podem escrever código não seguro.
Os snippets de código a seguir ilustram esses problemas.
Exemplo 1: Possível corrupção de memória devido ao multithreading no modo de usuário
O código do modo kernel que precisa acessar a memória do modo de usuário deve fazer isso em um __try/__except bloco para garantir que a memória seja válida. O snippet 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
}
}
Esse snippet investiga a memória primeiro, que é uma etapa importante, mas frequentemente ignorada.
No entanto, um problema que pode ocorrer nesse 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 à 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 cópia:
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 reduza o problema causado pelo multithreading, ela ainda não é segura porque o compilador não está ciente de vários threads e, portanto, pressupõe um único thread de execução. Como uma otimização, o compilador pode perceber que já tem uma cópia do valor para o qual Ptr->Size aponta em sua pilha e, portanto, pode não copiar para LocalSize.
Solução de controladores de acesso em modo usuário
A interface UMA resolve os problemas encontrados ao acessar a memória do modo de usuário do modo kernel. A UMA fornece:
Investigação automática: a investigação explícita (ProbeForRead/ProbeForWrite) não é mais necessária, pois todas as funções UMA garantem a segurança do endereço.
Acesso volátil: todos os DDIs UMA usam semântica volátil para evitar otimizações de compilador.
Facilidade de portabilidade: o conjunto abrangente de DDIs UMA facilita a portabilidade do código existente pelos clientes, garantindo que a memória do modo de usuário seja acessada com segurança e corretamente.
Exemplo usando UMA DDI
Usando a estrutura de modo de usuário definida anteriormente, o snippet de código a seguir demonstra como usar 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 uso da UMA
A interface UMA é fornecida como parte do WDK (Windows Driver Kit):
- 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.
A UMA funciona em todas as versões do Windows, não apenas nas mais recentes. 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. Em 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 acessoras do modo de usuário devem ser executadas em um SEH (manipulador de exceção estruturado) devido a possíveis exceções ao acessar a memória do modo de usuário.
Tipos de DDIs do acessador no modo de usuário
A UMA fornece vários DDIs para diferentes tipos de acesso à memória no modo de usuário. A maioria desses DDIs é 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éricos para tipos de dados fundamentais
A UMA fornece seis variantes de função para ler e gravar tipos de dados simples. Por exemplo, as seguintes funções estão disponíveis para valores BOOLEAN:
| Nome da Função | Description |
|---|---|
| LerBooleanoDoUsuário | Leia um valor da memória do modo de usuário. |
| ReadBooleanFromUserAcquire | Leia um valor da memória do modo de usuário com semântica de aquisição para ordenação de memória. |
| ReadBooleanFromMode | Leia a partir da memória do modo de usuário ou do modo kernel com base em um parâmetro de modo. |
| WriteBooleanToUser | Escreva um valor na memória do modo de usuário. |
| WriteBooleanToUserRelease | Escreva um valor na memória em modo de usuário com semântica de liberação para a ordenação de memória. |
| WriteBooleanToMode | Escreva na memória do modo de usuário ou do modo kernel com base em um parâmetro de modo. |
Para funções ReadXxxFromUser , o parâmetro De origem deve apontar para o VAS (espaço de endereço virtual) no modo de usuário. O mesmo é verdadeiro nas versões ReadXxxFromMode quando Mode == UserMode.
Para ReadXxxFromMode, quando Mode == KernelMode, o parâmetro De origem deve apontar para o VAS do modo kernel. Se a definição DBG do pré-processador estiver definida, a operação falha rapidamente com o código FAST_FAIL_KERNEL_POINTER_EXPECTED.
Nas funções WriteXxxToUser, o parâmetro Destination deve apontar para o VAS em modo de usuário. O mesmo é verdadeiro nas versões WriteXxxToMode quando Mode == UserMode.
DDIs de manipulação de memória e cópia
A UMA fornece funções para copiar e mover a memória entre os modos de usuário e kernel, incluindo variantes para cópias nãotemporais e alinhadas. Essas funções são marcadas com anotações que indicam possíveis exceções do SEH e requisitos de IRQL (APC_LEVEL máximo).
Os exemplos incluem CopyFromUser, CopyToMode e CopyFromUserToMode.
Macros como CopyFromModeAligned e CopyFromUserAligned incluem a investigação de alinhamento para segurança antes de executar a operação de cópia.
Macros como CopyFromUserNonTemporal e CopyToModeNonTemporal fornecem cópias nãotemporais que evitam a poluição do cache.
Estruturar macros de leitura/gravação
As macros para ler e gravar estruturas entre modos garantem a compatibilidade de tipos e o alinhamento, chamando funções auxiliares com parâmetros de tamanho e modo. Exemplos incluem WriteStructToMode, ReadStructFromUser e suas variantes alinhadas.
Funções de preenchimento e zeragem de memória
DDIs são fornecidos para preencher ou zerar a memória em espaços de endereço de usuário ou modo, com parâmetros que especificam destino, tamanho, valor de preenchimento e modo. Essas funções também carregam anotações SEH e IRQL.
Exemplos incluem FillUserMemory e ZeroModeMemory.
Operações intertravadas
A UMA inclui operações intertravadas para acesso à memória atômica, que são essenciais para manipulações de memória thread-safe em ambientes simultâneos. Os DDIs são fornecidos 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 da cadeia de caracteres
Funções para determinar os comprimentos de cadeia de caracteres com segurança da memória do usuário ou do sistema são incluídas, dando suporte a cadeias de caracteres ANSI e de caracteres largos. Essas funções são projetadas para gerar exceções no acesso à memória não seguro e são restritas ao IRQL.
Exemplos incluem StringLengthFromUser e WideStringLengthFromMode.
Inteiros grandes e acessadores de cadeia de caracteres Unicode
A UMA fornece DDIs para ler e gravar tipos de LARGE_INTEGER, ULARGE_INTEGER e UNICODE_STRING entre o usuário e a memória do modo. As variantes têm semântica de aquisição e liberação com parâmetros de modo para segurança e exatidão.
Os exemplos incluem ReadLargeIntegerFromUser, WriteUnicodeStringToMode e WriteULargeIntegerToUser.
Adquirir e liberar semântica
Em algumas arquiteturas, como o ARM, a CPU pode reordenar acessos de memória. Todos os DDIs genéricos têm uma implementação de Adquirir/Liberar se você precisar de uma garantia de que os acessos de memória não sejam reordenados no modo de acesso do usuário.
- Adquirir semântica impede a reordenação da carga em relação a outras operações de memória.
- A semântica de versão impede a reordenação do repositório em relação a outras operações de memória.
Exemplos de semântica de aquisição e lançamento na UMA incluem ReadULongFromUserAcquire e WriteULongToUserRelease.
Para obter mais informações, consulte Adquirir e liberar semântica.
Práticas recomendadas
- Sempre use UMA DDIs ao acessar a memória de modo usuário a partir do código de kernel.
- Trate exceções com os blocos apropriados .
- Use DDIs baseados em modo quando seu código pode lidar com o modo de usuário e a memória do modo kernel.
- Considere a semântica de aquisição/liberação quando a ordenação de memória é 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 a hardware futuro
Os acessadores do modo de usuário foram projetados para dar suporte a recursos futuros de segurança de hardware, como:
- SMAP (Prevenção de Acesso ao Modo Supervisor): 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 do ARM.
Usando DDIs UMA de forma consistente, os drivers serão compatíveis com esses aprimoramentos de segurança quando estiverem habilitados em versões futuras do Windows.