
... Ao ler o ataque descrito neste artigo, lembre-se de que ele se aplica aos chips ESP32 de revisão 0 e 1. Os ESP32 V3s mais recentes suportam a funcionalidade de desabilitação do carregador de inicialização UART usada neste ataque.
Bootloader UART
No ESP32, o bootloader UART é implementado no código ROM. Isso possibilita, entre outras coisas, gravar programas em uma memória flash externa. Implementar o carregador de inicialização UART como código armazenado em ROM é uma solução comum. É bastante confiável devido ao fato de que tal código não é facilmente danificado. Se essa funcionalidade fosse baseada no código armazenado na memória flash externa, qualquer dano a essa memória levaria à completa inoperabilidade do microcontrolador.
Normalmente, o acesso a tal funcionalidade é organizado quando o chip é carregado em um modo especial, no modo de boot. A escolha deste modo é feita através de jumpers de contato (ou jumpers), configurados antes da reinicialização do dispositivo. O ESP32 usa pino para isso
G0
.
O carregador de inicialização UART oferece suporte a muitosinstruções que podem ser usadas para ler / escrever memória e registros, e até mesmo para executar programas de SRAM.
▍ Execução de código arbitrário
O carregador UART suporta o carregamento e a execução de código arbitrário usando um comando
load_ram
. O ESP32 SDK inclui todas as ferramentas necessárias para compilar o código que pode ser executado a partir do SRAM. Por exemplo, o fragmento de código a seguir gera uma string SRAM CODE\n
para a interface serial.
void __attribute__((noreturn)) call_start_cpu0()
{
ets_printf("SRAM CODE\n");
while (1);
}
A ferramenta
esptool.py
, que faz parte do ESP32 SDK, pode ser usada para carregar binários compilados na SRAM. Então, esses arquivos podem ser executados.
esptool.py --chip esp32 --no-stub --port COM3 load_ram code.bin
Curiosamente, o carregador de inicialização UART não pode ser desativado. Portanto, sempre haverá acesso a ele, mesmo se a inicialização segura e a criptografia da memória flash estiverem ativadas.
▍ Medidas de segurança adicionais
Obviamente, a menos que medidas adicionais de segurança sejam tomadas, a disponibilidade constante do carregador de inicialização UART tornará a inicialização segura e os mecanismos de criptografia da memória flash praticamente inúteis. Portanto, Espressif implementou mecanismos de segurança adicionais que são baseados na tecnologia eFuse.
Esses são os bits usados para configurar os parâmetros de segurança, que são armazenados em uma memória especial, geralmente chamada de memória OTP (One-Time-Programmable Memory). Os bits nessa memória só podem mudar de 0 para 1, mas não na direção oposta. Isso garante que, se um bit habilitando um recurso tiver sido definido, ele nunca será apagado novamente. Quando o ESP32 está operando no modo bootloader UART, os seguintes bits da memória OTP são usados para desativar certos recursos:
DISABLE_DL_ENCRYPT
: -.DISABLE_DL_DECRYPT
: -.DISABLE_DL_CACHE
: MMU- -.
Estamos mais interessados no bit de memória OTP
DISABLE_DL_DECRYPT
, pois desativa a descriptografia transparente dos dados armazenados na memória flash.
Se este bit não estiver definido, então, ao carregar o microcontrolador usando o bootloader UART, você pode organizar o acesso simples aos dados armazenados na memória flash, trabalhando com eles como se fosse um texto comum.
Se este bit for definido, então, no modo de inicialização usando o carregador de inicialização UART, apenas dados criptografados podem ser lidos da memória. A funcionalidade de criptografia Flash, totalmente implementada no hardware e transparente para o processador, é ativada apenas quando o ESP32 inicializa no modo Normal.
Ao realizar o ataque de que estamos falando aqui, todos esses bits são definidos como 1.
Dados SRAM persistentes após reinicialização a quente do dispositivo
A SRAM usada pelo microcontrolador ESP32 é bastante comum. O mesmo é usado por muitos chips. Geralmente é usado em conjunto com a ROM e é responsável por iniciar o primeiro carregador de inicialização a partir da memória flash. Essa memória é conveniente para usar nos estágios iniciais de carregamento, uma vez que nada precisa ser configurado antes de usá-la.
A experiência de pesquisas anteriores nos diz que os dados armazenados na SRAM não mudam até que sejam sobrescritos ou até que nenhuma eletricidade seja fornecida às células de memória. Após uma reinicialização a frio (ou seja, um ciclo de liga / desliga) do chip, o conteúdo da SRAM será redefinido para seu estado padrão. Cada chip de tal memória é distinguido por um estado único (pode-se dizer, semi-aleatório) dos bits definidos para os valores 0 e 1.
Mas após uma reinicialização a quente, quando o chip é reinicializado sem desligar a energia, pode acontecer que os dados armazenados na SRAM permaneçam os mesmos. Isto é mostrado na figura a seguir.

Impacto das reinicializações a frio (acima) e a quente (abaixo) no conteúdo SRAM
Decidimos descobrir se o descrito acima é verdadeiro para o ESP32. Descobrimos que você pode usar um temporizador de watchdog de hardware para executar uma inicialização a quente suave. Você pode forçar este temporizador a disparar mesmo quando o chip estiver no modo de inicialização usando o carregador de inicialização UART. Como resultado, você pode usar esse mecanismo para colocar o ESP32 no modo de inicialização normal.
Usando o código de teste, que foi carregado na SRAM e executado usando o bootloader UART, determinamos que os dados na SRAM, de fato, persistem após uma reinicialização a quente iniciada pelo watchdog timer. E isso significa que, tendo registrado o que precisamos na SRAM, podemos inicializar o ESP32 normalmente.
Então surgiu a questão diante de nós sobre como podemos usar isso.
O caminho para o fracasso
Presumimos que poderíamos tirar proveito do fato de que os dados são salvos na SRAM após uma reinicialização a quente para um ataque. Nosso primeiro ataque foi que escrevemos algum código para SRAM usando o bootloader UART e, em seguida, usando o watchdog timer, executamos uma reinicialização a quente do dispositivo. Em seguida, causamos um travamento ao executá-lo enquanto o código da ROM sobrescreve esse código com o código do carregador de inicialização do flash durante a inicialização normal.
Tínhamos essa ideia depois de transformar o processo de transferência de dados em processo de execução de código no decorrer de experimentos anteriores . Então notamos que o chip começa a executar o código a partir do endereço inicial antes que o bootloader conclua a cópia.
Às vezes, para conseguir algo, você só precisa experimentar ...
▍ Código carregado na SRAM e usado para realizar o ataque
Aqui está o código que escrevemos para SRAM usando o bootloader 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 o seguinte (os números dos itens da lista correspondem aos números especificados nos comentários):
- Um único manipulador de comando de comando que zera o cronômetro de watchdog.
- Um análogo
NOP
baseado em instruçõesaddi
. - Código de montagem que gera uma string para a interface serial
Raelize!
.
▍ Escolher o momento do ataque
Tínhamos uma janela de ataque relativamente pequena à nossa disposição, começando
F
conforme mostrado na figura a seguir. Sabíamos por experiências anteriores que o código do bootloader está sendo copiado da memória flash neste ponto.

A janela de ataque é representada por F A
falha deve ser feita antes que o conteúdo SRAM seja completamente sobrescrito pelo código de bootloader correto da memória flash.
▍ Ciclo de Ataque
Em cada um de nossos experimentos, realizamos as seguintes etapas para verificar se a ideia de ataque funcionou. A organização bem-sucedida da falha deve ter resultado na saída para a interface de linha serial
Raelize!
.
- Defina o pino
G0
baixo e execute uma inicialização a frio para entrar no modo de carregador de inicialização UART. - Usando um comando
load_ram
para executar o código de ataque da SRAM. - Envia o programa
A
para uma reinicialização a quente e para retornar ao modo de inicialização normal. - Organização de falha no processo de cópia do bootloader da memória flash utilizando o código da ROM.
▍Resultados
Depois de realizar esse experimento por mais de um dia, tendo feito mais de um milhão de vezes, ainda não tivemos sucesso.
▍ Resultado inesperado
Mas, apesar de não termos conseguido o que queríamos, nós, analisando os resultados dos experimentos, encontramos algo inesperado.
Em um experimento, os dados foram exibidos na interface serial indicando que uma falha resultou em uma exceção
IllegalInstruction
(instrução inválida). É assim que parecia:
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
Ao tentar causar uma falha de chip, essas exceções ocorrem com bastante frequência. O mesmo é verdade para o ESP32. Para a maioria dessas exceções, o registro é
PC
definido com o valor esperado (ou seja, o endereço correto está localizado lá). Raramente acontece que PC
apareça um significado tão interessante.
A exceção é
IllegalInstruction
lançada porque 0x661b661b
não há instrução correta no endereço . Decidimos que esse valor PC
deve vir de algum lugar do registro e que por si só não pode aparecer lá.
Em busca de uma explicação, analisamos o código que carregamos na SRAM. A visualização do código binário, cujo fragmento é mostrado abaixo, nos permitiu descobrir rapidamente a resposta à nossa pergunta. Ou seja, é fácil encontrar o significado aqui
0x661b661b
... É representado por duas instruções addi a6, a6, 1
, com a ajuda das quais o analógico é implementado no 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 uma "sala de manobra" com essas instruções, usando-as de maneira semelhante a como as sequências de comandos
NOP
são frequentemente usadas em exploits para atrasar a execução do código até o momento certo. Não esperávamos que essas instruções acabassem no registro PC
.
Mas nós, é claro, não éramos contra o uso disso. Decidimos que poderíamos carregar dados da SRAM em um registrador
PC
durante um travamento causado quando os dados da memória flash foram copiados por meio do código ROM.
Rapidamente percebemos que agora tínhamos todos os ingredientes para preparar um ataque que contornaria a inicialização segura e os sistemas de criptografia de flash em uma única falha. Aqui usamos a experiência adquirida durante a execução do ataque descrito anteriormentequando conseguimos controlar o registro
PC
.
Caminho para o sucesso
Para este ataque, usamos a maior parte do código que foi carregado anteriormente na SRAM usando o carregador de inicialização UART. Apenas comandos para saída de caracteres para a interface serial foram removidos deste código, pois agora nosso objetivo era definir o registro
PC
para o valor que precisávamos, ou seja, obter a capacidade de controlar o 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);
}
Depois de compilar este código, nós, em sua versão binária, substituímos as instruções
addi
por um endereço 0x4005a980
. Nesse endereço está uma função na ROM que envia dados para a interface serial. Uma chamada bem-sucedida para essa função nos informa sobre um ataque bem-sucedido.
Nós nos preparamos para lidar com falhas que fossem consistentes com o que causou a exceção em um experimento anterior
IllegalInstruction
. Depois de um tempo, descobrimos a conclusão bem-sucedida de vários experimentos para carregar o PC
endereço fornecido no registro . O controle de caso PC
muito provavelmente significa que podemos executar código arbitrário.
“Por que isso é possível?
O título desta seção contém uma boa pergunta que não é fácil de responder.
Infelizmente, não temos uma resposta clara. Certamente não esperávamos que a manipulação de dados permitisse o controle de registros
PC
. Temos várias explicações para isso, mas não podemos afirmar com certeza absoluta que qualquer uma delas seja verdadeira.
Uma explicação é que durante um travamento, ambos os operandos da instrução
ldr
usados para carregar o valor a0
. Isso é semelhante ao que vimos neste ataque, onde ganhamos controle indireto sobre o registro PC
, modificando os dados.
Além disso, é possível que o código armazenado na ROM contenha funcionalidades que contribuam para o sucesso deste ataque. Ou seja, devido a uma falha, podemos executar o código correto da ROM, o que leva ao fato de os dados da SRAM serem carregados no registrador
PC
.
Para descobrir o que exatamente nos permitiu realizar esse ataque, precisamos fazer mais pesquisas. Mas se você olhar o assunto pelos olhos de quem decidiu hackear o chip, temos conhecimento suficiente para criar um exploit baseado na possibilidade de influenciar o registro
PC
.
Extraia o conteúdo da memória flash como texto simples
Podemos escrever no registrador
PC
o que quisermos, mas ainda não podemos recuperar o conteúdo da memória flash como texto simples. Portanto, decidiu-se aproveitar as vantagens dos recursos do carregador de inicialização UART.
Ou seja, decidimos ir direto para o carregador de inicialização UART enquanto o chip está no modo de inicialização normal. Para realizar esse ataque, reescrevemos as instruções
addi
no código carregado na RAM, usando o endereço inicial do código do bootloader UART ( 0x40007a19
).
O carregador de inicialização UART envia a linha mostrada abaixo para a interface serial. Podemos usar esse fato para determinar o sucesso de um ataque.
waiting for download\n"
Assim que o experimento for bem-sucedido, podemos simplesmente usá-lo
esptool.py
para executar o comando read_mem
e acessar os dados de texto simples na memória flash. Por exemplo, o comando a seguir lê 4 bytes do espaço de endereço flash externo ( 0x3f400000
).
esptool.py --no-stub --before no_reset --after no_reset read_mem 0x3f400000
Infelizmente, esse comando não funcionou. Por alguma razão, a resposta do processador parecia
0xbad00bad
, indicando que estamos tentando ler dados da memória não alocada.
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.
Percebemos que muitas configurações são feitas no início do carregador de inicialização UART. Presumimos que essas configurações também podem afetar o MMU.
Só para tentar outra coisa, decidimos ir direto para o gerenciador de comandos do próprio gerenciador de inicialização UART (
0x40007a4e
). Depois de nos encontrarmos no manipulador, podemos enviar independentemente o comando read_mem
diretamente para a interface serial:
target.write(b'\xc0\x00\x0a\x04\x00\x00\x00\x00\x00\x00\x00\x40\x3f\xc0')
Infelizmente, se você for direto para o manipulador, a linha exibida após entrar no carregador de inicialização UART (ou seja -
waiting for download\n
) não será exibida. Por causa disso, perdemos uma maneira simples e conveniente de identificar experimentos bem-sucedidos. Como resultado, decidimos enviar o comando acima em todas as experiências, independentemente de terem ou não sucesso. Usamos um tempo limite serial muito curto para minimizar o tempo limite adicional associado a esse tempo limite, o que quase sempre é o caso.
Depois de um tempo, vimos os resultados das primeiras experiências bem-sucedidas!
Resultado
Neste artigo, descrevemos um ataque ao ESP32, no qual contornamos os sistemas seguros de inicialização e criptografia da memória flash, arranjando apenas uma falha no microcontrolador. Além disso, usamos uma vulnerabilidade explorada durante o ataque para extrair o conteúdo da memória flash criptografada em texto simples.
Podemos usar o FIRM para avançar neste ataque .

Progresso do ataque
Aqui está uma breve descrição do que acontece nas diferentes etapas do ataque acima:
- Ativar (a escolha de ferramentas para realizar um ataque) - o complexo Riscure Inspector FI é usado aqui .
- Injetar (atacar) - um efeito eletromagnético é realizado no microcontrolador sob investigação.
- Glitch ( ) — , (, , ).
- Fault ( ) — , , , . , - .
- Exploit ( ) — UART , SRAM, . UART
PC
read_mem
. - Goal ( ) — - .
Curiosamente, o sucesso desse ataque depende de dois pontos fracos do ESP32. O primeiro ponto fraco é que o carregador de inicialização UART não pode ser desabilitado. Como resultado, está sempre disponível. O segundo ponto fraco é a persistência dos dados na SRAM após uma reinicialização a quente do dispositivo. Isso permite usar o bootloader UART para preencher a SRAM com dados arbitrários.
Num relatório de informação , que se refere ao ataque, a empresa Espressif informa que nas versões mais recentes do ESP32 existem mecanismos que tornam impossível tal ataque.
Todos os sistemas embarcados padrão são vulneráveis a ataques de interrupção do dispositivo. Portanto, não é surpreendente que o microcontrolador ESP32 também seja vulnerável a ataques de canal lateral. Chips como esses simplesmente não são projetados para resistir a tais ataques. Mas, o mais importante, isso não significa que esses ataques não apresentem nenhum risco.
Nossa pesquisa mostrou que explorar os pontos fracos do chip permite ataques e interrupções bem-sucedidos. A maioria dos ataques que podem ser aprendidos em fontes abertas usam abordagens tradicionais, onde o foco principal é ignorar verificações. Não vimos muitos relatos de ataques como o que descrevemos.
Estamos confiantes de que todo o potencial de tais ataques ainda não foi totalmente explorado. Até recentemente, a maioria dos pesquisadores estudava apenas métodos de interromper o funcionamento dos chips (etapas Ativar, Injetar, Glitch), mas fomos além, considerando a possibilidade de trabalhar com um chip vulnerável após uma falha (etapas Falha, Exploração, Objetivo).

Pesquisa até 2020 e além de 2020
Estamos confiantes de que o uso criativo de novos modelos de falha de chip levará a um aumento nos métodos de ataque que usam estratégias interessantes de exploração de vulnerabilidade para atingir uma ampla variedade de objetivos.
Se você está interessado no tópico levantado neste material, então aqui , aqui e aqui - outros materiais dedicados ao estudo de ESP32.
Você encontrou na prática a invasão de qualquer dispositivo por métodos semelhantes aos discutidos neste artigo?

