Implementazione di dm-verity

Android 4.4 e versioni successive supportano l'avvio verificato tramite la funzionalità del kernel device-mapper-verity (dm-verity) opzionale, che fornisce un controllo trasparente dell'integrità dei dispositivi a blocchi. dm-verity aiuta a prevenire i rootkit persistenti che possono mantenere i privilegi di 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 funzione 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 usando un albero hash crittografico. Per ogni blocco (tipicamente 4k), c'è un hash SHA256.

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

dm-verity-hash-table

Figura 1. Tabella hash dm-verity

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

Operazione

La protezione dm-verity risiede nel kernel. Quindi, se il software di rooting compromette il sistema prima che il kernel si attivi, manterrà tale accesso. Per mitigare questo rischio, la maggior parte dei produttori verifica il kernel utilizzando una chiave masterizzata nel dispositivo. Quella 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 e infine 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 del suo contenuto e confrontarlo con un valore memorizzato. Tuttavia, il tentativo di verificare un intero dispositivo a blocchi può richiedere un periodo prolungato e consumare gran parte della potenza di un dispositivo. I dispositivi impiegherebbero lunghi periodi per avviarsi e quindi sarebbero notevolmente scaricati 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é la lettura del blocco è un'operazione così costosa, la latenza introdotta da questa verifica a livello di blocco è relativamente nominale.

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

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

Correzione dell'errore in avanti

Android 7.0 e versioni successive migliorano la solidità 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 l'overhead di spazio e aumentare il numero di blocchi danneggiati che possono essere recuperati. Per maggiori dettagli su FEC, vedere Avvio verificato rigorosamente applicato con correzione degli errori .

Implementazione

Riepilogo

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

Vedi TheChromium Projects - Verified Boot per una descrizione dettagliata dell'albero hash e della tabella dm-verity.

Generazione dell'albero hash

Come descritto nell'introduzione, l'albero hash è parte integrante di dm-verity. Lo strumento cryptsetup genererà un albero hash 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 suddivisa al livello 0 in 4k blocchi, ciascuno assegnato a un hash SHA256. Il livello 1 è formato unendo solo quegli hash SHA256 in blocchi 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 non possono rientrare in un singolo blocco. Quando ottieni SHA256 di quel blocco, hai l'hash radice 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 alberi di hash 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 layer 2 su quelli del layer 1, gli hash del layer 3 su quelli del layer 2 e così via. Scrivi tutto questo su disco. Si noti che questo non fa riferimento al livello 0 dell'hash radice.

Ricapitolando, l'algoritmo generale per costruire l'albero hash è il seguente:

  1. Scegli un sale casuale (codifica esadecimale).
  2. Suddividi 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 albero di hash.
  7. Ripeti i passaggi da 2 a 6 utilizzando il livello precedente come origine per il successivo finché non avrai un solo hash.

Il risultato è un singolo hash, che è il tuo root hash. Questo e il tuo salt vengono utilizzati durante la costruzione della tua 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 l'hash_start, la posizione iniziale dell'albero hash (in particolare, il suo numero di blocco dall'inizio dell'immagine).

Vedere cryptsetup per una descrizione dettagliata dei campi della tabella di mappatura del target di verità.

Firmare la tabella dm-verity

Firma la tabella dm-verity per produrre una firma della tabella. Quando si verifica una partizione, la firma della tabella viene convalidata per prima. Questo viene fatto contro una chiave sull'immagine di avvio in una posizione fissa. Le chiavi sono in genere incluse nei sistemi di compilazione dei produttori per l'inclusione automatica sui dispositivi in ​​una posizione fissa.

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

  1. Aggiungere 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 nei metadati

Raggruppa la firma della tabella e la tabella dm-verity nei metadati di verità. L'intero blocco di metadati è versionato in modo che possa essere esteso, ad esempio per aggiungere un secondo tipo di firma o modificare alcuni ordini.

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

Questo assicura che tu non abbia 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 di Verity

Campo Scopo Misurare Valore
numero magico utilizzato da fs_mgr come controllo di integrità 4 byte 0xb001b001
versione utilizzato per la versione del blocco di metadati 4 byte attualmente 0
firma la firma del tavolo in forma imbottita 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 è imbottita da 0 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 con diverse impostazioni read-ahead e prefetch_cluster per trovare la migliore configurazione per il tuo dispositivo.