Condividi tramite


Funzioni di accesso in modalità utente

Le funzioni di accesso in modalità utente (UMA) sono un set di DDI progettato per accedere in modo sicuro e modificare la memoria in modalità utente dal codice in modalità kernel. Queste DDI consentono di risolvere le vulnerabilità di sicurezza comuni e gli errori di programmazione che possono verificarsi quando i driver in modalità kernel accedono alla memoria in modalità utente.

Il codice in modalità kernel che accede o modifica la memoria in modalità utente sarà presto necessario per usare UMA.

Possibili problemi durante l'accesso alla memoria in modalità utente dalla modalità kernel

Quando il codice in modalità kernel deve accedere alla memoria in modalità utente, si verificano diversi problemi:

  • Le applicazioni in modalità utente possono passare puntatori dannosi o non validi al codice in modalità kernel. La mancanza di convalida corretta può causare danneggiamento della memoria, arresti anomali o vulnerabilità di sicurezza.

  • Il codice in modalità utente è multithreading. Di conseguenza, thread diversi potrebbero modificare la stessa memoria in modalità utente tra accessi separati in modalità kernel, probabilmente causando un danneggiamento della memoria del kernel.

  • Gli sviluppatori in modalità kernel spesso dimenticano di sondare la memoria in modalità utente prima di accedervi, il che rappresenta un problema di sicurezza.

  • I compilatori presuppongono l'esecuzione a thread singolo e potrebbero ottimizzare gli accessi alla memoria ridondanti. I programmatori non a conoscenza di tali ottimizzazioni possono scrivere codice non sicuro.

I frammenti di codice seguenti illustrano questi problemi.

Esempio 1: Possibile danneggiamento della memoria a causa del multithreading in modalità utente

Il codice in modalità kernel che deve accedere alla memoria in modalità utente deve farlo all'interno di un __try/__except blocco per assicurarsi che la memoria sia valida. Il frammento di codice seguente mostra un modello tipico per l'accesso alla memoria in modalità utente:

// User-mode structure definition
typedef struct _StructWithData {
    ULONG Size;
    CHAR* Data[1];
} StructWithData;

// Kernel-mode call that accesses user-mode memory
void MySysCall(StructWithData* Ptr) {
    __try {
        // Probe user-mode memory to ensure it's valid
        ProbeForRead(Ptr, sizeof(StructWithData), 1);

        // Allocate memory in the kernel
        PVOID LocalData = ExAllocatePool2(POOL_FLAG_PAGED, Ptr->Size);
        
        // Copy user-mode data into the heap allocation
        RtlCopyMemory(LocalData, Ptr->Data, Ptr->Size);
    } __except (…) {
        // Handle exceptions
    }
}

Questo frammento esplora prima la memoria, che è un passaggio importante ma spesso trascurato.

Tuttavia, un problema che può verificarsi in questo codice è dovuto al multithreading in modalità utente. In particolare, Ptr->Size potrebbe cambiare dopo la chiamata a ExAllocatePool2 , ma prima della chiamata a RtlCopyMemory, causando potenzialmente il danneggiamento della memoria nel kernel.

Esempio 2: Possibili problemi causati da ottimizzazioni del compilatore

Un tentativo di risolvere il problema di multithreading nell'esempio 1 potrebbe essere quello di copiare Ptr->Size in una variabile locale prima dell'allocazione e della copia:

void MySysCall(StructWithData* Ptr) {
    __try {
        // Probe user-mode memory to ensure it's valid
        ProbeForRead(Ptr, sizeof(StructWithData), 1);
        
        // Read Ptr->Size once to avoid possible memory change in user mode
        ULONG LocalSize = Ptr->Size;
        
        // Allocate memory in the kernel
        PVOID LocalData = ExAllocatePool2(POOL_FLAG_PAGED, LocalSize);
        
        //Copy user-mode data into the heap allocation
        RtlCopyMemory(LocalData, Ptr, LocalSize);
    } __except (…) {}
}

Sebbene questo approccio mitiga il problema causato dal multithreading, non è ancora sicuro perché il compilatore non è a conoscenza di più thread e presuppone quindi un singolo thread di esecuzione. Come ottimizzazione, il compilatore potrebbe verificare che abbia già una copia del valore a cui Ptr->Size punta nello stack e quindi evitare di effettuare la copia in LocalSize.

Soluzione delle funzioni di accesso in modalità utente

L'interfaccia UMA risolve i problemi riscontrati durante l'accesso alla memoria in modalità utente dalla modalità kernel. UMA fornisce:

  • Sondaggio automatico: il sondaggio esplicito (ProbeForRead/ProbeForWrite) non è più necessario, perché tutte le funzioni UMA garantiscono la sicurezza degli indirizzi di memoria.

  • Accesso volatile: tutte le DDI UMA USANO la semantica volatile per impedire le ottimizzazioni del compilatore.

  • Facilità di portabilità: il set completo di DDI UMA consente ai clienti di convertire facilmente il codice esistente per l'uso di DDI UMA, assicurando che la memoria in modalità utente sia accessibile in modo sicuro e corretto.

Esempio di utilizzo di UMA DDI

Usando la struttura in modalità utente definita in precedenza, il frammento di codice seguente illustra come usare UMA per accedere in modo sicuro alla memoria in modalità utente.

void MySysCall(StructWithData* Ptr) {
    __try {

        // This UMA call probes the passed user-mode memory and does a
        // volatile read of Ptr->Size to ensure it isn't optimized away by the compiler.
        ULONG LocalSize = ReadULongFromUser(&Ptr->Size);
        
        // Allocate memory in the kernel.
        PVOID LocalData = ExAllocatePool2(POOL_FLAG_PAGED, LocalSize);
        
        //This UMA call safely copies UM data into the KM heap allocation.
        CopyFromUser(&LocalData, Ptr, LocalSize);
        
        // To be safe, set LocalData->Size to be LocalSize, which was the value used
        // to make the pool allocation just in case LocalData->Size was changed.
        ((StructWithData*)LocalData)->Size = LocalSize;

    } __except (…) {}
}

Implementazione e utilizzo di UMA

L'interfaccia UMA viene fornita come parte di Windows Driver Kit (WDK):

  • Le dichiarazioni di funzione si trovano nel file di intestazione usermode_accessors.h .
  • Le implementazioni della funzione si trovano in una libreria statica denominata umaccess.lib.

UMA funziona su tutte le versioni di Windows, non solo sulla versione più recente. È necessario usare la versione più recente di WDK per ottenere rispettivamente le dichiarazioni e le implementazioni delle funzioni da usermode_accessors.h e umaccess.lib. Il driver risultante verrà eseguito correttamente nelle versioni precedenti di Windows.

Umaccess.lib offre un'implementazione sicura di livello inferiore per tutte le DDI. Nelle versioni con riconoscimento UMA del kernel di Windows, tutti i driver avranno tutte le relative funzioni reindirizzate a una versione più sicura implementata in ntoskrnl.exe.

Tutte le funzioni di accesso in modalità utente devono essere eseguite all'interno di un gestore di eccezioni strutturato (SEH) a causa di potenziali eccezioni durante l'accesso alla memoria in modalità utente.

Tipi di DDI di accesso in modalità utente

UMA offre diverse DDI per diversi tipi di accesso alla memoria in modalità utente. La maggior parte di queste DDI è destinata ai tipi di dati fondamentali, ad esempio BOOLEAN, ULONG e puntatori. Inoltre, UMA fornisce DDIs per l'accesso alla memoria di massa, il recupero della lunghezza delle stringhe e le operazioni sincronizzate.

DDI generici per i tipi di dati fondamentali

UMA fornisce sei varianti di funzione per la lettura e la scrittura di tipi di dati semplici. Ad esempio, per i valori BOOLEAN sono disponibili le funzioni seguenti:

Nome funzione Description
ReadBooleanFromUser Leggere un valore dalla memoria in modalità utente.
ReadBooleanFromUserAcquire Leggere un valore dalla memoria in modalità utente con semantica di acquisizione per l'ordinamento della memoria.
ReadBooleanFromMode Leggere dalla memoria in modalità utente o in modalità kernel in base a un parametro di modalità.
WriteBooleanToUser Scrivere un valore nella memoria in modalità utente.
WriteBooleanToUserRelease Scrivere un valore nella memoria in modalità utente con semantica di rilascio per l'ordinamento della memoria.
WriteBooleanToMode Scrivere nella memoria in modalità utente o in modalità kernel in base a un parametro di modalità.

Per le funzioni ReadXxxFromUser, il parametro Source deve puntare allo spazio degli indirizzi virtuali in modalità utente. Lo stesso vale nelle versioni ReadXxxFromMode quando Mode == UserMode.

Per ReadXxxFromMode, quando Mode == KernelMode, il parametro Source deve puntare al VAS in modalità kernel. Se viene definita la definizione del preprocessore DBG, l'operazione si interrompe rapidamente con il codice FAST_FAIL_KERNEL_POINTER_EXPECTED.

Nelle funzioni WriteXxxToUser il parametro Destination deve puntare al vaS in modalità utente. Lo stesso vale nelle versioni WriteXxxToMode quando Mode == UserMode.

DDI di modifica della copia e della memoria

UMA fornisce funzioni per la copia e lo spostamento della memoria tra le modalità utente e kernel, incluse le varianti per le copie non temporali e allineate. Queste funzioni sono contrassegnate con annotazioni che indicano potenziali eccezioni SEH e requisiti IRQL (max APC_LEVEL).

Esempi includono CopyFromUser, CopyToMode e CopyFromUserToMode.

Le macro come CopyFromModeAligned e CopyFromUserAligned includono il probe di allineamento per motivi di sicurezza prima di eseguire l'operazione di copia.

Le macro come CopyFromUserNonTemporal e CopyToModeNonTemporal forniscono copie nonmporali che evitano l'inquinamento della cache.

Macro di lettura/scrittura della struttura

Le macro per la lettura e la scrittura di strutture tra le modalità garantiscono la compatibilità e l'allineamento dei tipi, richiamando funzioni di supporto con parametri di dimensioni e modalità. Gli esempi includono WriteStructToMode, ReadStructFromUser e le relative varianti allineate.

Funzioni di riempimento e memoria zero

Le DDI vengono fornite per riempire o azzerare la memoria negli spazi di indirizzi utente o di modalità, con parametri che specificano destinazione, lunghezza, valore di riempimento e modalità. Queste funzioni contengono anche annotazioni SEH e IRQL.

Gli esempi includono FillUserMemory e ZeroModeMemory.

Operazioni interbloccate

UMA includono operazioni interlocked per l'accesso alla memoria atomica, che sono essenziali per le manipolazioni di memoria thread-safe in ambienti concorrenti. Le DDI sono disponibili sia per i valori a 32 bit che per i valori a 64 bit, con versioni destinate alla memoria utente o memoria in modalità kernel.

Gli esempi includono InterlockedCompareExchangeToUser, InterlockedOr64ToMode e InterlockedAndToUser.

DDI di lunghezza delle stringhe

Le funzioni per determinare in modo sicuro le lunghezze delle stringhe dalla memoria utente o dalla memoria di sistema sono incluse, supportando sia stringhe ANSI che stringhe a caratteri larghi. Queste funzioni sono progettate per generare eccezioni per l'accesso non sicuro alla memoria e sono vincolate da IRQL.

Ad esempio StringLengthFromUser e WideStringLengthFromMode.

Funzioni di accesso per stringhe Unicode e interi di grandi dimensioni

UMA fornisce DDIs per leggere e scrivere i tipi LARGE_INTEGER, ULARGE_INTEGER e UNICODE_STRING tra memoria utente e memoria modalità. Le varianti hanno una semantica di acquisizione e rilascio con parametri di modalità per la sicurezza e la correttezza.

Gli esempi includono ReadLargeIntegerFromUser, WriteUnicodeStringToMode e WriteULargeIntegerToUser.

Semantica di acquisizione e rilascio

In alcune architetture, ad esempio ARM, la CPU può riordinare gli accessi alla memoria. Le DDI generiche hanno tutte un'implementazione acquire/release se è necessaria una garanzia che gli accessi alla memoria non siano riordinati per l'accesso in modalità utente.

  • Acquisire semantica impedisce il riordinamento del carico rispetto ad altre operazioni di memoria.
  • La semantica di rilascio impedisce il riordinamento dell'archivio rispetto ad altre operazioni di memoria.

Esempi di semantica di acquisizione e rilascio in UMA includono ReadULongFromUserAcquire e WriteULongToUserRelease.

Per altre informazioni, vedere Semantica di acquisizione e rilascio.

Procedure consigliate

  • Usare sempre DDI UMA quando si accede alla memoria in modalità utente dal codice del kernel.
  • Gestire le eccezioni con blocchi appropriati __try/__except .
  • Usare le DDI basate sulla modalità quando il codice potrebbe gestire sia la modalità utente che la memoria in modalità kernel.
  • Prendere in considerazione la semantica di acquisizione/rilascio quando l'ordinamento della memoria è importante per il caso d'uso.
  • Convalidare i dati copiati dopo averlo copiato nella memoria kernel per garantire la coerenza.

Supporto hardware futuro

Le funzioni di accesso in modalità utente sono progettate per supportare funzionalità di sicurezza hardware future, ad esempio:

  • SMAP (Prevenzione dell'accesso in modalità supervisore): impedisce al codice kernel di accedere alla memoria in modalità utente, ad eccezione di funzioni designate, ad esempio UMA DDI.
  • ARM PAN (Privileged Access Never): protezione simile nelle architetture ARM.

Usando UMA DDIs in modo coerente, i driver saranno compatibili con questi miglioramenti della sicurezza quando sono abilitati nelle versioni future di Windows.