Condividi tramite


Gestione del buffer

Uno degli errori più comuni all'interno di qualsiasi driver è correlato alla gestione del buffer, in cui i buffer non sono validi o troppo piccoli. Questi errori possono consentire sovraccarichi del buffer o causare crash di sistema, compromettendo la sicurezza. Questo articolo illustra alcuni dei problemi comuni relativi alla gestione del buffer e come evitarli. Identifica anche il codice di esempio WDK che illustra le tecniche di gestione del buffer appropriate.

Tipi di buffer e indirizzi non validi

Dal punto di vista di un driver, i buffer sono disponibili in una delle due varietà seguenti:

  • Buffer paginati, che potrebbero non risiedere in memoria.

  • Buffer non paginati, che devono risiedere in memoria.

Un indirizzo di memoria non valido non è né paginato né non paginato. Poiché il sistema operativo funziona per risolvere un errore di pagina causato dalla gestione errata del buffer, vengono impiegato i passaggi seguenti:

  • Isola l'indirizzo non valido in uno degli intervalli di indirizzi "standard" (indirizzi del kernel con paging, indirizzi del kernel senza paging o indirizzi utente).

  • Genera il tipo di errore appropriato. Il sistema gestisce sempre gli errori del buffer tramite un controllo dei bug, come PAGE_FAULT_IN_NONPAGED_AREA, o tramite un'eccezione, come STATUS_ACCESS_VIOLATION. Se l'errore è un controllo dei bug, il sistema interromperà l'operazione. Nel caso di un'eccezione, il sistema richiama gestori eccezioni basati su stack. Se nessuno dei gestori di eccezioni gestisce l'eccezione, il sistema richiama un controllo dei bug.

Indipendentemente dal fatto che un qualsiasi percorso di accesso possa essere chiamato da un programma dell'applicazione e causare un controllo dei bug da parte del driver, ciò rappresenta una violazione della sicurezza all'interno del driver. Tale violazione consente a un'applicazione di causare attacchi Denial of Service all'intero sistema.

Presupposti ed errori comuni

Uno dei problemi più comuni in questo settore è che gli autori di driver presuppongono troppo sull'ambiente operativo. Alcuni presupposti e errori comuni includono:

  • Un driver controlla semplicemente se il bit più significativo è impostato nell'indirizzo. L'uso di un modello a bit fisso per determinare il tipo di indirizzo non funziona in tutti i sistemi o scenari. Ad esempio, questo controllo non funziona nei computer basati su x86 quando il sistema usa il Four Gigabyte Tuning (4GT). Quando si usa 4GT, gli indirizzi in modalità utente impostano il bit elevato per il terzo gigabyte dello spazio indirizzi.

  • Un driver che esclusivamente usa ProbeForRead e ProbeForWrite per convalidare l'indirizzo. Queste chiamate assicurano che l'indirizzo sia un valido indirizzo in modalità utente al momento della sonda. Tuttavia, non ci sono garanzie che questo indirizzo rimarrà valido dopo l'operazione di sondaggio. Pertanto, questa tecnica introduce una condizione di competizione sottile che può portare a arresti anomali irreproducibili periodici.

    Le chiamate ProbeForRead e ProbeForWrite sono ancora necessarie. Se un driver omette il probe, gli utenti possono inserire indirizzi validi in modalità kernel che un blocco di __try e __except (gestione strutturata delle eccezioni) non intercetterà, creando così una grande falla di sicurezza.

    In sintesi, sono necessari sia la verifica che la gestione strutturata delle eccezioni.

    • Il controllo verifica che l'indirizzo sia un indirizzo in modalità user-mode e che la lunghezza del buffer sia compresa nell'intervallo di indirizzi utente.

    • Un __try/__except blocco protegge dall'accesso.

    Si noti che ProbeForRead convalida solo che l'indirizzo e la lunghezza rientrano nel possibile intervallo di indirizzi in modalità utente (leggermente inferiore a 2 GB per un sistema senza 4GT, ad esempio), non se l'indirizzo di memoria è valido. Al contrario, ProbeForWrite tenta di accedere al primo byte in ogni pagina della lunghezza specificata per verificare che questi byte siano indirizzi di memoria validi.

  • Un driver che si basa su funzioni di gestione della memoria, ad esempio MmIsAddressValid , per assicurarsi che l'indirizzo sia valido. Come descritto per le funzioni probe, questa situazione introduce una race condition che può causare arresti anomali irriproducibili.

  • Un driver non riesce a usare la gestione strutturata delle eccezioni. Le __try/except funzioni all'interno del compilatore usano il supporto a livello di sistema operativo per la gestione delle eccezioni. Le eccezioni a livello di kernel vengono restituite al sistema tramite una chiamata a ExRaiseStatus o a una delle funzioni correlate. Se un driver non utilizza la gestione strutturata delle eccezioni per ogni chiamata che potrebbe generare un'eccezione, ciò provoca un bug check (in genere KMODE_EXCEPTION_NOT_HANDLED).

    È un errore usare la gestione strutturata delle eccezioni per il codice che non dovrebbe generare errori. Questo utilizzo maschera solo bug reali che altrimenti verrebbero trovati. L'inserimento di un __try/__except wrapper al livello di distribuzione principale della routine non è la soluzione corretta per questo problema, anche se a volte è la soluzione istintiva tentata dagli sviluppatori di driver.

  • Un driver che presuppone che il contenuto della memoria utente rimanga stabile. Si supponga, ad esempio, che un driver abbia scritto un valore in una posizione di memoria in modalità utente e quindi più avanti nella stessa routine si faccia nuovamente riferimento a tale posizione di memoria. Un'applicazione dannosa potrebbe apportare modifiche attive a tale memoria dopo la fase di scrittura e, di conseguenza, causare l'arresto anomalo del driver.

Per i file system, questi problemi sono gravi perché i file system si basano in genere sull'accesso diretto ai buffer utente (il metodo di trasferimento METHOD_NEITHER). Tali driver manipolano direttamente i buffer utente e pertanto devono incorporare metodi precauzionali per la gestione del buffer per evitare arresti anomali a livello di sistema operativo. L'I/O veloce passa sempre puntatori di memoria non elaborati, quindi i driver devono proteggersi da problemi simili se è supportato un I/O veloce.

Codice di esempio per la gestione del buffer

Il WDK contiene numerosi esempi di convalida del buffer nel codice di esempio del driver del file system fastfat e CDFS, tra cui:

  • La funzione FatLockUserBuffer in fastfat\deviosup.c usa MmProbeAndLockPages per bloccare le pagine fisiche dietro il buffer utente e MmGetSystemAddressForMdlSafe in FatMapUserBuffer per creare un mapping virtuale per le pagine bloccate.

  • La funzione FatGetVolumeBitmap in fastfat\fsctl.c usa ProbeForRead e ProbeForWrite per convalidare i buffer utente nell'API di deframmentazione.

  • La funzione CdCommonRead in cdfs\read.c usa __try e __except attorno al codice per azzerare i buffer utente. Il codice di esempio in CdCommonRead sembra usare le try parole chiave e except . Nell'ambiente WDK queste parole chiave in C sono definite in termini di estensioni __try del compilatore e __except. Chiunque usi codice C++ deve usare i tipi di compilatore nativi per gestire correttamente le eccezioni, come __try è una parola chiave C++, ma non una parola chiave C, e fornirà una forma di gestione delle eccezioni C++ non valida per i driver del kernel.