Najlepsze rozwiązania dotyczące ograniczania zachowania o wysokim poziomie uprawnień w sterownikach trybu jądra
W tym temacie przedstawiono podsumowanie niebezpiecznych wzorców programowania, które mogą prowadzić do wykorzystywania i nadużywania kodu sterownika jądra systemu Windows. Ten temat zawiera zalecenia programistyczne i przykłady kodu ułatwiające ograniczenie zachowania uprzywilejowanego. Stosowanie się do tych najlepszych praktyk pomoże zwiększyć bezpieczeństwo wykonywania uprzywilejowanych operacji w jądrze Windows.
Omówienie niebezpiecznego zachowania sterownika
Chociaż oczekuje się, że sterowniki systemu Windows wykonują czynności wymagające wysokich uprawnień w trybie jądra, brak przeprowadzania kontroli zabezpieczeń i dodawania ograniczeń dotyczących zachowań uprzywilejowanych jest niedopuszczalny. Program zgodności sprzętu systemu Windows (WHCP), dawniej WHQL, wymaga nowych przesłanych sterowników, aby spełnić to wymaganie.
Przykłady niezabezpieczonego i niebezpiecznego zachowania obejmują, ale nie tylko:
- Zapewnianie możliwości odczytu i zapisu w dowolnych rejestrach specyficznych dla maszyny (MSR)
- Zapewnienie możliwości zakończenia dowolnych procesów
- Zapewnienie możliwości odczytu i zapisu w portach wejściowych i wyjściowych
- Zapewnianie możliwości odczytu i zapisu jądra, pamięci fizycznej lub urządzenia
Zapewnianie możliwości odczytywania i zapisywania msrs
Zwiększanie bezpieczeństwa odczytu z msr
W tym przykładzie ReadMsr sterownik umożliwia niebezpieczne zachowanie, zezwalając, aby wszystkie dowolne rejestry były odczytywane arbitralnie przy użyciu rejestru specyficznego dla modelu __readmsr. Może to spowodować nadużycie przez złośliwe procesy w trybie użytkownika.
Func ReadMsr(int dwMsrIdx)
{
int value = __readmsr(dwMsrIdx); // Unsafe, can read from any MSR
return value;
}
Jeśli twój scenariusz wymaga odczytu z MSRs, sterownik musi zawsze sprawdzić, czy rejestr, z którego następuje odczyt, jest ograniczony do oczekiwanego indeksu lub zakresu. Podano dwa przykłady, jak zaimplementować operację bezpiecznego odczytu.
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;
}
Zwiększanie bezpieczeństwa zapisu w msrs
W pierwszym przykładzie WriteMsr, sterownik umożliwia niebezpieczne zachowanie, pozwalając na dowolne zapisywanie do wszystkich rejestrów. Może to spowodować nadużycie przez złośliwe procesy w celu podniesienia poziomu uprawnień w trybie użytkownika i zapisu do wszystkich MSR.
Func WriteMsr(int dwMsrIdx)
{
int value = __writemsr(dwMsrIdx); // Unsafe, can write to any MSR
return value;
}
Jeśli Twój scenariusz wymaga zapisu do MSR-ów, sterownik musi zawsze sprawdzać, czy rejestr, w który jest dokonywany zapis, jest ograniczony do oczekiwanego indeksu lub zakresu. Dwa przykłady implementacji bezpiecznej operacji zapisu są następujące.
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;
}
Zapewnianie możliwości kończenia procesów
Szczególną ostrożność należy stosować podczas implementowania funkcji w sterowniku, co pozwala na zakończenie procesów. Chronione procesy i procesy typu protected process light (PPL), takie jak te używane przez rozwiązania antymalware i antywirusowe, nie mogą być zakończone. Uwidacznianie tej funkcji umożliwia osobom atakującym zakończenie ochrony zabezpieczeń w systemie.
Jeśli scenariusz wymaga zakończenia procesu, należy zaimplementować następujące testy w celu ochrony przed dowolnym kończeniem procesu przy użyciu PsLookupProcessByProcessId i 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;
}
Zapewnianie możliwości odczytu i zapisu w portach wejściowych i wyjściowych
Zwiększanie bezpieczeństwa odczytu z portów wejścia/wyjścia
Należy zachować ostrożność, udostępniając możliwość odczytu danych wejściowych/wyjściowych portów (We/Wy). Ten przykład kodu, który używa __indword, jest niebezpieczny.
Func ArbitraryInputPort(int inPort)
{
dwResult = __indword(inPort); // Unsafe, allows for arbitrary reading from Input Port
return dwResult;
}
Aby zapobiec nadużyciom i wykorzystaniu sterownika, oczekiwany port wejściowy musi być ograniczony do wymaganej granicy użycia.
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;
}
Zwiększanie bezpieczeństwa zapisu do portów wejścia/wyjścia (IO)
Należy zachować ostrożność, zapewniając możliwość zapisywania do wejścia/wyjścia portu (I/O). Ten przykład kodu, który używa __outword, jest niebezpieczny.
Func ArbitraryOutputPort(int outPort, DWORD dwValue)
{
__outdword(OutPort, dwValue); // Unsafe, allows for arbitrary writing to Output Port
}
Aby zapobiec nadużyciom i wykorzystaniu sterownika, oczekiwany port wejściowy musi być ograniczony do wymaganej granicy użycia.
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;
}
}
Zapewnianie możliwości odczytu i zapisu jądra, pamięci fizycznej lub urządzenia
Zwiększanie bezpieczeństwa Memcpy
Ten przykładowy kod przedstawia niekonseksowane i niebezpieczne użycie bezpiecznej pamięci fizycznej.
Func ArbitraryMemoryCopy(src, dst, length)
{
memcpy(dst, src, length); // Unsafe, can read and write anything from physical memory
}
Jeśli scenariusz wymaga odczytu i zapisu jądra, pamięci fizycznej lub urządzenia, sterownik musi zawsze sprawdzić, czy źródła i miejsca docelowe są ograniczone do oczekiwanych indeksów lub zakresów.
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;
}
}
Zwiększanie zabezpieczeń elementu ZwMapViewOfSection
Poniższy przykład ilustruje niebezpieczną i niewłaściwą metodę odczytu i zapisu pamięci fizycznej z trybu użytkownika przy użyciu ZwOpenSection i ZwMapViewOfSection interfejsów API.
Func ArbitraryMap(PHYSICAL_ADDRESS Address)
{
ZwOpenSection(&hSection, ... ,"\Device\PhysicalMemory");
ZwMapViewOfSection(hSection, -1, 0, 0, 0, Address, ...);
}
Aby zapobiec nadużyciom i wykorzystaniu zachowania odczytu/zapisu sterownika przez złośliwe procesy trybu użytkownika, sterownik musi zweryfikować adres wejściowy i ograniczyć mapowanie pamięci tylko do wymaganej granicy użycia w scenariuszu.
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;
}
}
Zwiększanie bezpieczeństwa elementu MmMapLockedPagesSpecifyCache
Poniższy przykład ilustruje niebezpieczną i niewłaściwą metodę odczytu i zapisu pamięci fizycznej z trybu użytkownika przy użyciu MmMapIoSpace, IoAllocateMdl i MmMapLockedPagesSpecifyCache API.
Func ArbitraryMap(PHYSICAL_ADDRESS paAddress)
{
lpAddress = MmMapIoSpace(paAddress, qwSize, ...);
pMdl = IoAllocateMdl( lpAddress, ...);
MmMapLockedPagesSpecifyCache(pMdl, UserMode, ... );
}
Aby zapobiec nadużyciom i wykorzystaniu zachowania odczytu/zapisu sterownika przez złośliwe procesy trybu użytkownika, sterownik musi zweryfikować adres wejściowy i ograniczyć mapowanie pamięci tylko do wymaganej granicy użycia w scenariuszu.
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;
}
}