Autor Thema: Wie kann man die Zuverlässigkeit von Software und Computern steigern?  (Gelesen 25039 mal)

Dimension

  • Beiträge: 155
    • Profil anzeigen
Gespeichert
Eine nicht vollständige Sammlung von Überlegungen, die Robustheit und Zuverlässigkeit von Software oder auch Hardware zu steigern:
  • um durch häufig wiederkehrende Fehler bei der Speicherverwaltung auftretende Verwundbarkeiten zu vermeiden, sollten Maßnahmen ergriffen werden, etwa die zuverlässige Prüfung von Allokations-Details, die Einhaltung der Speichergrenzen und der Zugriffsrechte, sowie das Erkennen von Überläufen .
  • bestimmte Algorithmen und Datenstrukturen sind anfälliger für fehlerhafte oder unstimmige Eingabewerte. Die Auswirkungen eines Fehlers sollten sich möglichst lokal auswirken und nicht die gesamte Struktur betreffen.
  • der Code bzw. die Schaltung sollte keinen Zustand speichern und vor jedem Aufruf zurückgesetzt werden.
  • je einfacher das Design der Architektur und die Implementierung sind, desto nachvollziehbarer ist das System. Dieses kann dann leichter validiert werden, am Besten unabhängig voneinander von mehreren Entwicklern.
  • um alle zu erwartenden Kombinationen und Pfade von Daten und Kontrollfluss validieren und die Auswirkung eines eventuellen Fehlers bestimmen zu können sollte deren Anzahl möglichst gering sein.
  • neben der Vermeidung resp. Behandlung von DIV-0 und Integer-Overflows sollte die Ausnahmen-Behandlung als besondere Form des Kontrollflusses beachtet werden.
  • als String vorliegende Eingabedaten sollten nicht interpretiert werden, so dass diese keinen Einfluss auf den Kontrollfluss haben.
  • zwischen nebenläufigem Code sollten keine Abhangigkeiten existieren.
  • separate Einheiten sollten so gut es geht isoliert werden und über definierte Schnittstellen mit der Außenwelt kommunizieren. Die Speicherverwaltung sollte den Speicher unterschiedlicher Prozesse voneinander trennen.
  • des Weiteren wäre es möglicherweise sinnvoll, ähnliche Maßnahmen redundant auszulegen und neben der Vermeidung auch eine mögliche Kompromittierung erkennen und signalisieren zu können.

Gruß
« Letzte Änderung: 15. August 2012, 14:21 von Dimension »

Svenska

  • Beiträge: 1 792
    • Profil anzeigen
Gespeichert
Aha.

kevin

  • Administrator
  • Beiträge: 2 767
    • Profil anzeigen
Gespeichert
Also der erste Punkt heißt im Klartext "manuelle Speicherverwaltung will man nicht". Das kann ich soweit noch nachvollziehen. Aber der Rest geht irgendwie stark in die Richtung "wenn man weniger tut, macht man weniger Fehler", oder? Es wäre natürlich toll, wenn mein E-Mail-Client weniger Bugs hätte, aber doch bitte nicht, wenn der Preis dafür ist, dass er keine Mails mehr lesen kann...
Thou shalt not follow the NULL pointer, for chaos and madness await thee at its end.

Svenska

  • Beiträge: 1 792
    • Profil anzeigen
Gespeichert
Hallo,

um das "Aha." noch etwas zu konkretisieren:

Manuelle, dynamische Speicherverwaltung will man nicht. Stimmt. Rein statisch (oder vollständig extern überwacht) ist für OS-Development allerdings schwierig bis unmöglich.

Fast alle deine Punkte laufen auf drei Ansätze hinaus:
- Codeausführung ausschließlich unter Aufsicht eines Hypervisors als "managed code only" (vgl. Java, .net und Scriptsprachen)
- saubere Schnittstellen, an denen jeweils geprüft wird, ob Unsinn hinein- oder hinauskommen soll
- try/except um jeden Codeblock herum

Alle Ansätze führen allerdings nicht zu besserem Code. Sie ermöglichen im Gegenteil, dass die Codequalität nahezu beliebig schlecht werden kann, weil die Runtime alle Probleme für dich auffängt. Java kippt ja auch grundsätzlich einen Stacktrace auf die Straße, wenn mal irgendwas schief geht. Außerdem landet in den oberen Schichten nur ein allgemeiner Fehler, wenn irgendwas schief läuft (Extrembeispiel: "Webseite kann nicht angezeigt werden." - egal, ob das nun kaputter HTML-Code, ein OOM in der HTTP-Lib war, der DNS-Resolver nichts findet oder der Ethernet-Treiber hustet. Fehlersuche ist dann doof.)

Gegen Garbage Collection habe ich persönlich etwas (leider nichts effektives), weil es die Latenz der Anwendung in die Höhe treibt. Als Beispiel kann man sich da Minecraft anschauen, bei dem die meisten "Optimierungen" in geschickteren Einstellungen des GCs liegen.

Und ein Hypervisor kann auch nur begrenzt Dinge erkennen und abfangen, da er ja eine turing-vollständige Welt abbildet. Die ganze Virtualisierung von Servern geht in die gleiche Richtung. Aber ob ein Einbruch in den Server oder in eine (schlecht konfigurierte) VM geschieht, ist mir egal - der Angreifer hat die Daten und damit sein Ziel erreicht.

Nicht zuletzt müssen Runtime und Hypervisor bis zu einem gewissen Grad außerhalb dieser Ideen entwickelt werden, weil sie eben systemspezifisch und ziemlich komplex sein müssen, um gute Performance zu liefern.

Für mich klingen deine Vorschläge wie aus einem Uni-Script abgeschrieben. Sinnvoll, aber nicht (überall) sinnvoll umsetzbar. :-) Und sie verlagern das Problem auf andere.

Gruß,
Svenska

Dimension

  • Beiträge: 155
    • Profil anzeigen
Gespeichert
Ich sehe das alles insgesamt recht locker. Weitere Details folgen...

Dimension

  • Beiträge: 155
    • Profil anzeigen
Gespeichert
Die Ausführung würde in der Tat von einem Hypervisor verwaltet werden, welcher Speicherzugriffe pro Funktionsblock in einem privilegierten Modus und den eigentlichen Code im Usermode ausführt. Das betrifft dann allerdings nur den kritischen Teil des Kernels, etwa den Scheduler. Diese "Trusted Computing Base" kann bei Microkerneln vergleichsweise klein gehalten werden.

Ergänzung zur Liste aus dem 1. Posting:
  • Typinformationen für Zeiger und Datenstrukturen sollten Bestandteil der Allokations-Details sein.
  • fest definierte Einsprungspunkte der Funktionsblöcke. An bestimmten Stellen, zumindest vor Daten sollte Code stehen, der das System anhält.
  • die Isolation von Einheiten und Zuständen soll bewirken, dass selbst bösartiger Code und besondere Kombinationen von Eingabewerten ohne Auswirkungen auf das Verhalten oder die Integrität des Systems bleiben.
  • dem entsprechend kann Usermode-Code keinen Kernel-Code verdrängen

Gruß

Dimension

  • Beiträge: 155
    • Profil anzeigen
Gespeichert
Die Sandbox realisiere ich wie folgt:

Ebenen:
1: Speicherverwaltung
2: Datenzugriff & Berechtigungen
3: Sandbox
4: Prozesse (in Containern strukturiert)

Die Sandbox führt User-Code in Ring 3 aus. Der Code wurde zuvor in eine spezielle 4KB-Seite geladen. Ein spezieller Compiler erzeugt diese entsprechenden 4KB-Frames. Aus Ring 3 ist nur eine spezielle Seite adressierbar, die Daten enthält, die zuvor aus dem Speicher des Prozesses kopiert wurden. Nach Ablauf der Berechnung werden die Daten wieder zurück geschrieben.

Treiber würden sich auf Ebene 4 und der Scheduler zwischen Ebene 2 und 3 befinden. Interrupts werden in eine Warteschlange geschrieben und von einem Prozess verarbeitet.

Die Berechtigungen eines Prozesses geben an, welcher Speicher zugreifbar ist und ob spezielle Befehle erlaubt sind. Befehle, die in Ring 3 verboten sind werden von der Sandbox emuliert, etwa Portzugriff und DMA (nur für System-Prozesse). Der Code von Ebene 1 bis 3 und System-Prozesse mit speziellen Berechtigungen sind kritisch für die Sicherheit des Systems.

Sicherheitskritischen Code schreibe ich wie folgt:
Überprüfen aller Parameter auf Wertebereich und erlaubte Kombinationen
Überprüfen der Grenzen für die Index- und Adressberechnung
Keine Arrays/Puffer auf dem Stack
Checks gegen alle möglichen Integer-Overflows, Division nur mit Immediate
Einfache Codestruktur: einfache deterministische Schleifen, keine Rekursion, keine Pointer-Magie, kaum verschachtelte Calls
Keine speziellen Spracherweiterungen oder Befehle

Der Code kommt in ein Segment ohne Schreibrechte. BIOS, IDT und GDT werden nicht gemappt. Weitere Segmente sind Daten+Stack für den Kernel, Code- sowie Daten-Frames für die Sandbox, MMIO, und das große Segment für den dynamische allozierbaren Speicher.

Ein Angriff würde sich auf die Segmente des Kernels konzentrieren, insbesondere auf das Code-Segment, auf den Stack (ROP), die Tabellen der Speicherverwaltung und die Bereiche der Festplatte, von der das System gebootet wird. Außerdem könnte versucht werden auf Speicher anderer Prozesse unerlaubt zuzugreifen und erweiterte Berechtigungen zu erhalten oder Das System durch viele Prozesse oder Einträge in den Verwaltungsdaten zu überlasten. Des weiteren könnte ein Angreifer versuchen die Rücksprungadresse auf dem Stack zu verändern, so dass die Instruktionen anders interpretiert werden.

Durch die Segmentierung werden Buffer-Overflows in die Kernel-Segmente oder die ungemappten Bereiche hardwareseitig verhindert, somit können nach dem Starten des Systems das Kernel-Codesegment, die IDT, die ISR und die GDT nicht mehr verändert werden. Ein Problem stellt hier allerdings noch DMA dar, weil auf 32-Bit Systemen jedes PCI-Gerät uneingeschränkt auf den gesamten Speicher zugreifen kann. Hier muss man der Hardware und ihren Treibern vertrauen und/oder die Treiber absichern.

Da der sicherheitskritische Teil des Kernels (ohne Treiber) nur einige 1000 Zeilen Code umfasst, kann er leicht geprüft werden.
« Letzte Änderung: 21. March 2013, 13:16 von Dimension »

Manello

  • Beiträge: 69
    • Profil anzeigen
Gespeichert
Zitat
bestimmte Algorithmen und Datenstrukturen sind anfälliger für fehlerhafte oder unstimmige Eingabewerte. Die Auswirkungen eines Fehlers sollten sich möglichst lokal auswirken und nicht die gesamte Struktur betreffen.
Willst du jetzt den Software Entwicklern solche Datenstrukturen verbieten?

Zitat
Sicherheiskritischen Code schreibe ich wie folgt:
Überprüfen aller Parameter auf Wertebereich und erlaubte Kombinationen
Überprüfen der Grenzen für die Index- und Adressberechnung
Dass würde den PC erstmal einiges an Leistung fordern.
Außerdem würden evt. einige Programme nicht mehr funktionieren.

Dimension

  • Beiträge: 155
    • Profil anzeigen
Gespeichert
Windows Programme brauchen eine Win-API
Linux-Programme brauchen eine Linux-API
Java-Programme brauchen eine JVM
.NET-Programme brauchen eine CLR
Javascript braucht einen JIT-Compiler

Das System läuft direkt auf x86-Hardware, somit weiss ich nicht, was du jetzt genau meinst.

Gruß

Svenska

  • Beiträge: 1 792
    • Profil anzeigen
Gespeichert
Hallo,

ich fasse zusammen, was ich da rauslese (bei Fehlern bitte korrigieren):
- du programmierst den Kernel extrem defensiv, auf Kosten der Performance
- du implementierst einen Hypervisor im Kernel
- der Hypervisor erlaubt nur Programme mit maximal 4 KB Code und 4 KB Daten
- es gibt keine Hardwaretreiber

Da du die Trusted Computing Base auf einige 1000 Zeilen Code beschränkst, sind Hardware-Treiber nicht Teil dieser Basis. Die müssen demnach zwangsweise im Ring 3 laufen. Da du Segmentierung erwähnst, läuft dein OS im 32-Bit-Modus und ich vermute, du wirst dann keine 64-Bit-CPU voraussetzen. Das wiederum heißt, dass du keine IOMMU zur Verfügung haben wirst und deine Ring-3-Hardwaretreiber beliebige DMA-Zugriffe machen dürfen, was natürlich sicherheitstechnisch nicht geht. Also keine Hardwaretreiber. :-)

Javascript braucht einen JIT-Compiler
Nö. Bis Google kam, wurde Javascript interpretiert.

Gruß,
Svenska

Manello

  • Beiträge: 69
    • Profil anzeigen
Gespeichert
Uhm aber wenn es ohne Treiber ist, müsstest du doch alle selber machen?
Und dass für alle Hardwareteile zu machen, wäre eigentlich fast unmöglich weil es
da so viele gibt. Auserdem könntest du die Hardware ohne Treiber auch
nicht komplett ausnutzen.

Svenska

  • Beiträge: 1 792
    • Profil anzeigen
Gespeichert
*hüstel* Wenn du es selber machst, dann schreibst du einen Treiber... was direkt bedeutet, dass es danach einen Treiber gibt. Ich schrieb "es gibt keine Hardwaretreiber". Das ist was anderes.

Dimension

  • Beiträge: 155
    • Profil anzeigen
Gespeichert
Ich schrieb "es gibt keine Hardwaretreiber". Das ist was anderes.
Und nicht so gedacht. Bus-, Chipsatz- und Gerätetreiber laufen als Prozess. (Siehe aktualisierten Post.)

Ein user-Prozess kann mit der entsprechenden Berechtigung Funktionen im Kontext des Prozesses mit den Treibern aufrufen.

kevin

  • Administrator
  • Beiträge: 2 767
    • Profil anzeigen
Gespeichert
Es geht darum, dass dein schön eingezäunter Ring-3-Treiber per DMA einfach den Zaun von außen aufmachen kann.
Thou shalt not follow the NULL pointer, for chaos and madness await thee at its end.

Dimension

  • Beiträge: 155
    • Profil anzeigen
Gespeichert
Die Berechtigungen geben Ports und MMIO-Bereich vor und der DMA wird vom Kernel ausgeführt. Geräte sollten bestenfalls nicht Busmaster werden dürfen, da hast du natürlich recht.

Der Kernel implementiert grundlegende Protokolle wie PRDT (ATA über DMA). Falls die Hardware unbedingt Busmaster werden muss, bekommt sie halt einen reservierten Speicherbereich.

Spezielle Berechtigungen, die den Kernel und die Treiber betreffen sind auf Systemprozesse beschränkt.
« Letzte Änderung: 20. March 2013, 20:56 von Dimension »

kevin

  • Administrator
  • Beiträge: 2 767
    • Profil anzeigen
Gespeichert
Und wie unterscheidet dein Kernel, ob hinter einem I/O-Port bzw. MMIO-Bereich ein Gerät steckt, das Busmaster werden könnte? Willst du etwa eine Whitelist anlegen, in der dann Tastatur*, serielle Schnittstelle und Floppy drin sind? Alle anderen interessantes Geräte müssen sowieso vom Kernel kontrolliert werden (PIC, PIT) oder können als Busmaster agieren (mehr oder weniger alle PCI-Geräte)

*) Den Tastaturcontroller kann man leider auch nicht erlauben, weil damit könnte man ja resetten, am A20 rumspielen oder was es da noch so alles gibt
Thou shalt not follow the NULL pointer, for chaos and madness await thee at its end.

Svenska

  • Beiträge: 1 792
    • Profil anzeigen
Gespeichert
Oder die Berechtigungen den MMIO-Bereich vorgeben und der DMA vom Kernel ausgeführt wird. Geräte dürfen nicht Busmaster werden, da hast du natürlich recht.
Wie willst du denn DMA vom Kernel ausführen lassen? Die Anweisung an die Hardware, einen Datenblock per DMA zu übertragen, ist hardwarespezifisch...

Dimension

  • Beiträge: 155
    • Profil anzeigen
Gespeichert
Sobald ein System-Prozess spezielle Berechtigungen anfordert, hat dieser eine gewisse Verantwortung.

Die Treiber sind weiterhin Teil des Betriebssystems und werden entsprechend robust sein. Solange der Treiber sich nur auf seine eigentliche Aufgabe konzentriert und keine optionalen Funktionen implementiert sehe ich da keine Gefahr.

Siehe auch bearbeiteten Post zu DMA.

Svenska

  • Beiträge: 1 792
    • Profil anzeigen
Gespeichert
Sobald ein System-Prozess spezielle Berechtigungen anfordert, hat dieser eine gewisse Verantwortung.
Damit fallen also alle Treiber unter die Trusted Computing Base. Hätten wir das also geklärt. :-)

Dimension

  • Beiträge: 155
    • Profil anzeigen
Gespeichert
Sobald ein System-Prozess spezielle Berechtigungen anfordert, hat dieser eine gewisse Verantwortung.
Damit fallen also alle Treiber unter die Trusted Computing Base. Hätten wir das also geklärt. :-)
Das tun sie. Und sie profitieren vom Buffer-Overflow-Schutz der Sandbox. Mittels sparsam vergebenen Berechtigungen weiss man, wo man genau hin schauen muss (in Container-Sandboxen werden diese für alle untergeordneten Prozesse abgefangen).

 

Einloggen