Zum Inhalt springen
Zyberum Cyber Security Firm
Menü
Secure CodingEmbedded

Secure Coding in Embedded C: Fünf Bugs, die wir oft finden

Ungeprüfte Längen, Integer-Überläufe, Format-Strings, schwacher Zufall, Debug-Reste: die fünf häufigsten Bug-Klassen in Firmware und wie du sie vermeidest.

Zyberum Security Team · Veröffentlicht am · 7 Min. Lesezeit

Firmware ist größtenteils C, und C macht genau das, was du ihm sagst. Wenn wir Embedded-Code reviewen oder ein Gerät per Reverse Engineering zerlegen, tauchen immer wieder dieselben fünf Bug-Klassen auf. Keine davon ist neu. Alle funktionieren noch.

1. Das Längenfeld, dem du vertraut hast

Eine Nachricht kommt mit einem Längenfeld an, und der Code kopiert genau so viele Bytes:

void handle_frame(const uint8_t *frame) {
    uint8_t payload[64];
    uint8_t len = frame[1];
    memcpy(payload, &frame[2], len);   /* len kontrolliert der Angreifer */
}

Kann len 200 sein, schreibt die Kopie weit über den Puffer hinaus. Auf einem Mikrocontroller ohne Speicherschutz ist das oft direkt Codeausführung.

Fix: Prüfe jede Länge gegen die Größe des Ziels und gegen die Anzahl der Bytes, die du wirklich empfangen hast, bevor du sie verwendest.

if (len > sizeof(payload) || len > received - 2) {
    return ERR_LENGTH;
}

2. Integer-Arithmetik, die überläuft

Auch Längenprüfungen gehen schief, wenn die Arithmetik überläuft:

uint16_t total = header_len + body_len;   /* läuft bei 65535 über */
if (total > sizeof(buffer)) return ERR;

Mit header_len = 65530 und body_len = 10 wird total zu 4, und die Prüfung geht durch. Vergleiche zwischen vorzeichenbehafteten und vorzeichenlosen Typen sorgen für dieselbe Art von Überraschung.

Fix: Prüfe jeden Operanden vor der Addition, nimm breitere Typen für Zwischenergebnisse und behandle Compiler-Warnungen zu Vorzeichenkonvertierungen als Fehler.

3. Format-Strings und andere Abkürzungen

Externe Daten direkt zu loggen, ist in Diagnosecode noch immer verbreitet:

printf(device_name);        /* falsch */
printf("%s", device_name);  /* richtig */

Zur selben Familie gehören strcpy, sprintf und gets. Diese Funktionen haben keine Ahnung, wie groß das Ziel ist.

Fix: Verbiete die unbegrenzten Funktionen in deinen Coding-Richtlinien und lass den Build fehlschlagen, wenn sie auftauchen.

4. Zufall, der keiner ist

Session-Tokens, Challenge-Werte für den Diagnosezugang, Nonces für die Verschlüsselung: Alle brauchen unvorhersagbare Zahlen. Wir finden regelmäßig Werte, die mit rand() erzeugt, mit der Uptime geseedet oder aus einem nicht initialisierten Hardware-Generator gezogen werden.

Kann ein Angreifer die Challenge vorhersagen, hilft der beste Algorithmus dahinter nichts.

Fix: Nutze den echten Hardware-Zufallszahlengenerator deiner MCU, prüfe seine Status-Flags und speise daraus einen sauberen deterministischen Generator, wenn du viele Werte brauchst.

5. Debug-Reste

Die ergiebigsten Findings sind oft gar keine Bugs, sondern Features, die niemand entfernt hat:

  • eine serielle Konsole mit Root-Shell
  • ein versteckter Diagnosebefehl, der die Authentifizierung überspringt
  • Testschlüssel und Standardpasswörter im Produktions-Image
  • ausführliche Fehlermeldungen, die Speicheradressen verraten

Fix: Leg die Konfiguration des Produktions-Builds explizit fest, prüfe, was sich vom Entwicklungs-Build unterscheidet, und kontrolliere das ausgelieferte Image, nicht den Source-Tree.

Was wirklich hilft

Regeln und Tools sind nötig, aber sie ersetzen keine Menschen, die wie Angreifer denken:

  1. Coding-Richtlinien (MISRA C, SEI CERT C), durchgesetzt per statischer Analyse in der CI.
  2. Fuzzing für jeden Parser und Protokoll-Handler. Die meisten Bugs von oben fallen innerhalb von Stunden heraus.
  3. Code-Review mit Fokus auf die Stellen, an denen externe Daten hereinkommen.
  4. Schulungen mit deinem eigenen Code. Entwickler merken sich den Bug, den sie selbst ausgenutzt haben.

Genau darum drehen sich unsere Arbeit im Bereich Secure Development und unsere Secure-Coding-Schulungen.

FAQ

Häufig gestellte Fragen

Reicht MISRA C für sicheren Code?

MISRA C ist eine gute Basis, weil es viele Quellen für undefiniertes Verhalten beseitigt. Geschrieben wurde es aber für Safety, nicht gegen Angreifer. Für Security kommen SEI CERT C, Code-Reviews mit Angreiferblick und Fuzzing jeder Schnittstelle dazu, die externe Daten annimmt.

Sollten wir auf Rust umsteigen?

Für neue Komponenten, die nicht vertrauenswürdige Eingaben parsen, beseitigen speichersichere Sprachen ganze Bug-Klassen und sind eine Überlegung wert. Die meiste Firmware wird aber noch viele Jahre C enthalten, deshalb bleiben sicheres C und Reviews nötig.

Wie finden wir diese Bugs in bestehendem Code?

Kombiniere drei Dinge: statische Analyse für die offensichtlichen Muster, Fuzzing für Parser und Protokoll-Handler und manuelles Review des Codes, der externe Eingaben, Authentifizierung und Updates verarbeitet.

AnrufenErstgespräch buchen

Such dir einen Termin aus

In neuem Tab öffnen