Implementando dm-verity

O Android 4.4 e superior oferece suporte à inicialização verificada por meio do recurso opcional do kernel device-mapper-verity (dm-verity), que fornece verificação de integridade transparente de dispositivos de bloco. O dm-verity ajuda a evitar rootkits persistentes que podem manter privilégios de root e comprometer dispositivos. Esse recurso ajuda os usuários do Android a garantir que, ao inicializar um dispositivo, ele esteja no mesmo estado de quando foi usado pela última vez.

Aplicativos potencialmente prejudiciais (PHAs) com privilégios de root podem se ocultar dos programas de detecção e se mascarar. O software de root pode fazer isso porque geralmente é mais privilegiado do que os detectores, permitindo que o software "mentira" para os programas de detecção.

O recurso dm-verity permite examinar um dispositivo de bloco, a camada de armazenamento subjacente do sistema de arquivos, e determinar se ele corresponde à configuração esperada. Ele faz isso usando uma árvore de hash criptográfica. Para cada bloco (normalmente 4k), há um hash SHA256.

Como os valores de hash são armazenados em uma árvore de páginas, somente o hash "raiz" de nível superior deve ser confiável para verificar o restante da árvore. A capacidade de modificar qualquer um dos blocos seria equivalente a quebrar o hash criptográfico. Consulte o diagrama a seguir para obter uma representação dessa estrutura.

dm-verity-hash-table

Figura 1. Tabela de hash dm-verity

Uma chave pública está incluída na partição de inicialização, que deve ser verificada externamente pelo fabricante do dispositivo. Essa chave é usada para verificar a assinatura desse hash e confirmar que a partição do sistema do dispositivo está protegida e inalterada.

Operação

A proteção dm-verity reside no kernel. Portanto, se o software de root comprometer o sistema antes que o kernel apareça, ele reterá esse acesso. Para mitigar esse risco, a maioria dos fabricantes verifica o kernel usando uma chave gravada no dispositivo. Essa chave não pode ser alterada depois que o dispositivo sai da fábrica.

Os fabricantes usam essa chave para verificar a assinatura no carregador de inicialização de primeiro nível, que por sua vez verifica a assinatura nos níveis subsequentes, no carregador de inicialização do aplicativo e, eventualmente, no kernel. Cada fabricante que deseja aproveitar a inicialização verificada deve ter um método para verificar a integridade do kernel. Assumindo que o kernel foi verificado, o kernel pode examinar um dispositivo de bloco e verificá-lo à medida que ele é montado.

Uma maneira de verificar um dispositivo de bloco é fazer um hash direto de seu conteúdo e compará-lo com um valor armazenado. No entanto, tentar verificar um dispositivo de bloco inteiro pode levar um longo período e consumir grande parte da energia de um dispositivo. Os dispositivos levariam longos períodos para inicializar e, em seguida, seriam significativamente drenados antes do uso.

Em vez disso, o dm-verity verifica os blocos individualmente e somente quando cada um é acessado. Quando lido na memória, o bloco é hash em paralelo. O hash é então verificado na árvore. E como a leitura do bloco é uma operação tão cara, a latência introduzida por essa verificação em nível de bloco é comparativamente nominal.

Se a verificação falhar, o dispositivo gera um erro de E/S indicando que o bloco não pode ser lido. Aparecerá como se o sistema de arquivos estivesse corrompido, como é esperado.

Os aplicativos podem optar por prosseguir sem os dados resultantes, como quando esses resultados não são necessários para a função principal do aplicativo. No entanto, se o aplicativo não puder continuar sem os dados, ele falhará.

Continue Correção de Erro

O Android 7.0 e superior melhora a robustez do dm-verity com a correção de erros à frente (FEC). A implementação do AOSP começa com o código comum de correção de erros Reed-Solomon e aplica uma técnica chamada intercalação para reduzir a sobrecarga de espaço e aumentar o número de blocos corrompidos que podem ser recuperados. Para obter mais detalhes sobre o FEC, consulte Inicialização verificada rigorosamente aplicada com correção de erros .

Implementação

Resumo

  1. Gere uma imagem do sistema ext4.
  2. Gere uma árvore de hash para essa imagem.
  3. Construa uma tabela dm-verity para essa árvore de hash.
  4. Assine essa tabela dm-verity para produzir uma assinatura de tabela.
  5. Agrupe a assinatura da tabela e a tabela dm-verity nos metadados da veracidade.
  6. Concatene a imagem do sistema, os metadados de verdade e a árvore de hash.

ConsulteThe Chromium Projects - Verified Boot para obter uma descrição detalhada da árvore de hash e da tabela dm-verity.

Gerando a árvore de hash

Conforme descrito na introdução, a árvore de hash é parte integrante do dm-verity. A ferramenta cryptsetup irá gerar uma árvore de hash para você. Alternativamente, um compatível é definido aqui:

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

Para formar o hash, a imagem do sistema é dividida na camada 0 em blocos de 4k, cada um atribuído a um hash SHA256. A camada 1 é formada juntando apenas os hashes SHA256 em blocos de 4k, resultando em uma imagem muito menor. A camada 2 é formada de forma idêntica, com os hashes SHA256 da camada 1.

Isso é feito até que os hashes SHA256 da camada anterior possam caber em um único bloco. Ao obter o SHA256 desse bloco, você tem o hash raiz da árvore.

O tamanho da árvore de hash (e o uso de espaço em disco correspondente) varia de acordo com o tamanho da partição verificada. Na prática, o tamanho das árvores de hash tende a ser pequeno, geralmente inferior a 30 MB.

Se você tem um bloco em uma camada que não é completamente preenchida naturalmente pelos hashes da camada anterior, você deve preenchê-lo com zeros para atingir os 4k esperados. Isso permite que você saiba que a árvore de hash não foi removida e, em vez disso, foi preenchida com dados em branco.

Para gerar a árvore de hash, concatene os hashes da camada 2 nos da camada 1, da camada 3 os hashes da camada 2 e assim por diante. Escreva tudo isso para o disco. Observe que isso não faz referência à camada 0 do hash raiz.

Para recapitular, o algoritmo geral para construir a árvore de hash é o seguinte:

  1. Escolha um sal aleatório (codificação hexadecimal).
  2. Unsparse sua imagem do sistema em blocos de 4k.
  3. Para cada bloco, obtenha seu hash SHA256 (salgado).
  4. Concatenar esses hashes para formar um nível
  5. Preencha o nível com 0s para um limite de bloco de 4k.
  6. Concatene o nível para sua árvore de hash.
  7. Repita as etapas 2 a 6 usando o nível anterior como fonte para o próximo até que você tenha apenas um único hash.

O resultado disso é um único hash, que é seu hash raiz. Este e seu salt são usados ​​durante a construção de sua tabela de mapeamento dm-verity.

Construindo a tabela de mapeamento dm-verity

Construa a tabela de mapeamento dm-verity, que identifica o dispositivo de bloco (ou destino) para o kernel e a localização da árvore de hash (que é o mesmo valor). Esse mapeamento é usado para geração e inicialização do fstab . A tabela também identifica o tamanho dos blocos e o hash_start, a localização inicial da árvore de hash (especificamente, seu número de bloco desde o início da imagem).

Consulte cryptsetup para obter uma descrição detalhada dos campos da tabela de mapeamento de destino de verdade.

Assinando a tabela dm-verity

Assine a tabela dm-verity para produzir uma assinatura de tabela. Ao verificar uma partição, a assinatura da tabela é validada primeiro. Isso é feito em uma chave na sua imagem de inicialização em um local fixo. As chaves são normalmente incluídas nos sistemas de compilação dos fabricantes para inclusão automática em dispositivos em um local fixo.

Para verificar a partição com esta assinatura e combinação de teclas:

  1. Adicione uma chave RSA-2048 em formato compatível com libmincrypt à partição /boot em /verity_key . Identifique a localização da chave usada para verificar a árvore de hash.
  2. No fstab da entrada relevante, adicione verify aos sinalizadores fs_mgr .

Agrupando a assinatura da tabela em metadados

Agrupe a assinatura da tabela e a tabela dm-verity nos metadados da veracidade. Todo o bloco de metadados é versionado para que possa ser estendido, como adicionar um segundo tipo de assinatura ou alterar alguma ordem.

Como uma verificação de sanidade, um número mágico é associado a cada conjunto de metadados da tabela que ajuda a identificar a tabela. Como o comprimento está incluído no cabeçalho da imagem do sistema ext4, isso fornece uma maneira de pesquisar os metadados sem conhecer o conteúdo dos dados em si.

Isso garante que você não optou por verificar uma partição não verificada. Nesse caso, a ausência desse número mágico interromperá o processo de verificação. Este número assemelha-se a:
0xb001b001

Os valores de byte em hexadecimal são:

  • primeiro byte = b0
  • segundo byte = 01
  • terceiro byte = b0
  • quarto byte = 01

O diagrama a seguir descreve a divisão dos metadados de verdade:

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

E esta tabela descreve esses campos de metadados.

Tabela 1. Campos de metadados Verity

Campo Propósito Tamanho Valor
número mágico usado por fs_mgr como uma verificação de sanidade 4 bytes 0xb001b001
versão usado para versionar o bloco de metadados 4 bytes atualmente 0
assinatura a assinatura da tabela no formulário acolchoado PKCS1.5 256 bytes
comprimento da mesa o comprimento da tabela dm-verity em bytes 4 bytes
tabela a tabela dm-verity descrita anteriormente bytes de comprimento da tabela
preenchimento esta estrutura é preenchida com 0 para 32k de comprimento 0

Otimizando dm-verity

Para obter o melhor desempenho do dm-verity, você deve:

  • No kernel, ative o NEON SHA-2 para ARMv7 e as extensões SHA-2 para ARMv8.
  • Experimente diferentes configurações de leitura antecipada e prefetch_cluster para encontrar a melhor configuração para seu dispositivo.