Software-Design Grundlagen der Sicherheit bei Embedded-Software
Schutzmaßnahmen auf der Betriebssystem- und Hardwareebene gegen Hackerangriffe sind alles andere als perfekt. Daher müssen Entwickler selbst gegen Schwachstellen der Embedded-Software vorgehen.
Anbieter zum Thema

Ich hatte mich auf eine Reise nach Lodz in Polen vorbereitet, der Stadt, in der meine Eltern als Kinder gelebt hatten. Ich war niemals zuvor dort gewesen.
Ich googelte den Namen der Stadt und stieß sehr schnell auf eine Geschichte, die überraschend und erschreckend war: Ein Schüler hatte eine TV-Fernbedienung so manipuliert, dass er damit die Straßenbahn der Stadt steuern konnte – und die Tram damit in eine riesige Modellbahnanlage verwandelte. Der Jugendliche veränderte mit seinem Infrarot-Spielzeug Weichenstellungen und brachte so einige Züge zum Entgleisen. In einem Fall wurden zwölf Menschen verletzt.
In jüngster Zeit haben Begriffe wie Stuxnet und Duqu Eingang in unseren Wortschatz gefunden. Die Sicherheit eingebetteter Systeme ist unerbittlichen Attacken ausgesetzt, darunter auch sogenannte SCADA-Systeme (Supervisory Contron and Data Acquisition), die Industrieanlagen, Verkehrsnetze oder Kraftwerke überwachen und steuern.
Viele Entwickler eingebetteter Software meinen, dass die Sicherheit der Systeme auf der Ebene des Systems Engineering gehandhabt werden sollte – oder von der Hardware, die die Software enthält. In der Tat kann vieles auf dieser Ebene erledigt werden, so zum Beispiel:
- Sichere Netzwerkprotokolle
- Firewalls
- Datenverschlüsselung
- Authentifizierung von Datenquellen
- Hardware-assistiertes Monitoring des Kontrollflusses
- Und Ähnliches.
Aber diese herkömmlichen Techniken reichen nicht aus. Das wurde beängstigend deutlich bei der letztjährigen Embedded Systems Conference in Boston in einem Vortrag zum Thema „Starke Verschlüsselung und korrektes Design sind nicht genug: Schützen Sie Ihr System vor Angriffen aus Seitenkanälen“ beschrieben. Der Referent skizzierte, wie Messungen des Stromverbrauchs, elektromagnetische Lecks, akustische Emissionen und Zeitmessungen Angreifern Informationen geben können, die dazu benutzt werden können, ein Gerät mit eingebetteter Software anzugreifen.
Klar ist, dass Verteidigungsmaßnahmen auf der System- der Hardwareebene nicht ausreichen. Viele Attacken nützen bekanntlich Schwachstellen in der Anwendungssoftware aus. Solche Lücken in unseren eingebetteten Systemen entstehen während des Designs und der Entwicklung der Software. Da Schutzmaßnahmen auf der System- und Hardwareebene alles andere als perfekt sind, müssen wir eine dritte Verteidigungslinie aufbauen, indem wir gegen Schwachstellen in unserer Anwendungssoftware vorgehen.
Natürlich wird die Verteidigungslinie in unserer Software nicht ganz perfekt sein. Das unmittelbare Ziel bei unserer Arbeit an dieser Verteidigungsstellung ist es, die Angriffsfenster, die in unserer Software existieren, zu reduzieren. Der erste Schritt ist es hier, wie ein Angreifer zu denken. Fragen Sie sich, wie ein Angreifer Ihr System und Ihre Software ausnützen könnte, um darin einzudringen.
Sie könnten dies als eine Bedrohungsanalyse ansehen. Benutzen Sie die Ergebnisse der Analyse dazu, um zu beschreiben, was Ihre Software nicht tun sollte. Sie könnten dies als „Missbrauchsfälle“ bezeichnen. Benutzen Sie diese Fälle, um zu planen, wie Ihre Software Attacken besser widerstehen, aushalten oder sich davon wieder erholen kann.
Angriffsfenster reduzieren
Vergessen Sie nicht, dass Angreifer bei eingebetteten Systemen einen großen Vorteil haben: Die meisten Embedded Systems sind bei der Ausführungszeit sehr eingeschränkt, oft müssen sie eine Mischung aus harten und weichen Echtzeitaufgaben erfüllen. Das zwingt uns, Anwendungssoftware zu entwerfen, die schlank und effektiv ist, etwa indem intensive Laufzeit- und Plausibilitätsprüfungen (zum Beispiel Assertions mit Invarianten) auf ein Minimum reduziert werden, um die Zeitbeschränkungen einzuhalten.
Unsere Angreifer haben keine solchen Beschränkungen in der Ausführungszeit: Sie sind zufrieden damit, Wochen oder Monate damit zu verbringen, ihre Attacken vorzubereiten und auszuführen – vielleicht probieren sie sogar ein und dieselbe Angriffsmethode mehrere Millionen Male aus und hoffen darauf, dass sie einmal Erfolg haben wird, oder sie versuchen jeden Tag eine andere Attacke, bis sie eines Tages auf ein offenes Angriffsfenster stößt.
Entwickler eingebetteter Software spielen das Security-Thema manchmal herunter und sagen: „Unser Gerät wird nie mit dem Internet oder einem anderen externen Kommunikationsnetz verbunden sein. Wir sind gegen Angriffe immun.“ Das ist leider nicht wahr. Erlauben Sie mir ein Gegenbeispiel:
Viele Geräte nutzen Analog-zu-Digital-Konverter (ADC) zur Sammlung von Daten. Die ADCs werden regelmäßig abgefragt, und die Applikation speichert die Datenpakete in einem Array. Die Applikation verarbeitet später diesen Daten-Array.
Aber ein Angreifer könnte dies aus einem ganz anderen Blickwinkel sehen, etwa so: „Was würde passieren, wenn ich den ADC mit elektrischen Signalen füttere, die einer exakten hexadezimalen Darstellung des Codes eines Schadprogramms entsprechen?“ Auf diese Weise kann der Angreifer seine Software in Ihren Rechner einschleusen. Ein Netzwerk oder das Internet ist dafür nicht nötig.
Angreifer brauchen nicht immer ein externes Netzwerk
Der Angreifer könnte nun darauf setzen, dass Ihre Software die ADC-Daten in einem Stapelspeicher (Stack) sichert (vielleicht indem Sie alloca() oder malloca() verwenden). Wenn er Glück hat, könnte er einen Array-Überlauf verursachen, vielleicht indem er den Hardware-Timer manipuliert, der die ADC-Abfrage steuert.
Ein typischer Stack ist auf der Grafik auf dieser Seite abgebildet. Wenn der Angreifer einen Array-Überlauf verursacht, könnte der Stack korrumpiert werden, wie das Schema rechts daneben zeigt. Beachten Sie, dass die Rücksprungadresse im Stack an einer Position gespeichert wurde, die hinter dem Ende des Arrays lag.
Wenn der Angreifer die Korrumpierung richtig plant, wird der Überlauf den Ort im Stack erreichen, an dem die aktuelle Rücksprungadresse gespeichert wurde. Das kann dazu benutzt werden, um an diesem Platz im Stack einen Zeiger auf den eigenen Code einzuschleusen. Wenn Ihr Code die Rücksprungadresse benutzt, geht die Steuerung auf den Code des Angreifers über.
Das wird als „Stack Smashing“-Attacke bezeichnet. Beachten Sie, dass sie in diesem Beispiel völlig ohne Internetzugang und ohne Verbindung zu einer externen Datenleitung ausgeführt wurde.
Design-Grundlagen für sichere Embedded-Software
Beim Design eingebetteter Software können Entwickler die Sicherheit verbessern, indem sie einige Grundsätze beachten:
1. Misstrauische Aufteilung
Teilen Sie die Funktionalität Ihrer Software in einzelne Blöcke auf, die sich gegenseitig nicht vertrauen, um die Angriffsfenster in jedem Block zu verkleinern. In der Embedded-Software nennen wir diese Blöcke oft Prozesse oder Subsysteme oder CSCIs.
Entwerfen Sie jeden Block unter der Annahme, dass Softwareblöcke, mit denen er interagiert, angegriffen wurden, und dass in diesen interagierenden Blöcken Software des Angreifers anstatt der normalen Anwendung läuft. Vertrauen Sie den Ergebnissen der interagierenden Blöcke nicht. Geben Sie Ihre Daten nicht über gemeinsamen Speicher für andere Blöcke frei.
Benutzen Sie stattdessen ordnungsgemäße Mechanismen der Interprozesskommunikation wie Message Queues des Betriebssystems, Sockets oder TIPC (Transparent Inter Process Communication). Prüfen Sie die Inhalte, die Sie erhalten.
Als Resultat der misstrauischen Aufteilung fällt nicht das gesamte System in die Hände eines Angreifers, wenn ein einzelner Block kompromittiert wurde.
2. Trennung der Privilegien
Halten Sie den Teil Ihres Codes, der mit speziellen Rechten ausgestattet ist, möglichst klein. Wenn der Angreifer in Software einbricht, die mit hohen Privilegien ausgeführt wird, dann kann der Angreifer ebenfalls mit hohen Privilegien operieren. Das gibt ihm ein extrabreites Angriffsfenster in Ihr System.
Vermeiden Sie also, dass Anwendungssoftware im Kernel-Modus oder Master- Modus oder Supervisor-Modus läuft… oder wie das in Ihrer jeweiligen CPU-Architektur heißen mag. Überlassen Sie diesen Modus dem Betriebssystem. Lassen Sie Ihre Software ausschließlich im User-Modus laufen. Dadurch wird Ihre CPU-Hardware darauf verpflichtet, das Angriffsfenster in Ihrer Software zu limitieren.
3. Löschen Sie sensitive Informationen
Löschen Sie jede wieder verwendbare Ressource, wenn Sie sie freigeben! Wenn Sie eine Ressource freigegeben haben – sei es ein Speicherpuffer oder ein Software-Hardware-Interface/Datenregister, könnte der nächste Benutzer genau derselben Ressource ein Angreifer sein. Angreifer auf eingebettete Systeme lieben es, diese Ressourcen zu ‚phishen’, genau wie Internet-Angreifer gerne ‚phishen’. Sie wären bestimmt überglücklich, die Daten zu lesen, die Sie im Puffer bearbeitet haben oder die an die Hardware zur Ausgabe übergeben wurden!
Die meisten Dienste, die Ressourcen in Embedded-Umgebungen freigeben, markieren die Ressourcen einfach als „verfügbar“. Sie lassen die alte Information stehen, die in der Ressource enthalten ist. Damit ist sie potenziell für andere Benutzer lesbar. Das wird gemacht, da es sehr viel schneller geht, als die Ressource explizit auf Null zu setzen.
Wenn also die Applikation mit einer Ressource fertig ist, liegt es an der Anwendung, die Freigabe vorzubereiten, indem sie all diese Elemente auf Null setzt:
- Heap-Puffer, Speicherpool-Puffer, Speicherpartitionssegmente,
- Statisch zugewiesene Speicherpuffer,
- Freigegebene Stack-Bereiche,
- Speichercache,
- Datei im Dateisystem,
- Datenregister für Hardware-Interfaces, Statusregister, Kontrollregister.
Schwachstellen, auf die man beim Coding achten muss
Während der Programmierung können Entwickler die Softwaresicherheit verbessern, indem sie eine Reihe bekannter Schwachstellen vermeiden.
1. Pufferüberlauf
Mit weitem Abstand ist der Pufferüberlauf die am weitesten verbreitete Gefahrenquelle, wenn man in der Sprache C programmiert. Es kann ganz einfach sein – zum Beispiel in das Element mit der Nummer 256 eines 256-teiligen Arrays Daten hineinzuschreiben.
Compiler erkennen verbotene Pufferzugriffe nicht immer als Softwarefehler.
Das kann ernsthafte Konsequenzen wie die „Code Injection“ oder die „Arc Injection“ haben – bei letzterer ändert der Angreifer den Kontrollfluss eines Programms, indem er die Rücksprungadresse auf dem Stack modifiziert. Bei der „Arc Injection“ braucht der Angreifer nicht einmal Code einzuschleusen. er kann zu irgendeiner Funktion im existierenden Code springen oder Gültigkeitsprüfungen oder Assertions überspringen.
Hier ein Beispiel einer Pufferüberlauf-Attacke: Ein eingebettetes Gerät misst die Wassertemperatur in einem Schwimmbecken und zeigt die Zeitanteile als Histogramm an, in denen das Wasser eine bestimmte Temperatur hatte.
Der Softwareentwickler baut einen Array mit 100 positiven Integerzahlen, bei denen jedes Element mit einer Gradangabe in Celsius korrespondiert: Element 0 für 0°, Element 1 für 1° und so weiter. Jedesmal, wenn der Sensor eine Messung der Wassertemperatur vornimmt, wird das korrespondierende Element des Arrays um den Wert 1 erhöht.
Um ganz sicher zu gehen, stattet der Entwickler den Temperatur-Array mit viel Spielraum aus. Er übersteigt den Temperaturbereich bei weitem, den ein menschlicher Körper aushalten würde.
Bis eines Tages der Angreifer den Sensor aus dem Wasser nimmt und mit einem Feuerzeug aufheizt. Sobald der Sensor einen Wert misst, der 100° übersteigt, verletzt die Software für das Histogramm-Update eine Speicheradresse, die hinter dem Ende des Temperatur-Arrays liegt. Wenn dort Daten liegen, hat der Angreifer die Daten korrumpiert. Steht dort Maschinencode, dann hat der Angreifer die ausführbare Software korrumpiert. In jedem Fall ist es ein schädlicher Eingriff. Bitte beachten Sie, dass dafür weder eine Internetverbindung noch eine Verbindung zu einem externen Netzwerk nötig war. Ein Feuerzeug genügte.
Wie können wir Pufferüberläufe vermeiden? Die Schwachstelle ist so weit verbreitet, dass ein mehrstufiger Ansatz am besten ist: Verhindern, Entdecken, Heilen. Im Beispiel unseres Schwimmbeckens sollten Sie explizit prüfen, dass keine Temperatur ermittelt wird, die der von Eis (unter 0°) oder heißem Dampf (über 100°) entspricht.
Verhindern Sie Pufferüberläufe auch, indem Sie gefährliche Bibliotheksfunktionen vermeiden (wie gets()) und Vorsicht bei anderen walten lassen (wie memcpy()).
Entdecken Sie Pufferüberläufe, indem Sie wie ein Anstreicher vorgehen: Erweitern Sie den Puffer an beiden Enden etwas. Füllen Sie die Erweiterungsbereiche mit ungewöhnlichem Inhalt, den ich „Anstrich“ nenne, zum Beispiel eine Falle in der Maschinensprache Ihres Prozessors. Dann prüfen Sie den „Anstrich“ wiederholt zur Laufzeit. Wenn der Anstrich überschrieben wurde, haben Sie einen Pufferüberlauf entdeckt.
2. Tricksereien mit Zeigern
Wenn ein Angreifer einen Datenzeiger modifizieren kann, dann kann der Angreifer nach eigenem Gusto an jede denkbare Stelle verweisen. Wenn ein Angreifer einen Funktionszeiger überschreiben kann, dann ist er auf einem guten Weg, seinen Code auf Ihrem Prozessor auszuführen.
3. Dynamische Speicherzuweisung
Die Verwendung dynamischer Speicherzuweisungen ist in vielen sicherheitskritischen Systemen wie zum Beispiel der Luft- und Raumfahrt verboten. Angreifer sind heiß darauf, solche Defekte zu finden, da sie eine günstige Gelegenheit bieten, die Sicherheit eines Embedded-Systems zu verletzen.
Ein besonders empfindlicher Fehler ist es, die Prüfung zu unterlassen, ob eine Anfrage zur Speicherzuweisung erfolgreich war oder nicht. Manche Mechanismen geben anstelle eines Zeigers auf einen Speicherpuffer den Wert Null aus, wenn kein verfügbarer Speicher mehr vorhanden ist. Wenn die Anwendungssoftware diesen Nullwert als Zeiger behandelt, wird sie in einen Puffer hineinschreiben, der an der Speicheradresse Null beginnt.
Viele Angreifer wären glücklich, wenn Ihre Software dies täte. Angreifer wissen, dass eingebettete Software oft eingeschränkte Speicherressourcen hat. Sie werden alles daransetzen, den Speicher auszuschöpfen, indem sie den Speicherzuweisungsmechanismus dazu zwingen, mehr Speicher als sonst üblich zu belegen – vielleicht durch Speicherlecks.
Sie können auch versuchen, Ihr Datenabfragesystem mit höheren Datenmengen oder Datenraten zu überfluten – in der Hoffnung, dass die Datenlawine Ihre Speicherkapazität erschöpft. Und dann? Wenn Ihre Software einen Speicherpuffer anfragt, aber die Prüfung unterlässt, ob die Zuweisung fehlgeschlagen ist, wird sie einen Puffer an der Adresse Null eröffnen und alles zertrampeln, was vorher dort gewesen ist.
Wenn zum Beispiel Ihre Flags für das Ein- und Ausschalten von Interrupts an dieser Stelle stehen, könnte dadurch die Verbindung zwischen der Software und den Hardware-Interfaces an der Peripherie unterbrochen werden. Das könnte dazu führen, dass Ihr System nicht mehr eingebettet ist.
4. Verunreinigte Daten
Daten, die von außen in ein eingebettetes System gelangen, darf man nicht vertrauen. Stattdessen müssen sie vor dem Gebrauch desinfiziert werden.
Das trifft auf alle Arten von Datenströmen zu, auch für die einfachsten Integerwerte. Angreifer halten Ausschau nach Extremwerten, die abnorme Effekte produzieren. Insnbesondere jagen sie nach unerwarteten Werten, also nach Situationen, in denen ein digitaler Mikroprozessor zu einem anderen Ergebnis kommen würde als ein Mensch mit Papier und Bleistift.
Sagen wir, ein Integerwert i soll den Wert 2.147.483.647 haben. Wenn ich zu diesem Wert auf Papier 1 addiere, dann bekomme ich +2.147.483.648. Aber wenn mein Mikroprozessor den Befehl i++ ausführen soll, würde -2.147.483.648 herauskommen, eine große negative Zahl. Für einen gewieften Angreifer wäre es ein Leichtes, diese Eigenart auszunutzen, um ein eingebettetes System zu verwüsten.
Eine nützliche Technik, Daten zu desinfizieren, ist das Whitelisting. Damit werden alle möglichen gültigen Werte für eine bestimmte Art von Daten beschrieben, und der Code akzeptiert nur diese Werte. Alle unerwarteten Werte werden als verunreinigt betrachtet und zurückgewiesen, bevor sie verarbeitet werden.
Zusammenfassung
Natürlich beschränken sich die Sicherheitskonzepte für eingebettete Systeme nicht auf drei Design-Grundsätze und vier Schwachstellen beim Coding. Angreifer sind kreativ! Sie finden immer neue Wege, die Sicherheit unserer Software und unserer Systeme zu bedrohen. Die Geschichte der Software-Sicherheit ändert sich stetig.
Ein Weg, sich auf dem Laufenden zu halten, ist es, eine Website namens CWE (Common Weakness Enumeration, http://cwe.mitre.org) zu besuchen, die eine kontinuierlich aktualisierte Liste von sicherheitsrelevanten Software-Schwachstellen bereitstellt. Die meisten davon sind für eingebettete Software genauso wichtig wie für Non-Embedded-Software. Ich würde bei der Liste „25 Most Dangerous Software Errors“ beginnen, die jedes Jahr aktualisiert wird.
Wir haben mehrere Wege kennengelernt, über die ein entschlossener Angreifer ein eingebettetes System unterminieren kann – sogar ohne Internetverbindung und ohne eine Verbindung zu einer externen Kommunikation.
Wir haben auch gesehen, dass Designer und Programmierer eingebetteter Software eine zusätzliche Verteidigungsschicht gegen bösartige Angriffe beisteuern können, jenseits davon, was auf der Hardware- und der Systemebene möglich ist. Die Embedded-Community muss sich der speziellen Probleme und Schwachstellen bewusst sein, die bei dieser Herausforderung an unsere eingebetteten Systeme von Bedeutung sind.
//FG
Literaturhinweise:
[1] „Schoolboy hacks into city's tram system“, in: Daily Telegraph vom 11. Januar 2008.
[2] C. Dougherty, K. Sayre, R.C. Seacord, D. Svoboda, K. Togashi: Secure Design Patterns, CERT Program, Software Engineering Institute, Carnegie Mellon University, Pittsburgh PA, Technical Report CMU/SEI-2009-TR-101, ESC-TR-2009-010.
[3] CWE, Common Weakness Enumeration, The MITRE Corporation, Bedford MA, cwe.mitre.org
* David Kalinsky ist Berater, Trainer und Dozent für Echtzeit- und Embedded-Programmierung in Sunnyvale/Kalifornien (USA)
(ID:33374870)