Bewährte Methoden für die Sicherheit von Windows-Treibern für Treiberentwickler
In diesem Thema werden die unsicheren Entwicklungsmuster zusammengefasst, die zu Ausbeutung und Missbrauch Ihres Windows-Treibercodes führen können. Dieses Thema enthält Entwicklungsempfehlungen und Codebeispiele. Im Anschluss an diese bewährten Methoden wird die Sicherheit des Verhaltens von privilegierten Rechten im Windows-Kernel verbessert.
Übersicht über unsicheres Treiberverhalten
Es wird zwar erwartet, dass Windows-Treiber ein hohes privilegiertes Verhalten im Kernelmodus ausführen, aber keine Sicherheitsüberprüfungen ausführen und Einschränkungen für privilegiertes Verhalten sicher hinzufügen, ist nicht akzeptabel. Das Windows Hardware Compatibility Program (WHCP), früher WHQL, erfordert neue Treiberübermittlungen, um diese Anforderung zu erfüllen.
Beispiele für unsicheres und gefährliches Verhalten umfassen, aber nicht beschränkt auf Folgendes:
- Bereitstellen der Möglichkeit zum Lesen und Schreiben in beliebige computerspezifische Register (MSRs)
- Bereitstellen der Möglichkeit, willkürliche Prozesse zu beenden
- Bereitstellen der Möglichkeit zum Lesen und Schreiben in Porteingabe und -ausgabe
- Bereitstellen der Möglichkeit zum Lesen und Schreiben von Kernel-, physischem oder Gerätespeicher
Bereitstellen der Möglichkeit zum Lesen und Schreiben von MSRs
Verbessern der Sicherheit beim Lesen von MSRs
Im ersten ReadMsr-Beispiel ermöglicht der Treiber unsicheres Verhalten, indem alle Register willkürlich gelesen werden können. Dies kann zu Missbrauch durch böswillige Prozesse im Benutzermodus führen.
Func ReadMsr(int dwMsrIdx)
{
int value = __readmsr(dwMsrIdx); // Unsafe, can read from any MSR
return value;
}
Wenn Für Ihr Szenario das Lesen von MSRs erforderlich ist, muss der Treiber immer überprüfen, ob das Register zum Lesen auf den erwarteten Index oder Bereich beschränkt ist. Zwei Beispiele für die Implementierung des sicheren Lesevorgangs folgen.
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;
}
Verbessern der Sicherheit des Schreibens an MSRs
Im ersten WriteMsr-Beispiel ermöglicht der Treiber unsicheres Verhalten, indem alle Register beliebig geschrieben werden können. Dies kann zu Missbrauch durch böswillige Prozesse führen, um berechtigungen im Benutzermodus zu erhöhen und in alle MSRs zu schreiben.
Func WriteMsr(int dwMsrIdx)
{
int value = __writemsr(dwMsrIdx); // Unsafe, can write to any MSR
return value;
}
Wenn Für Ihr Szenario das Schreiben in MSRs erforderlich ist, muss der Treiber immer überprüfen, ob das Zu schreibende Register auf den erwarteten Index oder Bereich beschränkt ist. Zwei Beispiele für die Implementierung des sicheren Schreibvorgangs folgen.
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;
}
Bereitstellen der Möglichkeit zum Beenden von Prozessen
Extreme Vorsicht muss bei der Implementierung von Funktionen in Ihrem Treiber verwendet werden, wodurch Prozesse beendet werden können. Geschützte Prozesse und geschützte Prozesslichtprozesse (PPL) wie solche, die von Antischadsoftware- und Virenschutzlösungen verwendet werden, dürfen nicht beendet werden. Durch das Verfügbarmachen dieser Funktionalität können Angreifer Sicherheitsschutz auf dem System beenden.
Wenn Ihr Szenario eine Beendigung des Prozesses erfordert, müssen die folgenden Prüfungen implementiert werden, um vor willkürlicher Beendigung von Prozessen zu schützen:
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;
}
Bereitstellen der Möglichkeit zum Lesen und Schreiben in Porteingabe und -ausgabe
Verbessern der Sicherheit des Lesens von Port-E/A
Vorsicht muss verwendet werden, wenn die Möglichkeit zum Lesen der Porteingabe/Ausgabe (E/A) bereitgestellt wird. Dieses Codebeispiel ist unsicher.
Func ArbitraryInputPort(int inPort)
{
dwResult = __indword(inPort); // Unsafe, allows for arbitrary reading from Input Port
return dwResult;
}
Um den Missbrauch und den Exploit des Treibers zu verhindern, muss der erwartete Eingabeport auf die erforderliche Nutzungsgrenze beschränkt werden.
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;
}
Verbessern der Sicherheit des Schreibens an Port IO
Vorsicht muss verwendet werden, wenn die Möglichkeit zum Schreiben in Port-Eingabe/Ausgabe (E/A) bereitgestellt wird. Dieses Codebeispiel ist unsicher.
Func ArbitraryOutputPort(int outPort, DWORD dwValue)
{
__outdword(OutPort, dwValue); // Unsafe, allows for arbitrary writing to Output Port
}
Um den Missbrauch und den Exploit des Treibers zu verhindern, muss der erwartete Eingabeport auf die erforderliche Nutzungsgrenze beschränkt werden.
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;
}
}
Bereitstellen der Möglichkeit zum Lesen und Schreiben von Kernel-, physischem oder Gerätespeicher
Verbesserung der Sicherheit von Memcpy
Dieser Beispielcode zeigt ungezwungene und unsichere Verwendung des sicheren Verwendens des physischen Speichers.
Func ArbitraryMemoryCopy(src, dst, length)
{
memcpy(dst, src, length); // Unsafe, can read and write anything from physical memory
}
Wenn Für Ihr Szenario das Lesen und Schreiben von Kernel-, physischem oder Gerätespeicher erforderlich ist, muss der Treiber immer überprüfen, ob die Quelle und die Ziele auf die erwarteten Indizes oder Bereiche beschränkt sind.
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;
}
}
Verbessern der Sicherheit von ZwMapViewOfSection
Das folgende Beispiel veranschaulicht die unsichere und unsachgemäße Methode zum Lesen und Schreiben des physischen Speichers aus dem Benutzermodus mithilfe der ZwOpenSection- und ZwMapViewOfSection-APIs.
Func ArbitraryMap(PHYSICAL_ADDRESS Address)
{
ZwOpenSection(&hSection, ... ,"\Device\PhysicalMemory");
ZwMapViewOfSection(hSection, -1, 0, 0, 0, Address, ...);
}
Um den Missbrauch und exploit des Lese-/Schreibverhaltens des Treibers durch böswillige Benutzermodusprozesse zu verhindern, muss der Treiber die Eingabeadresse überprüfen und die Speicherzuordnung nur auf die erforderliche Nutzungsgrenze für das Szenario beschränken.
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;
}
}
Verbessern der Sicherheit von MmMapLockedPagesSpecifyCache
Das folgende Beispiel veranschaulicht die unsichere und unsachgemäße Methode zum Lesen und Schreiben des physischen Speichers aus dem Benutzermodus mithilfe der MmMapIoSpace-, IoAllocateMdl- und MmMapLockedPagesSpecifyCache-APIs.
Func ArbitraryMap(PHYSICAL_ADDRESS paAddress)
{
lpAddress = MmMapIoSpace(paAddress, qwSize, ...);
pMdl = IoAllocateMdl( lpAddress, ...);
MmMapLockedPagesSpecifyCache(pMdl, UserMode, ... );
}
Um den Missbrauch und exploit des Lese-/Schreibverhaltens des Treibers durch böswillige Benutzermodusprozesse zu verhindern, muss der Treiber die Eingabeadresse überprüfen und die Speicherzuordnung nur auf die erforderliche Nutzungsgrenze für das Szenario beschränken.
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;
}
}