1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
|
= Sichere Software-Entwicklung im systemnahen Bereich
:author: Jan Klemkow, Benjamin Franzke
:lang: de
:toc:
== Kurzfassung
Dieses Dokument beschreibt Sicherheitsrisiken von Systemprogrammen mit dem Fokus
auf UNIX-Systeme.
Systemnahe meint in diesem Dokument alle Programme welche direkt auf die
Systemschnittstellen (System-Calls) zugreifen.
Es werden einleitend Problemfelder von Systemnahen Programmen geschildert und
ein Historischer Vergleich zu Anwendungsprogrammen gezogen.
:numbered:
== Einleitung
Sicherheit fuer die Software-Entwicklung im systemnahen Bereich spielt eine
wesentliche Rolle.
Da es hier oft nicht nur um die Anwendung selbst sondern auch um die Sicherheit
des Gesamtsystems geht.
Systemdienste werden in vielen Faellen mit hoeheren Rechten ausgefuehrt.
Gelingt es einem Angreifer einen solchen Dienst zu uebernehmen, erlangt er
selbst hoehere Rechte auf dem Zielsystem.
Bei Endanwendungen ist dieses in der Regel nicht der Fall, sodass der Angreifer,
bei einem erfolgreichen Angriff, nur Teile des Zielsystems unter seine Kontrolle
bringen kann.
Historisch betrachtet hat sich die Lage in diesem Bereich in den vergangenen
zehn Jahren verbessert.
Die grossen Angriffe, wie etwa der Sasser-Wurm, welche sich zu Beginn der
2000er Jahre noch auf Systemdienste richteten, sind heute eher selten geworden.
Momentan stehen die Endanwendungen wie Browser oder Dokumentenbetrachter im
Fokus der Angriffe.
//Im systemnahen Bereich gibt es viele Programme welche fuer die Sicherheit des
//Gesamtsystems eine enorme Rolle spielt.
// Angefangen beim Kernel des Betriebssystems
//== Angriffsvektoren
In den folgenden Kapiteln werden verschiedene Angriffsvektoren und
Schwachstellen von Systemprogrammen erlaeutert.
Zudem werden werden verschiedene Verteidigung s Massnahmen erlaeutert, welche
das Risiko der Angriffsmoeglichkeiten senken.
== Standard-C-Bibliothek
Viele Systemprogramme sind in der Programmiersprache ``C'' geschrieben.
Deren Standardbiblothek unterlag im Laufe der Zeit einer ganzen Reihe von
Veraenderungen, welche unter anderem die Sicherheit von Programmen erhoehten.
Eines der groessten Probleme in ``C''-Programmen sind Funktionen zur
Verarbeitung von Zeichenketten.
Da Zeichenketten hier im Gegensatz zu anderen Programmiersprachen nicht als
komplexes Objekt sondern als loser Speicherbereich betrachtet werden.
Somit muss ein Programmierer bei Zeichenketten immer den dahinterliegenden
Speicherbereich und dessen Laenge bei jeder Operation bedenken.
//Da dieses seit langer Zeit immer wieder fuer Sicherheitsprobleme sorgt, wurden
//viele Funktionen neu geschrieben.
Bei diesen Funktionen ist das Problem, dass diese die Laengen von
Speicherbereichen nicht beachten.
Dadurch kommt es schnell zu Pufferueberlaeufen und Zugriffen auf nicht
allozierte Speicherbereiche.
Dadurch koennen Angreiffer eigenen Programmcode einschleusen und ausfuehren
lassen, sowie Programme zum Absturz bringen.
Als Fallbeispiel wird die Funktion +strcpy(3)+ erklaert.
Diese Funktion wird dafuer benutzt um eine Zeichenkette von einem Speicherplatz
zu einem anderen zu kopieren.
Als Parameter bekommt die Funktion die Startadressen von Quelle und Ziel im
Speicher.
Der Funktion ist dabei die Groesse der jeweiligen Speicherplaetze nicht bekannt.
Es wird nun eine Speicherzelle nach der Anderen kopiert, solange bis in der
Quelle ein NUL-Byte auftaucht.
Dabei kann die Funktion weder sicherstellen, dass sie nicht ueber den
Quellpuffer hinaus Bytes kopiert, sowie dass sie nicht ueber die Grenzen des
Zielpuffers hinaus schreibt.
Durch dieses Verhalten, kommt es immer wieder zu Pufferueberlaeufen, welche
zu unvorhersagbaren Folgen fuer den weiteren Programmablauf fuehren.
Um dieses Problem zu loesen wurden neue Zeichenkettenfunktionen in die
Standard-C-Bibliothek auf genommen, welche als Argument maximal Laengen
uebergeben bekommen.
[source,c]
-------------------
strcpy(char *dst, const char *src);
strncpy(char *dst, const char *src, size_t len);
strlcpy(char *dst, const char *src, size_t dstsize);
-------------------
Hierbei gibt es zwei Ansaetze mit Laengen umzugehen.
TODO: strncpy() strlcpy()
* gets()
* strlen()
* strcpy()
* str..()
== Netzwerk-Programmierung
Ein grosser Angriffsvektor auf Computer-Systeme sind laufenden Dienste,
welche ueber Netzwerk und Internet erreichbar sind.
Ein Fehler in ihrer Programmierung koennte von ausserhalb ausgenutzt werden,
da Internetdienste eine staendige weltweite Erreichbarkeit haben.
Somit kann ein solcher Dienst potentiell zur jeder Zeit von jedem Ort aus
angegriffen werden.
Zum Schutz solcher Dienste gibt es verschiedene Mechanismen, welche im Folgenden
erlaeutert werden.
=== Privilege-Separation
Bei der Privilege-Seperation wird ein Programm in verschiedene Prozesse mit
unterschiedlichen Berechtigungen auf geteilt.
Das Ziel dabei ist es, Programmcode mit so wenig Rechten wie moeglich laufen zu
lassen.
Als Beispiel soll der Window-Compositer Wayland genannt...
TODO: wayland privilege separater erklaeren
Im einfachsten Fall startet ein Daemon mit Root rechten und holt sich alle
Ressourcen, wie etwas Sockets mit Well-Known-Ports oder eine Filedescriptor auf
eine Datei, gibt dann die rechte ab und laeuft im user-mode weiter.
=== Privilege-Revocation
Die meisten Anwendung benoetigen ausschliesslich waehrend der Initialisierung
hoehere Rechte, um etwa einen Socket mit einem Well-Known-Port zu oeffnen.
Zur Laufzeit werden diese hoeheren Rechte dann nicht mehr benoetigt.
Somit sollten diesen Prozesse nach ihrer Initialisierung diese Rechte wieder
abgeben und mit normalen Benutzer-Rechen weiter laufen.
=== Service-Seperation
Da auf einem Computer zumeist mehr als nur ein Dienst laeuft, welcher mit einem
Netzwerk oder dem Internet verbunden ist, ist auch die Angriffsmoeglichkeit auf
dieses System sehr hoch.
Vor Allem beim Servern, welche viele Dienste wie HTTP, FTP, SMTP, POP3 und
viele mehr anbieten ist die Gefahr einer Sicherheitsluecke sehr hoch.
Sollte einer dieser Dienste ueber eine Sicherheitsluecke von einem Angreifer
uebernommen werden, so koennte diese ebenfalls die anderen Dienste und deren
Daten manipulieren.
Um dieses zu vermeiden, koennen diese Dienste in kuenstlichen Umgebungen
von einander getrennt werden.
Eine Variante dafuer ist der System-Call +chroot(2)+.
Dieser System-Call wechselt fuer einen Prozess und dessen Kind-Prozesse das
Root-Verzeichnis.
Somit kann ein Prozess auf Dateien ausserhalb des neuen
Root-Verzeichnisses nicht mehr zugreifen.
Bereits geoeffnete Dateien koennen aber weiterhin verwendet werden,
auch wenn diese Ausserhalb des neuen Root-Verzeichnisses liegen.
Ein Beispiel fuer die Anwendung dieser Technik ist der Apache-Http-Server,
welcher meist in das Verzeichnis +/var/www+ als Root-Verzeichnis wechselt.
Der +chroot(2)+-System-Call ist in erster Linie aber keine Sicherheitsfunktion.
Es gibt definierte Wege aus einem chroot wieder heraus zu wechseln.
Das Konzept von ``Jails'' wie etwas im FreeBSD-Betriebssystem verwendet werden,
sind dafuer ausgelegt verschiedene Prozesse sicher von einander zu trennen.
Bei den ``Jails'' werden die System-Calls des eingeschlossenen Processes
explizit gefiltert, um die Beeinflussung von anderen Prozessen zu verhindern.
Um verschiedene Prozesse von einander zu trennen kann auch das
UNIX-Rechte-Verwaltung verwendet werden.
Jeder Service sollte unter einem separaten Nutzer-Konto laufen.
Es sollte vermieden werden mehrere Dienste mit den selben Nutzungsrechten laufen
zu lassen, da sich diese untereinander beeinflussen koennen.
Sollte ein Dienst keine eigenen Dateien benoetigen, kann dieser unter dem
speziellen Nutzer ``nobody'' ausgefuehrt werden.
Unter vielen Unix-Systemen wird dieser speziell fuer diese Aufgabe verwendet.
=== Beschraenkung der Erreichbarkeit
Sollte ein Dienst nur fuer einen bestimmten Kreis von Nutzern bestimmt sein,
dann sollte man die Erreichbarkeit des Dienstes auf diesen Kreis beschraenken.
Systemdienste muessen nur in seltenen Faellen eine Weltweite Erreichbarkeit
haben.
Kommunikation innerhalb eines Hosts sollen immer ueber das Loopback-Interface
gefuehrt werden.
Vor allem in Windows-Systemen ist es in der Vergangenheit oft grossen Problemen
mit Wuermern gekommen, weil verwundbare Dienste ueber das Netzwerk erreichbar
waren.
Fuer die Kommunikation von Diensten innerhalb einer Broadcast-Domain sollen
link-local-Adressen verwendet werden.
Im Hinblick auf IPv6 gibt es in diesem Bereich viele Spezialadressen welche
fuer die verschiedensten Zwecke genutzt werden koennen.
In jedem Fall sollte die Erreichbarkeit von Diensten soweit wie moeglich
eingeschraenkt werden.
Auch Tunnel-Techniken wie IPSec und andere VPN-Loesungen koennen verwendet
werden, um einen Dienst ausschliesslich einem ausgewaehlten Nutzern zugaenglich
zu machen.
== Kernel
Beim erstellen einer sicherheitskritischen Anwendung ist nicht nur die
Architektur und Programmierung der Anwendung selbst wichtig.
Die Umgebung in der die Anwendung laeuft ist ebenfalls ein wichtiger Faktor,
fuer die Sicherheit eines Systems.
So kann die Wahl eines anderen Betriebssystems dafuer ausschlaggebend sein, ob
eine Programmierfehler zu einem Sicherheitsrisiko wird oder nicht.
Im Folgenden werden einige Aspekte des Kernel-Designs auf ihre Sicherheit fuer
das Gesamtsystem betrachtet.
* interrupt blockierung
* Treiber Userland(minix) / OpenSource (nvidia)
=== Kernel-Architektur
Die meisten Betriebssysteme verwenden ein monolitisches Kernel-Design.
In diesem werden viele Aufgaben des Betriebssystems im Kernel erledigt.
Das bedeutet, dass der Programm-Code mit sehr hohen Berechtigungen
ausgefuehrt wird.
Ein Grossteil dieser Aufgaben koennen auch von User-Prozessen uebernommen
werden.
Dieses hat den Vorteil, dass Schutzfunktionen des Prozessors genutzt werden
koennen.
Bei Betriebssystemen mit einem Mikro-Kernel-Design ist dieses anders.
Dort werden viele Aufgaben an User-Space-Prozesse ausgelagert, zum Beispiel die
Geraetetreiber.
==== Nvidia-Treiber-Problem
Im +CVE-2012-0946+ wird eine Sicherheitsluecke im Nvidia Grafikkartentreiber
fuer Linux, Solaris und FreeBSD beschrieben, welche das Erlangen von hoeheren
Rechten ermoeglicht.
Das Ausnutzen dieses Programmierfehlers ist nur moeglich, weil der Treiber
im privilegierten Modus ausgefuehrt wird.
==== Minix
Bei dem Betriebssystem Minix etwa, werden Geraetetreiber als User-Prozess
gestartet.
Dadurch unterliegen sie den gleichen Sicherheitsvorkehrungen wie alle anderen
nicht privilegierten Prozesse.
So werden z. B. Speicherbereichsverletzungen vom Prozessor erkannt.
Ein fehlerhaft programmierter Treiber kann so nicht mehr das gesamte System
gefaehrden.
Dieses Kernel-Architektur hat grosse Performence-Nachteile gegenueber eines
Monolitischen-Designs.
Denn will einen Anwendung Beispielsweise mit der ueber das Netzwerk ein Paket
versenden, so muss fuer dieses Paket zwei mal der Userspace-Kernelspace-Kontext
gewechselt werden.
Dieses kostet enorm viel Zeit, da der gesamte Prozessorkontext fuer den
Uebergang von Anwendung zum Kernel, sowie vom Kernel zum Netzwerkkartentreiber
gesichert werden muss.
In einem monolitischen Design gaebe es nur einen Kontextwechsel von zum Kernel,
welche das Netzwerkmodule enthaelt.
Somit muss man an dieser Stelle Performence und Sicherheit abwiegen und fuer
den konkreten Einzelfall entscheide welches wichtiger ist.
=== Kernel-Programmierung
Fuer die Entwicklung von Kernel-Modulen muessen ebenfalls hohes Mass an
Sicherheit und Sorgfalt gelten.
Hat eine Anwendung im Userspace-Bereich noch einen ganzen virtuellen
Speicherraum und den Prozessor-Kontext noch fuer sich alleine, muessen diese
Ressourcen innerhalb des Kernels geteilt werden.
Um fuer kritische Aufgaben einen unterbrechungsfreien Programmablauf zu
garantieren, koennen im Kernel Hardware-Interrupts blockiert werden.
Diese muessen nach Beendigung der Arbeit in jedem Fall wieder freigegeben
werden.
Ein haeufiger Fehler an dieser Stelle ist fehlen von Interrupt-Freigaben, in
Fehlerbehandlungsroutinen.
In einer Funktion wird dabei fuer eine wichtige Aufgabe ein Unterbrechungsfreier
Prozessorkontext erzeugt, indem die Interrupts blockiert werden.
Am Ende der Routine wird die Blockierung wieder aufgehoben und die Funktion
verlassen.
Der Fehler liegt dann oft in den Fehlerbehandlungen mitten in der Routine.
Es wird Beispielsweise ein Zeiger auf +NULL+ geprueft und im Fehlerfall
die Funktion mit einem Rueckgabewert verlassen, welcher einen Fehler anzeigt.
Vergessen wurde aber die Interrupts wieder freizugeben.
Die Funktion in die zurueck gesprungen wird, erwartet einen Interrupt z. B.
von Tastatur, Maus oder Netzwerkkarte, aber wird nie einen erhalten.
Dadurch kann das gesamte System zum Stillstand kommen.
// vim: set syntax=asciidoc spell spelllang=de,en:
|