Práticas recomendadas para restringir o comportamento de alto privilégio em controladores de modo kernel
Este tópico resume os padrões de desenvolvimento inseguros que podem levar à exploração e abuso do código do driver do kernel do Windows. Este tópico fornece recomendações de desenvolvimento e exemplos de código para ajudar a restringir o comportamento privilegiado. Seguir essas práticas recomendadas ajudará a melhorar a segurança da execução de comportamentos privilegiados no kernel do Windows.
Visão geral do comportamento do motorista inseguro
Embora se espere que os drivers do Windows executem um comportamento de alto privilégio no modo kernel, não executar verificações de segurança e adicionar restrições no comportamento privilegiado é inaceitável. O Programa de Compatibilidade de Hardware do Windows (WHCP), anteriormente WHQL, requer novos envios de driver para cumprir esse requisito.
Exemplos de comportamento inseguro e perigoso incluem, mas não estão limitados a, o seguinte:
- Fornecer a capacidade de ler e escrever em registos arbitrários específicos da máquina (MSRs)
- Fornecer a capacidade de encerrar processos arbitrários
- Fornecer a capacidade de ler e gravar nas portas de entrada e saída
- Proporcionar a capacidade de ler e escrever na memória do kernel, física ou do dispositivo
Proporcionando a possibilidade de ler e escrever MSRs
Reforçar a segurança da leitura de MSRs
Neste exemplo ReadMsr, o driver permite um comportamento inseguro, permitindo que todo e qualquer registo seja lido arbitrariamente usando o registo intrínseco específico do modelo __readmsr. Isso pode resultar em abuso por processos mal-intencionados no modo de usuário.
Func ReadMsr(int dwMsrIdx)
{
int value = __readmsr(dwMsrIdx); // Unsafe, can read from any MSR
return value;
}
Se o seu cenário exigir a leitura de MSRs, o driver deve sempre verificar se o registro para ler está restrito ao índice ou intervalo esperado. Seguem-se dois exemplos de como implementar a operação de leitura segura.
Func ConstrainedReadMsr(int dwMsrIdx)
{
int value = 0;
if (dwMsrIdx == expected_index) // Blocks from reading anything
{
value = __readmsr(dwMsrIdx); // Can only read the expected MSR
}
else
{
return error;
}
return value;
}
// OR
Func ConstrainedReadMsr(int dwMsrIdx)
{
int value = 0;
if (min_range <= dwMsrIdx <= max_range) // Blocks from reading anything
{
value = __readmsr(dwMsrIdx); // Can only from the expected range of MSRs
}
else
{
return error;
}
return value;
}
Melhorar a segurança da escrita para MSRs
No primeiro exemplo de WriteMsr, o driver permite um comportamento inseguro, permitindo que todo e qualquer registro seja gravado arbitrariamente. Isso pode resultar em abuso por processos mal-intencionados para elevar privilégios em modo de utilizador e gravar em todos os MSRs.
Func WriteMsr(int dwMsrIdx)
{
int value = __writemsr(dwMsrIdx); // Unsafe, can write to any MSR
return value;
}
Se o seu cenário exigir gravação em MSRs, o driver deve sempre verificar se o registro para gravar está restrito ao índice ou intervalo esperado. Seguem-se dois exemplos de como implementar a operação de escrita segura.
Func ConstrainedWriteMsr(int dwMsrIdx)
{
int value = 0;
if (dwMsrIdx == expected_index) // Blocks from reading anything
{
value = __writemsr(dwMsrIdx); // Can only write to the expected constrained MSR
}
else
{
return error;
}
return value;
}
// OR
Func ConstrainedWriteMSR(int dwMsrIdx)
{
int value = 0;
if (min_range <= dwMsrIdx <= max_range) // Blocks from reading anything
{
value = __writemsr(dwMsrIdx); // Can only write to the expected constrained MSR
}
else
{
return error;
}
return value;
}
Fornecer a capacidade de encerrar processos
Extremo cuidado deve ser usado ao implementar a funcionalidade em seu driver que permite que os processos sejam encerrados. Os processos protegidos e os processos PPL (protected process light), como os utilizados por soluções antimalware e antivírus, não devem ser encerrados. Expor essa funcionalidade permite que os invasores encerrem as proteções de segurança no sistema.
Se o seu cenário exigir o encerramento do processo, as seguintes verificações devem ser implementadas para proteger contra o encerramento arbitrário do processo, usando PsLookupProcessByProcessId e PsIsProtectedProcess
:
Func ConstrainedProcessTermination(DWORD dwProcessId)
{
// Function to check if a process is a Protected Process Light (PPL)
NTSTATUS status;
BOOLEAN isPPL = FALSE;
PEPROCESS process;
HANDLE hProcess;
// Open the process
status = PsLookupProcessByProcessId(processId, &process);
if (!NT_SUCCESS(status)) {
return FALSE;
}
// Check if the process is a PPL
if (PsIsProtectedProcess(process)) {
isPPL = TRUE;
}
// Dereference the process
ObDereferenceObject(process);
return isPPL;
}
Permitir ler e gravar nas portas de entrada e saída
Aumentar a segurança da leitura da Porta IO
Deve-se ter cuidado ao fornecer a capacidade de ler para a porta de entrada/saída (E/S). Este exemplo de código que usa __indword não é seguro.
Func ArbitraryInputPort(int inPort)
{
dwResult = __indword(inPort); // Unsafe, allows for arbitrary reading from Input Port
return dwResult;
}
Para evitar o abuso e a exploração do driver, a porta de entrada esperada deve ser restrita ao limite de uso necessário.
Func ConstrainedInputPort(int inPort)
{
// The expected input port must be constrained to the required usage boundary to prevent abuse
if(inPort == expected_InPort)
{
dwResult = __indword(inPort);
}
else
{
return error;
}
return dwResult;
}
Aumentando a segurança da gravação na Porta de E/S
É necessário ter cuidado ao permitir a capacidade de gravar na porta de entrada/saída (E/S). Este exemplo de código que usa __outword não é seguro.
Func ArbitraryOutputPort(int outPort, DWORD dwValue)
{
__outdword(OutPort, dwValue); // Unsafe, allows for arbitrary writing to Output Port
}
Para evitar o abuso e a exploração do driver, a porta de entrada esperada deve ser restrita ao limite de uso necessário.
Func ConstrainedOutputPort(int outPort, DWORD dwValue)
{
// The expected output port must be constrained to the required usage boundary to prevent abuse
if(outPort == expected_OutputPort)
{
__outdword(OutPort, dwValue); // checks on InputPort
}
else
{
return error;
}
}
Fornecendo a capacidade de ler e escrever na memória do kernel, física ou de dispositivo.
Reforçar a segurança do Memcpy
Este código de exemplo mostra o uso irrestrito e inseguro do uso seguro da memória física.
Func ArbitraryMemoryCopy(src, dst, length)
{
memcpy(dst, src, length); // Unsafe, can read and write anything from physical memory
}
Se o seu cenário requer leitura e gravação de kernel, memória física ou de dispositivo, o driver deve sempre verificar se a origem e os destinos estão restritos aos índices ou intervalos esperados.
Func ConstrainedMemoryCopy(src, dst, length)
{
// valid_src and valid_dst must be constrained to required usage boundary to prevent abuse
if(src == valid_Src && dst == valid_Dst)
{
memcpy(dst, src, length);
}
else
{
return error;
}
}
Melhorando a segurança do ZwMapViewOfSection
O exemplo a seguir ilustra o método inseguro e impróprio para ler e gravar memória física do modo de usuário utilizando o ZwOpenSection e ZwMapViewOfSection APIs.
Func ArbitraryMap(PHYSICAL_ADDRESS Address)
{
ZwOpenSection(&hSection, ... ,"\Device\PhysicalMemory");
ZwMapViewOfSection(hSection, -1, 0, 0, 0, Address, ...);
}
Para evitar o abuso e a exploração do comportamento de leitura/gravação do driver por processos de modo de usuário mal-intencionado, o driver deve validar o endereço de entrada e restringir o mapeamento de memória apenas ao limite de uso necessário para o cenário.
Func ConstrainedMap(PHYSICAL_ADDRESS paAddress)
{
// expected_Address must be constrained to required usage boundary to prevent abuse
if(paAddress == expected_Address)
{
ZwOpenSection(&hSection, ... ,"\Device\PhysicalMemory");
ZwMapViewOfSection(hSection, -1, 0, 0, 0, paAddress, ...);
}
else
{
return error;
}
}
Aprimorando a segurança de MmMapLockedPagesSpecifyCache
O exemplo a seguir ilustra o método inseguro e impróprio para ler e gravar na memória física a partir do modo de utilizador utilizando as APIs MmMapIoSpace, IoAllocateMdl e MmMapLockedPagesSpecifyCache.
Func ArbitraryMap(PHYSICAL_ADDRESS paAddress)
{
lpAddress = MmMapIoSpace(paAddress, qwSize, ...);
pMdl = IoAllocateMdl( lpAddress, ...);
MmMapLockedPagesSpecifyCache(pMdl, UserMode, ... );
}
Para evitar o abuso e a exploração do comportamento de leitura/gravação do driver por processos de modo de usuário mal-intencionado, o driver deve validar o endereço de entrada e restringir o mapeamento de memória apenas ao limite de uso necessário para o cenário.
Func ConstrainedMap(PHYSICAL_ADDRESS paAddress)
{
// expected_Address must be constrained to required usage boundary to prevent abuse
if(paAddress == expected_Address && qwSize == valid_Size)
{
lpAddress = MmMapIoSpace(paAddress, qwSize, ...);
pMdl = IoAllocateMdl( lpAddress, ...);
MmMapLockedPagesSpecifyCache(pMdl, UserMode, ... );
}
else
{
return error;
}
}