Procedimientos recomendados de seguridad de controladores de Windows para desarrolladores de controladores
En este tema se resumen los patrones de desarrollo no seguros que pueden provocar la explotación y el abuso del código del controlador de Windows. En este tema se proporcionan recomendaciones de desarrollo y ejemplos de código. Seguir estos procedimientos recomendados ayudará a mejorar la seguridad de realizar un comportamiento con privilegios en el kernel de Windows.
Información general sobre el comportamiento del controlador no seguro
Aunque se espera que los controladores de Windows realicen un comportamiento con privilegios elevados en modo kernel, no realizar comprobaciones de seguridad y agregar restricciones en el comportamiento con privilegios es inaceptable de forma segura. El Programa de compatibilidad de hardware de Windows (WHCP), anteriormente WHQL, requiere nuevos envíos de controladores para cumplir este requisito.
Entre los ejemplos de comportamiento no seguro y peligroso se incluyen, entre otros, los siguientes:
- Proporcionar la capacidad de leer y escribir en registros específicos de la máquina arbitraria (MSR)
- Proporcionar la capacidad de finalizar procesos arbitrarios
- Proporcionar la capacidad de leer y escribir en la entrada y salida del puerto
- Proporcionar la capacidad de leer y escribir kernel, físico o memoria del dispositivo
Proporcionar la capacidad de leer y escribir MSR
Mejora de la seguridad de la lectura de MSR
En el primer ejemplo de ReadMsr, el controlador permite un comportamiento no seguro al permitir que todos los registros se lean arbitrariamente. Esto puede dar lugar a abusos por procesos malintencionados en modo de usuario.
Func ReadMsr(int dwMsrIdx)
{
int value = __readmsr(dwMsrIdx); // Unsafe, can read from any MSR
return value;
}
Si el escenario requiere lectura de MSR, el controlador siempre debe comprobar que el registro que se va a leer está restringido al índice o intervalo esperados. A continuación se muestran dos ejemplos de cómo implementar la operación de lectura 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;
}
Mejora de la seguridad de escribir en MSR
En el primer ejemplo de WriteMsr, el controlador permite un comportamiento no seguro al permitir que los registros y todos los registros se escriban arbitrariamente. Esto puede dar lugar a abusos por procesos malintencionados para elevar privilegios en modo de usuario y escribir en todos los MSR.
Func WriteMsr(int dwMsrIdx)
{
int value = __writemsr(dwMsrIdx); // Unsafe, can write to any MSR
return value;
}
Si el escenario requiere escribir en MSR, el controlador siempre debe comprobar que el registro en el que escribir está restringido al índice o intervalo esperados. A continuación se muestran dos ejemplos de cómo implementar la operación de escritura 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;
}
Proporcionar la capacidad de finalizar procesos
Se debe tener precaución extrema al implementar la funcionalidad en el controlador, lo que permite que los procesos finalicen. Los procesos protegidos y los procesos protegidos de luz de proceso (PPL), como los que usan las soluciones antimalware y antivirus, no deben terminarse. Exponer esta funcionalidad permite a los atacantes finalizar las protecciones de seguridad en el sistema.
Si el escenario requiere la finalización del proceso, se deben implementar las siguientes comprobaciones para protegerse frente a la terminación arbitraria del proceso:
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;
}
Proporcionar la capacidad de leer y escribir en la entrada y salida del puerto
Mejora de la seguridad de lectura desde E/S por puerto
Se debe tener precaución, al proporcionar la capacidad de leer en Puerto entrada/salida (E/S). Este ejemplo de código no es seguro.
Func ArbitraryInputPort(int inPort)
{
dwResult = __indword(inPort); // Unsafe, allows for arbitrary reading from Input Port
return dwResult;
}
Para evitar el abuso y la vulnerabilidad del controlador, el puerto de entrada esperado debe restringirse al límite de uso necesario.
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;
}
Mejora de la seguridad de escritura en E/S por puerto
Se debe tener precaución, al proporcionar la capacidad de escribir en la entrada y salida del puerto (E/S). Este ejemplo de código no es seguro.
Func ArbitraryOutputPort(int outPort, DWORD dwValue)
{
__outdword(OutPort, dwValue); // Unsafe, allows for arbitrary writing to Output Port
}
Para evitar el abuso y la vulnerabilidad del controlador, el puerto de entrada esperado debe restringirse al límite de uso necesario.
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;
}
}
Proporcionar la capacidad de leer y escribir kernel, físico o memoria del dispositivo
Mejora de la seguridad de Memcpy
Este código de ejemplo muestra un uso no entrenado y no seguro del uso seguro de la memoria física.
Func ArbitraryMemoryCopy(src, dst, length)
{
memcpy(dst, src, length); // Unsafe, can read and write anything from physical memory
}
Si el escenario requiere lectura y escritura de kernel, memoria física o de dispositivo, el controlador siempre debe comprobar que el origen y los destinos están restringidos a los índices o 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;
}
}
Mejora de la seguridad de ZwMapViewOfSection
En el ejemplo siguiente se muestra el método no seguro e incorrecto para leer y escribir memoria física desde el modo de usuario mediante las API ZwOpenSection y ZwMapViewOfSection.
Func ArbitraryMap(PHYSICAL_ADDRESS Address)
{
ZwOpenSection(&hSection, ... ,"\Device\PhysicalMemory");
ZwMapViewOfSection(hSection, -1, 0, 0, 0, Address, ...);
}
Para evitar el abuso y la vulnerabilidad de seguridad del comportamiento de lectura y escritura del controlador por procesos de modo de usuario malintencionados, el controlador debe validar la dirección de entrada y restringir la asignación de memoria solo al límite de uso necesario para el escenario.
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;
}
}
Mejora de la seguridad de MmMapLockedPagesSpecifyCache
En el ejemplo siguiente se muestra el método no seguro e incorrecto para leer y escribir memoria física desde el modo de usuario mediante las API MmMapIoSpace, IoAllocateMdl y MmMapLockedPagesSpecifyCache.
Func ArbitraryMap(PHYSICAL_ADDRESS paAddress)
{
lpAddress = MmMapIoSpace(paAddress, qwSize, ...);
pMdl = IoAllocateMdl( lpAddress, ...);
MmMapLockedPagesSpecifyCache(pMdl, UserMode, ... );
}
Para evitar el abuso y la vulnerabilidad de seguridad del comportamiento de lectura y escritura del controlador por procesos de modo de usuario malintencionados, el controlador debe validar la dirección de entrada y restringir la asignación de memoria solo al límite de uso necesario para el escenario.
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;
}
}