... 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\n
a 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):
- Un controlador de comando de comando único que restablece el temporizador de vigilancia.
- Un análogo
NOP
basado en instruccionesaddi
. - 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
G0
bajo y realice un arranque en frío para ingresar al modo de cargador de arranque UART. - Usar un comando
load_ram
para ejecutar código de ataque desde SRAM. - Envía el programa
A
para 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
PC
establece en el valor esperado (es decir, la dirección correcta se encuentra allí). Rara vez ocurre que PC
aparece un significado tan interesante.
Se
IllegalInstruction
lanza la excepción porque 0x661b661b
no hay una instrucción correcta en la dirección . Decidimos que este valor PC
deberí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
NOP
se 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
PC
durante 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
PC
en 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
addi
con 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 PC
dirección dada en el registro . Es PC
muy 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
ldr
utilizan para cargar el valor en a0
. Esto es similar a lo que vimos en este ataque, donde obtuvimos control indirecto sobre el registro PC
modificando 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
PC
lo 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
addi
en 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.py
para ejecutar el comando read_mem
y 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
0xbad00bad
indicar 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_mem
directamente 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:
- Activar (la elección de herramientas para llevar a cabo un ataque): aquí se utiliza el complejo Riscure Inspector FI .
- Inyectar (ataque): se lleva a cabo un efecto electromagnético en el microcontrolador bajo investigación.
- Glitch ( ) — , (, , ).
- Fault ( ) — , , , . , - .
- Exploit ( ) — UART , SRAM, . UART
PC
read_mem
. - 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?