Práticas recomendadas para restringir o comportamento com privilégios elevados em drivers de modo kernel
Este tópico resume os padrões de desenvolvimento não seguros que podem levar à exploração e abuso do código de 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 de executar comportamentos privilegiados no kernel do Windows.
Visão geral do comportamento inseguro do motorista
Embora seja esperado que os drivers do Windows executem um comportamento privilegiado alto no modo kernel, não executar verificações de segurança e adicionar restrições ao comportamento privilegiado é inaceitável. O WHCP (Programa de Compatibilidade de Hardware do Windows), anteriormente WHQL, requer novos envios de driver para atender a esse requisito.
Exemplos de comportamentos não seguros e perigosos incluem, mas não se limitam a:
- Fornecendo a capacidade de ler e gravar em MSRs (registros específicos do computador) arbitrários
- Fornecendo a capacidade de encerrar processos arbitrários
- Oferecer a capacidade de ler e gravar na entrada e saída da porta
- Fornecendo a capacidade de ler e gravar o kernel, a memória física ou do dispositivo
Fornecer a capacidade de ler e gravar MSRs
Aprimorando a segurança da leitura de MSRs
Neste exemplo do ReadMsr, o driver permite um comportamento não seguro, permitindo que todos os registros sejam lidos arbitrariamente usando o registro específico do modelo __readmsr intrínseco. 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 deverá sempre verificar se o registro a ser lido está restrito ao índice ou intervalo esperado. Dois exemplos de como implementar a operação de leitura segura seguem.
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;
}
Aprimorar a segurança da gravação em MSRs
No primeiro exemplo do WriteMsr, o driver permite um comportamento não seguro, permitindo que todos os registros sejam gravados arbitrariamente. Isso pode resultar em abuso por parte de processos mal-intencionados para elevar o privilégio no modo de usuário 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 a gravação em MSRs, o driver deverá sempre verificar se o registro a ser gravado está restrito ao índice ou intervalo esperado. Dois exemplos de como implementar a operação de gravação segura seguem.
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;
}
Fornecendo a capacidade de encerrar processos
É necessário ter extrema cautela ao implementar a funcionalidade em seu driver, o que permite que os processos sejam encerrados. Processos protegidos e processos de PPL (Processo Leve Protegido), como aqueles usados 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 cenário exigir o encerramento do processo, as seguintes verificações deverão 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;
}
Oferecer a capacidade de ler e gravar na entrada e saída da porta
Aumentar a segurança da leitura da porta E/S
É necessário ter cuidado ao permitir a capacidade de leitura da entrada/saída de porta (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;
}
Aumentar a segurança da gravação na porta E/S
Deve-se ter cuidado ao fornecer 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 gravar memória do kernel, memória física ou memória do dispositivo
Aprimorando a segurança do Memcpy
Esse 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 cenário exigir leitura e gravação de kernel, memória física ou de dispositivo, o driver sempre deverá 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;
}
}
Aprimorando a segurança de ZwMapViewOfSection
O exemplo a seguir ilustra o método inseguro e inadequado para ler e gravar memória física no modo de usuário utilizando as APIs ZwOpenSection e ZwMapViewOfSection.
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 mal-intencionados do modo de usuário, o driver deve validar o endereço de entrada e restringir o mapeamento de memória apenas para o 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 não seguro e inadequado para ler e gravar memória física do modo de usuário utilizando as APIs
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 mal-intencionados do modo de usuário, o driver deve validar o endereço de entrada e restringir o mapeamento de memória apenas para o 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;
}
}