Experience Embedded

Professionelle Schulungen, Beratung und Projektunterstützung

Zuverlässige und sichere Gerätetreiber

Autor: André Schmitz, Green Hills Software

Beitrag - Embedded Software Engineering Kongress 2015

 

Die größten Schwachstellen eines Software-Systems sind meist die Gerätetreiber. Sie können oft auf alle Hardware-Register und den DMA-Controller zugreifen, laufen im Supervisor-Mode der CPU und haben den ersten Kontakt mit den von außen kommenden Daten. Nicht zuletzt ist dies der Grund dafür, dass viele Verwundbarkeiten und Absturzursachen von Software in den Treibern zu finden sind. Dieser Beitrag zeigt Methoden und Technologien mit denen Gerätetreiber "sicher" gemacht werden können, das heißt, die Auswirkung eines Angriff auf die Treiber oder einer Fehlfunktion durch Softwarefehler kann minimiert und damit die Robustheit des Gesamtsystems verbessert werden.

Was ist ein Treiber

Ich gehe davon aus, dass die meisten Leser wissen, was ein Treiber ist. Trotzdem möchte ich die wichtigsten Aspekte kurz zusammenfassen. Ein Gerätetreiber ist die Softwarekomponente, die direkten Zugriff auf die Hardware hat und der Applikation eine Abstraktion in Form eines Hardware-unabhängigen Interfaces (API) darstellt [1]. Als solches kann ein Treiber sowohl Ereignisse (Interrupts) zur Anwendung signalisieren als auch Daten zwischen Anwendung und Hardware übertragen. Dies kann dann zum Beispiel auch unter Verwendung eines DMA- oder PCI-Controllers geschehen.

Hin und wieder werden auch Protokoll-Stacks, wie z.B. TCP/IP oder USB-Host-Stack, als Treiberkomponenten betrachtet. Man kann sich darüber streiten, ob es sich hier um Treiber oder eher um Target-unabhängige "Middleware" handelt, die eng mit einem Treiber zusammenarbeitet. Die Überlegungen in diesem Papier finden jedoch ebenso Anwendung auf diese Stacks.

In einfachen Embedded-Systemen kann ein Treiber sehr einfach sein und z.B. nur ein paar GPIOs aktivieren oder ein Relais steuern. Einfache eventgesteuerte Ereignisse können sogar direkt im Interrupt-Handler bearbeitet werden. Für komplexere Operationen bedarf es mehr Code, der dann zum Beispiel in einer oder mehreren Tasks abläuft. Der Code dieser Tasks greift dann direkt auf die Hardware zu und stellt den höheren Softwareschichten ein abstraktes Interface zur Hardware zur Verfügung.

Warum sind Treiber kritische Komponenten von Software

Nahezu alle Daten, die ein Embedded-System bearbeitet, wandern durch einen Treiber wenn sie die Grenzen des Systems passieren. Treiber laufen außerdem oft im privilegierten Modus der CPU, damit sie direkt auf die Hardwareregister und den Speicher zugreifen können. In diesem Fall kann der Treiber aber auch auf anderen Speicher oder andere Register zugriefen, die nichts mit seiner eigentlichen Aufgabe zu tun haben. Egal, ob gewollt oder ungewollt besteht die Möglichkeit, dass ein Gerätetreiber auf beliebige andere Geräte zugreift oder beliebigen Speicher manipuliert. Dies verleiht dem Treiber einen großen Einfluss auf die Zuverlässig­keit und Sicherheit des Gesamtsystems.

Eine Fehlfunktion des Treibers kann dazu führen, dass die Daten unerlaubt verändert werden (Integrität). Ein Softwarefehler kann das System zum Absturz bringen (Zuverlässigkeit) oder einem potentiellen Angreifer Zugriff auf vertrauliche Daten geben (Vertraulichkeit). Letztlich sind die Treiber daher eine vitale Komponente des Gesamtsystems.

Wenn Treiber nahe am Betriebssystem arbeiten oder direkt auf Hardware zugreifen können, dann können Fehlfunktionen gravierende Auswirkungen haben. Eine Fehlfunktion im Treibercode im Supervisor-Mode der CPU (Abbildung 1, siehe PDF) kann das gesamte System zum Absturz bringen. Eine Schwachstelle in diesem Code kann einem Angreifer erlauben, die Kontrolle über das gesamte System zu erlangen. Selbst ein virtueller Treiber, der im User-Mode läuft (Abbildung 2, siehe PDF), kann ein Problem darstellen, wenn der Prozess zu viele Rechte erhält.

Wenn es um Angriffsszenarien geht, dann findet man Puffer-Überläufe als häufigsten Grund für Verwundbarkeiten von Software. Sucht man in der Vulnerability Notes Database nach den kritischsten Verwund­bar­keiten in Software, dann findet man bei mehr als der Hälfte der gemeldeten Verwundbarkeiten einen Puffer-Überlauf als Ursache [2].

Methoden zum Schutz

Die potentiellen Gefahren eines Treibers kann man einschränken, indem man sich die Prinzipien des High Assurance Software Engineering zunutze macht. Diese Prinzipien umfassen Themen wie

  1. minimale Rechtevergabe
  2. Unterteilung der Software in Komponenten
  3. Minimierung der Komplexität der Software
  4. Verwendung eines sicheren Entwicklungsprozesses

 

Speziell zum Schutz gegen die berüchtigten Puffer-Überläufe gibt es Möglichkeiten, die gefährdeten Stellen in der Software mithilfe von statischer Codeanalyse zu finden (Entwicklungsprozess). Falls diese noch nicht alle Stellen findet, kann man mit einem geeigneten Compiler automatische Laufzeit-Checks in den Code einbauen lassen, die diese Überläufe sogar während der Ausführung des Codes erkennen können und den Ablauf des Programms dann unterbrechen. Dies ist während der Entwicklung sehr hilfreich, kann aber auch im produktiven System verwendet werden.

Microkernel

Die Punkte 1 und 2 kann man besonders gut mithilfe eines Micro-Kernels adressie­ren. Die Verwendung eines Micro-Kernels, oder eher noch eines Separation-Kernels, erlaubt es, die Treiber im User-Mode auszuführen (minimale Rechte) und einzelne Treiber separiert voneinander und separiert von der Applikation laufen zu lassen (Unterteilung in Komponenten). Dies ist dann ein sogenannter "User-Mode-Treiber". In einem gut strukturierten System basierend auf einem Separation-Kernel erhält ein User-Mode-Treiber nur Zugriff auf die Hardwarekomponenten, die er für seine korrekte Funktion wirklich braucht, d.h. er "sieht" nur die Register des von ihm zu steuernden Hardwaremoduls (Abbildung 2, siehe PDF). Auch bekommt er nur Zugriff auf die Speicher­bereiche, die er für seine Arbeit tatsächlich benötigt, und auf keine anderen. Zum Beispiel sieht der Display-Controller nur den Frame-Buffer im RAM sowie die Konfigurationsregister des Controllers, oder der Ethernet-Treiber sieht nur den Bereich mit den Speicher für die Ethernet-Puffer und deren Deskriptoren.

DMA

Sehr spannend wird die Sache aber zum Beispiel beim Zugriff auf einen DMA-Controller, dessen Verwendung aus Performancesicht sinnvoll sein kann. Kritisch wird der Zugriff in einem Microkernel dann, wenn der User-Mode-Treiber direkt den DMA-Controller konfigurieren kann. In diesem Fall hat der Treiber nämlich die Möglichkeit, jeden beliebigen Speicher zu adressieren und von dort zu lesen oder dorthin zu schreiben. Und wenn dieser Treiber dann gehackt wird und der Angreifer die Kontrolle über den Treiber-Prozess erhält, dann hat der Angreifer auch wieder Kontrolle über das Gesamtsystem, weil er mittels DMA-Controller auf jeden Speicher zugreifen kann. Aber genau das will man ja eigentlich mit einem User-Mode-Treiber auf einem Microkernel vermeiden.

Man muss sich dann mit Punkt 4 helfen, indem man alle Treiber, die auf den DMA-Controller zugreifen, nach einem sicheren Entwicklungsprozess entwickelt, so dass der Treibercode eine hohe Vertrauenswürdigkeit hat. Alternativ kann man sich aber auch viel Aufwand sparen, indem man einen Microkernel verwendet, der es doch erlaubt, kleine, vertrauenswürdige Fragmente eines Treibers im privilegierten Modus der CPU laufen zu lassen, also direkt neben dem Kernel (Abbildung 2, links, siehe PDF). Natürlich muss in diesem Fall der Treiber auch sehr gut validiert sein (Entwicklungs­prozess), aber da es sich hier um sehr wenig und sehr einfachen Code handelt (Minimierung der Komplexität), ist der damit verbundene Aufwand deutlich kleiner als der für den DMA-Treiber im User-Mode.

Zusammenfassung

Wie oben beschrieben können schlecht designte oder unüberlegt programmierte Gerätetreiber ein Embedded-System angreifbar machen oder sogar zum Absturz bringen. Sie bieten eine große Angriffsfläche und haben oft sehr viele Privilegien, die einem Softwarefehler ein hohes Wirkpotential geben. Die Verwendung der Grundprinzipien des High Assurance Software Engineering kann dieses Problem extrem verkleinern. Dazu gehört nicht zuletzt die Verwendung eines Separation-Kernels, bei dem User-Mode-Treiber verwendet werden, der es aber auch erlaubt, vertrauenswürdigen Treibercode im Kernel-Mode auszuführen, falls dies sinnvoll ist.

Referenzen

[1] https://de.wikipedia.org/wiki/Gerätetreiber

[2] http://www.kb.cert.org/vuls/bymetric?open&start=1&count=20

 

 

Beitrag als PDF downloaden

 


Implementierung - unsere Trainings & Coachings

Wollen Sie sich auf den aktuellen Stand der Technik bringen?

Dann informieren Sie sich hier zu Schulungen/ Seminaren/ Trainings/ Workshops und individuellen Coachings von MircoConsult zum Thema Implementierung /Embedded- und Echtzeit-Softwareentwicklung.

 

Training & Coaching zu den weiteren Themen unseren Portfolios finden Sie hier.


Implementierung - Fachwissen

Wertvolles Fachwissen zum Thema Implementierung/ Embedded- und Echtzeit-Softwareentwicklung steht hier für Sie zum kostenfreien Download bereit.

Zu den Fachinformationen

 
Fachwissen zu weiteren Themen unseren Portfolios finden Sie hier.