Programmierer sollten Möglichkeiten ausschöpfen

Fehlende Datensicherheit unter Unix muß nicht sein

04.10.1991

Oft ist zu hören, daß Unix ein unsicheres Betriebssystem sei. Dabei wird meistens übersehen, daß eine Reihe von Eigenschaften vorhanden sind, mit denen Vertraulichkeit und Integrität von Informationen gewährleistet werden können. Die Eigenschaften von Unix, so die Autoren Cornelia Persy und Helmut Meitner*, müssen bei der System- und Anwendungsprogrammierung und bei der Systemadministration nur konsequent genutzt werden.

Das Betriebssystem Unix zeichnet sich insbesondere durch den möglichen Einsatz auf Rechnern unterschiedlicher Hersteller und unterschiedlicher Große aus. Es ist deshalb ein Betriebssystem, das sich sehr gut als Basis für zukünftige verteilte, offene Rechnersysteme eignet. Die Möglichkeiten, die Unix dem Programmierer und dem Administrator bietet, sollen hier im Überblick dargestellt werden. Grundkenntnisse des Lesers werden vorausgesetzt.

Für den sicheren Unix-Betrieb sind bezüglich der Programmierung drei Aspekte von Bedeutung. Als erstes muß die Arbeitsumgebung des Programmierers gesichert sein. Andere Benutzer dürfen keine Möglichkeit haben, Dateien des Programmierers anzusehen, zu kopieren oder gar zu verändern. Zweitens müssen sowohl bei der Programmierung mit der Programmiersprache C, die meistens bei Unix verwendet wird, als auch beim Einsatz der Unix-Kommandosprache (zum Beispiel sh oder csh) die Eigenschaften des Betriebssystems bekannt sein und genutzt werden.

Erfolgt die Programmierung im Team, so ist als dritter Aspekt die Zusammenarbeit so zu gestalten, daß der Zugriff auf Dateien geregelt ist und Versionen von Programmteilen verwaltet werden können. Alle drei Aspekte werden in diesem Beitrag im Überblick erläutert. Für eine detailliertere Einarbeitung in das Thema eignen sich unter anderem die Bücher von Wood und Kochan, sowie Noack und Hennig (siehe Literaturhinweise).

Der Bereich im Katalogbaum, der dem Programmierer zugeordnet ist, kann durch geeignetes Setzen der Zugriffsrechte vor unerwünschten Zugriffen geschützt werden. Der Gruppe sollte das Schreibrecht für den Katalog des Programmierers abgesprochen werden. Alle anderen Benutzer erhalten nach Möglichkeit gar keine Zugriffsrechte. Je nach Anforderung kann es sinnvoll sein, der Gruppe auch das Recht, den Katalog zu lesen sowie in den Katalog zu springen, zu entziehen. Mit dem Kommando chmod können die Zugriffsrechte wie folgt definiert werden:

chmod 750 $HOME

Die Zugriffsrechte sind als drei Oktalzahlen angegeben, wobei die erste Oktalzahl die Rechte für den Benutzer selbst, die zweite die Rechte für die Gruppe und die letzte die Rechte für alle anderen Benutzer angibt. Bei der Kodierung der Zugriffsrechte steht 4 für Lesen, 2 für Schreiben und 1 für Ausführen (bei normalen Dateien) oder Hineinspringen (bei Katalogen). Durch Angabe von Summen werden mehrere Zugriffsrechte gesetzt, zum Beispiel steht 5 für Lesen und Ausführen beziehungsweise Hineinspringen. Die Systemvariable $HOME gibt den Namen des Benutzerkataloges an.

Die Arbeitsumgebung des Benutzers wird durch Initialisierungsdateien der Shell definiert. Bei der sh-Shell ist dies die Datei .profile. Bei der csh-Shell werden die beiden Dateien .cshrc und .login verwendet. Da diese Dateien Interna über die Arbeitsumgebung enthalten, sollte nur der Benutzer Zugriff darauf haben, das heißt, die Zugriffsrechte sollten am Beispiel des .profile (mit chmod 600 .profile) definiert werden.

In der Initialisierungsdatei ist der Pfad für die Kommando suche und die Standardbelegung für die Zugriffsrechte zu definieren. Bei der Pfaddefinition muß darauf geachtet werden, daß das aktuelle Verzeichnis, das mit Punkt bezeichnet wird, am Ende des Suchpfades steht. Nur so werden stets die gewünschten Unix-Befehle verwendet und nicht irgendein modifiziertes Programm des aktuellen Kataloges. Folgende Pfaddefinition wäre möglich:

PATH = /bin:/usr/bin:/usr/ucb:$HOME/bin:.

Für die Vergabe von Zugriffsrechten auf neu erstellte Dateien kann mit dem unmask-Befehl eine Standardbelegung definiert werden:

unmask 027

Solch eine Anweisung ist nach Möglichkeit Bestandteil der Initialisierungsdatei. Die Maskierung des angegebenen Befehls bewirkt, daß bei jeder neu angelegten Datei der Benutzer selbst alle Zugriffsrechte hat, der Gruppe das Schreibrecht und den anderen Benutzern alle Rechte entzogen werden.

Als weitere Maßnahme zur Sicherung der Arbeitsumgebung ist zu berücksichtigen, wann man sich zuletzt eingeloggt hatte. Dadurch kann erkannt werden, ob es einem anderen Benutzer gelungen ist, sich unter der Benutzerberechtigung anzumelden. Die Ausgabe des Datums und der Zeit des letzten Einloggens erhält man bei den meisten Unix-Versionen automatisch beim login-Aufruf. Die entsprechenden Informationen werden aus der Datei /usr/adm/lastlog gelesen.

Bei Unix-Versionen die diese lastlog-Datei noch nicht haben, kann eine eigene lastlog-Datei Abhilfe schaffen (Die Fehlermeldung beim ersten Aufruf durch das Fehlen von .lastlog kann ignoriert werden.):

echo "Letztes login: "'cat $HOME/.lastlog' date $HOME/.lastlog

Mit dem Betriebssystemaufruf creat() kann aus einem C-Programm heraus eine neue Datei angelegt werden. Als Argumente erhält der Aufruf den Namen der Datei und die Zugriffsrechte, die für die Datei gesetzt werden sollen. Als Ergebnis liefert der Aufruf einen Verweis (Dateideskriptor) zurück. Bei Verwendung dieses Aufrufs sollte genau überlegt werden, welche Zugriffsrechte minimal ausreichend sind Äquivalent zu creat() kann auch der Aufruf open() verwendet werden.

Werden Dateien nur während des Programmlaufs benötigt, bietet es sich an, diese mit tmpfile() zu erzeugen. Die Datei wird dann in keinen Katalog eingetragen. Ein Zugriff ist nur über den Verweis möglich. Beim Entfernen des Verweises wird die Datei automatisch gelöscht. Bei Unix-Versionen die den tmpfile()-Aufruf nicht anbietet, läßt sich derselbe Effekt auch mit dem Aufruf unlink() nach dem Öffnen der Datei erzielen. Existierende Dateien können mit open() zum Lesen oder Schreiben geöffnet werden. Auch dieser Aufruf liefert einen Verweis als Ergebnis.

Unter Angabe des Verweises kann mit read() aus der Datei gelesen und mit write() in die Datei geschrieben werden. Zu beachten ist, daß die Zugriffsrechte nur beim Öffnen der Datei überprüft werden. Eine Veränderung der Zugriffsrechte einer geöffneten Datei hat keine Auswirkungen. Dies ist insbesondere bei Programmen wichtig, die Unterprozesse starten, da Verweise auf geöffnete Dateien an Unterprozesse weitergegeben werden. Dadurch kann der Zugriff außer Kontrolle geraten.

Werden Dateien von mehreren Prozessen verwendet, so ist zu verhindern, daß zwei Prozesse gleichzeitig darauf zugreifen. Mit der Funktion lockf() können Teile von Dateien verriegelt werden.

Benutzer und Gruppen werden in Unix durch eine eindeutige, vom Systemverwalter zu vergebende Nummer identifiziert. Wird ein Programm und damit ein Prozeß im Betriebssystem gestartet, so merkt sich die Prozeßverwaltung des Betriebssystems die Nummer des Benutzers, der den Prozeß gestartet hat und die Nummer der Gruppe, unter der er gerade eingeloggt ist. Diese Nummern werden als reale Benutzer beziehungsweise reale Gruppennummern bezeichnet.

Zusätzlich gibt es noch eine effektive Benutzer- und Gruppennummer. Sie werden zur Zugriffskontrolle verwendet und unterscheiden sich von der realen Nummer, wenn das s-Bit (setid-Bit) bei der Programmdatei gesetzt ist. In diesem Fall wird die effektive Nummer entsprechend der Benutzernummer beziehungsweise der Gruppennummer des Besitzers der Programmdatei gesetzt.

Die einzelnen Nummern können durch die entsprechenden get-Befehle abgefragt werden. Insbesondere kann durch den Aufruf getuid() festgestellt werden, wer das Programm gestartet hat. Dies ist eine wichtige Information für das Audit. Die effektiven Nummern können durch die Aufrufe setuid() und setgid() verändert werden. Werden die Funktionen von einem normalen Benutzer (das heißt nicht root) aufgerufen, so erlauben sie lediglich, die effektive Nummer auf die vorhergehende effektive oder auf die reale Nummer zu setzen. Der Benutzer root kann durch die Aufrufe die effektiven Nummern beliebig setzen.

In einem C-Programm können die Zugriffsrechte auf Dateien abgefragt und verändert werden. Die Funktion access() erhält als Argumente einen Dateinamen und Zugriffsrechte. Je nachdem, ob die Zugriffsrechte vorhanden sind oder nicht, liefert die Funktion als Ergebnis wahr oder falsch. Zur Überprüfung des Zugriffs werden die reale Benutzer-beziehungsweise Gruppenidentifikation verwendet, nicht die effektive. Die effektiven Nummern werden vom Betriebssystem bei der Prüfung der Zugriffsrechte von Prozessen verwendet.

Eine Abfrage, wie die Zugriffsrechte für Besitzer, Gruppe oder andere gesetzt sind, ermöglicht die Funktion stat(). Sie liefert als Ergebnis eine Struktur mit Statusinformationen einer Datei. Die Zugriffsrechte sind Bestandteil dieser Struktur.

Analog zum Shell-Befehl umask gibt es einen Betriebssystemaufruf umask(), der die Standardbelegung des Modus (Zugriffsrechte und s-Bits) der Dateien festlegt, die von dem Prozeß neu erstellt werden. Bei bestehenden Dateien kann der Modus mit der Funktion chmod() verändert werden.

Die Zugriffsrechte sind jeweils für Besitzer der Datei, eine Gruppe und alle anderen Benutzer angegeben. Der Besitzer der Datei und die Gruppe können durch die Funktion chown() geändert werden. Die Funktion benötigt drei Argumente: den Dateinamen, die Benutzernummer und die Gruppennummer. Durch den Aufruf von chown() werden automatisch die s-Bits zurückgesetzt. Dies ist erforderlich, da es sonst möglich wäre die Privilegien eines anderen Benutzers zu erhalten.

Für den Umgang mit sensitiven Daten bietet Unix die Möglichkeit der Verschlüsselung mit einem DES-Algorithmus. Der DES (Data Encryption Standard) wurde in den USA vom National Bureau of Standards für den Einsatz in DV-Systemen der öffentlichen Hand vorgeschrieben.

Mit der Funktion setkey() kann der Schlüssel für DES definiert werden. Die eigentliche Ver- und Entschlüsselung erfolgt mit der Funktion encrypt(). Als erstes Argument wird ein 64-Bit-Block angegeben, der verschlüsselt oder entschlüsselt werden soll.

Das zweite Argument gibt an, ob eine Ver- oder Entschlüsselung durchgeführt werden soll. Beide Funktionen arbeiten mit einem 64-Bit-Block. Da der benötigte Schlüssel bei setkey() nur 56 Bit lang sein muß, wird von dem 64-Bit-Block jedes achte Bit ignoriert. Zur Verschlüsselung von Paßwörtern wird die Funktion crypt() verwendet. Sie ist eine Kombination von setkey() und encrypt().

Das Problem bei der Verschlüsselung unter Unix ist, daß der DES-Algorithmus den amerikanischen Exportbeschränkungen unterliegt und dadurch außerhalb der USA nicht verfügbar ist. In den Vereinigten Staaten sind jedoch Bestrebungen im Gange, diese Exportbestimmungen zu lockern.

Durch die C-Bibliotheksfunktion getpass() ist es möglich, in C-Programmen ein Paßwort abzufragen. Die Funktion erhält als Argument eine Zeichenkette, die als Eingabeaufforderung ausgegeben werden soll (zum Beispiel "Enter password:"), schaltet die Bildschirmausgabe ab, liest ein bis zu acht Zeichen langes Paßwort ein und schaltet die Bildschirmausgabe wieder ein.

Dadurch kann das Paßwort ansichtbar eingegeben werden.

Das eingegebene Paßwort kann nun mit der crypt()-Funktion verschlüsselt und mit dem in /etc/passwd eingetragenen Paßwort verglichen werden. Das in /etc/passwd eingetragene Paßwort erhält man über die Funktion getpwuid().

Aus einem C-Programm heraus kann ein neuer Prozeß erzeugt und damit ein weiteres Programm gestartet werden. Die grundlegenden Funktionen dazu sind fork() und exec*. Die Bezeichnung exec*() ist eine abkürzende Schreibweise für eine Reihe von exec-Funktionen. Mit der Funktion fork() wird ein neuer Prozeß (Kind-Prozess) erzeugt, der eine exakte Kopie des Programms ist, das fork() aufgerufen hat (Eltern-Prozeß). Der Kind-Prozeß erhält damit die gleiche reale und effektive Benutzer- und Gruppennummer, die gleiche Standardbelegung für den Modus von neuen Dateien (umask) und alle offenen Dateien des Eltern-Prozesses.

Das ursprüngliche Programm läuft nun zweimal. In der Regel wird der Kind-Prozeß durch einen exec*()-Aufruf mit einem neuen Programm überlagert. Dabei ist zu beachten, daß

_ das neue Programm die gleiche reale und effektive Benutzer- und Gruppennummer erhält,

_ falls bei dem neuen Programm das s-Bit des Besitzers (der Gruppe) gesetzt ist, die Benutzernummer (Gruppennummer) auf die effektive Nummer des Besitzers (der Gruppe) der Programmdatei gesetzt wird,

_ das neue Programm die Standardbelegung für den Modus von neuen Dateien (umask) übernimmt,

_ alle offenen Dateien, bei denen nicht das "schließe-bei-exec"-Flag gesetzt ist, übergeben werden.

Bevor ein neuer Prozeß erzeugt wird, sollte genau geprüft werden, ob die effektiven Nummern ungleich den realen sind und ob dies erforderlich ist. Die umask sollte nochmals überprüft werden, damit der Kind-Prozeß nicht Dateien mit zu geringem Zugriffschutz anlegt. Bei den offenen Dateien ist besonders kritisch zu prüfen, ob nicht Dateien geschlossen werden können, so daß diese dem Kind-Prozeß nicht bekannt werden.

Die Funktionen system() und popen() sind eine Kombination von fork() und exec(), wobei als neues Programm ein /bin/sh-Shell gestartet wird. Um einen Mißbrauch zu verhindern, muß bei Shellaufrufen darauf geachtet werden, daß

_ absolute Pfadnamen (das heißt ausgehend von /) verwendet werden,

_ der Pfad explizit vor dem Aufruf gesetzt wird,

_ die Trennzeichen für Eingabefelder (IFS) vor dem Aufruf richtig definiert werden.

Werden diese Maßnahmen nicht durchgeführt, so kann es einem Hacker gelingen, daß statt dem angegebenen Programm ein anderes ausgeführt wird.

Durch den Einsatz aller drei Maßnahmen ist sichergestellt, daß auch wirklich die Kommandos ausgeführt werden, die erwünscht sind. ein system()-Aufruf könnte etwa folgendermaßen aussehen:

system ("IFS ='/t/n'; PATH = /bin:/usr/bin; export IFS

PATH;

/local/bin/sortieren");

In dem Beispiel wird eine Shellprozedur "sortieren" aufgerufen. Diese Prozedur verwendet intern weitere Shell-Befehle. Um sicherzugehen, daß immer die gewünschten Shell-Befehle verwendet werden, auch wenn keine absoluten Pfadangaben verwendet wurden, wird vor dem Aufruf der Pfad gesetzt. Die drei Maßnahmen sollten auch bei der Programmierung von Shell-Prozeduren angewendet werden - insbesondere dann, wenn für die Datei, in der die Shell-Prozedur steht, das s-Bit gesetzt ist.

Besondere Vorsicht ist angebracht, wenn in einer Shell-Prozedur oder in einem C-Programm mit dem system()-Aufruf ein Kommando aufgerufen wird, das einen Shell-Escape bietet. Das heißt, das Kommando ermöglicht das Starten von weiteren Shells. Solche Kommandos sind beispielsweise mail, write, readnews, nroff, troff, dc, edit, ex, vi, ed, sed und awk. Solche Kommandos sollten nicht verwendet werden, wenn das Programm im setuid- oder setgid-Modus läuft, da sonst alle weiteren Shells auch unter der gesetzten effektiven Nummer laufen.

Wird Software in Teamarbeit entwickelt, so müssen mehrere Programmierer auf die Programmdateien Zugriff haben. Dabei ist zu gewährleisten, daß nicht zwei Programmierer gleichzeitig dieselbe Datei editieren und daß im nachhinein festgestellt werden kann, wer eine bestimmte Änderung am Programm durchgeführt hat. Dazu bietet Unix das Programmpaket SCCS (Source Code Control System).

Durch den Einsatz von SCCS kann zu einem bestimmten Zeitpunkt genau ein Programmierer auf eine Programmdatei schreibend zugreifen. Die Änderungen werden von SCCS vermerkt und lassen sich nachvollziehen. Auch können vorhergehende Programmversionen nachträglich wieder erstellt werden.

Nachdruck mit freundlicher Genehmigung der Zeitschrift für Kommunikations- und EDV-Sicherheit (KES).

*Cornelia Persy ist Gruppenleiterin der Zentralabteilung Forschung und Entwicklung bei der Siemens AG in München.

Helmut Meitner ist Projektleiter am Fraunhofer-Institut für Arbeitswirtschaft und Organisation (IAO) im Bereich Informationssicherheit beim Einsatz verteilter Informationssysteme. Er ist Mitglied im VDI-Arbeitsausschuß A7 "Informationssicherheit in der Bürokommunikation".