blatann

BLE-Exploits mit Python und nRF52840 entwickeln

Lernziel

Du lernst, wie blatann über die Connectivity-Firmware mit dem nRF52840-Dongle kommuniziert. Du installierst die Bibliothek, flashst die Firmware, und setzt einen vollständigen BLE-Exploit in Python um — vom Verbindungsaufbau bis zum gezielten Write auf eine Charakteristik.

Was ist blatann?

blatann ist eine Python-Bibliothek für BLE-Kommunikation, die direkt mit dem nRF52840-Dongle von Nordic Semiconductor kommuniziert. Anders als Bleak oder ähnliche Bibliotheken, die den BLE-Stack des Betriebssystems verwenden, spricht blatann über eine serielle Verbindung mit der sogenannten Connectivity-Firmware, die auf dem Dongle läuft.

Das gibt dir eine Abstraktionsschicht, die deutlich tiefer liegt als bei systemeigenen BLE-APIs. Du kannst Connection-Parameter direkt setzen, Pairing explizit steuern und denselben Hardware-Dongle nutzen, den du in Phase 2 als Sniffer betrieben hast — nur mit anderer Firmware.

blatann vs. Bleak: die wichtigste Unterscheidung

Bleak ist einfacher zu installieren und läuft auf dem eingebauten Bluetooth-Adapter. Es eignet sich gut für Standard-BLE-Kommunikation. blatann erfordert einen nRF52840-Dongle, bietet dafür aber direkten Chip-Level-Zugriff: Du kannst Verbindungen ohne Verschlüsselung aufbauen, beliebige Handles schreiben, und Connection Intervals und PHY manuell kontrollieren. Für Sicherheitsanalysen und PoC-Exploits ist blatann die richtige Wahl.

Installation

Voraussetzungen

  • Python 3.9 oder neuer
  • nRF52840-Dongle (Nordic Semiconductor PCA10059)
  • nRF Connect for Desktop (für Firmware-Flashing)
# Virtuelle Umgebung erstellen (empfohlen)
python3 -m venv ble-env
source ble-env/bin/activate  # Windows: ble-env\Scripts\activate

# blatann installieren
pip install blatann==0.6.0

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

Connectivity-Firmware flashen

Der nRF52840-Dongle muss mit der Connectivity-Firmware bespielt werden, bevor blatann ihn ansprechen kann. Das ist eine einmalige Einrichtung pro Dongle.

# Pfad zur mitgelieferten Firmware finden
python3 -c "import blatann, os; print(os.path.dirname(blatann.__file__))"
# Firmware liegt unter: .../blatann/sdk/connectivity/
# Dateiname: connectivity_4.1.4_usb_with_s132_5.1.0.hex
  1. nRF Connect for Desktop öffnen
  2. Programmer-App starten
  3. nRF52840-Dongle anschließen (im Bootloader-Modus: Mode-Taste beim Einstecken halten)
  4. .hex-Datei aus dem blatann-Paket auswählen
  5. Flash-Vorgang starten — dauert ca. 60 Sekunden

Serielle Port-Berechtigungen (Linux)

blatann kommuniziert mit dem Dongle über /dev/ttyACM0. Ohne Gruppenrechte schlägt die Verbindung mit einem Permission denied-Fehler fehl. Prüfe mit ls -la /dev/ttyACM* und füge deinen Nutzer zur dialout-Gruppe hinzu:

sudo usermod -a -G dialout $USER
# Danach neu anmelden oder: newgrp dialout

Grundlegendes Beispiel: LED-Strip ansprechen

Der LED-Strip aus dem Fallbeispiel dieser Arbeit verwendet keinerlei Verschlüsselung. Steuerbefehle werden im Format 7e [len] [opcode] [params...] ef auf eine einzige Charakteristik geschrieben. Das macht ihn zum idealen Einstiegsbeispiel für blatann.

from blatann import BleDevice

# Konstanten — aus Phase 1 (JADX) und Phase 2 (Wireshark) bekannt
SERIAL_PORT = "/dev/ttyACM0"           # Linux; Windows: "COM3"
TARGET_MAC  = "AA:BB:CC:DD:EE:FF"      # MAC aus nRF Connect oder Wireshark
WRITE_UUID  = "0000ffd9-0000-1000-8000-00805f9b34fb"


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


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

    print(f"[*] Verbinde mit {mac}...")
    peer = ble_device.connect(mac).wait(timeout=15)

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

    print("[*] Entdecke GATT-Dienste...")
    peer.discover_services().wait(timeout=10)

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

    payload = build_color_cmd(r, g, b)
    char.write(payload).wait(timeout=5)
    print(f"[+] Farbe gesetzt: RGB({r}, {g}, {b})")
    print(f"    Payload: {payload.hex()}")  # 7e070503ff000010ef für Rot

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


if __name__ == "__main__":
    set_color(SERIAL_PORT, TARGET_MAC, 255, 0, 0)  # Rot

Funktionen im Überblick

FunktionBeschreibung
BleDevice(port)Dongle-Verbindung initialisieren
ble_device.open()Serielle Verbindung aufbauen
ble_device.connect(mac).wait()BLE-Verbindung zum Zielgerät
peer.discover_services().wait()GATT-Baum vollständig einlesen
peer.database.find_characteristic(uuid)Charakteristik nach UUID suchen
char.read().wait()Charakteristik auslesen
char.write(data).wait()Bytes auf Charakteristik schreiben
char.subscribe(callback).wait()Notifications aktivieren
ble_device.scanner.start()BLE-Geräte scannen
peer.disconnect().wait()Verbindung sauber trennen

Erweiterte Nutzung

Notifications empfangen

Viele Geräte senden Messdaten oder Bestätigungen über Notifications. blatann erlaubt es, Callback-Funktionen zu registrieren, die bei jeder eingehenden Notification aufgerufen werden.

def on_notification(characteristic, event_args):
    data = event_args.value
    print(f"[<] Notification: {data.hex()}")


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

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

    notify_char = peer.database.find_characteristic(notify_uuid)
    notify_char.subscribe(on_notification).wait()

    print("[*] Warte auf Notifications (10 Sekunden)...")
    import time
    time.sleep(10)

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

Connection-Parameter einstellen

blatann erlaubt es, Connection-Intervall und Slave-Latency direkt zu kontrollieren. Das ist nützlich, wenn ein Gerät auf bestimmte Timing-Parameter angewiesen ist oder du Timeouts simulieren möchtest.

from blatann.gap.gap_types import ConnectionParameters

# Verbindung mit angepassten Parametern aufbauen
conn_params = ConnectionParameters(
    min_conn_interval_ms=15,
    max_conn_interval_ms=30,
    timeout_ms=4000,
    slave_latency=0,
)

peer = ble_device.connect(mac, connection_params=conn_params).wait(timeout=15)

# Alternativ: Parameter einer bestehenden Verbindung ändern
peer.set_connection_parameters(100, 120, 6000)

Pairing und Bonding

Für Geräte, die Pairing erfordern, bietet blatann ein explizites Pairing-Interface. Du setzt die Sicherheitsparameter direkt nach dem Verbindungsaufbau und löst das Pairing dann separat aus.

from blatann.gap.smp_types import IoCapabilities

# Just Works: kein Passcode, keine I/O-Fähigkeiten → automatisches Pairing
peer.security.set_security_params(
    passcode_pairing=False,
    io_capabilities=IoCapabilities.NO_INPUT_NO_OUTPUT,
    bond=False,
)

# Pairing anfordern (nutzt die zuvor gesetzten Parameter)
peer.security.pair().wait(timeout=30)
print("[+] Pairing abgeschlossen: Level", peer.security.security_level)

Just Works: Kein Schutz gegen MITM

Der Pairing-Modus "Just Works" (kein PIN, kein ECDH-Key-Vergleich) schützt nicht gegen Man-in-the-Middle-Angriffe. Er entspricht Security Mode 1, Level 2 (unauthentifizierte Verschlüsselung) im BLE-Sicherheitsmodell. Die Verbindung wird zwar verschlüsselt, aber der Temporary Key ist 0 — ein passiver Angreifer, der das Pairing mitschneidet, kann alles entschlüsseln. Echter MITM-Schutz erfordert mindestens Passkey Entry (SM1 Level 3).

Mehrere Geräte gleichzeitig ansprechen

blatann unterstützt mehrere gleichzeitige Verbindungen. Das erlaubt koordinierte Multi-Device-Skripte, zum Beispiel um Race Conditions oder Broadcast-Schwachstellen zu demonstrieren.

def connect_all(port: str, mac_list: list[str]) -> list:
    ble_device = BleDevice(port)
    ble_device.open()

    peers = []
    for mac in mac_list:
        peer = ble_device.connect(mac).wait(timeout=15)
        if peer is not None:
            peer.discover_services().wait()
            peers.append(peer)
            print(f"[+] Verbunden: {mac}")

    return peers

Bekannte Probleme

ProblemUrsacheLösung
Permission denied: /dev/ttyACM0Fehlende Dialout-Gruppesudo usermod -a -G dialout $USER
Failed to open deviceFalsche Firmware oder Dongle nicht erkanntConnectivity-Firmware erneut flashen
Connection timeoutGerät nicht in Reichweite oder im SchlafmodusGerät aufwecken, Dongle näher heranhalten
Characteristic not foundFalsche UUID oder Service Discovery zu frühdiscover_services().wait() vollständig abwarten
Firmware version mismatchblatann-Version passt nicht zur Firmware-VersionFirmware aus dem installierten blatann-Paket nehmen (blatann/sdk/connectivity/)

Debugging aktivieren

Wenn eine Verbindung unerwartet abbricht oder ein Write fehlschlägt, hilft das blatann-Debug-Log weiter. Es zeigt jeden ATT-Befehl auf der Drahtebene.

# Vor dem Skript ausführen:
export BLATANN_LOG_LEVEL=DEBUG

# Oder im Skript:
import logging
logging.basicConfig(level=logging.DEBUG)

Zusammenfassung

  • blatann kommuniziert über die Connectivity-Firmware direkt mit dem nRF52840-Dongle — das erlaubt Chip-Level-Kontrolle ohne OS-Bluetooth-Stack.
  • Installation: pip install blatann==0.6.0 + Connectivity-Firmware flashen + Dialout-Gruppe setzen.
  • Das Grundmuster jedes Scripts: BleDevice öffnen → verbinden → discover_services → Charakteristik suchen → lesen/schreiben/subscriben → trennen.
  • Notifications, Connection-Parameter und Pairing sind vollständig programmierbar.
  • Der häufigste Stolperstein: fehlende Serielle-Port-Berechtigungen auf Linux.