Implementazione di dm-verity

Android 4.4 e versioni successive supportano l'avvio verificato tramite la funzionalità kernel opzionale device-mapper-verity (dm-verity), che fornisce un controllo trasparente dell'integrità dei dispositivi a blocchi. dm-verity aiuta a prevenire rootkit persistenti che possono mantenere i privilegi root e compromettere i dispositivi. Questa funzione aiuta gli utenti Android ad essere sicuri che quando si avvia un dispositivo si trovi nello stesso stato in cui è stato utilizzato l'ultima volta.

Le applicazioni potenzialmente dannose (PHA) con privilegi di root possono nascondersi dai programmi di rilevamento e mascherarsi in altro modo. Il software di rooting può farlo perché spesso è più privilegiato dei rilevatori, consentendo al software di "mentire" ai programmi di rilevamento.

La funzionalità dm-verity ti consente di esaminare un dispositivo a blocchi, il livello di archiviazione sottostante del file system, e determinare se corrisponde alla configurazione prevista. Lo fa utilizzando un hash tree crittografico. Per ogni blocco (tipicamente 4k), esiste un hash SHA256.

Poiché i valori hash sono archiviati in un albero di pagine, solo l'hash "root" di livello superiore deve essere considerato attendibile per verificare il resto dell'albero. La possibilità di modificare uno qualsiasi dei blocchi equivarrebbe a rompere l'hash crittografico. Vedere il diagramma seguente per una rappresentazione di questa struttura.

dm-verity-tabella-hash

Figura 1. tabella hash dm-verity

Nella partizione di avvio è inclusa una chiave pubblica, che deve essere verificata esternamente dal produttore del dispositivo. Quella chiave viene utilizzata per verificare la firma dell'hash e confermare che la partizione di sistema del dispositivo è protetta e invariata.

Operazione

La protezione dm-verity risiede nel kernel. Pertanto, se il software di rooting compromette il sistema prima che venga attivato il kernel, manterrà tale accesso. Per mitigare questo rischio, la maggior parte dei produttori verifica il kernel utilizzando una chiave masterizzata nel dispositivo. Tale chiave non è modificabile una volta che il dispositivo lascia la fabbrica.

I produttori utilizzano quella chiave per verificare la firma sul bootloader di primo livello, che a sua volta verifica la firma sui livelli successivi, il bootloader dell'applicazione ed eventualmente il kernel. Ogni produttore che desideri sfruttare l'avvio verificato dovrebbe disporre di un metodo per verificare l'integrità del kernel. Supponendo che il kernel sia stato verificato, il kernel può esaminare un dispositivo a blocchi e verificarlo mentre viene montato.

Un modo per verificare un dispositivo a blocchi è eseguire direttamente l'hashing dei suoi contenuti e confrontarli con un valore memorizzato. Tuttavia, il tentativo di verificare un intero dispositivo a blocchi può richiedere un periodo prolungato e consumare gran parte dell'energia del dispositivo. I dispositivi impiegherebbero lunghi periodi per avviarsi e quindi verrebbero scaricati in modo significativo prima dell'uso.

Invece, dm-verity verifica i blocchi individualmente e solo quando si accede a ciascuno di essi. Quando viene letto in memoria, il blocco viene sottoposto ad hashing in parallelo. L'hash viene quindi verificato sull'albero. E poiché leggere il blocco è un’operazione molto costosa, la latenza introdotta da questa verifica a livello di blocco è relativamente nominale.

Se la verifica fallisce, il dispositivo genera un errore I/O che indica che il blocco non può essere letto. Apparirà come se il filesystem fosse stato danneggiato, come previsto.

Le applicazioni possono scegliere di procedere senza i dati risultanti, ad esempio quando tali risultati non sono necessari per la funzione principale dell'applicazione. Tuttavia, se l'applicazione non può continuare senza i dati, fallirà.

Correzione degli errori in avanti

Android 7.0 e versioni successive migliorano la robustezza di dm-verity con la correzione degli errori in avanti (FEC). L'implementazione AOSP inizia con il comune codice di correzione degli errori Reed-Solomon e applica una tecnica chiamata interleaving per ridurre lo spazio in eccesso e aumentare il numero di blocchi danneggiati che possono essere recuperati. Per ulteriori dettagli su FEC, consulta Avvio verificato rigorosamente applicato con correzione errori .

Implementazione

Riepilogo

  1. Genera un'immagine del sistema ext4.
  2. Genera un albero hash per quell'immagine.
  3. Costruisci una tabella dm-verity per quell'hash tree.
  4. Firma la tabella dm-verity per produrre una firma della tabella.
  5. Raggruppa la firma della tabella e la tabella dm-verity nei metadati verity.
  6. Concatena l'immagine di sistema, i metadati di verità e l'albero hash.

Consulta The Chromium Projects - Verified Boot per una descrizione dettagliata dell'hash tree e della tabella dm-verity.

Generazione dell'albero di hash

Come descritto nell'introduzione, l'albero hash è parte integrante di dm-verity. Lo strumento cryptsetup genererà un hash tree per te. In alternativa, uno compatibile è definito qui:

<your block device name> <your block device name> <block size> <block size> <image size in blocks> <image size in blocks + 8> <root hash> <salt>

Per formare l'hash, l'immagine del sistema viene divisa al livello 0 in blocchi da 4k, a ciascuno assegnato un hash SHA256. Il livello 1 è formato unendo solo gli hash SHA256 in blocchi da 4k, risultando in un'immagine molto più piccola. Il livello 2 è formato in modo identico, con gli hash SHA256 del livello 1.

Questo viene fatto fino a quando gli hash SHA256 del livello precedente possono rientrare in un unico blocco. Quando ottieni lo SHA256 di quel blocco, hai l'hash root dell'albero.

La dimensione dell'albero hash (e il corrispondente utilizzo dello spazio su disco) varia in base alla dimensione della partizione verificata. In pratica, la dimensione degli hash tree tende ad essere piccola, spesso inferiore a 30 MB.

Se hai un blocco in un livello che non è completamente riempito naturalmente dagli hash del livello precedente, dovresti riempirlo con zeri per ottenere i 4k previsti. Ciò ti consente di sapere che l'albero hash non è stato rimosso ed è invece completato con dati vuoti.

Per generare l'albero hash, concatenare gli hash del livello 2 su quelli del livello 1, il livello 3 gli hash su quelli del livello 2 e così via. Scrivi tutto questo su disco. Tieni presente che questo non fa riferimento al livello 0 dell'hash root.

Ricapitolando, l’algoritmo generale per costruire l’hash tree è il seguente:

  1. Scegli un sale casuale (codifica esadecimale).
  2. Dividi l'immagine del tuo sistema in blocchi da 4K.
  3. Per ogni blocco, ottieni il suo hash SHA256 (salato).
  4. Concatena questi hash per formare un livello
  5. Riempi il livello con 0 fino a un limite di blocco di 4k.
  6. Concatena il livello al tuo hash tree.
  7. Ripeti i passaggi 2-6 utilizzando il livello precedente come fonte per quello successivo finché non avrai un solo hash.

Il risultato è un singolo hash, che è il tuo hash root. Questo e il tuo sale vengono utilizzati durante la costruzione della tabella di mappatura dm-verity.

Creazione della tabella di mappatura dm-verity

Costruisci la tabella di mappatura dm-verity, che identifica il dispositivo a blocchi (o destinazione) per il kernel e la posizione dell'albero hash (che è lo stesso valore). Questa mappatura viene utilizzata per la generazione e l'avvio fstab . La tabella identifica anche la dimensione dei blocchi e hash_start, la posizione iniziale dell'hash tree (nello specifico, il numero del blocco dall'inizio dell'immagine).

Vedere cryptsetup per una descrizione dettagliata dei campi della tabella di mappatura dei target Verity.

Firma la tabella dm-verity

Firma la tabella dm-verity per produrre una firma della tabella. Quando si verifica una partizione, viene convalidata prima la firma della tabella. Questa operazione viene eseguita rispetto a una chiave sull'immagine di avvio in una posizione fissa. Le chiavi sono generalmente incluse nei sistemi di build dei produttori per l'inclusione automatica sui dispositivi in ​​una posizione fissa.

Per verificare la partizione con questa firma e combinazione di tasti:

  1. Aggiungi una chiave RSA-2048 in formato compatibile con libmincrypt alla partizione /boot in /verity_key . Identificare la posizione della chiave utilizzata per verificare l'albero hash.
  2. Nel fstab per la voce pertinente, aggiungi verify ai flag fs_mgr .

Raggruppamento della firma della tabella in metadati

Raggruppa la firma della tabella e la tabella dm-verity nei metadati verity. L'intero blocco di metadati è dotato di versione in modo che possa essere esteso, ad esempio per aggiungere un secondo tipo di firma o modificare l'ordine.

Come controllo di integrità, a ogni set di metadati della tabella viene associato un numero magico che aiuta a identificare la tabella. Poiché la lunghezza è inclusa nell'intestazione dell'immagine di sistema ext4, ciò fornisce un modo per cercare i metadati senza conoscere il contenuto dei dati stessi.

Questo ti assicura di non aver scelto di verificare una partizione non verificata. In tal caso, l'assenza di questo numero magico interromperà il processo di verifica. Questo numero assomiglia a:
0xb001b001

I valori dei byte in esadecimale sono:

  • primo byte = b0
  • secondo byte = 01
  • terzo byte = b0
  • quarto byte = 01

Il diagramma seguente illustra la suddivisione dei metadati di verità:

<magic number>|<version>|<signature>|<table length>|<table>|<padding>
\-------------------------------------------------------------------/
\----------------------------------------------------------/   |
                            |                                  |
                            |                                 32K
                       block content

E questa tabella descrive quei campi di metadati.

Tabella 1. Campi dei metadati Verity

Campo Scopo Misurare Valore
numero magico utilizzato da fs_mgr come controllo di integrità 4 byte 0xb001b001
versione utilizzato per controllare la versione del blocco di metadati 4 byte attualmente 0
firma la firma della tabella in formato imbottito PKCS1.5 256 byte
lunghezza del tavolo la lunghezza della tabella dm-verity in byte 4 byte
tavolo la tabella dm-verity descritta in precedenza byte di lunghezza della tabella
imbottitura questa struttura è riempita con 0 fino a 32k di lunghezza 0

Ottimizzazione di dm-verity

Per ottenere le migliori prestazioni da dm-verity, dovresti:

  • Nel kernel, attiva NEON SHA-2 per ARMv7 e le estensioni SHA-2 per ARMv8.
  • Sperimenta diverse impostazioni di read-ahead e prefetch_cluster per trovare la migliore configurazione per il tuo dispositivo.