Suche
Close this search box.

Die Sache mit dem Vertrauen

Made by DALL-E
Made by DALL-E

Im Alltag sind wir darauf angewiesen, dass von uns genutzte Objekte so funktionieren, wie wir das von ihnen erwarten. Bei einem Bürostuhl mit nur zwei Beinen oder ohne Sitzfläche erkennen wir auf den ersten Blick, dass dieser (sofern es sich nicht um ein elaboriertes Designerstück handelt) seine Funktion nicht erfüllen wird. Auch können wir uns schon im Möbelgeschäft ein Bild über dessen Funktionstüchtigkeit und Belastbarkeit machen, dasselbe gilt für die meisten anderen üblichen Alltagsgegenstände. Das Risiko, einen Bürostuhl zu kaufen der sich erst nach dem Kauf als krass untauglich erweist (bei dem zum Beispiel die Rückenlehne bei der kleinsten Berührung nach hinten kippt), ist daher sehr klein.

Daran, dass das bei technischen und vor allem bei IT-Geräten anders ist, haben wir uns unterdessen gewöhnt. Vielen fehlt das Wissen, um die Technik im notwendigen Detail zu verstehen. Die IT-Industrie wird nicht müde darin, immer und immer wieder zu betonen, wie funktional und sicher ihre Produkte seien und dass es zu deren Verwendung keine technischen Kenntnisse brauche (und man doch bitte gar nicht zu verstehen versuchen soll, wie Dinge funktionieren). Entsprechend sind weitergehende Informationen und somit auch der Quellcode von kommerzieller Software dann schnell man gut gehütete Geschäftsgeheimnisse des jeweiligen Anbieters.

Als Alternative bietet sich die Open Source-Community an, bei der technische Offenheit im Zentrum stehen. Das Mantra “all bugs are shallow” (also die Idee, dass dank dem allen zugänglichen Quellcode Software-Fehler schnell gefunden werden, da viele Augen darauf schauen können) will betonen, dass man damit gegenüber den Closed Source-Produkten von Microsoft, Apple etc. einen Transparenz-Vorteil habe. Dass sich diese vielen (in Realität aber oft wenigen) Augen auf viele Software-Stücke verteilen und einzelne Open Source-Komponenten nur von einem einzigen Entwickler betreut werden, wird dabei gerne ausgeblendet. Der Versuch, über die Kompromittierung von xz das für entfernte Zugriffe auf Millionen via Internet erreichbare Computer verwendete, weltweit eingesetzte ssh-Programm mit einer Backdoor zu versehen, zeigt die damit verbundenen Risiken. Trotz der vielen Augen, und trotz verschiedener Absicherungsmassnahmen, war die Komplexität so hoch, dass ausser dem Angreifer niemand den Gesamtüberblick hatte und der Angriff auf ssh drum fast erfolgreich gewesen wäre. Open Source alleine ist also auch in der IT keine Gewähr dafür, dass man statt einem Bürostuhl nicht ein Schaukelpferd erhält.

Mit anderen Worten: Auch bei frei zugänglichem Quellcode hat man weder eine Garantie dafür, dass die Software fehlerfrei ist noch dass sie keine sicherheitskritischen Lücken enthält. Drum bleibt einem auch bei der Verwendung von Open Source-Produkten schlussendlich nichts anderes übrig, als den EntwicklerInnen und den Entwicklungsprozessen zu vertrauen.

Dass dieses Vertrauen trotz offen zugänglichem Quellcode leicht missbraucht werden kann, hat Ken Thompson, einer der Väter des Unix-Betriebssystems (vereinfacht gesagt ist Unix ein Vorläufer von Linux), bereits 1983 anhand eines eindrücklichen Beispiels gezeigt. Konkret hat er 1975 den C-Compiler der damaligen Unix-Version (C ist die Programmiersprache, in der Unix und Linux geschrieben sind, ein Compiler übersetzt den textuellen Programmcode in etwas was die CPU ausführen kann) so manipuliert, dass dieser beim Übersetzen des Login-Programs eine Backdoor (also ein versteckter Zugang) einbaut. Da Unix-Software zu dieser Zeit meist in bereits compilierter Form auf Bändern (wie Musikkassetten, einfach in gross) verteilt wurde, gelangten die entsprechend manipulierten Programme durch einen simplen Update auf die Zielsysteme (oder zumindest ein Zielsystem). Gedacht war das ganze vermutlich eher als Spass unter Kollegen und als Machbarkeitsstudie (Unix war 1974 vor allem in US-Universitäten und -Forschungslaboren im Einsatz).

Um nachzuvollziehen, wie er die Manipulation gemacht hat und wieso Dritte diese nicht ohne weiteres bemerkt haben, müssen wir zuerst verstehen, wie aus menschlich lesbarem Programmtext der – durch eine CPU – ausführbare Maschinencode entsteht.

Einschub für Non-Nerds: Auf jedem Unix/Linux-System läuft, vereinfacht gesagt, ein Programm namens login, welcher den Benutzer beim Start zur Eingabe von Username und Passwort auffordert, diese Informationen mit den registrierten Useraccounts und deren Passwörtern abgleicht und im Erfolgsfall den Zugang zum Computer erlaubt. Es liegt auf der Hand, dass die Vertrauenswürdigkeit dieses Prozesses zentral ist, und dass eine Manipulation desselben einem Eindringling ungehinderten Zugang erlauben könnte?. Wen das jetzt an xz erinnert: Dort wurde das Programm ssh angegreifen, welches für Logins übers Internet eine ähnliche Funktion hat wie login für Menschen, die direkt vor dem Rechner sitzen.

Vom Programmtext zum Maschinencode

CPUs verstehen nur Binärcode (also eine lange Abfolge von Nullen und Einsen), damit lässt sich aber schlecht direkt programmieren. Selbst Programmcode, der direkt die Befehlscodes der jeweiligen CPU enthält, liegt zum einfacheren Schreiben und Lesen(!) in Textform vor, und muss vor der Ausführung zuerst übersetzt werden. In den meisten Fällen werden Programme aber in Programmiersprachen geschrieben, welche den CPU-Befehlscode weitgehend abstrahieren: Einerseits sind sie schlicht leichter zu schreiben und zu lesen, andererseits werden sie so auch von der jeweiligen CPU unabhängig.

Das klassische einfache Beispiel für ein Programm ist „Hello, World“, welches nichts anderes macht, als den Text „Hello, World“ auf dem Bildschirm anzuzeigen. In einem an C (der auf Unix/Linux-Systemen nach wie vor vorherrschenden Sprache) angelehnten Pseudocode könnte das wie folgt aussehen:

function helloworld() {
  print("Hello, World!")
}

Wird dieser Programmtext nun kompiliert (also in Maschinencode umgewandelt), kann es anschliessend von jedem ausgeführt werden, auch wenn man den Programmtext nicht versteht (oder gar kennt). Man kann anschliessend den Programmtext auch ändern oder löschen: solange das durchs Kompilieren entstandene Programm weiterhin unverändert besteht, kann es weiterhin ausgeführt werden. Im Grunde genommen passiert dasselbe, wenn man zum Beispiel Microsoft Word ausführt: Dort ist der Programmtext nur Microsoft selbst bekannt, vertrieben wird ausschliesslich das kompilierte Produkt.

Etwas komplexer wird es nun, wenn man sich bewusst wird, dass auch der C-Compiler selbst in C geschrieben ist: wie kann man auf einem neuen Computersystem den C-Compiler kompilieren, wenn man dort noch gar keinen hat? Dazu gibt es primär zwei mögliche Lösungen:

  • Man nutzt in einem ersten Schritt ein bestehendes Computer-System und passt den C-Compiler dort so an, dass er den binären Maschinencode des neuen Systems erzeugt. Anschliessend überträgt man das so erzeugte Programm aufs neue System. Diese Variante wird „Cross-Compilation“ genannt, sie kommt in der Praxis vor allem dort zum Einsatz, wo das Ziel-System selbst gar keine Umgebung für einen C-Compiler braucht/enthält, wie zum Beispiel bei IoT-Geräten, Waschmaschinen, Zahnbürsten (egal ob internet-fähig oder nicht), Routern oder schon nur Smartphones (weder iOS- noch Android-Geräte können im Normalfall auf dem Gerät selbst neue Apps kompilieren).
  • Man startet auf dem neuen System selbst mit einem kleinen in Maschinensprache geschriebenen Minimalprogramm, welches nur ein Teil des Sprachumfangs von C kompilieren kann. Diesen Minimalcompiler erweitert man dann Schritt für Schritt um weitere Funktionen und zieht sich so quasi an den eigenen Haaren aus dem Sumpf. Diese Variante wird „Bootstrapping“ genannt, und kommt heutzutage vor allem zur Anwendung, wenn jemand eine neue Programmiersprache entwickelt, dazu aber zuerst einmal einen Compiler entwickeln muss.

Mit Bootstrapping kann man also in einem Compiler Schritt für Schritt neue Möglichkeiten und Funktionen implementieren. Bevor wir damit eine vor neugierigen Augen versteckte Funktion implementieren, müssen wir noch kurz anschauen, wie ein Login-Programm funktioniert und wie man es manipulieren kann wenn man Zugang zu dessen Quellcode hat.

Wie manipuliert man nun das Login-Programm?

Das Kernstück eines Login-Programms ist die Verifikation von Benutzername und Passwort. Stark vereinfacht könnte der Code dazu etwa folgendermassen aussehen:

function login_allowed(username, password) {
  // Falls der Benutzer dem System bekannt ist
  if (exists(username)) {
    // Passwörter werden nicht im Klartext gespeichert sondern verschlüsselt,
    // um zu vermeiden, dass ein Angreifer alle Passwörter erhält, wenn er
    // die Passwort-Datei kopiert
    encrypted_password = encrypt(password)
    if (encrypted_password == password(username)) {
      return LOGIN_ALLOWED
    }
  }
  return LOGIN_FAILED
}

Falls es sich der Angreifer einfach machen will, ersetzt er im obigen Pseudocode einfach das letzte Statement durch return LOGIN_ALLOWED, damit kann man sich dann mit beliebigen Usernamen und Passwörtern einloggen. Da dies aber früher oder später auffallen würde (da sich normale User ja zwischendurch vertippen), ist es sinnvoller, nur die Passwort-Verifikation zu manipulieren.

function login_allowed(username, password) {
  // Falls der Benutzer dem System bekannt ist
  if (exists(username)) {
    // Hier ist die Manipulation
    if (password == "SESAM-OEFFNE-DICH") {
      return LOGIN_ALLOWED
    }
    // Passwörter werden nicht im Klartext gespeichert sondern verschlüsselt,
    // um zu vermeiden, dass ein Angreifer alle Passwörter erhält, wenn er
    // die Passwort-Datei kopiert
    encrypted_password = encrypt(password)
    if (encrypted_password == password(username)) {
      return LOGIN_ALLOWED
    }
  }
  return LOGIN_FAILED
}

Wenn der Angreifer dieses manipulierte Programm nun auf dem System kompiliert und installiert, kann er im Wissen um das manipulierte Passwort jedes Account übernehmen. Das mag jetzt etwas an den Haaren herbeigezogen tönen (wenn ein Angreifer in der Lage ist, ein neues Login-Programm zu installieren, dann hat er ja schon volle Kontrolle übers System), aber einerseits bleibt ihm so der Zugang auch dann erhalten, wenn die ursprünglich genutzte Lücke geschlossen wurde. Und andererseits werden Betriebssysteme typischerweise kompiliert verbreitet/installiert, die Login-Lücke würde sich also auf weitere Systeme quasi fortpflanzen.

Wie sichert man eine solche Modifikation gegen neugierige Augen?

Nun wäre der Angriff natürlich für jede, die sich den Quellcode des Login-Programms anschaut, sehr offensichtlich. Um dies zu vermeiden, könnte der Angreifer nach erfolgter Manipulation den alten Quellcode-Zustand wieder herstellen, womit die Lücke nur noch im ausführbaren Login-Programm selber wäre (und nicht mehr im Quellcode).

So weit, so gut, aber früher oder später wird wohl auch das Login-Programm neu kompiliert werden (aus dem Original-Quelltext, ohne unsere Lücke), damit würde auch die Backdoor wieder verschwinden. Was also tun, um das zu verhindern?

Hier kommt nun das oben erwähnte Bootstrapping zum Zuge. Zum Kompilieren des Login-Programs wird der C-Compiler benötigt, und auch der liegt schlussendlich im Quellcode vor. Also braucht es drei weitere Schritte, um sicherzustellen, dass die Passwort-Lücke unerkannt im System bleibt, selbst wenn alles neu kompiliert wird:

  • Der Angreifer muss den Quellcode des C-Compilers so erweitern, dass dieser erkennt, dass das Login-Programm kompiliert wird, und in diesem Fall die Lücke jeweils direkt einbauen (auch wenn sie im Quellcode gar nicht vorkommt),
  • Der Angreifer muss im weiteren den C-Compiler so erweitern, dass er erkennt, dass er sich selbst kompiliert, und dann sowohl den Code für die Password-Lücke wie auch für die Manipulation des Compilers selbst einbauen,
  • Nach der Erzeugung des neuen C-Compilers können die Änderungen aus dem Quelltext wieder entfernt werden, da der Compiler diese ja von selbst automatisch hinzufügt.
Schematischer Ablauf zur Erzeugung des modifizierten C-Compilers

Als Ergebnis erhalten wir ein System, in welchem sowohl das Login-Programm wie auch der C-Compiler manipuliert sind, diese Manipulation von aussen nicht erkennbar ist, und sie auch bei einer Neukompilation der beiden Programme erhalten bleiben. Solange der Angreifer zumindest den C-Compiler als ausführbares Programm auf andere Systeme verteilen kann, kann er so auch Backdoors in Systemen installieren, auf die er im Grunde genommen gar nie Zugriff hatte.

Einschub für Nerds: Russ Cox hat im Oktober 2023 in einem lesenswerten Blogpost den von Ken Thompson verwendeten Quellcode analysiert und die Manipulationen im Detail beschrieben.

Was lernen wir daraus?

Wir haben gesehen, wie simpel es im Grunde genommen für einen Insider ist, eine Backdoor einzubauen und so gut zu verstecken, dass sie weder bemerkbar noch ohne weiteres wieder entfernbar ist. Und auch wenn es heutzutage (wie sich ebenfalls bei xz gezeigt hat) aufwändiger ist, einen solchen Angriff umzusetzen (da Programmtext-Änderungen ja für alle nachvollziehbar und sichtbar sind, wurde der Schadcode bei xz in einem Testdatensatz versteckt), unmöglich ist es nicht. Und gerade Infrastruktur-Geräte wie Internet-Router sind immer wieder mal in den Schlagzeilen, weil der Login-Schutz etwas gar einfach zu umgehen war.

Es erklärt auch, wieso

  • eVoting-System inhärent als unsicher betrachtet werden (da Angriffe wie der oben beschriebene nicht ohne weiteres erkennbar sind)
  • in Security-Kreisen stark auf Reproducible Builds geachtet wird, also der Möglichkeit, eine sicherheitskritische Komponente wie ein sicherer Messenger oder ein eVoting-System aus dem Sourcecode heraus in der eigenen Umgebung kompilieren zu können (wobei dies voraussetzt, dass der Compiler selbst ebenfalls vertrauenswürdig ist). Mit diesem Ansatz kann das Risiko reduziert werden, dass ein Anbieter zwar Sicherheit verspricht und im veröffentlichten Quellcode auch zeigt, im anschliessend verteilten Programm aber trotzdem eine Backdoor einbaut.

Am Schluss bleibt es eine Vertrauensfrage, egal ob es das genutzte Betriebssystem nun von einem kommerziellen Hersteller stammt oder ob es sich um ein Open Source-Produkt wie eine Linux-Distribution handelt.

dnip.ch mit Deiner Spende unterstützen

Wir wollen, dass unsere Inhalte frei verfügbar sind, weil wir es wichtig finden, dass möglichst viele Menschen in unserem Land die politischen Dimensionen der Digitalisierung erkennen und verstehen können.

Damit das möglich ist, sind wir auf deine Unterstützung angewiesen. Jeder Beitrag und sei er noch so klein, hilft uns, unsere Aufgabe wahrzunehmen.

2 Antworten

  1. Wunderbar. Eigentlich nicht neu, aber trotzdem muss das bei mir erst mal sacken, weil dieser Klassiker so gut in die Gegenwart geholt wurde. Schlussendlich gilt das Geschriebene auch noch, wenn ausschliesslich „digital signierte“ Scripte wie in meiner aktuellen Umgebung zum Einsatz kommen: Auch der JIT-Compiler könnte manipuliert sein.

    (persönlicher Background: der C-Klassiker von Kernighan/Ritchie in der Ausgabe von 1983 steht bei mir im Bücherregal).

  2. Reproducible Builds sind nicht nur ein Thema für sicherheitskritische Apps, 96.9 Prozent von Debian ist reproducible.
    https://tests.reproducible-builds.org/debian/bookworm/index_suite_amd64_stats.html

    Da bei Debian alles als Quellcode vorliegt und von Debian kompiliert wird (statt vom jeweiligen Autor der Software), bringt das schon recht viel Transparenz. Es ist eben nicht so, dass man zehntausenden Entwicklern auf dem ganzen Globus vertrauen muss, sondern nur den (immer namentlich bekannten) Debian-Entwicklern und deren Prozessen. Und im Zweifel kann man das immer (automatisiert) prüfen, weil es eben reproduzierbar ist.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Weitere Beiträge