Implementa dm-verity

Android 4.4 y versiones posteriores admiten el inicio verificado a través de la función opcional del kernel device-mapper-verity (dm-verity), que proporciona una verificación de integridad transparente de los dispositivos de almacenamiento en bloque. dm-verity ayuda a evitar rootkits persistentes que pueden conservar privilegios de administrador y vulnerar los dispositivos. Esta función ayuda a los usuarios de Android a asegurarse de que, cuando se inicia un dispositivo, este se encuentre en el mismo estado que cuando se usó por última vez.

Las aplicaciones potencialmente dañinas (APD) con privilegios de raíz pueden ocultarse de los programas de detección y enmascararse de otras maneras. El software de acceso raíz puede hacer esto porque, a menudo, tiene más privilegios que los detectores, lo que le permite “mentir” a los programas de detección.

La función dm-verity te permite ver un dispositivo de almacenamiento en bloques, la capa de almacenamiento subyacente del sistema de archivos y determinar si coincide con la configuración esperada. Para ello, usa un árbol de hash criptográfico. Para cada bloque (por lo general, 4 K), hay un hash SHA256.

Debido a que los valores de hash se almacenan en un árbol de páginas, solo se debe confiar en el hash “raiz” de nivel superior para verificar el resto del árbol. La capacidad de modificar cualquiera de los bloques equivaldría a romper el hash criptográfico. Consulta el siguiente diagrama para ver una representación de esta estructura.

dm-verity-hash-table

Figura 1: Tabla hash de dm-verity

Se incluye una clave pública en la partición de inicio, que el fabricante del dispositivo debe verificar de forma externa. Esa clave se usa para verificar la firma de ese hash y confirmar que la partición del sistema del dispositivo esté protegida y sin cambios.

Operación

La protección de dm-verity se encuentra en el kernel. Por lo tanto, si el software de acceso raíz vulnera el sistema antes de que se inicie el kernel, retiene ese acceso. Para mitigar este riesgo, la mayoría de los fabricantes verifican el kernel con una clave quemada en el dispositivo. Esa clave no se puede cambiar una vez que el dispositivo sale de fábrica.

Los fabricantes usan esa clave para verificar la firma en el bootloader de primer nivel, que a su vez verifica la firma en los niveles posteriores, el bootloader de la app y, finalmente, el kernel. Cada fabricante que desee aprovechar el inicio verificado debe tener un método para verificar la integridad del kernel. Si se supone que se verificó el kernel, este puede ver un dispositivo de almacenamiento en bloques y verificarlo a medida que se activa.

Una forma de verificar un dispositivo de almacenamiento en bloques es generar un hash directamente de su contenido y compararlo con un valor almacenado. Sin embargo, intentar verificar un dispositivo de almacenamiento en bloques completo puede llevar un período prolongado y consumir mucha energía. Los dispositivos tardarían períodos prolongados en iniciarse y, luego, se agotarían considerablemente antes de usarlos.

En su lugar, dm-verity verifica los bloques de forma individual y solo cuando se accede a cada uno. Cuando se lee en la memoria, el bloque se hash en paralelo. Luego, el hash se verifica en el árbol. Y, como leer el bloque es una operación tan costosa, la latencia que introduce esta verificación a nivel del bloque es comparativamente nominal.

Si la verificación falla, el dispositivo genera un error de E/S que indica que no se puede leer el bloque. Parece que el sistema de archivos se dañó, como se espera.

Las apps pueden optar por continuar sin los datos resultantes, por ejemplo, cuando esos resultados no son necesarios para la función principal de la app. Sin embargo, si la app no puede continuar sin los datos, falla.

Corrección de errores por anticipado

Android 7.0 y versiones posteriores mejoran la robustez de dm-verity con la corrección de errores por anticipado (FEC). La implementación de AOSP comienza con el código de corrección de errores Reed-Solomon común y aplica una técnica llamada intercalación para reducir la sobrecarga de espacio y aumentar la cantidad de bloques dañados que se pueden recuperar. Para obtener más detalles sobre la FEC, consulta Inicio verificado de aplicación estricta con corrección de errores.

Implementación

Resumen

  1. Genera una imagen del sistema ext4.
  2. Genera un árbol hash para esa imagen.
  3. Compila una tabla dm-verity para ese árbol de hash.
  4. Firma esa tabla de dm-verity para generar una firma de tabla.
  5. Agrupa la firma de la tabla y la tabla dm-verity en los metadatos de Verity.
  6. Concatena la imagen del sistema, los metadatos de Verity y el árbol de hash.

Consulta Los proyectos de Chromium: Inicio verificado para obtener una descripción detallada del árbol de hash y la tabla dm-verity.

Genera el árbol hash

Como se describe en la introducción, el árbol de hash es fundamental para dm-verity. La herramienta cryptsetup genera un árbol de hash por ti. Como alternativa, aquí se define uno compatible:

<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 el hash, la imagen del sistema se divide en la capa 0 en bloques de 4 K, a cada uno de los cuales se le asigna un hash SHA256. La capa 1 se forma uniendo solo esos valores hash SHA256 en bloques de 4K, lo que genera una imagen mucho más pequeña. La capa 2 se forma de manera idéntica, con los hashes SHA256 de la capa 1.

Esto se hace hasta que los valores hash SHA256 de la capa anterior puedan caber en un solo bloque. Cuando obtienes el SHA256 de ese bloque, tienes el hash raíz del árbol.

El tamaño del árbol de hash (y el uso correspondiente del espacio en el disco) varía según el tamaño de la partición verificada. En la práctica, el tamaño de los árboles hash suele ser pequeño, a menudo inferior a 30 MB.

Si tienes un bloque en una capa que no se completa de forma natural con los valores hash de la capa anterior, debes rellenarlo con ceros para lograr los 4 K esperados. Esto te permite saber que el árbol de hash no se quitó y, en su lugar, se completó con datos en blanco.

Para generar el árbol hash, concatena los valores hash de la capa 2 con los de la capa 1, los de la capa 3 con los de la capa 2, y así sucesivamente. Escribe todo esto en el disco. Ten en cuenta que esto no hace referencia a la capa 0 del hash raíz.

En resumen, el algoritmo general para construir el árbol hash es el siguiente:

  1. Elige una sal aleatoria (codificación hexadecimal).
  2. Descompacta la imagen del sistema en bloques de 4K.
  3. Para cada bloque, obtén su hash SHA256 (salado).
  4. Concatena estos valores hash para formar un nivel
  5. Agrega ceros al nivel hasta alcanzar un límite de bloque de 4K.
  6. Concatena el nivel a tu árbol hash.
  7. Repite los pasos del 2 al 6 con el nivel anterior como fuente del siguiente hasta que solo tengas un hash.

El resultado es un hash único, que es tu hash raíz. Esta y tu sal se usan durante la construcción de tu tabla de asignación de dm-verity.

Compila la tabla de asignación de dm-verity

Compila la tabla de asignación de dm-verity, que identifica el dispositivo de almacenamiento en bloques (o destino) para el kernel y la ubicación del árbol de hash (que es el mismo valor). Esta asignación se usa para la generación y el inicio de fstab. La tabla también identifica el tamaño de los bloques y hash_start, la ubicación de inicio del árbol hash (específicamente, su número de bloque desde el principio de la imagen).

Consulta cryptsetup para obtener una descripción detallada de los campos de la tabla de asignación de destino de Verity.

Firma la tabla dm-verity

Firma la tabla dm-verity para generar una firma de tabla. Cuando se verifica una partición, primero se valida la firma de la tabla. Esto se hace con una clave en la imagen de inicio en una ubicación fija. Por lo general, las claves se incluyen en los sistemas de compilación de los fabricantes para su inclusión automática en dispositivos en una ubicación fija.

Para verificar la partición con esta firma y combinación de claves, haz lo siguiente:

  1. Agrega una clave RSA-2048 en formato compatible con libmincrypt a la partición /boot en /verity_key. Identifica la ubicación de la clave que se usa para verificar el árbol de hash.
  2. En el archivo fstab de la entrada relevante, agrega verify a las marcas fs_mgr.

Combina la firma de la tabla en los metadatos

Combina la firma de la tabla y la tabla dm-verity en los metadatos de Verity. Todo el bloque de metadatos tiene control de versiones para que se pueda extender, por ejemplo, para agregar un segundo tipo de firma o cambiar algún orden.

Como verificación de coherencia, se asocia un número mágico con cada conjunto de metadatos de la tabla que ayuda a identificarla. Debido a que la longitud se incluye en el encabezado de la imagen del sistema ext4, esto proporciona una forma de buscar los metadatos sin conocer el contenido de los datos.

Esto garantiza que no hayas elegido verificar una partición no verificada. Si es así, la ausencia de este número mágico detendrá el proceso de verificación. Este número se parece a 0xb001b001.

Los valores de bytes en hexadecimal son los siguientes:

  • primer byte = b0
  • segundo byte = 01
  • tercer byte = b0
  • cuarto byte = 01

En el siguiente diagrama, se muestra el desglose de los metadatos de Verity:

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

En esta tabla, se describen esos campos de metadatos.

Tabla 1: Campos de metadatos de Verity

Campo Propósito Tamaño Valor
número mágico que usa fs_mgr como verificación de coherencia 4 bytes 0xb001b001
version Se usa para crear una versión del bloque de metadatos. 4 bytes actualmente 0
firma la firma de la tabla en formato PKCS1.5 relleno 256 bytes
longitud de la tabla la longitud de la tabla dm-verity en bytes 4 bytes
mesa la tabla dm-verity que se describió antes bytes de longitud de la tabla
relleno esta estructura tiene un padding de 0 hasta 32,000 de longitud 0

Optimiza dm-verity

Para obtener el mejor rendimiento de dm-verity, debes hacer lo siguiente:

  • En el kernel, activa NEON SHA-2 para ARMv7 y las extensiones SHA-2 para ARMv8.
  • Experimenta con diferentes parámetros de configuración de read-ahead y prefetch_cluster para encontrar la mejor configuración para tu dispositivo.