Udostępnij za pośrednictwem


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 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;
	}
}

Zobacz też

lista kontrolna dotycząca zabezpieczeń sterownika