1 pontos por GN⁺ 2025-04-16 | Ainda não há comentários. | Compartilhar no WhatsApp
  • 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:41014 e 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-CBC e 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 .apk do app Android foi extraído, e o classes.dex foi 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 em assets/index.android.bundle
    • O código de exemplo incluía uma conexão com wss://smartdeviceapi.---.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 DNS do Pi-hole, esse domínio foi apontado para a estação de trabalho local 192.168.0.10, e o tráfego do IP do dispositivo 192.168.0.61 foi filtrado no Wireshark
  • O dispositivo estava enviando pacotes UDP para a porta 41014 da 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 TXD0 e RXD0, 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 Bridge do Flipper Zero
    • O TX do Flipper Zero foi conectado ao RX do ESP32
    • O RX do Flipper Zero foi conectado ao TX do ESP32
    • GND foi conectado a GND
  • Ao conectar pelo Putty em COM7 a 115200, 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 KiB de espaço total e 0 KiB de espaço disponível
  • A aplicação estava lendo os seguintes arquivos
    • serial
    • dev_key.key
    • SmartDevice-root-ca.crt
    • SmartDevice-signer-ca.crt
    • server_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 pino IO0 conectado ao GND
  • 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 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 16K
    • otadata: dados OTA de 8K
    • phy_init: dados PHY de 4K
    • factory: partição de app de 768K
    • ota_0, ota_1: partições de app OTA de 768K cada
    • storage: 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 nvs continha o SSID e a senha do WiFi, e os logs históricos também mostravam credenciais de WiFi usadas anteriormente
  • A partição FAT storage foi montada como um disco virtual com OSFMount para inspeção
  • O armazenamento continha os seguintes arquivos
    • dev_info
    • dev_key.key
    • serial
    • server_config
    • SmartDevice-root-ca.crt
    • SmartDevice-signer-ca.crt
    • wifi_config
  • dev_key.key era uma chave privada de curva elíptica que começava com -----BEGIN EC PRIVATE KEY-----, verificada com openssl ec -in dev_key.key -text -noout
  • Os dois arquivos .crt eram certificados que começavam com -----BEGIN CERTIFICATE-----, verificados com openssl 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 factory em 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.elf com 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 SymbolImportScript do Ghidra, foram importados os rótulos de funções da ROM do ESP32, facilitando a identificação de funções comuns da ROM, como printf

Pistas de criptografia encontradas por strings

  • Em Defined Strings no Ghidra, foram rastreadas as strings vistas no log serial e strings próximas
  • As strings próximas continham as seguintes pistas
    • Message CRC error
    • Seed Error
    • PRNG fail
    • ECDH setup failed
    • mbedtls_ecdh_gen_public failed
    • mbedtls_ecdh_compute_shared failed
    • MBED HKDF failed
    • Write 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 packet mostra 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! imprimia CapSense 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 como StartCapSenseService
  • A instrução que chamava StartCapSenseService foi substituída por nop, removendo a inicialização do serviço do painel de controle
  • Os bytes foram modificados na imagem bruta part.3.factory e gravados novamente no offset 0x10000, 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 protocolo
    • 00 31: tamanho do pacote
    • 02: identificador da mensagem
    • 01 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 inteligente
    • 0x82: primeira resposta enviada pelo servidor em nuvem
    • 0x01: pacotes posteriores enviados pelo dispositivo inteligente
    • 0x81: 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

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 01 no 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
  • A configuração do HKDF era a seguinte
    • Hash: SHA-256
    • salt: segredo compartilhado ECDH
    • input: valor aleatório de 32 bytes gerado pelo dispositivo
    • info: serial de 9 bytes do dispositivo
    • Tamanho da chave de saída: 0x10, ou seja, 16 bytes
  • A função chamadora transmitia 0x22 bytes ao anexar o valor aleatório de 32 bytes depois de 00 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 GenerateNetworkKey logo 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, FILTER1 e FILTER2

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 messageId igual a 2 sã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 pacote
    • 64 00: ID da transação
    • 29 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ção
    • connect: envia o UUID do firmware atual, e o servidor responde com informações de firmware, configuração, hora e endereço do servidor
    • mirror_data: o servidor altera o estado do dispositivo, ou o dispositivo informa ao servidor o estado alterado
    • keep_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 AssistantMQTT BrokerCustom ServerSmart 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_data do dispositivo é publicado no broker MQTT e marcado como retained
  • 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 de 1 a 4
  • 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.

Ainda não há comentários.