Condividi tramite


Procedure consigliate per la sicurezza dei driver di Windows per sviluppatori di driver

Questo argomento riepiloga i modelli di sviluppo non sicuri che possono causare lo sfruttamento e l'abuso del codice del driver di Windows. In questo argomento vengono forniti suggerimenti sullo sviluppo ed esempi di codice. Seguendo queste procedure consigliate, sarà possibile migliorare la sicurezza dell'esecuzione del comportamento con privilegi nel kernel di Windows.

Panoramica del comportamento del driver unsafe

Anche se è previsto che i driver di Windows eseguano un comportamento con privilegi elevati in modalità kernel, non eseguendo controlli di sicurezza e aggiungendo vincoli sul comportamento con privilegi non è accettabile. Il programma whcp (Windows Hardware Compatibility Program), in precedenza WHQL, richiede nuovi invii di driver per rispettare questo requisito.

Esempi di comportamenti non sicuri e pericolosi includono, ad esempio, quanto segue:

Possibilità di leggere e scrivere richieste richieste gestite

Miglioramento della sicurezza della lettura dalle richieste pull

Nel primo esempio di ReadMsr, il driver consente un comportamento non sicuro consentendo la lettura arbitraria di tutti i registri e di tutti i registri. Ciò può causare abusi da processi dannosi in modalità utente.

Func ReadMsr(int dwMsrIdx) 
{
	int value = __readmsr(dwMsrIdx); // Unsafe, can read from any MSR
	return value;
}

Se lo scenario richiede la lettura dalle richieste pull, il driver deve sempre verificare che il registro da cui eseguire la lettura sia vincolato all'indice o all'intervallo previsto. Di seguito sono riportati due esempi di come implementare l'operazione di lettura sicura.

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

Miglioramento della sicurezza della scrittura negli msr

Nel primo esempio di WriteMsr, il driver consente un comportamento non sicuro consentendo la scrittura arbitraria di tutti i registri e di tutti i registri. Ciò può causare abusi da parte di processi dannosi per elevare i privilegi in modalità utente e scrivere in tutte le richieste del servizio gestito.

Func WriteMsr(int dwMsrIdx) 
{
	int value = __writemsr(dwMsrIdx); // Unsafe, can write to any MSR
	return value;
}

Se lo scenario richiede la scrittura nelle richieste msr, il driver deve sempre verificare che il registro in cui scrivere sia vincolato all'indice o all'intervallo previsto. Di seguito sono riportati due esempi di come implementare l'operazione di scrittura sicura.

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

Possibilità di terminare i processi

Attenzione estrema deve essere usata quando si implementano funzionalità nel driver che consente di terminare i processi. I processi protetti e i processi PPL (Protected Process Light), come quelli usati da soluzioni antivirus e antimalware, non devono essere terminati. L'esposizione di questa funzionalità consente agli utenti malintenzionati di terminare le protezioni di sicurezza nel sistema.

Se lo scenario richiede la terminazione del processo, è necessario implementare i controlli seguenti per proteggersi dalla terminazione arbitraria del processo:

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

Possibilità di leggere e scrivere nell'input e nell'output della porta

Miglioramento della sicurezza della lettura da I/O delle porte

È necessario prestare attenzione, quando si offre la possibilità di leggere l'input/output della porta (I/O). Questo esempio di codice non è sicuro.

Func ArbitraryInputPort(int inPort) 
{
	dwResult = __indword(inPort); // Unsafe, allows for arbitrary reading from Input Port
	return dwResult; 
}

Per evitare l'abuso e l'exploit del driver, la porta di input prevista deve essere vincolata al limite di utilizzo richiesto.

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

Miglioramento della sicurezza della scrittura in I/O della porta

È necessario prestare attenzione, quando si offre la possibilità di scrivere nell'input/output della porta (I/O). Questo esempio di codice non è sicuro.

Func ArbitraryOutputPort(int outPort, DWORD dwValue) 
{
	__outdword(OutPort, dwValue); // Unsafe, allows for arbitrary writing to Output Port
}

Per evitare l'abuso e l'exploit del driver, la porta di input prevista deve essere vincolata al limite di utilizzo richiesto.

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

Possibilità di leggere e scrivere kernel, memoria fisica o del dispositivo

Miglioramento della sicurezza di Memcpy

Questo codice di esempio illustra l'uso non vincolato e non sicuro dell'uso sicuro della memoria fisica.

Func ArbitraryMemoryCopy(src, dst, length) 
{
	memcpy(dst, src, length); // Unsafe, can read and write anything from physical memory
}

Se lo scenario richiede la lettura e la scrittura di kernel, memoria fisica o del dispositivo, il driver deve sempre verificare che l'origine e le destinazioni siano vincolate agli indici o agli intervalli previsti.

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

Miglioramento della sicurezza di ZwMapViewOfSection

L'esempio seguente illustra il metodo unsafe e non corretto per leggere e scrivere memoria fisica dalla modalità utente usando le API ZwOpenSection e ZwMapViewOfSection.

Func ArbitraryMap(PHYSICAL_ADDRESS Address)
{
	ZwOpenSection(&hSection, ... ,"\Device\PhysicalMemory");
	ZwMapViewOfSection(hSection, -1, 0, 0, 0, Address, ...);
}

Per evitare abusi e exploit del comportamento di lettura/scrittura del driver da processi in modalità utente malintenzionati, il driver deve convalidare l'indirizzo di input e vincolare il mapping della memoria solo al limite di utilizzo richiesto per lo scenario.

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

Miglioramento della sicurezza di MmMapLockedPagesSpecifyCache

L'esempio seguente illustra il metodo unsafe e non corretto per leggere e scrivere memoria fisica dalla modalità utente usando le API MmMapIoSpace, IoAllocateMdl e MmMapLockedPagesSpecifyCache.

Func ArbitraryMap(PHYSICAL_ADDRESS paAddress)
{
	lpAddress = MmMapIoSpace(paAddress, qwSize, ...);
	pMdl = IoAllocateMdl( lpAddress, ...);
	MmMapLockedPagesSpecifyCache(pMdl, UserMode, ... );
}

Per evitare abusi e exploit del comportamento di lettura/scrittura del driver da processi in modalità utente malintenzionati, il driver deve convalidare l'indirizzo di input e vincolare il mapping della memoria solo al limite di utilizzo richiesto per lo scenario.

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

Vedi anche

Elenco di controllo per la sicurezza dei driver