Integrità del flusso di controllo

A partire dal 2016, circa l'86% di tutte le vulnerabilità su Android è correlato alla sicurezza della memoria. La maggior parte delle vulnerabilità viene sfruttata da malintenzionati che modificano il normale flusso di controllo di un'app per eseguire attività dannose arbitrarie con tutti i privilegi dell'app sfruttata. L'integrità del flusso di controllo (CFI) è un meccanismo di sicurezza che non consente modifiche al grafico del flusso di controllo originale di un binario compilato, rendendo molto più difficile eseguire questi attacchi.

In Android 8.1 abbiamo attivato l'implementazione di CFI di LLVM nello stack multimediale. In Android 9, abbiamo attivato CFI in più componenti e anche nel kernel. CFI di sistema è attivo per impostazione predefinita, ma devi abilitare CFI del kernel.

La CFI di LLVM richiede la compilazione con Link-Time Optimization (LTO). LTO conserva la rappresentazione del bitcode LLVM dei file oggetto fino al momento del collegamento, il che consente al compilatore di ragionare meglio su quali ottimizzazioni possono essere eseguite. L'attivazione dell'ottimizzazione interprocedurale riduce le dimensioni del file binario finale e migliora le prestazioni, ma aumenta il tempo di compilazione. Nei test su Android, la combinazione di LTO e CFI comporta un sovraccarico trascurabile per le dimensioni e le prestazioni del codice; in alcuni casi entrambi sono migliorati.

Per maggiori dettagli tecnici su CFI e su come vengono gestiti altri controlli di trasferimento del controllo, consulta la documentazione di progettazione di LLVM.

Esempi e origine

CFI viene fornito dal compilatore e aggiunge l'instrumentazione al file binario durante il tempo di compilazione. Supportiamo CFI nella toolchain Clang e nel sistema di compilazione Android in AOSP.

CFI è abilitato per impostazione predefinita per i dispositivi Arm64 per l'insieme di componenti in /platform/build/target/product/cfi-common.mk. È anche attivato direttamente nei file makefile/blueprint di un insieme di componenti multimediali, come /platform/frameworks/av/media/libmedia/Android.bp e /platform/frameworks/av/cmds/stagefright/Android.mk.

Implementazione di CFI di sistema

CFI è abilitato per impostazione predefinita se utilizzi Clang e il sistema di compilazione Android. Poiché CFI contribuisce a proteggere gli utenti Android, non devi disattivarlo.

Infatti, ti consigliamo vivamente di attivare CFI per i componenti aggiuntivi. I candidati ideali sono codice nativo privilegiato o codice nativo che elabora input utente non attendibili. Se utilizzi clang e il sistema di compilazione Android, puoi attivare CFI nei nuovi componenti aggiungendo alcune righe ai makefile o ai file blueprint.

Supporto di CFI nei makefile

Per attivare CFI in un file make, ad esempio /platform/frameworks/av/cmds/stagefright/Android.mk, aggiungi:

LOCAL_SANITIZE := cfi
# Optional features
LOCAL_SANITIZE_DIAG := cfi
LOCAL_SANITIZE_BLACKLIST := cfi_blacklist.txt
  • LOCAL_SANITIZE specifica CFI come sanificatore durante la build.
  • LOCAL_SANITIZE_DIAG attiva la modalità diagnostica per CFI. La modalità di diagnostica stampa informazioni di debug aggiuntive in logcat durante gli arresti anomali, il che è utile durante lo sviluppo e il test delle build. Assicurati di rimuovere la modalità di diagnostica dalle build di produzione.
  • LOCAL_SANITIZE_BLACKLIST consente ai componenti di disattivare selettivamente l'instrumentazione CFI per singole funzioni o file sorgente. Puoi utilizzare una lista nera come ultima risorsa per risolvere eventuali problemi riscontrati dagli utenti che potrebbero altrimenti esistere. Per maggiori dettagli, vedi Disattivazione di CFI.

Supporto di CFI nei file di progetto

Per attivare CFI in un file blueprint, ad esempio /platform/frameworks/av/media/libmedia/Android.bp, aggiungi:

   sanitize: {
        cfi: true,
        diag: {
            cfi: true,
        },
        blacklist: "cfi_blacklist.txt",
    },

Risoluzione dei problemi

Se abiliti CFI nei nuovi componenti, potresti riscontrare alcuni problemi con errori di mancata corrispondenza del tipo di funzione ed errori di mancata corrispondenza del tipo di codice assembly.

Gli errori di mancata corrispondenza del tipo di funzione si verificano perché CFI limita le chiamate indirette solo alle funzioni che hanno lo stesso tipo dinamico del tipo statico utilizzato nella chiamata. CFI limita le chiamate di funzioni membro virtuali e non virtuali solo per passare a oggetti che sono una classe derivata del tipo statico dell'oggetto utilizzato per effettuare la chiamata. Ciò significa che, quando hai un codice che viola una di queste ipotesi, la strumentazione aggiunta da CFI verrà interrotta. Ad esempio, lo stack trace mostra un SIGABRT e logcat contiene una riga relativa al controllo del flusso dell'integrità che rileva una mancata corrispondenza.

Per risolvere il problema, assicurati che la funzione chiamata abbia lo stesso tipo dichiarato staticamente. Ecco due CL di esempio:

Un altro problema possibile è il tentativo di attivare CFI in un codice che contiene chiamate indirette all'assembly. Poiché il codice assembly non viene digitato, si verifica una mancata corrispondenza del tipo.

Per risolvere il problema, crea wrapper di codice nativo per ogni chiamata di assembly e assegna ai wrapper la stessa firma della funzione del puntatore di chiamata. Il wrapper può quindi chiamare direttamente il codice assembly. Poiché i rami diretti non sono strumentati da CFI (non possono essere riassegnati in fase di runtime e quindi non rappresentano un rischio per la sicurezza), questo risolverà il problema.

Se ci sono troppe funzioni di assembly e non possono essere corrette tutte, puoi anche inserire nella blacklist tutte le funzioni che contengono chiamate indirette all'assembly. Questa operazione non è consigliata perché disattiva i controlli CFI su queste funzioni, aprendo la superficie di attacco.

Disattivazione di CFI

Non abbiamo osservato alcun sovraccarico delle prestazioni, quindi non dovresti dover disattivare CFI. Tuttavia, se si verifica un impatto sull'utente, puoi disattivare selettivamente CFI per singole funzioni o file sorgente fornendo un file di lista nera di sanificazione al momento della compilazione. La lista nera indica al compilatore di disattivare l'instrumentazione CFI nelle posizioni specificate.

Il sistema di compilazione Android fornisce il supporto per le liste nere per componente (che consentono di scegliere i file sorgente o le singole funzioni che non riceveranno l'instrumentazione CFI) sia per Make sia per Soong. Per ulteriori dettagli sul formato di un file della lista nera, consulta la documentazione di Clang upstream.

Convalida

Al momento non sono disponibili test CTS specifici per CFI. Assicurati invece che i test CTS vengano superati con o senza CFI abilitato per verificare che CFI non influisca sul dispositivo.