Hackear ESP32 omitiendo el arranque seguro y el cifrado de flash (CVE-2020-13629)

Realizamos un estudio del microcontrolador Espressif ESP32 para resistencia a ataques realizado por el método de introducción de fallas en el funcionamiento de chips (Fault Injection). Poco a poco, nos hemos movido para encontrar vulnerabilidades que nos permitan eludir los mecanismos de arranque seguro y cifrado de flash con solo una falla causada por un campo electromagnético. Además, después de llevar a cabo con éxito el ataque, no solo pudimos ejecutar código arbitrario, sino que también recibimos los datos de la memoria flash descifrados. Espressif ha informado de esta vulnerabilidad en la base de datos CVE con el código CVE-2020-13629







... Al leer el ataque descrito en este artículo, tenga en cuenta que se aplica a los chips ESP32 de revisión 0 y 1. Los ESP32 V3 más nuevos admiten la función de desactivación del cargador de arranque UART utilizada en este ataque.



Cargador de arranque UART



En ESP32, el cargador de arranque UART se implementa en código ROM. Esto hace posible, entre otras cosas, escribir programas en una memoria flash externa. Implementar el cargador de arranque UART como código almacenado en ROM es una solución común. Es bastante confiable debido al hecho de que dicho código no se daña fácilmente. Si esta funcionalidad se basara en el código almacenado en la memoria flash externa, cualquier daño a dicha memoria conduciría a la inoperabilidad completa del microcontrolador.



Normalmente, el acceso a dicha funcionalidad se organiza cuando el chip se carga en un modo especial, en modo de arranque. La elección de este modo se realiza mediante puentes de contacto (o puentes), configurados antes de reiniciar el dispositivo. El ESP32 usa un pin para esto G0.



El cargador de arranque UART admite muchosinstrucciones que se pueden usar para leer / escribir memoria y registros e incluso ejecutar programas desde SRAM.



▍Ejecución de código arbitrario



El cargador UART admite la carga y ejecución de código arbitrario mediante comando load_ram. El SDK de ESP32 incluye todas las herramientas necesarias para compilar código que se puede ejecutar desde SRAM. Por ejemplo, el siguiente fragmento de código envía una cadena SRAM CODE\na la interfaz en serie.



void __attribute__((noreturn)) call_start_cpu0()
{
    ets_printf("SRAM CODE\n");
    while (1);
}


La herramienta esptool.py, que es parte del ESP32 SDK, se puede usar para cargar binarios compilados en SRAM. Entonces estos archivos se pueden ejecutar.



esptool.py --chip esp32 --no-stub --port COM3 load_ram code.bin


Curiosamente, el cargador de arranque UART no se puede deshabilitar. Por lo tanto, siempre hay acceso a él, incluso si el arranque seguro y el cifrado de la memoria flash están habilitados.



▍Medidas de seguridad adicionales



Obviamente, a menos que se tomen medidas de seguridad adicionales, la disponibilidad constante del cargador de arranque UART hará que el arranque seguro y los mecanismos de cifrado de la memoria flash sean prácticamente inútiles. Por lo tanto, Espressif ha implementado mecanismos de seguridad adicionales que se basan en la tecnología eFuse.



Estos son los bits que se utilizan para configurar los parámetros de seguridad, que se almacenan en una memoria especial a menudo denominada memoria OTP (memoria programable una vez). Los bits en dicha memoria solo pueden cambiar de 0 a 1, pero no en la dirección opuesta. Esto asegura que si se ha configurado un bit que habilita una función, nunca se volverá a borrar. Cuando el ESP32 está funcionando en el modo de cargador de arranque UART, los siguientes bits de la memoria OTP se utilizan para deshabilitar ciertas capacidades:



  • DISABLE_DL_ENCRYPT: -.
  • DISABLE_DL_DECRYPT: -.
  • DISABLE_DL_CACHE: MMU- -.


Estamos más interesados ​​en el bit de memoria OTP DISABLE_DL_DECRYPT, ya que deshabilita el descifrado transparente de los datos almacenados en la memoria flash.



Si este bit no está configurado, entonces, al cargar el microcontrolador usando el gestor de arranque UART, puede organizar el acceso simple a los datos almacenados en la memoria flash, trabajando con ellos como con texto ordinario.



Si este bit está establecido, entonces, en el modo de arranque usando el cargador de arranque UART, solo se pueden leer datos encriptados de la memoria. La funcionalidad de cifrado flash, completamente implementada en hardware y transparente para el procesador, se habilita solo cuando el ESP32 arranca en modo Normal.



Al realizar el ataque del que estamos hablando aquí, todos estos bits se ponen a 1.



Los datos SRAM persisten después del reinicio en caliente del dispositivo



La SRAM utilizada por el microcontrolador ESP32 es bastante común. Lo mismo lo utilizan muchos chips. Por lo general, se usa junto con la ROM y es responsable de iniciar el primer cargador de arranque desde la memoria flash. Esta memoria es conveniente de usar en las primeras etapas de carga, ya que no es necesario configurar nada antes de usarla.



La experiencia de investigaciones anteriores nos dice que los datos almacenados en SRAM no cambian hasta que se sobrescriben o hasta que no se suministra más electricidad a las celdas de memoria. Después de un reinicio en frío (es decir, un ciclo de encendido / apagado) del chip, el contenido de SRAM se restablecerá a su estado predeterminado. Cada chip de dicha memoria se distingue por un estado único (se podría decir, semi-aleatorio) de los bits establecidos en los valores 0 y 1.



Pero después de un reinicio en caliente, cuando el chip se reinicia sin apagar la alimentación, puede suceder que los datos almacenados en la SRAM sigan siendo los mismos que antes. Esto se muestra en la siguiente figura.





Impacto de los reinicios en frío (arriba) y en caliente (abajo) en el contenido de SRAM



Decidimos averiguar si lo anterior es cierto para el ESP32. Descubrimos que puede usar un temporizador de vigilancia de hardware para realizar un arranque en caliente suave. Puede forzar que este temporizador se active incluso cuando el chip está en modo de arranque utilizando el cargador de arranque UART. Como resultado, puede utilizar este mecanismo para poner el ESP32 en modo de arranque normal.



Usando el código de prueba, que se cargó en SRAM y se ejecutó usando el cargador de arranque UART, determinamos que los datos en SRAM, de hecho, persisten después de un reinicio en caliente iniciado por el temporizador de vigilancia. Y esto significa que nosotros, habiendo registrado lo que necesitamos en SRAM, podemos arrancar el ESP32 como de costumbre.



Entonces surgió la pregunta sobre cómo podemos usar esto.



El camino al fracaso



Asumimos que podríamos aprovechar el hecho de que los datos se guardan en SRAM después de un reinicio en caliente para un ataque. Nuestro primer ataque fue que escribimos un código en SRAM usando el cargador de arranque UART, y luego, usando el temporizador de vigilancia, realizamos un reinicio en caliente del dispositivo. Luego hicimos un bloqueo al ejecutarlo mientras el código ROM sobrescribe ese código con el código del cargador de arranque flash durante el arranque normal.



Tuvimos esta idea después de convertir el proceso de transferencia de datos en el proceso de ejecución de código en el curso de experimentos anteriores . Luego notamos que el chip comienza a ejecutar el código desde la dirección de inicio antes de que el cargador de arranque complete la copia.



A veces, para lograr algo, solo necesitas intentarlo ...



▍Código cargado en SRAM y utilizado para realizar el ataque



Aquí está el código que escribimos en SRAM usando el cargador de arranque UART.



#define a "addi a6, a6, 1;"
#define t a a a a a a a a a a
#define h t t t t t t t t t t
#define d h h h h h h h h h h

void __attribute__((noreturn)) call_start_cpu0() {
    uint8_t cmd;

    ets_printf("SRAM CODE\n");

    while (1) {

        cmd = 0;
        uart_rx_one_char(&cmd);

        if(cmd == 'A') {                                    // 1
            *(unsigned int *)(0x3ff4808c) = 0x4001f880;
            *(unsigned int *)(0x3ff48090) = 0x00003a98;
            *(unsigned int *)(0x3ff4808c) = 0xc001f880;
        }
    }

    asm volatile ( d );                                     // 2

    "movi a6, 0x40; slli a6, a6, 24;"                       // 3
    "movi a7, 0x00; slli a7, a7, 16;"
    "xor a6, a6, a7;"
    "movi a7, 0x7c; slli a7, a7, 8;"
    "xor a6, a6, a7;"
    "movi a7, 0xf8;"
    "xor a6, a6, a7;"

    "movi a10, 0x52; callx8  a6;" // R
    "movi a10, 0x61; callx8  a6;" // a            
    "movi a10, 0x65; callx8  a6;" // e               
    "movi a10, 0x6C; callx8  a6;" // l               
    "movi a10, 0x69; callx8  a6;" // i               
    "movi a10, 0x7A; callx8  a6;" // z               
    "movi a10, 0x65; callx8  a6;" // e               
    "movi a10, 0x21; callx8  a6;" // !               
    "movi a10, 0x0a; callx8  a6;" // \n               

    while(1);
}


Este código implementa lo siguiente (los números de los elementos de la lista corresponden a los números especificados en los comentarios):



  1. Un controlador de comando de comando único que restablece el temporizador de vigilancia.
  2. Un análogo NOPbasado en instrucciones addi.
  3. Código de ensamblaje que envía una cadena a la interfaz en serie Raelize!.


▍Elegir el momento del ataque



Tuvimos una ventana de ataque relativamente pequeña, comenzando con F, como se muestra en la siguiente figura. Sabíamos por experimentos anteriores que el código del cargador de arranque se está copiando de la memoria flash en este momento.





La ventana de ataque está representada por F La



falla debe ocurrir antes de que el contenido de SRAM se sobrescriba por completo con el código correcto del cargador de arranque de la memoria flash.



▍ Ciclo de ataque



En cada uno de nuestros experimentos, tomamos los siguientes pasos para verificar que la idea del ataque funcionó. La organización exitosa de la falla debería haber dado como resultado una salida a la interfaz de línea serial Raelize!.



  • Establezca el pin G0bajo y realice un arranque en frío para ingresar al modo de cargador de arranque UART.
  • Usar un comando load_rampara ejecutar código de ataque desde SRAM.
  • Envía el programa Apara reiniciar en caliente y volver al modo de inicio normal.
  • Organización de un fallo en el proceso de copia del gestor de arranque desde la memoria flash utilizando el código de la ROM.


▍Resultados



Después de realizar este experimento durante más de un día, habiéndolo realizado más de un millón de veces, todavía no lo logramos.



▍ Resultado inesperado



Pero, a pesar de que no logramos lograr lo que queríamos, analizando los resultados de los experimentos encontramos algo inesperado.



En un experimento, se mostraron datos en la interfaz en serie que indicaban que una falla resultó en una excepción IllegalInstruction(instrucción no válida). Así es como se veía:



ets Jun  8 2016 00:22:57
rst:0x10 (RTCWDT_RTC_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
configsip: 0, SPIWP:0xee
clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
mode:DIO, clock div:2
load:0x3fff0008,len:4
load:0x3fff000c,len:3220
load:0x40078000,len:4816
load:0x40080400,len:18640
entry 0x40080740
Fatal exception (0): IllegalInstruction
epc1=0x661b661b, epc2=0x00000000, epc3=0x00000000, 
excvaddr=0x00000000, depc=0x00000000


Al intentar provocar una falla en el chip, estas excepciones ocurren con bastante frecuencia. Lo mismo ocurre con el ESP32. Para la mayoría de estas excepciones, el registro se PCestablece en el valor esperado (es decir, la dirección correcta se encuentra allí). Rara vez ocurre que PCaparece un significado tan interesante.



Se IllegalInstructionlanza la excepción porque 0x661b661bno hay una instrucción correcta en la dirección . Decidimos que este valor PCdebería ingresar al registro desde algún lugar y que por sí solo no puede aparecer allí.



En busca de una explicación, analizamos el código que cargamos en SRAM. Ver el código binario, un fragmento del cual se muestra a continuación, nos permitió encontrar rápidamente la respuesta a nuestra pregunta. Es decir, es fácil encontrar el significado aquí.0x661b661b... Está representado por dos instrucciones addi a6, a6, 1, con la ayuda de las cuales se implementa el análogo en el código NOP.



00000000  e9 02 02 10 28 04 08 40  ee 00 00 00 00 00 00 00  |....(..@........|
00000010  00 00 00 00 00 00 00 01  00 00 ff 3f 0c 00 00 00  |...........?....|
00000020  53 52 41 4d 20 43 4f 44  45 0a 00 00 00 04 08 40  |SRAM CODE......@|
00000030  50 09 00 00 00 00 ff 3f  04 04 fe 3f 4d 04 08 40  |P......?...?M..@|
00000040  00 04 fe 3f 8c 80 f4 3f  90 80 f4 3f 98 3a 00 00  |...?...?...?.:..|
00000050  80 f8 01 c0 54 7d 00 40  d0 92 00 40 36 61 00 a1  |....T}.@...@6a..|
00000060  f5 ff 81 fc ff e0 08 00  0c 08 82 41 00 ad 01 81  |...........A....|
00000070  fa ff e0 08 00 82 01 00  4c 19 97 98 1f 81 ef ff  |........L.......|
00000080  91 ee ff 89 09 91 ee ff  89 09 91 f0 ff 81 ee ff  |................|
00000090  99 08 91 ef ff 81 eb ff  99 08 86 f2 ff 5c a9 97  |.............\..|
000000a0  98 c5 1b 66 1b 66 1b 66  1b 66 1b 66 1b 66 3e 0c  |...f.f.f.f.f.f>.|
000000b0  1b 66 1b 66 1b 66 1b 66  1b 66 1b 66 1b 66 1b 66  |.f.f.f.f.f.f.f.f|
000000c0  1b 66 1b 66 1b 66 1b 66  1b 66 1b 66 1b 66 1b 66  |.f.f.f.f.f.f.f.f|
000000d0  1b 66 1b 66 1b 66 1b 66  1b 66 1b 66 1b 66 1b 66  |.f.f.f.f.f.f.f.f|
...
00000330  1b 66 1b 66 1b 66 1b 66  1b 66 1b 66 1b 66 1b 66  |.f.f.f.f.f.f.f.f|
00000340  1b 66 1b 66 1b 66 1b 66  1b 66 1b 66 1b 66 1b 66  |.f.f.f.f.f.f.f.f|
00000350  1b 66 1b 66 1b 66 1b 66  1b 66 1b 66 1b 66 1b 66  |.f.f.f.f.f.f.f.f|


Preparamos un "margen de maniobra" con estas instrucciones, usándolas de una manera similar a como las secuencias de comandos NOPse usan a menudo en exploits para retrasar la ejecución del código hasta el momento adecuado. No esperábamos que estas instrucciones terminaran en el registro PC.



Pero, por supuesto, no estábamos en contra de usar esto. Decidimos que podíamos cargar datos de SRAM en un registro PCdurante un bloqueo causado cuando los datos de la memoria flash se copiaban mediante código ROM.



Rápidamente nos dimos cuenta de que ahora teníamos todos los ingredientes para preparar un ataque que eludiría los sistemas de cifrado flash y de arranque seguro con una sola falla. Aquí utilizamos la experiencia obtenida durante la ejecución del ataque descrito anteriormente.cuando logramos tomar el control del registro PC.



Camino al éxito



En este ataque, usamos la mayor parte del código que se cargó previamente en SRAM usando el gestor de arranque UART. Solo los comandos para enviar caracteres a la interfaz serial se han eliminado de este código, ya que ahora nuestro objetivo era configurar el registro PCen el valor que necesitábamos, es decir, obtener la capacidad de controlar el sistema.



#define a "addi a6, a6, 1;"
#define t a a a a a a a a a a
#define h t t t t t t t t t t
#define d h h h h h h h h h h

void __attribute__((noreturn)) call_start_cpu0() {
    uint8_t cmd;
   
    ets_printf("SRAM CODE\n");

    while (1) {

        cmd = 0;
        uart_rx_one_char(&cmd);

        if(cmd == 'A') {
            *(unsigned int *)(0x3ff4808c) = 0x4001f880;
            *(unsigned int *)(0x3ff48090) = 0x00003a98;
            *(unsigned int *)(0x3ff4808c) = 0xc001f880;
        }
    }

    asm volatile ( d );

    while(1);
}


Después de compilar este código, nosotros, directamente en su versión binaria, reemplazamos las instrucciones addicon una dirección 0x4005a980. En esta dirección hay una función en la ROM que envía datos a la interfaz en serie. Una llamada exitosa a esta función nos informaría sobre un ataque exitoso.



Nos preparamos para manejar fallas que fueran consistentes con lo que causó la excepción en un experimento anterior IllegalInstruction. Después de un tiempo, descubrimos la finalización exitosa de varios experimentos para cargar la PCdirección dada en el registro . Es PCmuy probable que el control de casos signifique que podemos ejecutar código arbitrario.



▍ ¿Por qué es esto posible?



El título de esta sección contiene una buena pregunta que no es fácil de responder.



Desafortunadamente, no tenemos una respuesta clara. Ciertamente no esperábamos que la manipulación de datos permitiera el control de registros PC. Tenemos varias explicaciones para esto, pero no podemos afirmar con total certeza que alguna de ellas sea cierta.



Una explicación es que durante un bloqueo, ambos operandos de la instrucción se ldrutilizan para cargar el valor en a0. Esto es similar a lo que vimos en este ataque, donde obtuvimos control indirecto sobre el registro PCmodificando los datos.



Además, es posible que el código almacenado en ROM implemente una funcionalidad que contribuya al éxito de este ataque. En otras palabras, debido a una falla, podemos ejecutar el código correcto desde la ROM, lo que lleva al hecho de que los datos de la SRAM se cargan en el registro PC.



Para saber qué es exactamente lo que nos permitió realizar este ataque, necesitamos investigar más. Pero si miras el asunto a través de los ojos de alguien que decidió piratear el chip, tenemos el conocimiento suficiente para crear un exploit basado en la posibilidad de influir en el registro PC.



Extraiga el contenido de la memoria flash como texto sin formato



Podemos escribir en el registro PClo que queramos, pero aún no podemos recuperar el contenido de la memoria flash como texto sin formato. Por lo tanto, se decidió aprovechar las capacidades del cargador de arranque UART.



Es decir, decidimos ir directamente al gestor de arranque UART mientras el chip está en modo de arranque normal. Para llevar a cabo este ataque, reescribimos las instrucciones addien el código cargado en la RAM, usando la dirección de inicio del código del cargador de arranque UART ( 0x40007a19) en su lugar .



El cargador de arranque UART envía la línea que se muestra a continuación a la interfaz en serie. Podemos utilizar este hecho para determinar el éxito de un ataque.



waiting for download\n"


Una vez que este experimento sea exitoso, simplemente podemos usarlo esptool.pypara ejecutar el comando read_memy acceder a los datos de texto sin formato en la memoria flash. Por ejemplo, el siguiente comando lee 4 bytes del espacio de direcciones flash externo ( 0x3f400000).



esptool.py --no-stub --before no_reset --after no_reset read_mem 0x3f400000


Desafortunadamente, tal comando no funcionó. Por alguna razón, la respuesta del procesador parecía 0xbad00badindicar que estamos tratando de leer datos de memoria no asignada.



esptool.py v2.8
Serial port COM8
Connecting....
Detecting chip type... ESP32
Chip is ESP32D0WDQ6 (revision 1)
Crystal is 40MHz
MAC: 24:6f:28:24:75:08
Enabling default SPI flash mode...
0x3f400000 = 0xbad00bad
Staying in bootloader.


Notamos que se realizan bastantes configuraciones al comienzo del cargador de arranque UART. Supusimos que estos ajustes también podrían afectar a la MMU.



Solo para probar otra cosa, decidimos ir directamente al controlador de comandos del propio cargador de arranque UART ( 0x40007a4e). Una vez que nos encontramos en el controlador, podemos enviar el comando de forma independiente read_memdirectamente a la interfaz serie:



target.write(b'\xc0\x00\x0a\x04\x00\x00\x00\x00\x00\x00\x00\x40\x3f\xc0')


Desafortunadamente, si va directamente al controlador, no se mostrará la línea que se muestra después de ingresar al gestor de arranque UART (es decir, - waiting for download\n). Debido a esto, perdemos una forma simple y conveniente de identificar experimentos exitosos. Como resultado, decidimos enviar el comando anterior en todos los experimentos, independientemente de si tuvieron éxito o no. Usamos un tiempo de espera en serie muy corto para minimizar el tiempo de espera adicional asociado con este tiempo de espera, que es casi siempre el caso.



Después de un tiempo, ¡vimos los resultados de los primeros experimentos exitosos!



Salir



En este artículo, describimos un ataque a ESP32, durante el cual omitimos los sistemas de arranque seguro y encriptación de la memoria flash, arreglando solo una falla en el microcontrolador. Además, utilizamos una vulnerabilidad explotada durante el ataque para extraer el contenido de la memoria flash cifrada en texto sin formato.



Podemos usar FIRM para superar este ataque .





Progreso del ataque



Aquí hay una breve descripción de lo que sucede en los diferentes pasos del ataque anterior:



  1. Activar (la elección de herramientas para llevar a cabo un ataque): aquí se utiliza el complejo Riscure Inspector FI .
  2. Inyectar (ataque): se lleva a cabo un efecto electromagnético en el microcontrolador bajo investigación.
  3. Glitch ( ) — , (, , ).
  4. Fault ( ) — , , , . , - .
  5. Exploit ( ) — UART , SRAM, . UART PC read_mem.
  6. Goal ( ) — - .


Curiosamente, el éxito de este ataque depende de dos debilidades en el ESP32. La primera debilidad es que el gestor de arranque UART no se puede deshabilitar. Como resultado, siempre está disponible. La segunda debilidad es la persistencia de datos en SRAM después de un reinicio en caliente del dispositivo. Esto permite usar el cargador de arranque UART para llenar la SRAM con datos arbitrarios.



En un informe informativo , que se refiere al ataque, la empresa Espressif informa que en las versiones más recientes de ESP32 existen mecanismos que hacen imposible tal ataque.



Todos los sistemas integrados estándar son vulnerables a los ataques de interrupción de dispositivos. Por lo tanto, no es sorprendente que el microcontrolador ESP32 también sea vulnerable a los ataques de canal lateral. Los chips como estos simplemente no están diseñados para resistir tales ataques. Pero, lo que es más importante, esto no significa que tales ataques no conlleven ningún riesgo.



Nuestra investigación ha demostrado que explotar las debilidades del chip permite ataques e interrupciones exitosos. La mayoría de los ataques de los que se puede obtener información a partir de fuentes abiertas utilizan enfoques tradicionales, donde el enfoque principal es eludir los controles. No hemos visto muchos informes de ataques como el que describimos.



Confiamos en que aún no se ha explorado plenamente el potencial de esos ataques. Hasta hace poco, la mayoría de los investigadores solo estudiaban métodos para interrumpir el funcionamiento de los chips (pasos Activate, Inject, Glitch), pero fuimos más allá, considerando la posibilidad de trabajar con un chip vulnerable después de una falla (pasos Fault, Exploit, Goal).





Investigación hasta 2020 y más allá de 2020



Estamos seguros de que el uso creativo de nuevos modelos de fallas de chips conducirá a un aumento de los métodos de ataque que utilizan estrategias interesantes de explotación de vulnerabilidades para lograr una amplia variedad de objetivos.



Si está interesado en el tema que se plantea en este material, aquí , aquí y aquí , otros materiales dedicados al estudio de ESP32.



¿Se ha encontrado en la práctica con la piratería de dispositivos con métodos similares a los discutidos en este artículo?










All Articles