ÍNDICE DE CONTEÚDO
Neste artigo trataremos, de forma fácil e rápida, um assunto muito importante para quem pretende comercializar produtos com o ESP32, a segurança do seu hardware com o código presente na memória flash, a fim de impedir clonagem, furto e etc.
Utilizaremos a ESP-IDF v4.0-dev-76-g96aa08a0f-dirty (Ubuntu) para todos artigos desta série e não será abordado sobre como utilizar a IDF, sendo dever do leitor conhecer o funcionamento. Mais sobre a IDF: https://github.com/espressif/esp-idf
Explicando em miúdos
Todo código (firmware) transferido ao ESP32 fica salvo, na maioria das versões, na memória flash externa, que diminui ainda mais a segurança, já que alguém pode simplesmente removê-la para leitura em um hardware externo e clonar, em segundos, nosso código que pode ter demorado anos para ser desenvolvido. Mesmo se a flash for embutida, como na versão “PICO”, é possível exportar todo conteúdo da flash com apenas um comando no terminal. Então, se todo conteúdo pode ser facilmente obtido, devemos nos proteger e é isso que a criptografia da flash do ESP32 nos proporciona.
Criptografia da flash
Atenção
- Não abordaremos todas funções e características da criptografia da flash, sendo necessário que você estude MUITO BEM a documentação oficial, a fim de evitar qualquer dor de cabeça que pode ocorrer conforme a IDF se atualiza. Não somos responsáveis por qualquer uso errado de sua parte.
- A criptografia da flash limita como e/ou quantas vezes é possível fazer upload de novos códigos. Caso feito incorretamente, você pode perder seu ESP32 e não será mais possível regrava-lo.
- Abordaremos, por motivos de didática, apenas sobre a criptografia com uma chave pré-gerada, assim, podemos regravar o ESP32 sem qualquer restrição de quantidade.
- Em ambientes que é necessário a maior segurança disponível, você não deve utilizar uma chave pré-gerada, deixando o próprio ESP32 gerar a sua, sendo individual para cada hardware. Além de também habilitar o Secure boot que não abordaremos aqui.
A criptografia da flash (AES-256) é uma característica presente no ESP32 que criptografa o conteúdo presente na flash. Quando habilitado, leituras físicas sem a chave não são suficientes para recuperar o conteúdo. Sendo assim, nos protegemos de quem tentar exportá-la para clonagem e etc. A chave é gravada em um bloco de eFuse, que pode ser protegido contra leitura e escrita (padrão) e, conhecendo a chave, podemos regravar códigos sem a limitação de quantidade, diferentemente do caso onde o ESP32 gera sua própria chave, onde estamos limitados em até 3 uploads físicos.
Vamos observar alguns itens relevantes sobre a criptografia:
- Em um upload plaintext, o binário original (cru) é enviado ao microcontrolador.
- Em um upload criptografado, o binário é enviado ao microcontrolador já criptografado pela IDF.
- O eFuse “FLASH_CRYPT_CNT” (7-bit) é responsável pela permissão de uploads plaintext, pela contagem de uploads físicos plaintext (até 3x) e pelo controle do bootloader para criptografar o conteúdo da flash. Pode ser protegido contra R/W. Após 3 uploads plaintext, este eFuse chegará em seu máximo e aceitará apenas uploads criptografados.
- Quando for um número par, o bootloader irá criptografar todo conteúdo da flash, logo, é necessário o upload plaintext.
- Quando for um número ímpar, o bootloader não irá criptografar o conteúdo da flash, logo, é necessário o upload criptografado.
- Se a chave não for conhecida (pré-gerada), temos no máximo 3 uploads físicos disponíveis (plaintext), que também nos permite desabilitar a criptografia. Se o “FLASH_CRYPT_CNT” for protegido enquanto ímpar, não será possível novos uploads plaintext.
- Os binários “Bootloader”, “Partition table”, “OTA DATA”, todas “APP (seu código)” e as partições marcadas com a flag “encrypted” na tabela de partição serão criptografados. Partições que não estiverem marcados com “encrypted” não serão criptografados e poderão ser lidos externamente, tenha atenção ao utilizar APIs para acesso da flash como NVS e SPIFFS.
- Se o “FLASH_CRYPT_CNT” não for protegido corretamente e/ou ainda houver alguma tentativa para upload plaintext, invasores podem inserir códigos maliciosos e ler o conteúdo de forma descriptografada pelo próprio ESP32 sem conhecimento da chave. Por causa disto, é comum protegê-lo contra escrita, a fim de evitar uploads plaintext.
Cientes dos detalhes básicos (há dezenas de detalhes extras na documentação oficial que você deve ler antes de efetuar os testes abaixo), vamos testar a criptografia da flash e ver se realmente funciona! Lembrando que utilizaremos a IDF no Ubuntu e quase todos comandos podem mudar de acordo com seu computador, projeto, endereços e muitas outras coisas. Os comandos utilizados aqui podem não servir para você, logo, terá que alterá-los de acordo com seu projeto, caminho de arquivos e etc. Este artigo é apenas uma demonstração e deve ser tomado como base, não seguido ao pé da letra. Os scripts utilizados estão na pasta da IDF, “esp-idf/components/esptool_py/esptool/”, tome bastante atenção ao uso dos caminhos dos arquivos e scripts, pois você pode estar tentando rodar o comando no local errado. Também tenha atenção na porta utilizada, seu ESP32 pode estar em uma porta diferente da nossa.
Primeiramente, vamos testar um código simples apenas para verificar sobre o que foi citado acima, sobre a leitura (clonagem) da memória flash sem proteção. Você pode ignorar essa parte.
1 2 3 4 5 6 7 8 9 |
void app_main() { while (1) { ESP_LOGI("ESP32", "Embarcados..."); vTaskDelay(pdMS_TO_TICKS(1000)); } } |
Quando fazemos um upload padrão para placa, o computador utiliza um script python (esptool.py) com o comando “make flash”, que basicamente compila e faz upload (plaintext). Com o código enviado, vamos fazer uma leitura (sumário) dos eFuses para comparação após ativar a criptografia:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
python espefuse.py --port /dev/ttyUSB0 summary espefuse.py v2.6 Connecting........__ EFUSE_NAME Description = [Meaningful Value] [Readable/Writeable] (Hex Value) ---------------------------------------------------------------------------------------- Security fuses: FLASH_CRYPT_CNT Flash encryption mode counter = 0 R/W (0x0) FLASH_CRYPT_CONFIG Flash encryption config (key tweak bits) = 0 R/W (0x0) CONSOLE_DEBUG_DISABLE Disable ROM BASIC interpreter fallback = 1 R/W (0x1) ABS_DONE_0 secure boot enabled for bootloader = 0 R/W (0x0) ABS_DONE_1 secure boot abstract 1 locked = 0 R/W (0x0) JTAG_DISABLE Disable JTAG = 0 R/W (0x0) DISABLE_DL_ENCRYPT Disable flash encryption in UART bootloader = 0 R/W (0x0) DISABLE_DL_DECRYPT Disable flash decryption in UART bootloader = 0 R/W (0x0) DISABLE_DL_CACHE Disable flash cache in UART bootloader = 0 R/W (0x0) BLK1 Flash encryption key = 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 R/W BLK2 Secure boot key = 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 R/W BLK3 Variable Block 3 = 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 R/W Efuse fuses: WR_DIS Efuse write disable mask = 0 R/W (0x0) RD_DIS Efuse read disablemask = 0 R/W (0x0) CODING_SCHEME Efuse variable block length scheme = 0 R/W (0x0) KEY_STATUS Usage of efuse block 3 (reserved) = 0 R/W (0x0) Config fuses: XPD_SDIO_FORCE Ignore MTDI pin (GPIO12) for VDD_SDIO on reset = 0 R/W (0x0) XPD_SDIO_REG If XPD_SDIO_FORCE, enable VDD_SDIO reg on reset = 0 R/W (0x0) XPD_SDIO_TIEH If XPD_SDIO_FORCE & XPD_SDIO_REG, 1=3.3V 0=1.8V = 0 R/W (0x0) SPI_PAD_CONFIG_CLK Override SD_CLK pad (GPIO6/SPICLK) = 0 R/W (0x0) SPI_PAD_CONFIG_Q Override SD_DATA_0 pad (GPIO7/SPIQ) = 0 R/W (0x0) SPI_PAD_CONFIG_D Override SD_DATA_1 pad (GPIO8/SPID) = 0 R/W (0x0) SPI_PAD_CONFIG_HD Override SD_DATA_2 pad (GPIO9/SPIHD) = 0 R/W (0x0) SPI_PAD_CONFIG_CS0 Override SD_CMD pad (GPIO11/SPICS0) = 0 R/W (0x0) DISABLE_SDIO_HOST Disable SDIO host = 0 R/W (0x0) Identity fuses: MAC Factory MAC Address = b4:e6:2d:96:dc:41 (CRC 84 OK) R/W CHIP_VER_REV1 Silicon Revision 1 = 1 R/W (0x1) CHIP_VERSION Reserved for future chip versions = 2 R/W (0x2) CHIP_PACKAGE Chip package identifier = 0 R/W (0x0) Calibration fuses: BLK3_PART_RESERVE BLOCK3 partially served for ADC calibration data = 0 R/W (0x0) ADC_VREF Voltage reference calibration = 1114 R/W (0x2) Flash voltage (VDD_SDIO) determined by GPIO12 on reset (High for 1.8V, Low/NC for 3.3V). |
Sabendo que este ESP32 não está protegido, podemos exportar o conteúdo da flash e usar para clonagem, engenharia reversa ou o que der na cabeça! Vamos ver se achamos a palavra usada no ESP_LOGI(), “Embarcados…”, ao ler o conteúdo da memória:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
python esptool.py --port /dev/ttyUSB0 read_flash 0x0 4096000 dump.bin esptool.py v2.6 Serial port /dev/ttyUSB0 Connecting.... Detecting chip type... ESP32 Chip is ESP32D0WDQ6 (revision 1) Features: WiFi, BT, Dual Core, 240MHz, VRef calibration in efuse, Coding Scheme None MAC: b4:e6:2d:96:dc:41 Uploading stub... Running stub... Stub running... 4096000 (100 %) 4096000 (100 %) Read 4096000 bytes at 0x0 in 367.1 seconds (89.3 kbit/s)... Hard resetting via RTS pin... |
Utilizando o comando “hexdump” para ler o arquivo gerado, conseguimos achar a palavra utilizada no código sem criptografia:
Agora que a clonagem foi demonstrada com apenas um comando no terminal, indicando que seu produto sem proteção está totalmente vulnerável nas mãos de alguém, vamos nos proteger ativando a criptografia.
-
Criando a chave
1 |
python espsecure.py generate_flash_encryption_key key.bin |
Utilizaremos o próprio script da IDF para criar uma chave (256b), mas você pode utilizar qualquer método ou chave que desejar. O comando exportará a chave no arquivo “key.bin”, que você deve deixar uma cópia dentro da pasta do seu projeto, ficando algo similar com o nosso:
-
Gravando a chave pré-gerada no eFuse
1 2 3 4 5 6 7 8 9 |
python espefuse.py --port /dev/ttyUSB0 burn_key flash_encryption key.bin espefuse.py v2.6 Connecting........_____... Write key in efuse block 1. The key block will be read and write protected (no further changes or readback). This is an irreversible operation. Type 'BURN' (all capitals) to continue. BURN Burned key data. New value: 56 f7 1f 29 a6 6c 01 20 c5 ee 6b 55 79 36 d7 90 73 b3 f6 2d 48 a2 dc 03 b8 ad dc cb 1b 31 fe e5 Disabling read/write to key efuse block… |
O comando acima gravou nossa chave no eFuse e automaticamente protegeu contra R/W, o que impede de qualquer um conseguir lê-la ou alterá-la. Apesar deste comando escrever no terminal a chave gravada, ao tentar ler a chave diretamente do eFuse (sumário) como feito anteriormente, é retornado:
1 2 |
BLK1 Flash encryption key = ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? -/- |
Agora com uma chave conhecida no eFuse, temos a possibilidade de uploads ilimitados (criptografados), o que é muito interessante na fase de desenvolvimento.
-
Ativando a criptografia da flash
Agora com a chave gravada, basta ativar a criptografia da flash no “menuconfig” em “Security features”.
Atenção, se você não gravou a chave pré-gerada, ao dar o upload de um novo código com a criptografia ativada, o próprio ESP32 irá gerar uma chave que nem você, nós ou a Espressif poderá ler, lhe restando 3 uploads (plaintext) e/ou tentativas para desativar a criptografia, tome cuidado! Atualizações OTA são ilimitadas mesmo sem conhecimento da chave.
Após ativar a criptografia, vamos refazer o upload do código utilizado anteriormente de forma padrão, como utilizado antes (upload plaintext). Nesse primeiro boot, o bootloader irá criptografar todo conteúdo da memória e reiniciará, esse processo pode demorar um pouco então aguarde. Após o ESP32 reiniciar, indicando que a criptografia foi ativada, vamos fazer uma nova leitura da flash para ver se encontramos a palavra “Embarcados…” novamente.
Além da palavra “Embarcados…” não ser encontrada, nenhum texto legível foi visto, o que anteriormente era facilmente visto (Strings utilizadas pela própria IDF). O mesmo endereço que encontramos a palavra anteriormente, agora, não passa de um texto corrompido para quem tentar ler sem a chave!
Se olharmos o sumário dos eFuses novamente, podemos ver que o “FLASH_CRYPT_CNT” foi de 0 para 1, indicando que agora só aceita uploads criptografados, logo, se utilizarmos o upload padrão (plaintext), o ESP irá ficar em boot-loop com a seguinte mensagem:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
make flash monitor -j4 ets Jun 8 2016 00:22:57 rst:0x1 (POWERON_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT) flash read err, 1000 ets_main.c 371 ets Jun 8 2016 00:22:57 rst:0x10 (RTCWDT_RTC_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT) flash read err, 1000 ets_main.c 371 ets Jun 8 2016 00:22:57 rst:0x10 (RTCWDT_RTC_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT) flash read err, 1000 ets_main.c 371 ets Jun 8 2016 00:22:57 |
O “FLASH_CRYPT_CNT” não é protegido contra escrita por padrão, logo, alguém ainda pode desativar a criptografia, fazer upload (plaintext) e ler sua flash de forma descriptografada, logo, devemos proteger este eFuse contra escrita ENQUANTO estiver em um número ímpar ou utilizar o Secure boot.
-
Protegendo o eFuse
Vamos proteger o eFuse “FLASH_CRYPT_CNT” contra escritas para impossibilitar qualquer tipo de upload sem conhecimento da chave, isso automaticamente permite que suas placas estejam protegidas de uploads não permitidos, forçando seu hardware a aceitar apenas códigos que tenham sidos criptografados com a chave. Apesar desta proteção funcionar com o mesmo intuito do Secure boot (impossibilitar uploads não permitidos), há alguns casos em que manter o Secure boot ativado pode ser melhor, entretanto, na maioria dos casos, protegendo o eFuse já não precisamos do Secure boot (pesquise melhor sobre isso se for usar em ambientes agressivos onde a segurança deve prevalecer).
Antes de proteger a escrita, devemos certificar-se que ele está em algum número ímpar através do sumário, que nos retornou “FLASH_CRYPT_CNT Flash encryption mode counter = 1 R/W (0x1)”
1 2 3 4 5 6 7 |
python espefuse.py --port /dev/ttyUSB0 write_protect_efuse FLASH_CRYPT_CNT espefuse.py v2.6 Connecting........_ Permanently write-disabling efuse FLASH_CRYPT_CNT. This is an irreversible operation. Type 'BURN' (all capitals) to continue. BURN |
A partir de agora, é impossível desabilitar a criptografia ou enviar códigos que não estejam criptografados pela chave presente no ESP32.
Ok, se o upload padrão não funciona mais, o que faremos? Criptografamos antes de enviar! Apesar de ser utilizado AES-256, a Espressif usa métodos diferentes de funcionamento (detalhes na documentação oficial), logo, precisamos criptografar pelo próprio script da IDF
-
Criptografando os binários
O upload agora deve ser feito “manualmente”, sendo necessário criptografar e enviar os binários criptografados. Essa parte depende muito de projeto para projeto, principalmente nos caminhos de arquivos e endereço da memória, tome muita atenção.
5.1 Crie uma pasta “enc” dentro do seu projeto, ela irá guardar os binários criptografados pelo script.
5.2 Com o terminal aberto na pasta do seu projeto, use o comando “make all” para descobrir o que e onde os arquivos são gravados.
1 2 3 4 5 6 7 8 9 10 11 |
make all -j4 Toolchain path: /home/ze/esp/xtensa-esp32-elf/bin/xtensa-esp32-elf-gcc Toolchain version: crosstool-ng-1.22.0-80-g6c4433a Compiler version: 5.2.0 Project is not inside a git repository, will not use 'git describe' to determine PROJECT_VER. App "esp32" version: 1 Python requirements from /home/ze/esp/esp-idf/requirements.txt are satisfied. To flash all build output, run 'make flash' or: python /home/ze/esp/esp-idf/components/esptool_py/esptool/esptool.py --chip esp32 --port /dev/ttyUSB0 --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 80m --flash_size detect 0x363000 /home/ze/esp/esp32/build/ota_data_initial.bin 0x1000 /home/ze/esp/esp32/build/bootloader/bootloader.bin 0x10000 /home/ze/esp/esp32/build/esp32.bin 0x8000 /home/ze/esp/esp32/build/partitions.bin |
Podemos ver que nesse nosso projeto, onde utilizamos OTA e uma tabela de partições (custom), é feito o upload de 4 binários nos seus respectivos endereços.
ota_data_initial.bin: 0x363000.
bootloader.bin: 0x1000.
esp32.bin (o código em si): 0x10000.
partitions.bin: 0x8000.
Iremos criptografá-los individualmente e posteriormente, efetuar o upload. Não se esqueça que os caminhos dos arquivos devem ser alterados para o seu projeto.
5.3 Com o terminal aberto na pasta de scripts, use os comandos abaixo e não se esqueça de tomar muita atenção com os endereços de memória e caminho dos binários.
1 2 3 4 5 6 7 |
python espsecure.py encrypt_flash_data --keyfile key.bin --address 0x1000 -o /home/ze/esp/esp32/enc/bootloader.bin /home/ze/esp/esp32/build/bootloader/bootloader.bin python espsecure.py encrypt_flash_data --keyfile key.bin --address 0x8000 -o /home/ze/esp/esp32/enc/partitions.bin /home/ze/esp/esp32/build/partitions.bin python espsecure.py encrypt_flash_data --keyfile key.bin --address 0x10000 -o /home/ze/esp/esp32/enc/esp32.bin /home/ze/esp/esp32/build/esp32.bin python espsecure.py encrypt_flash_data --keyfile key.bin --address 0x363000 -o /home/ze/esp/esp32/enc/ota_data_initial.bin /home/ze/esp/esp32/build/ota_data_initial.bin |
5.4 Faça o upload dos binários criptografados pelo comando:
1 |
python esptool.py --chip esp32 --port /dev/ttyUSB0 --baud 921600 write_flash --flash_mode dio --flash_freq 80m --flash_size detect 0x1000 /home/ze/esp/esp32/enc/bootloader.bin 0x8000 /home/ze/esp/esp32/enc/partitions.bin 0x10000 enc/esp32.bin 0x363000 /home/ze/esp/esp32/enc/ota_data_initial.bin |
Após o upload criptografado, o ESP32 iniciará normalmente como se nada tivesse acontecido. Esse método apesar de ser manual, pode ser automatizado por um script (bash), basta colocá-los num arquivo e executar no terminal.
Agora que a proteção do ESP32 esta ativada, podemos ficar mais relaxados na questão sobre alguém clonar nosso firmware, já que não será possível sem a chave gravada. Você deve ler toda documentação oficial e também pode ser interessante utilizar o Secure boot sem criptografia da flash, análise seu projeto e mãos na massa!
Referências
Olá , essa metodologia se aplica também a códigos usando micropython ?
boa noite,
tem como me ajudar nesse erro ?
A fatal error occurred: MD5 of file does not match data in flash!
Estou tendo esse erro no esp 32 depois que compilei um código , agora não aceita mais nenhum
Parabêns pelo artigo, realmente muito bom e útil. Uma coisa que estou tentando é gerar o binário criptografado para entregar para outra pessoa, é possível? Nestes exemplos o binário sempre fica descriptografado, abraços,
Willian Henrique
Sim, com os próprios scripts da IDF que geram o binário criptografado antes de fazer o upload. Você vai utiliza-los pra criptografar e exportar esses arquivos pra algum local e daí basta entregar ao cliente.
Muito legal, mas e para o caso de verificar o conteúdo da Flash automaticamente? Tem algum exemplo de CRC self-test? Para certificar de que não ocorreu corrompimento do firmware em algum momento, chips STM32 tem um módulo de hardware CRC, o ESP32 tem esta funcionalidade?
Sim, sua pergunta me parece estar mais ligada com atualizações remotas (OTA), mas de qualquer maneira, o arquivo binário enviado ao ESP32 tem “Magic Byte” e também vários bytes de CRC. O “hardware boot”, responsável por selecionar qual partição sera “bootada”, já faz a verificação se a partição esta OK ou corrompida.
Parabéns pelo artigo! Muito útil.
Vale apenas acompanhar o desenrolar da vulnerabilidade de segurança que encontraram no chip. Veja o CVE (https://nvd.nist.gov/vuln/detail/CVE-2019-17391) e o artigo do descobridor da falha (https://limitedresults.com/2019/11/pwn-the-esp32-forever-flash-encryption-and-sec-boot-keys-extraction/).
Abs!
Ótimo artigo José, muito didático e detalhado…