Hackeando dispositivos de casa inteligente (2024)
(jmswrnr.com)- Para controlar diretamente pelo Home Assistant um purificador de ar baseado em ESP32, preso ao app e à nuvem do fabricante, a rota de controle remoto foi submetida a engenharia reversa e substituída por um servidor local
- Com análise do app, desvio de DNS e capturas no Wireshark, foi confirmado que o dispositivo enviava pacotes UDP para
smartdeviceep.---.com:41014e usava um protocolo próprio, não DTLS padrão - Por meio de conexão UART e dump da flash de 4 MB, foram obtidos
dev_key.key, certificados, configurações de servidor e de WiFi, e a estrutura do firmware foi analisada com Ghidra e esp32knife - Os pacotes combinavam um cabeçalho de 13 bytes, CRC-16 nos 2 bytes finais, geração de chaves ECDH/HKDF,
AES-128-CBCe serialização MessagePack; com um patch no firmware, o segredo compartilhado foi impresso no log serial e a descriptografia teve sucesso - A configuração final combinou um proxy MITM, um servidor local e uma ponte MQTT baseada em Mosquitto, controlando de forma estável a energia e a velocidade do ventilador por algumas semanas via MQTT Fan do Home Assistant
Transformando um purificador de ar dependente da nuvem em controle local
- O objetivo era controlar pelo Home Assistant um purificador de ar que só se conectava ao app móvel do fabricante e a uma conta na nuvem
- Ao alternar Bluetooth, WiFi e 5G no celular, foi confirmado que o app controlava o dispositivo apenas por meio de uma conexão com a internet, não por Bluetooth ou WiFi local
- Como valores de controle, como a velocidade do ventilador, trafegavam em algum ponto entre o dispositivo e o servidor na nuvem, o trecho de rede se tornou o principal ponto de ataque
- Ao interceptar o tráfego e alterar valores, seria possível controlar o dispositivo
- Ao emular as respostas do servidor, seria possível fazê-lo funcionar sem internet e sem a nuvem do fabricante
- O conteúdo de engenharia reversa tem fins educacionais, e informações sensíveis específicas do produto, como chaves privadas, domínio e endpoints de API, foram ofuscadas ou removidas
- Modificar o dispositivo pode invalidar a garantia ou danificá-lo permanentemente
Análise do app e captura de tráfego UDP
- O
.apkdo app Android foi extraído, e oclasses.dexfoi aberto com dex2jar e jd-gui para inspecionar seu interior - Em
MainActivity.class, foi confirmado que o app era baseado em React Native, e uma conexão WebSocket segura foi encontrada emassets/index.android.bundle- O código de exemplo incluía uma conexão com
wss://smartdeviceapi.---.com
- O código de exemplo incluía uma conexão com
- O domínio do servidor na nuvem acessado pelo dispositivo foi identificado usando o recurso de consulta de DNS do Pi-hole
- Com o recurso
Local DNSdo Pi-hole, esse domínio foi apontado para a estação de trabalho local192.168.0.10, e o tráfego do IP do dispositivo192.168.0.61foi filtrado no Wireshark - O dispositivo estava enviando pacotes UDP para a porta
41014da estação de trabalho
Configuração de relay e indícios de protocolo próprio
- Como o DNS local fazia o domínio da nuvem resolver para a estação de trabalho, o IP real do servidor foi consultado usando o resolvedor DNS da Cloudflare
1.1.1.1 - Usando node-udp-forwarder, a estação de trabalho passou a atuar como relay UDP entre o dispositivo e o servidor na nuvem
- O primeiro pacote na inicialização e a resposta do servidor foram capturados, mas pareciam bytes aleatórios, sem strings legíveis, indicando possível criptografia
- O Wireshark não reconheceu os pacotes como DTLS, e o formato de cabeçalho da especificação DTLS também diferia dos pacotes capturados
- Como não parecia ser um protocolo padrão, foi necessário fazer engenharia reversa diretamente da estrutura dos pacotes e do método de criptografia
Desmontagem do ESP32 e acesso serial
- Ao desmontar o dispositivo, apareceram a PCB principal, a porta de conexão do ventilador e o cabo flat do painel de controle frontal
- O controlador principal estava marcado como
ESP32-WROOM-32D, um microcontrolador da família ESP32 com WiFi e Bluetooth - Foram consultados materiais sobre engenharia reversa de ESP32 no repositório ESP32-reversing
- No datasheet do ESP32, foram identificados os pinos
TXD0eRXD0, e os pontos de conexão serial foram encontrados seguindo as trilhas ligadas aos furos de depuração na PCB - A conexão UART foi configurada com o
USB-UART Bridgedo Flipper Zero- O
TXdo Flipper Zero foi conectado aoRXdo ESP32 - O
RXdo Flipper Zero foi conectado aoTXdo ESP32 GNDfoi conectado aGND
- O
- Ao conectar pelo Putty em
COM7a115200, o log de inicialização foi exibido
Arquivos e configuração de servidor revelados pelo log de inicialização
- O log serial indicou que o ESP32 era um chip com 2 núcleos de CPU, WiFi/BT/BLE e flash externa de 4 MB
- A aplicação estava sendo executada a partir da partição
factory - O sistema de arquivos FAT foi montado, exibindo
122 KiBde espaço total e0 KiBde espaço disponível - A aplicação estava lendo os seguintes arquivos
serialdev_key.keySmartDevice-root-ca.crtSmartDevice-signer-ca.crtserver_config
- A configuração do servidor continha
smartdeviceep.---.com:41014
Dump da flash e estrutura de partições
- Para inicializar o ESP32 no modo
Download Boot, o dispositivo foi ligado com o pinoIO0conectado aoGND - A flash completa de 4 MB foi extraída usando esptool
- O comando foi
esptool -p COM7 -b 115200 read_flash 0 0x400000 flash.bin
- O comando foi
- O dump foi feito várias vezes para confirmar a leitura correta e salvo como backup para permitir regravação em caso de problema
- O dump foi analisado com esp32knife, obtendo
partitions.csv - A estrutura de partições incluía os seguintes itens
nvs: armazenamento chave-valor de 16Kotadata: dados OTA de 8Kphy_init: dados PHY de 4Kfactory: partição de app de 768Kota_0,ota_1: partições de app OTA de 768K cadastorage: partição de dados FAT de 1M
- Segundo uma dica de um leitor, esse dump da flash poderia ter sido protegido se a criptografia de flash estivesse ativada, mas ela não estava ativada neste dispositivo
Chaves e certificados encontrados no armazenamento
- O estado mais recente da partição
nvscontinha o SSID e a senha do WiFi, e os logs históricos também mostravam credenciais de WiFi usadas anteriormente - A partição FAT
storagefoi montada como um disco virtual com OSFMount para inspeção - O armazenamento continha os seguintes arquivos
dev_infodev_key.keyserialserver_configSmartDevice-root-ca.crtSmartDevice-signer-ca.crtwifi_config
dev_key.keyera uma chave privada de curva elíptica que começava com-----BEGIN EC PRIVATE KEY-----, verificada comopenssl ec -in dev_key.key -text -noout- Os dois arquivos
.crteram certificados que começavam com-----BEGIN CERTIFICATE-----, verificados comopenssl x509 - Como os certificados e a chave do dispositivo estavam armazenados no próprio dispositivo, aumentou a probabilidade de que fossem usados para criptografar os dados dos pacotes UDP
Configuração do ambiente de análise no Ghidra
- A imagem da partição
factoryem execução foi aberta e analisada no CodeBrowser do Ghidra - Como o ESP32 usa o conjunto de instruções Xtensa, foi selecionada a linguagem
Tensilica Xtensa 32-bit little-endian - Como a imagem bruta da partição não refletia corretamente o mapeamento de memória virtual, foi gerado um
part.3.factory.elfcom o esp32knife e ele foi importado novamente - Também foi publicado um commit que modifica o esp32knife para dar suporte ao segmento
RTC_DATA - As estruturas dos periféricos e o mapa de memória do ESP32 foram carregados com o SVD-Loader-Ghidra
- Com o
SymbolImportScriptdo Ghidra, foram importados os rótulos de funções da ROM do ESP32, facilitando a identificação de funções comuns da ROM, comoprintf
Pistas de criptografia encontradas por strings
- Em
Defined Stringsno Ghidra, foram rastreadas as strings vistas no log serial e strings próximas - As strings próximas continham as seguintes pistas
Message CRC errorSeed ErrorPRNG failECDH setup failedmbedtls_ecdh_gen_public failedmbedtls_ecdh_compute_shared failedMBED HKDF failedWrite ECC conn packet
- mbedtls é uma biblioteca open source que implementa primitivas criptográficas, manipulação de certificados X509, SSL/TLS e DTLS
- Como funções ECDH e HKDF são usadas diretamente, e não DTLS, a análise indicou que a troca de chaves e a derivação de chaves foram implementadas dentro de um protocolo próprio
- A string
ECC conn packetmostra que o pacote de conexão inicial está relacionado ao processo de troca de chaves ECDH
Patch do firmware para remover a dependência do painel de controle
- Como era inconveniente analisar a PCB conectada ao ventilador e ao painel de controle, o painel de controle foi desconectado, mas durante o boot ocorria um panic com o log
No Cap device found! - A função próxima à string
No Cap device found!imprimiaCapSense Init, então foi considerada a lógica de inicialização da entrada capacitiva do painel frontal - No Ghidra, essa função foi nomeada como
InitCapSense, e o serviço que a chamava comoStartCapSenseService - A instrução que chamava
StartCapSenseServicefoi substituída pornop, removendo a inicialização do serviço do painel de controle - Os bytes foram modificados na imagem bruta
part.3.factorye gravados novamente no offset0x10000, mas o dispositivo não inicializou por erro de checksum da imagem do ESP32 - Com base na lógica interna do esptool, foi adicionado um script para corrigir o checksum da partição de aplicativo
- Ao gravar a imagem com o checksum restaurado, o dispositivo funcionou normalmente mesmo sem o painel de controle, e a modificação do firmware foi bem-sucedida
Estrutura do cabeçalho dos pacotes e do CRC
- Após comparar pacotes em vários boots, os primeiros 13 bytes eram semelhantes e o restante parecia estar criptografado
- O formato do cabeçalho do pacote era o seguinte
55: byte mágico para identificação do protocolo00 31: tamanho do pacote02: identificador da mensagem01 23 45 67 89 AB CD EF FF: serial de 9 bytes do dispositivo
- O padrão dos IDs de mensagem era o seguinte
0x02: primeiro pacote enviado pelo dispositivo inteligente0x82: primeira resposta enviada pelo servidor em nuvem0x01: pacotes posteriores enviados pelo dispositivo inteligente0x81: respostas posteriores enviadas pelo servidor
- O bit mais significativo distingue requisições do cliente e respostas do servidor, e o bit menos significativo distingue a troca inicial dos pacotes posteriores
- Seguindo a função que referencia a string
Message CRC error, foi identificada a lógica de verificação de CRC - Os últimos 2 bytes eram um checksum CRC-16 sobre todo o restante do pacote
- O polinômio era
0x1021 - O valor inicial era
0xFFFF - A verificação foi confirmada da mesma forma em vários pacotes capturados
- O polinômio era
Fluxo de geração de chaves ECDH/HKDF
- No pacote que parecia ser a troca inicial de chaves, os dados excluindo os 13 bytes de cabeçalho e os 2 bytes de CRC tinham 32 bytes, o que corresponde ao tamanho de uma chave pública de 256 bits
- Na requisição do cliente, havia
00 01no início, e como o valor não mudava a cada boot, ele foi tratado como um descritor de dados - No Ghidra, a função de geração de chaves foi encontrada seguindo as strings de erro e resumida em nível de pseudocódigo por comparação com o código-fonte do mbedtls
- A função de geração de chaves executava as seguintes ações
- Gerava um par de chaves ECDH com
mbedtls_ecdh_gen_public - Parecia sobrescrever a chave gerada com outra chave na memória
- Carregava outra chave pública
- Calculava o segredo compartilhado com
mbedtls_ecdh_compute_shared - Gerava um valor aleatório de 32 bytes com
mbedtls_ctr_drbg_random - Derivava a chave final com
mbedtls_hkdf
- Gerava um par de chaves ECDH com
- A configuração do HKDF era a seguinte
- Hash:
SHA-256 salt: segredo compartilhado ECDHinput: valor aleatório de 32 bytes gerado pelo dispositivoinfo: serial de 9 bytes do dispositivo- Tamanho da chave de saída:
0x10, ou seja, 16 bytes
- Hash:
- A função chamadora transmitia
0x22bytes ao anexar o valor aleatório de 32 bytes depois de00 01, o que coincide com o formato do pacote inicial de troca de chaves capturado
Saída do segredo compartilhado e descriptografia AES
- Para calcular a chave final de descriptografia, era necessário obter o segredo compartilhado ECDH
- Em vez de usar depuração via JTAG, o firmware foi modificado sobrescrevendo o local da lógica CapSense já desativada com uma função customizada que imprimia o segredo compartilhado pela serial
- Uma chamada de função foi inserida em
GenerateNetworkKeylogo após a geração do segredo compartilhado, e os 32 bytes foram impressos usando o ponteiro da chave em um registrador - No boot, depois de
Write ECC conn packet, o segredo compartilhado era impresso em hexadecimal, e o valor não mudava mesmo após várias reinicializações - A chave de saída do HKDF também foi confirmada com outro patch, permitindo reproduzir a mesma lógica de geração de chaves a partir dos pacotes capturados
- Dentro da função de criptografia, foi encontrada uma tabela estática que começava com
63 7C 77 7B F2 6B 6F C5, coincidente com a AES Forward S-Box do mbedtls - O método final de criptografia era AES-128-CBC, e o valor aleatório de 16 bytes dentro do pacote era usado como IV
- Nos pacotes descriptografados, foram identificados valores legíveis como
mirror_data_get,FAN_SPEED,BOOST,FILTER1eFILTER2
Implementação do proxy MITM
- Como a chave privada do dispositivo e a lógica de derivação de chaves foram obtidas, e os dados dinâmicos necessários ficam expostos na rede, foi possível escrever um proxy MITM sem modificar o firmware
- O script em Node.js cria um socket UDP local e um socket UDP para o servidor em nuvem, encaminhando pacotes nos dois sentidos
- Os pacotes recebidos do dispositivo inteligente são registrados em log e enviados ao servidor em nuvem; os pacotes recebidos do servidor em nuvem são registrados em log e enviados ao dispositivo inteligente
- Pacotes com
messageIdigual a2são tratados como pacotes de troca de chaves, e o valor aleatório contido neles é usado para calcular a chave AES dos pacotes seguintes - Ao operar o dispositivo pelo app móvel e acumular logs MITM, foram identificados os formatos de requisição e resposta necessários para implementar um servidor local
Estrutura das mensagens MessagePack
- Os dados descriptografados ainda estavam em um formato de serialização binária
- O cabeçalho interno dos dados parecia ser um ID e um comprimento em little-endian
01 00: ID do pacote64 00: ID da transação29 00: comprimento dos dados serializados
- O formato de serialização foi parcialmente submetido a engenharia reversa manualmente, mas a verificação mostrou que era MessagePack
- Usando uma implementação como
msgpackr, foi fácil decodificar os dados binários em forma de JSON - As principais mensagens identificadas foram as seguintes
- Troca de chaves: o dispositivo envia ao servidor bytes aleatórios a serem usados no HKDF
mirror_data_get: obtém do servidor o estado inicial durante a inicializaçãoconnect: envia o UUID do firmware atual, e o servidor responde com informações de firmware, configuração, hora e endereço do servidormirror_data: o servidor altera o estado do dispositivo, ou o dispositivo informa ao servidor o estado alteradokeep_alive: o dispositivo envia periodicamente seu estado, como RSSI, RTT, perda de pacotes, número de conexões e uptime
Ponte MQTT e integração com o Home Assistant
- Foi usado MQTT para conectar o Home Assistant ao servidor personalizado
- No Home Assistant, foi configurado o add-on Mosquitto, um broker MQTT de código aberto
- A estrutura de conexão é
Home Assistant↔MQTT Broker↔Custom Server↔Smart Device - O servidor personalizado funciona da seguinte forma
- Quando o dispositivo solicita o estado com
mirror_data_get, responde usando o valor retained do broker MQTT ou um valor padrão - Quando o Home Assistant envia um comando de alteração de estado para um tópico MQTT, o servidor personalizado o repassa ao dispositivo
- Se o estado do dispositivo mudar por qualquer motivo, o pacote
mirror_datado dispositivo é publicado no broker MQTT e marcado como retained
- Quando o dispositivo solicita o estado com
- A fonte da verdade do estado é sempre o dispositivo
- Se a atualização de estado falhar, ela não é exibida no broker MQTT como se tivesse sido atualizada
- Mesmo que o estado seja alterado pelo painel físico de controle, isso é refletido no broker MQTT
- Usando a integração MQTT Fan do Home Assistant, o purificador de ar foi mapeado como um dispositivo de ventilador
- No
configuration.yaml, foram configurados o tópico de estado de energia, o tópico de comando, o tópico de estado da velocidade do ventilador, o tópico de comando da velocidade do ventilador e o intervalo de velocidades de1a4 - O DNS local do Pi-hole foi configurado para resolver o domínio de nuvem do fabricante para o servidor personalizado, fazendo com que o servidor local atuasse como servidor do dispositivo
Avaliação de segurança e resultados
- O fabricante implementou um protocolo próprio em vez de um protocolo padrão como DTLS
- Não está claro se cada dispositivo tem uma chave privada exclusiva, mas há desvantagens em qualquer caso
- Se todos os dispositivos compartilharem a mesma chave privada do firmware, basta fazer engenharia reversa de um único dispositivo para tentar ataques MITM contra outros dispositivos
- Se cada dispositivo tiver uma chave privada exclusiva, o servidor precisa manter um mapeamento entre número de série e chave do dispositivo, e, se esses dados forem perdidos, o servidor não conseguirá responder à comunicação dos dispositivos
- Como há uma chave privada estática incluída no firmware, um atacante pode obter a chave a partir de um único dump de firmware e realizar um ataque MITM
- A implementação não é completamente ruim do ponto de vista de segurança, e o ataque ainda exige acesso físico
- A implementação própria tornou a comunicação de rede opaca, mas segurança por obscuridade está mais para algo que bloqueia por algum tempo ataques comuns voltados a implementações padrão, sendo um obstáculo superável para atacantes
- O objetivo final, a integração com o Home Assistant, foi alcançado, e o purificador de ar funcionou sem problemas por várias semanas
- Também foi configurada uma automação para colocar o purificador de ar em modo boost por algum tempo quando os níveis de PM2.5 ou VOC ficam altos demais em um monitor de ar separado
Ainda não há comentários.