Smart-Home-Gerät hacken (2024)
(jmswrnr.com)- Um einen ESP32-basierten Luftreiniger, der an Hersteller-App und Cloud gebunden ist, direkt über Home Assistant zu steuern, wurde der Remote-Steuerungspfad per Reverse Engineering analysiert und durch einen lokalen Server ersetzt.
- Durch App-Analyse, DNS-Umleitung und Wireshark-Mitschnitte wurde festgestellt, dass das Gerät UDP-Pakete an
smartdeviceep.---.com:41014sendet und statt Standard-DTLS ein eigenes Protokoll verwendet. - Über eine UART-Verbindung und einen 4-MB-Flash-Dump wurden
dev_key.key, Zertifikate, Serverkonfiguration und WiFi-Einstellungen extrahiert; die Firmware-Struktur wurde mit Ghidra und esp32knife analysiert. - Die Pakete kombinierten einen 13-Byte-Header und eine abschließende 2-Byte-CRC-16, ECDH/HKDF-Schlüsselerzeugung,
AES-128-CBCund MessagePack-Serialisierung; durch einen Firmware-Patch, der das Shared Secret im seriellen Log ausgab, gelang die Entschlüsselung. - Der finale Aufbau bestand aus MITM-Proxy, lokalem Server und einer MQTT-Bridge auf Basis von Mosquitto; über MQTT Fan in Home Assistant konnten Stromversorgung und Lüftergeschwindigkeit über mehrere Wochen stabil gesteuert werden.
Einen cloudabhängigen Luftreiniger auf lokale Steuerung umstellen
- Ziel war es, einen Luftreiniger, der nur mit der mobilen App und dem Cloud-Konto des Herstellers verbunden ist, über Home Assistant zu steuern.
- Durch Umschalten von Bluetooth, WiFi und 5G am Smartphone zeigte sich, dass die App das Gerät nicht über lokales Bluetooth oder WiFi steuert, sondern ausschließlich über die Internetverbindung.
- Da Steuerwerte wie die Lüftergeschwindigkeit irgendwo zwischen Gerät und Cloud-Server ausgetauscht werden, wurde der Netzwerkabschnitt zum zentralen Angriffspunkt.
- Wenn man den Traffic abfängt und Werte verändert, lässt sich das Gerät steuern.
- Wenn man Serverantworten emuliert, kann es auch ohne Internet und Hersteller-Cloud betrieben werden.
- Die Reverse-Engineering-Inhalte dienen Bildungszwecken; produktspezifische sensible Informationen wie private Schlüssel, Domains und API-Endpunkte wurden verschleiert oder entfernt.
- Modifikationen am Gerät können die Garantie ungültig machen oder das Gerät dauerhaft beschädigen.
App-Analyse und Mitschnitt des UDP-Traffics
- Die
.apkder Android-App wurde extrahiert undclasses.dexmit dex2jar und jd-gui geöffnet, um das Innenleben zu untersuchen. - In
MainActivity.classzeigte sich, dass die App auf React Native basiert; inassets/index.android.bundlewurde eine sichere WebSocket-Verbindung gefunden.- Der Beispielcode enthielt eine Verbindung zu
wss://smartdeviceapi.---.com.
- Der Beispielcode enthielt eine Verbindung zu
- Mit der DNS-Abfrageansicht von Pi-hole wurde die Domain des Cloud-Servers ermittelt, zu dem sich das Gerät verbindet.
- Über die Funktion
Local DNSvon Pi-hole wurde diese Domain auf die lokale Workstation192.168.0.10geleitet, und in Wireshark wurde der Traffic der Geräte-IP192.168.0.61gefiltert. - Das Gerät sendete UDP-Pakete an Port
41014der Workstation.
Relay-Aufbau und Hinweise auf ein proprietäres Protokoll
- Da das lokale DNS die Cloud-Domain auf die Workstation auflöste, wurde die tatsächliche Server-IP über den Cloudflare DNS resolver
1.1.1.1abgefragt. - Mit node-udp-forwarder übernahm die Workstation die Rolle eines UDP-Relays zwischen Gerät und Cloud-Server.
- Das erste Paket beim Booten und die Serverantwort wurden mitgeschnitten, wirkten jedoch ohne lesbare Zeichenfolgen wie zufällige Bytes, was auf Verschlüsselung hindeutete.
- Wireshark erkannte die Pakete nicht als DTLS, und auch das Header-Format der DTLS-Spezifikation unterschied sich von den aufgezeichneten Paketen.
- Da es sich offenbar nicht um ein Standardprotokoll handelte, mussten Paketstruktur und Verschlüsselungsverfahren direkt per Reverse Engineering untersucht werden.
ESP32 zerlegen und seriellen Zugriff herstellen
- Nach dem Zerlegen des Geräts waren die Hauptplatine, der Anschluss für den Lüfter und das Flachbandkabel zum vorderen Bedienfeld sichtbar.
- Der Hauptcontroller war als
ESP32-WROOM-32Dgekennzeichnet, ein Mikrocontroller der ESP32-Familie mit WiFi- und Bluetooth-Funktionen. - Als Referenz wurden die Materialien zum ESP32-Reverse-Engineering im Repository ESP32-reversing genutzt.
- Im ESP32-Datenblatt wurden die Pins
TXD0undRXD0identifiziert; anschließend wurden die Leiterbahnen zu Debug-Pinholes auf der Platine verfolgt, um die seriellen Anschlusspunkte zu finden. - Mit der
USB-UART Bridgedes Flipper Zero wurde eine UART-Verbindung aufgebaut.- Flipper Zero
TXwurde mit ESP32RXverbunden. - Flipper Zero
RXwurde mit ESP32TXverbunden. GNDwurde mitGNDverbunden.
- Flipper Zero
- Nach dem Verbinden in Putty mit
COM7und115200Baud wurden Boot-Logs ausgegeben.
Dateien und Serverkonfiguration aus dem Boot-Log
- Das serielle Log gab aus, dass der ESP32 ein Chip mit 2 CPU-Kernen, WiFi/BT/BLE und 4 MB externem Flash ist.
- Die Anwendung lief aus der Partition
factory. - Ein FAT-Dateisystem wurde gemountet; angezeigt wurden
122 KiBGesamtspeicher und0 KiBverfügbarer Speicher. - Die Anwendung las die folgenden Dateien:
serialdev_key.keySmartDevice-root-ca.crtSmartDevice-signer-ca.crtserver_config
- Die Serverkonfiguration enthielt
smartdeviceep.---.com:41014.
Flash-Dump und Partitionsstruktur
- Um den ESP32 im Modus
Download Bootzu starten, wurde das Gerät eingeschaltet, während der PinIO0mitGNDverbunden war. - Mit esptool wurde der gesamte 4-MB-Flash gedumpt.
- Der Befehl lautete
esptool -p COM7 -b 115200 read_flash 0 0x400000 flash.bin.
- Der Befehl lautete
- Der Dump wurde mehrfach durchgeführt, um korrektes Auslesen zu bestätigen, und als Backup gesichert, damit er bei Problemen wieder geflasht werden konnte.
- Der Dump wurde mit esp32knife analysiert, wodurch
partitions.csvgewonnen wurde. - Die Partitionsstruktur enthielt folgende Einträge:
nvs: 16K-Key-Value-Speicherotadata: 8K-OTA-Datenphy_init: 4K-PHY-Datenfactory: 768K-App-Partitionota_0,ota_1: jeweils 768K-OTA-App-Partitionstorage: 1M-FAT-Datenpartition
- Einem Hinweis eines Lesers zufolge hätte dieser Flash-Dump geschützt sein können, wenn Flash-Verschlüsselung aktiviert gewesen wäre; auf diesem Gerät war sie jedoch nicht aktiviert.
Im Speicher gefundene Schlüssel und Zertifikate
- Der neueste Zustand der Partition
nvsenthielt WiFi-SSID und Passwort; in den Verlaufslogs waren außerdem früher verwendete WiFi-Zugangsdaten zu sehen. - Die FAT-Partition
storagewurde mit OSFMount wie ein virtuelles Laufwerk gemountet und untersucht. - Im Speicher befanden sich folgende Dateien:
dev_infodev_key.keyserialserver_configSmartDevice-root-ca.crtSmartDevice-signer-ca.crtwifi_config
dev_key.keywar ein Private Key für Elliptic Curve, der mit-----BEGIN EC PRIVATE KEY-----begann und mitopenssl ec -in dev_key.key -text -nooutgeprüft wurde.- Die beiden
.crt-Dateien waren Zertifikate, die mit-----BEGIN CERTIFICATE-----begannen, und wurden mitopenssl x509geprüft. - Da Zertifikate und Geräteschlüssel auf dem Gerät gespeichert waren, lag nahe, dass sie zur Verschlüsselung der UDP-Paketdaten verwendet wurden.
Ghidra-Analyseumgebung einrichten
- Das laufende
factory-Partitionsimage wurde im CodeBrowser von Ghidra geöffnet und analysiert - Da der ESP32 den Xtensa-Befehlssatz verwendet, wurde die Sprache
Tensilica Xtensa 32-bit little-endianausgewählt - Da das rohe Partitionsimage das virtuelle Memory-Mapping nicht korrekt abbildete, wurde mit esp32knife
part.3.factory.elferzeugt und erneut importiert - Ein Commit, der esp32knife um Unterstützung für das Segment
RTC_DATAerweitert, wurde ebenfalls veröffentlicht - Mit SVD-Loader-Ghidra wurden die Peripheriestruktur und die Memory-Map des ESP32 geladen
- Mit Ghidras
SymbolImportScriptwurden Labels für ESP32-ROM-Funktionen importiert, um gemeinsame ROM-Funktionen wieprintfleichter identifizieren zu können
Über Strings gefundene Hinweise auf Verschlüsselung
- In Ghidras
Defined Stringswurden die im seriellen Log sichtbaren Strings und die umgebenden Strings nachverfolgt - Unter den umgebenden Strings fanden sich folgende Hinweise
Message CRC errorSeed ErrorPRNG failECDH setup failedmbedtls_ecdh_gen_public failedmbedtls_ecdh_compute_shared failedMBED HKDF failedWrite ECC conn packet
- mbedtls ist eine Open-Source-Bibliothek, die kryptografische Primitive, die Verarbeitung von X509-Zertifikaten sowie SSL/TLS und DTLS implementiert
- Da ECDH- und HKDF-Funktionen direkt verwendet werden, DTLS jedoch nicht, wurde analysiert, dass Schlüsselaustausch und Schlüsselableitung innerhalb eines eigenen Protokolls implementiert sind
- Der String
ECC conn packetzeigt, dass das erste Verbindungspaket mit dem ECDH-Schlüsselaustauschprozess zusammenhängt
Firmware-Patch zum Entfernen der Abhängigkeit vom Bedienpanel
- Da die Analyse bei angeschlossener PCB an Lüfter und Bedienpanel unpraktisch war, wurde das Bedienpanel getrennt; beim Booten trat jedoch zusammen mit dem Log
No Cap device found!eine Panic auf - Die Funktion in der Nähe des Strings
No Cap device found!gibtCapSense Initaus und wurde daher als Initialisierungslogik für die kapazitive Eingabe des Frontpanels interpretiert - In Ghidra wurde diese Funktion
InitCapSensegenannt, der aufrufende DienstStartCapSenseService - Die Aufrufinstruktion von
StartCapSenseServicewurde durchnopersetzt, um den Start des Bedienpanel-Dienstes zu entfernen - Im rohen Image
part.3.factorywurden Bytes geändert und es erneut an Offset0x10000geflasht, doch wegen eines ESP32-Image-Checksum-Fehlers bootete es nicht - Auf Basis der internen Logik von esptool wurde ein Skript zum Reparieren der Checksumme der App-Partition hinzugefügt
- Nach dem Flashen des Images mit reparierter Checksumme funktionierte das Gerät auch ohne Bedienpanel normal, und die Firmware-Modifikation war erfolgreich
Paket-Header und CRC-Struktur
- Beim Vergleich der Pakete über mehrere Bootvorgänge hinweg zeigte sich, dass die ersten 13 Byte ähnlich waren und der Rest verschlüsselt wirkte
- Das Paket-Header-Format sah wie folgt aus
55: Magic-Byte zur Protokollidentifikation00 31: Paketlänge02: Nachrichtenkennung01 23 45 67 89 AB CD EF FF: 9-Byte-Geräteseriennummer
- Das Muster der Message-IDs war wie folgt
0x02: erstes vom Smart-Gerät gesendetes Paket0x82: erste Antwort des Cloud-Servers0x01: nachfolgende vom Smart-Gerät gesendete Pakete0x81: nachfolgende Antworten des Servers
- Das höchstwertige Bit unterscheidet Client-Anfragen von Server-Antworten, die unteren Bits unterscheiden den initialen Austausch von späteren Paketen
- Über die Funktionsreferenz auf den String
Message CRC errorwurde die CRC-Prüflogik nachvollzogen - Die letzten 2 Byte waren eine CRC-16-Checksumme über den gesamten restlichen Paketinhalt
- Das Polynom war
0x1021 - Der Initialwert war
0xFFFF - Dies wurde bei mehreren mitgeschnittenen Paketen auf dieselbe Weise verifiziert
- Das Polynom war
Ablauf der ECDH/HKDF-Schlüsselerzeugung
- In einem Paket, das wie der erste Schlüsselaustausch aussah, waren die Daten ohne den 13-Byte-Header und die 2-Byte-CRC 32 Byte lang, was zur Größe eines 256-Bit Public Keys passt
- Der Client-Anfrage war
00 01vorangestellt; da sich der Wert bei jedem Bootvorgang nicht änderte, wurde er wie ein Datendeskriptor behandelt - In Ghidra wurde über die Fehler-Strings die Schlüsselerzeugungsfunktion gefunden und durch Vergleich mit dem mbedtls-Quellcode auf Pseudocode-Ebene zusammengefasst
- Die Schlüsselerzeugungsfunktion führt folgende Aktionen aus
- Erzeugt mit
mbedtls_ecdh_gen_publicein ECDH-Schlüsselpaar - Es ist ein Muster zu sehen, bei dem der erzeugte Schlüssel durch einen anderen Schlüssel im Speicher überschrieben wird
- Lädt einen anderen Public Key
- Berechnet mit
mbedtls_ecdh_compute_shareddas Shared Secret - Erzeugt mit
mbedtls_ctr_drbg_randomeinen 32-Byte-Zufallswert - Leitet mit
mbedtls_hkdfden finalen Schlüssel ab
- Erzeugt mit
- Die HKDF-Konfiguration sah wie folgt aus
- Hash:
SHA-256 salt: ECDH Shared Secretinput: vom Gerät erzeugter 32-Byte-Zufallswertinfo: 9-Byte-Geräteseriennummer- Größe des Ausgabeschlüssels:
0x10, also 16 Byte
- Hash:
- Die aufrufende Funktion hängte an
00 01den 32-Byte-Zufallswert an und sendete0x22Byte; dies entspricht dem Format des mitgeschnittenen ersten Schlüsselaustauschpakets
Ausgabe des Shared Secrets und AES-Entschlüsselung
- Um den finalen Entschlüsselungsschlüssel zu berechnen, wurde das ECDH Shared Secret benötigt
- Statt JTAG-Debugging wurde die Firmware so gepatcht, dass an der Position der bereits deaktivierten CapSense-Logik eine Custom-Funktion überschrieben wurde, die das Shared Secret seriell ausgibt
- Direkt nachdem in
GenerateNetworkKeydas Shared Secret erzeugt wurde, wurde ein Funktionsaufruf eingefügt; über den Schlüsselzeiger im Register wurden 32 Byte ausgegeben - Beim Booten wurde nach
Write ECC conn packetdas Shared Secret hexadezimal ausgegeben, und der Wert änderte sich auch nach mehreren Reboots nicht - Auch der HKDF-Ausgabeschlüssel wurde mit einem separaten Patch bestätigt, und die identische Schlüsselerzeugungslogik ließ sich für mitgeschnittene Pakete reproduzieren
- Innerhalb der Verschlüsselungsfunktion wurde eine statische Tabelle gefunden, die mit
63 7C 77 7B F2 6B 6F C5beginnt und mit der AES Forward S-Box von mbedtls übereinstimmt - Das finale Verschlüsselungsverfahren war AES-128-CBC, und der 16-Byte-Zufallswert im Paket wurde als IV verwendet
- In den entschlüsselten Paketen wurden lesbare Werte wie
mirror_data_get,FAN_SPEED,BOOST,FILTER1undFILTER2確認iert
Implementierung eines MITM-Proxys
- Da der private Geräteschlüssel und die Schlüsselableitungslogik vorlagen und die nötigen dynamischen Daten im Netzwerk offengelegt werden, konnte ohne Firmware-Patch ein MITM-Proxy geschrieben werden
- Das Node.js-Skript erstellt einen lokalen UDP-Socket und einen UDP-Socket für den Cloud-Server und leitet Pakete in beide Richtungen weiter
- Von dem Smart-Gerät empfangene Pakete werden protokolliert und anschließend an den Cloud-Server gesendet; vom Cloud-Server empfangene Pakete werden protokolliert und anschließend an das Smart-Gerät gesendet
- Pakete mit
messageIdgleich2werden als Schlüsselaustauschpakete betrachtet; mit dem darin enthaltenen Zufallswert wird der AES-Schlüssel für die folgenden Pakete berechnet - Beim Bedienen des Geräts über die mobile App wurden MITM-Logs gesammelt, um die für die Implementierung eines lokalen Servers nötigen Anfrage- und Antwortformen zu ermitteln
MessagePack-Nachrichtenstruktur
- Die entschlüsselten Daten lagen weiterhin in einem binären Serialisierungsformat vor
- Der interne Daten-Header sah nach einer ID und einer Länge im Little-Endian-Format aus
01 00: Paket-ID64 00: Transaktions-ID29 00: Länge der serialisierten Daten
- Das Serialisierungsformat wurde zunächst teilweise per Reverse Engineering untersucht; wie sich herausstellte, handelte es sich um MessagePack
- Mit Implementierungen wie
msgpackrließen sich die Binärdaten leicht in JSON-Form dekodieren - Die wichtigsten identifizierten Nachrichten waren:
- Schlüsselaustausch: Das Gerät sendet zufällige Bytes an den Server, die für HKDF verwendet werden
mirror_data_get: Ruft beim Booten den Anfangszustand vom Server abconnect: Sendet die aktuelle Firmware-UUID; der Server antwortet mit Informationen zu Firmware, Einstellungen, Zeit und Serveradressemirror_data: Der Server ändert den Gerätestatus, oder das Gerät meldet dem Server einen geänderten Statuskeep_alive: Das Gerät sendet regelmäßig Statusdaten wie RSSI, RTT, Paketverluste, Anzahl der Verbindungen und Uptime
MQTT-Bridge und Home-Assistant-Integration
- Zur Verbindung von Home Assistant mit einem Custom Server wurde MQTT verwendet
- In Home Assistant wurde das Add-on Mosquitto, ein Open-Source-MQTT-Broker, eingerichtet
- Die Verbindungsstruktur lautet
Home Assistant↔MQTT Broker↔Custom Server↔Smart Device - Der Custom Server arbeitet folgendermaßen:
- Wenn das Gerät per
mirror_data_getden Status anfordert, antwortet er mit dem retained Wert des MQTT-Brokers oder mit einem Standardwert - Wenn Home Assistant einen Befehl zur Statusänderung an ein MQTT-Topic sendet, leitet der Custom Server ihn an das Gerät weiter
- Wenn sich der Gerätestatus aus irgendeinem Grund ändert, published der Custom Server das
mirror_data-Paket des Geräts an den MQTT-Broker und setzt es auf retain
- Wenn das Gerät per
- Die Source of Truth für den Status ist immer das Gerät
- Schlägt ein Status-Update fehl, wird es im MQTT-Broker nicht so angezeigt, als sei es aktualisiert worden
- Auch wenn sich der Status über das physische Bedienpanel ändert, wird dies im MQTT-Broker abgebildet
- Über die MQTT-Fan-Integration von Home Assistant wurde der Luftreiniger als Lüftergerät gemappt
- In
configuration.yamlwurden Topic für den Stromstatus, Command-Topic, Topic für den Lüftergeschwindigkeitsstatus, Command-Topic für die Lüftergeschwindigkeit sowie der Geschwindigkeitsbereich1bis4konfiguriert - Pi-hole Local DNS wurde so eingerichtet, dass die Cloud-Domain des Herstellers auf den Custom Server aufgelöst wird, sodass der lokale Server die Rolle des Geräteservers übernimmt
Sicherheitsbewertung und Ergebnis
- Der Hersteller implementierte ein eigenes Protokoll statt eines Standardprotokolls wie DTLS
- Es ist nicht sicher, ob jedes Gerät einen eigenen privaten Schlüssel hat; in jedem Fall gibt es Nachteile
- Wenn alle Geräte denselben privaten Firmware-Schlüssel teilen, kann bereits das Reverse Engineering eines einzigen Geräts ausreichen, um MITM-Angriffe auf andere Geräte zu versuchen
- Wenn jedes Gerät einen eigenen privaten Schlüssel hat, muss der Server eine Zuordnung von Seriennummern zu Geräteschlüsseln speichern; bei Verlust dieser Daten kann der Server nicht mehr auf die Gerätekommunikation antworten
- Da die Firmware einen statischen privaten Schlüssel enthält, kann ein Angreifer den Schlüssel aus einem einzigen Firmware-Dump gewinnen und einen MITM-Angriff durchführen
- Die Implementierung ist aus Sicherheitssicht nicht völlig schlecht, und für einen Angriff ist weiterhin physischer Zugriff erforderlich
- Die Eigenimplementierung machte die Netzwerkkommunikation undurchsichtig, aber Security through obscurity kann gängige Angriffe auf Standardimplementierungen höchstens vorübergehend abhalten und ist für Angreifer ein überwindbares Hindernis
- Das eigentliche Ziel, die Home-Assistant-Integration, wurde erreicht, und der Luftreiniger lief mehrere Wochen lang problemlos
- Außerdem wurde eine Automatisierung eingerichtet, die den Luftreiniger für eine bestimmte Zeit in den Boost-Modus versetzt, wenn ein separater Luftmonitor zu hohe PM2.5- oder VOC-Werte misst
Noch keine Kommentare.