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 overflow del buffer o causare arresti anomali del sistema, che possono compromettere la sicurezza del sistema. 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 di paging, che potrebbero o meno risiedere in memoria.

  • Buffer non di paging, che devono risiedere in memoria.

Un indirizzo di memoria non valido non è di paging né di pagine. 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 non di pagina o indirizzi utente).

  • Genera il tipo di errore appropriato. Il sistema gestisce sempre gli errori del buffer da un controllo di bug, ad esempio PAGE_FAULT_IN_NONPAGED_AREA, o da un'eccezione, ad esempio 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 qualsiasi percorso di accesso che un programma dell'applicazione possa chiamare che causa un controllo di bug da parte del driver è una violazione di 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 elevato è 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 l'ottimizzazione 4 GIGABYTE (4GT). Quando si usa 4GT, gli indirizzi in modalità utente impostano il bit elevato per il terzo gigabyte dello spazio indirizzi.

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

    Le chiamate ProbeForRead e ProbeForWrite sono ancora necessarie. Se un driver omette il probe, gli utenti possono passare indirizzi validi in modalità kernel che un __try e __except blocca (gestione delle eccezioni strutturate) non intercetta e quindi apre un grande buco di sicurezza.

    La linea inferiore è che sono necessari sia il probe che la gestione strutturata delle eccezioni:

    • Il probe verifica che l'indirizzo sia un indirizzo in modalità utente 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 irriducibili.

  • 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. Un driver non riesce a usare la gestione strutturata delle eccezioni intorno a qualsiasi chiamata che potrebbe generare un'eccezione causerà un controllo dei bug (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 invio principale della routine non è la soluzione corretta per questo problema, anche se a volte è la soluzione riflessiva tentata dagli autori di driver.

  • Driver presupponendo che il contenuto della memoria utente rimanga stabile. Si supponga, ad esempio, che un driver abbia scritto un valore in un percorso di memoria in modalità utente e quindi più avanti nella stessa routine a cui si fa riferimento a tale percorso di memoria. Un'applicazione dannosa potrebbe modificare attivamente tale memoria dopo la 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

WdK contiene numerosi esempi di convalida del buffer nel codice di esempio del driver di 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 intorno al codice per zero 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.