Zum Inhalt springen

„Pufferüberlauf“ – Versionsunterschied

aus Wikipedia, der freien Enzyklopädie
[ungesichtete Version][gesichtete Version]
Inhalt gelöscht Inhalt hinzugefügt
K Literatur: Typo
Relotex (Diskussion | Beiträge)
K Baustein für fehlende Belege
 
(154 dazwischenliegende Versionen von mehr als 100 Benutzern, die nicht angezeigt werden)
Zeile 1: Zeile 1:
{{Belege|Im Artikel sind Belege nur gelegentlich zu finden.|Dieser Artikel}}
'''Pufferüberläufe''' (engl. ''buffer overflow'') gehören zu den häufigsten [[Sicherheitslücke]]n in aktueller [[Software]], die sich u. a. über das [[Internet]] ausnutzen lassen.
Im Wesentlichen werden bei einem Pufferüberlauf durch Fehler im Programm zu große Datenmengen in einen dafür zu kleinen [[Speicherbereich]] geschrieben, wodurch dem Ziel-Speicherbereich nachfolgende Informationen im Speicher überschrieben werden.


'''Pufferüberläufe''' ({{enS|buffer overflow}}), nicht zu verwechseln mit [[Stapelüberlauf|Stapelüberläufen]] (englisch ''‚{{lang|en|stack overflows}}‘''), gehören zu den häufigsten [[Sicherheitslücke|Sicherheitslücken]] in aktueller [[Software]], die sich u. a. über das [[Internet]] ausnutzen lassen können. Im Wesentlichen werden bei einem Pufferüberlauf durch Fehler im Programm zu große Datenmengen in einen dafür zu kleinen reservierten [[Speicherbereich]] – den [[Puffer (Informatik)|Puffer]] – geschrieben, wodurch nach dem Ziel-Speicherbereich liegende Speicherstellen überschrieben werden.
Das kann zu einem Absturz des betreffenden Programms, zur Verfälschung von Anwendungsdaten oder zur Beschädigung von Datenstrukturen der [[Laufzeitumgebung]] des Programms führen. Durch letzteres kann die [[Rücksprungadresse]] eines [[Unterprogramm]]s mit beliebigen Daten überschrieben werden, wodurch dann auch in von einem Angreifer übersandten Daten übermittelter [[Maschinencode]] mit den Privilegien des für den Pufferüberlauf anfälligen Prozesses ausgeführt werden kann. Dieser Code hat in der Regel das Ziel, dem Angreifer einen komfortableren Zugang zum System zu verschaffen, damit dieser das System dann für seine Zwecke verwenden kann. Pufferüberläufe in verbreiteter [[Server]]- und [[Client]]software werden auch von [[Internetwurm|Internetwürmern]] ausgenutzt.


Ursache für unerwünschtes Schreiben außerhalb des Puffers kann nicht nur eine übergroße Datenmenge, sondern auch ein [[Arithmetischer Überlauf|Überlauf]] (oder sonstige fehlerhafte Berechnung) der Zieladresse sein, die anzeigt, wo der Datensatz in den Puffer geschrieben werden soll. In diesem Fall wird von einem ''‚{{lang|en|pointer overflow}}‘'' (vom englischen ''{{lang|en|pointer}}'', für „[[Zeiger (Informatik)|Zeiger]]“) gesprochen.
Besonders begehrtes Ziel ist bei [[Unix]]-Systemen der [[Root Account|Root]]-Zugang, der dem Angreifer sämtliche Zugriffsrechte verleiht. Das bedeutet aber nicht, wie oft missverstanden, dass ein Pufferüberlauf, der „nur“ zu den Privilegien eines „normalen“ Benutzers führt, ungefährlich ist. Das Erreichen des begehrten [[Root Account|Root]]-Zugangs ist oft viel einfacher, wenn man bereits Benutzerrechte hat.

== Gefahren durch Pufferüberläufe ==
Ein Pufferüberlauf kann zum Absturz des betreffenden Programms, zur Verfälschung von Daten oder zur Beschädigung von Datenstrukturen der [[Laufzeitumgebung]] des Programms führen. Durch Letzteres kann die [[Rücksprungadresse]] eines [[Unterprogramm]]s mit beliebigen Daten überschrieben werden, wodurch ein Angreifer durch Übermittlung von beliebigem [[Maschinensprache|Maschinencode]] beliebige Befehle mit den Privilegien des für den Pufferüberlauf anfälligen Prozesses ausführen kann. Dieser Code hat in der Regel das Ziel, dem Angreifer einen komfortableren Zugang zum System zu verschaffen, damit dieser das System dann für seine Zwecke verwenden kann. Pufferüberläufe in verbreiteter [[Server]]- und [[Client]]software werden auch von [[Internetwurm|Internetwürmern]] ausgenutzt.


Angriffe mit Pufferüberläufen sind ein wichtiges Thema in der [[Computersicherheit]] und [[Netzwerksicherheit]]. Sie können nicht nur über jegliche Art von Netzwerken, sondern auch lokal auf dem System versucht werden. Behoben werden sie in der Regel nur durch kurzfristig gelieferte Fehlerkorrekturen ([[Patch (Software)|Patches]]) der Hersteller.
Angriffe mit Pufferüberläufen sind ein wichtiges Thema in der [[Computersicherheit]] und [[Netzwerksicherheit]]. Sie können nicht nur über jegliche Art von Netzwerken, sondern auch lokal auf dem System versucht werden. Behoben werden sie in der Regel nur durch kurzfristig gelieferte Fehlerkorrekturen ([[Patch (Software)|Patches]]) der Hersteller.


Neben Nachlässigkeiten bei der Programmierung werden Pufferüberläufe vor allem durch auf der [[Von-Neumann-Architektur]] basierende Computersysteme ermöglicht, gemäß welcher Daten und Programm im gleichen Speicher liegen. Durch diese Hardwarenähe sind sie auch nur unter assemblierten oder kompilierten [[Programmiersprache]]n ein Problem. Interpretierte Sprachen sind, abgesehen von Fehlern im [[Interpreter]], in der Regel nicht anfällig, da die Speicherbereiche für Daten immer unter vollständiger Kontrolle des Interpreters sind.
Interpretierte Sprachen sind, abgesehen von Fehlern im [[Interpreter]], in der Regel nicht anfällig, da der Speicher vom Interpreter, und nicht vom Programm selbst, verwaltet wird.


== Programmiersprachen ==
== Programmiersprachen ==
Die wesentlichste Ursache für Pufferüberläufe ist die Verwendung von Programmiersprachen, die nicht die Möglichkeit bieten, Grenzen von [[Speicherbereich]]en automatisch zu überwachen, um eine [[Bereichsüberschreitung]] von Speicherbereichen zu verhindern. Hierzu gehört besonders die Sprache [[C (Programmiersprache)|C]], die das Hauptgewicht auf [[Performance (Informatik)|Performance]] (und ursprünglich Einfachheit des Compilers) legt und auf eine Überwachung verzichtet, sowie die C-Weiterentwicklung [[C++]]. Hier ist ein Programmierer teilweise gezwungen, von Hand den entsprechenden Code zu generieren, wobei oft entweder absichtlich oder aus Nachlässigkeit darauf verzichtet wird. Die Überprüfung ist häufig auch fehlerhaft implementiert, da während der Programmtests diese Programmteile meist nicht oder ungenügend getestet werden. Daneben stellt der (im Falle von C++) komplexe Sprachumfang und die Standardbibliothek sehr viele fehleranfällige Konstrukte zur Verfügung, zu denen es in vielen Fällen kaum eine Alternative gibt.
Die wesentlichste Ursache für Pufferüberläufe ist die Verwendung von [[Programmiersprache]]n, die nicht die Möglichkeit bieten, Grenzen von [[Speicherbereich]]en automatisch zu überwachen, um eine [[Bereichsüberschreitung]] von Speicherbereichen zu verhindern. Dazu gehört besonders die Sprache [[C (Programmiersprache)|C]], die das Hauptgewicht auf [[Rechenleistung|Performance]] (und ursprünglich Einfachheit des Compilers) legt und auf eine Überwachung verzichtet, sowie die C-Weiterentwicklung [[C++]]. Hier ist ein Programmierer teilweise gezwungen, den entsprechenden Code von Hand zu generieren, wobei oft absichtlich oder aus Nachlässigkeit darauf verzichtet wird. Die Überprüfung ist häufig auch fehlerhaft implementiert, da während der Programmtests diese Programmteile meist nicht oder ungenügend getestet werden. Daneben stellen der (im Fall von C++) komplexe Sprachumfang und die Standardbibliothek sehr viele fehleranfällige Konstrukte zur Verfügung, zu denen es in vielen Fällen kaum eine Alternative gibt.


Die im professionellen Bereich häufig verwendete Programmiersprache [[C++]] bietet theoretisch (eingeschränkte) Möglichkeiten zur automatischen Überprüfung von Feldgrenzen. Sie ist aber ursprünglich als C-Aufsatz entwickelt worden und stellt daher eine vollständige Obermenge von C inklusive der Gefahren dar. Aus Gewohnheit, Kompatibilitätsgründen zu vorhandenem C-Code und Systemaufrufen in C-Konvention sowie aus Bequemlichkeits- und Performancegründen wird von diesen Möglichkeiten aber nicht immer Gebrauch gemacht (die Laufzeitüberprüfungen sind im Gegensatz zu Sprachen wie beispielsweise [[Pascal (Programmiersprache)|Pascal]] oder [[Ada (Programmiersprache)|Ada]] nicht Bestandteil der Sprache, sondern meist in der Sprache selbst geschriebener Bibliotheken oder müssen gar vom Programmierer manuell erstellt werden, sodass sie ihrerseits aufwändig, fehleranfällig und schwieriger durch den Compiler zu unterstützen und damit oft laufzeitmäßig relativ „teuer“ sind).
Die häufig verwendete Programmiersprache C++ bietet nur eingeschränkte Möglichkeiten zur automatischen Überprüfung von Feldgrenzen. Als Weiterentwicklung der Programmiersprache C übernimmt sie die meisten Eigenschaften von C, wobei sich aber das Risiko von Pufferüberläufen bei Benutzung von modernen Sprachmitteln (u. a. automatische Speicherverwaltung) weitestgehend vermeiden lässt. Aus Gewohnheit, Kompatibilitätsgründen zu vorhandenem C-Code, Systemaufrufen in C-Konvention sowie aus Performancegründen wird von diesen Möglichkeiten aber nicht immer Gebrauch gemacht. Laufzeitüberprüfungen sind im Gegensatz zu Sprachen wie beispielsweise [[Pascal (Programmiersprache)|Pascal]] oder [[Ada (Programmiersprache)|Ada]] nicht Bestandteil der Sprache, lassen sich aber in einigen Anwendungsfällen (z. B. mit [[Smart Pointer]]n) nachrüsten.


Da die meisten Programmiersprachen auch Standardbibliotheken definieren, bedeutet die Wahl einer Sprache meist auch die Verwendung der entsprechenden Standardbibliotheken. Im Fall von C und C++ enthält die Standardbibliothek eine Anzahl von gefährlichen Funktionen, die zum Teil gar keine sichere Verwendung zulassen und zu denen zum Teil keine Alternativen bestehen.
Da die meisten Programmiersprachen auch Standardbibliotheken definieren, bedeutet die Wahl einer Sprache meist auch die Verwendung der entsprechenden Standardbibliotheken. Im Fall von C und C++ enthält die Standardbibliothek eine Anzahl von gefährlichen Funktionen, die zum Teil gar keine sichere Verwendung zulassen und zu denen zum Teil keine Alternativen bestehen.


Auf Programmiersprachenebene kann die Gefahr von Pufferüberläufen durch die Verwendung von Programmiersprachen, die konzeptionell moderner als C/C++ sind, verringert oder ausgeschlossen werden. Ein sehr viel geringeres Risiko besteht zum Beispiel in Programmiersprachen der Pascal-Familie [[Modula]], [[Delphi (Programmiersprache)|Delphi]] oder [[Ada (Programmiersprache)|Ada]]. Ausgeschlossen sind Pufferüberläufe beispielsweise in der Programmiersprache [[Java (Programmiersprache)|Java]], da die Ausführung im [[Bytecode]] überwacht wird.
Auf Programmiersprachenebene kann die Gefahr von Pufferüberläufen durch die Verwendung von Programmiersprachen, die konzeptionell sicherer als C++ oder C sind, verringert oder ausgeschlossen werden. Ein sehr viel geringeres Risiko besteht zum Beispiel in Programmiersprachen wie [[Rust (Programmiersprache)|Rust]], [[Java (Programmiersprache)|Java]], [[C-Sharp|C#]] oder der Pascal-Familie [[Modula]], [[Object Pascal]] oder Ada.

Fast ausgeschlossen sind Pufferüberläufe beispielsweise in der Programmiersprache [[Java (Programmiersprache)|Java]], da die Ausführung im [[Bytecode]] überwacht wird. Aber auch in Java gibt es einerseits Pufferüberläufe, deren Ursache im Laufzeitsystem liegt und von denen mehrere [[Java Runtime Environment|JRE]]-Versionen betroffen sind.<ref>[http://www.linux-community.de/Neues/story?storyid=21756 ''Schwachstelle im Sun Java Runtime Environment''] – ''LinuxCommunity'', am 17. Januar 2007</ref><ref>[http://www.scip.ch/?vuldb.2842 Sun Java JRE bis 1.5.x GIF Image Handler Pufferüberlauf] – ''vuldb.com'', am 22. Januar 2007 (letzte Änderung am 7. Juli 2015)</ref>


== Prozessoren und Programmierstil ==
== Prozessoren und Programmierstil ==
Weitere Eigentümlichkeiten sowohl der Sprachen C und C++ sowie die Eigentümlichkeiten der am häufigsten eingesetzten [[Prozessor]]en machen das Auftreten von Pufferüberläufen wahrscheinlich. Die Programme in diesen Sprachen bestehen zum Teil aus [[Unterprogramm]]en. Diese Programme besitzen lokale Variablen.
Weitere Eigentümlichkeiten von C und C++ sowie der am häufigsten eingesetzten [[Prozessor]]en machen das Auftreten von Pufferüberläufen wahrscheinlich. Die Programme in diesen Sprachen bestehen zum Teil aus [[Unterprogramm]]en. Diese besitzen lokale Variablen.


Bei modernen Prozessoren ist es üblich, die Rücksprungadresse eines Unterprogramms und die lokalen Variablen auf einen als [[Stapelspeicher|Stack]] bezeichneten Bereich zu legen. Dabei werden beim Unterprogrammaufruf ''zunächst'' die Rückkehradresse und ''danach'' die lokalen Variablen auf den Stack gelegt. Bei modernen Prozessoren wie dem [[Intel]] [[Pentium]] wird der Stack durch eingebaute Prozessorbefehle verwaltet und wächst zwingend ''nach unten''. Werden Felder oder Zeichenketten in den lokalen Variablen verwendet, werden diese meist ''nach oben'' beschrieben. Wird die Feldgrenze nicht geprüft, kann man damit durch Überschreiten des Feldes die Rückkehradresse auf dem Stack erreichen und gegebenenfalls absichtlich modifizieren.
Bei modernen Prozessoren ist es üblich, die Rücksprungadresse eines Unterprogramms und dessen lokale Variablen auf einen als [[Stapelspeicher|Stack]] bezeichneten Bereich zu legen. Dabei werden beim Unterprogrammaufruf ''zunächst'' die Rücksprungadresse und ''danach'' die lokalen Variablen auf den Stack gelegt. Bei modernen Prozessoren wie dem [[Intel]] [[Pentium]] wird der Stack durch eingebaute Prozessorbefehle verwaltet und wächst zwingend ''nach unten''. Werden Felder oder Zeichenketten in den lokalen Variablen verwendet, werden diese meist ''nach oben'' beschrieben. Wird die Feldgrenze nicht geprüft, kann man damit durch Überschreiten des Feldes die Rückkehradresse auf dem Stack erreichen und gegebenenfalls absichtlich modifizieren.


Das folgende Programmstück in C, das in ähnlicher Form oft verwendet wird, zeigt einen solchen Pufferüberlauf:
Das folgende Programmstück in C, das in ähnlicher Form oft verwendet wird, zeigt einen solchen Pufferüberlauf:


<syntaxhighlight lang="c">
void input_line()
void input_line()
{ char line[1000]; // Feld ist eigentlich Zeiger
{
if (gets(line)) // gets erhält Zeiger, keine Überprüfung
parse_line(line);
char line[1000];
if (gets(line)) // gets erhält Zeiger auf das Array, keine Längeninformation
}
puts(line); // puts schreibt den Inhalt von line nach stdout
}
</syntaxhighlight>


Bei Prozessoren, die den Stack nach unten beschreiben, sieht der Stack bei Aufruf von ''gets'' (Funktion der Standard-Bibliothek von C) so aus:
Bei Prozessoren, die den Stack nach unten beschreiben, sieht dieser vor dem Aufruf von ''gets'' (Funktion der Standard-Bibliothek von C) so aus (wenn man vom eventuell vorhandenen Base Pointer absieht<ref>''[http://www.a-m-i.de/tips/stack/stack.php Was ist ein Stack?]''. 2. Juni 2012.</ref>):


{| border="0"
{| border="0"
| bgcolor="9999FF" | Rücksprungadresse
|class="hintergrundfarbe6"| Rücksprungadresse
|
|
|-
|-
| bgcolor="BBBBFF" | 1000. Zeichen
|class="hintergrundfarbe5"| 1000. Zeichen
|
|
|-
|-
|class="hintergrundfarbe5"| … …
| bgcolor="BBBBFF" | ...
|
|
|-
|-
| bgcolor="BBBBFF" | 3. Zeichen
|class="hintergrundfarbe5"| 3. Zeichen
|
|
|-
|-
| bgcolor="BBBBFF" | 2. Zeichen
|class="hintergrundfarbe5"| 2. Zeichen
|
|
|-
|-
| bgcolor="BBBBFF" | 1. Zeichen
|class="hintergrundfarbe5"| 1. Zeichen
| ←Stackpointer
| <math>\leftarrow</math>Stackpointer
|}
|}


:'''Der Stack wächst nach unten, die Variable wird nach oben überschrieben'''
: '''Der Stack wächst nach unten, die Variable wird nach oben überschrieben'''


''gets'' liest eine Zeile von der Eingabe und schreibt die Zeichen ab ''line[0]'' in den Stack. Es überprüft die Länge der Zeile nicht. Gemäß der Semantik von C erhält ''gets'' nur die Speicheradresse als Pointer, jedoch keinerlei Information über die verfügbare Länge. Wenn man jetzt 1004 Zeichen eingibt, überschreiben die letzten 4 Bytes die Rücksprungadresse (unter der Annahme, dass eine Adresse hier 4 Bytes groß ist), die man auf ein Programmstück innerhalb des Stack richten kann. In den ersten 1000 Zeichen kann man gegebenenfalls ein [[Shellcode|geeignetes Programm]] eingeben.
''gets'' liest eine Zeile von der Eingabe und schreibt die Zeichen ab ''line[0]'' in den Stack. Es überprüft die Länge der Zeile nicht. Gemäß der Semantik von C erhält ''gets'' nur die Speicheradresse als Pointer, jedoch keinerlei Information über die verfügbare Länge. Wenn man jetzt 1004 Zeichen eingibt, überschreiben die letzten 4 Bytes die Rücksprungadresse (unter der Annahme, dass eine Adresse hier 4 Bytes groß ist), die man auf ein Programmstück innerhalb des Stack richten kann. In den ersten 1000 Zeichen kann man gegebenenfalls ein [[Shellcode|geeignetes Programm]] eingeben.


:00@45eA/%A@4 ... ... ... ... ... ... ... ... ... ... ... ... .. 0A&%
: <code>00@45eA/%A@4</code> ... ... ... ... ... ... <code>0A&%</code>
:''Eingabe, wird von gets in den Stack geschrieben (1004 Zeichen)''
: ''Eingabe, wird von gets in den Stack geschrieben (1004 Zeichen)''
{| border="0"
{| border="0"
| bgcolor="9999FF" | modifizierte Rücksprungadresse
|class="hintergrundfarbe6"| modifizierte Rücksprungadresse
|
|
|-
|-
| bgcolor="BBBBFF" | line, 1000. Zeichen
|class="hintergrundfarbe5"| line, 1000. Zeichen
|
|
|-
|-
|class="hintergrundfarbe5"| …
| bgcolor="BBBBFF" | ...
|
| ...
|-
|-
| bgcolor="BBBBFF" | line, 5. Zeichen
|class="hintergrundfarbe5"| line, 5. Zeichen
| drittes Byte im Code
| drittes Byte im Code
|-
|-
| bgcolor="BBBBFF" | line, 4. Zeichen
|class="hintergrundfarbe5"| line, 4. Zeichen
| zweites Byte im Code
| zweites Byte im Code
|-
|-
| bgcolor="BBBBFF" | line, 3. Zeichen
|class="hintergrundfarbe5"| line, 3. Zeichen
| Ziel der Rücksprungadresse, Programmcodestart
| Ziel der Rücksprungadresse, Programmcodestart
|-
|-
| bgcolor="BBBBFF" | line, 2. Zeichen
|class="hintergrundfarbe5"| line, 2. Zeichen
|
|
|-
|-
| bgcolor="BBBBFF" | line, 1. Zeichen
|class="hintergrundfarbe5"| line, 1. Zeichen
| ←Stackpointer
| <math>\leftarrow</math>Stackpointer
|}
|}


:'''Überschreiben der Rücksprungadresse und Programmcode im Stack'''
: '''Überschreiben der Rücksprungadresse und Programmcode im Stack'''


Falls das Programm höhere Privilegien besitzt als der Benutzer, kann dieser unter Ausnutzung des Pufferüberlaufs durch eine spezielle Eingabe diese Privilegien erlangen.
Falls das Programm höhere Privilegien besitzt als der Benutzer, kann dieser unter Ausnutzung des Pufferüberlaufs durch eine spezielle Eingabe diese Privilegien erlangen.
Zeile 92: Zeile 99:
== Gegenmaßnahmen ==
== Gegenmaßnahmen ==
=== Programmerstellung ===
=== Programmerstellung ===
Eine sehr nachhaltige Gegenmaßnahme besteht in der Verwendung [[Typsicherheit|typsicherer]] Programmiersprachen und -werkzeuge, wie zum Beispiel [[Java (Programmiersprache)|Java]] oder [[C-Sharp|C#]], bei denen die Einhaltung von zugewiesenen Speicherbereichen gegebenenfalls schon beim Übersetzen in Maschinensprache mit dem [[Compiler]] kontrolliert, aber spätestens zur [[Laufzeit (Informatik)|Laufzeit]] mit entsprechendem Programmcode überwacht wird. Es ist hierbei unerlässlich, dass das Verändern von [[Zeiger (Informatik)|Zeigervariablen]] nur nach strengen, einschränkenden Regeln erfolgen darf, und es ist in diesem Zusammenhang auch hilfreich, wenn ausschließlich das [[Laufzeitsystem]] [[automatische Speicherbereinigung]]en durchführt.
Bei der Erstellung von Programmen sollte unbedingt auf die Überprüfung aller Feldgrenzen geachtet werden. Hier ist besonders die Verantwortung des Programmierers gefragt. Sofern keine ausreichenden Kenntnisse in der Programmierung unter hardwarenahen Sprachen vorhanden sind, sollte die Verwendung von Programmiersprachen, die automatisch Feldgrenzen überwachen, in Erwägung gezogen werden. Dies ist jedoch nicht immer möglich. Bei Verwendung von C++ sollte die Verwendung von Feldern im C-Stil soweit wie möglich vermieden werden.


Bei der Erstellung von Programmen muss also auf die Überprüfung aller Feldgrenzen geachtet werden. Dies liegt bei nicht-typsicheren Programmiersprachen in der Verantwortung des Programmierers. Allerdings sollte vorzugsweise die Verwendung von Programmiersprachen, die automatisch Feldgrenzen überwachen, in Erwägung gezogen werden, was jedoch nicht immer ohne weiteres möglich ist. Bei Verwendung von C++ sollte die Verwendung von Feldern im C-Stil so weit wie möglich vermieden werden.
void input_line()
{ char line[1000]; // Feld ist eigentlich Zeiger
if (fgets(line, sizeof line, stdin)) // fgets überprüft die Länge
parse_line(line);
}


<syntaxhighlight lang="c">
:'''Gegenmaßnahme: fgets überprüft die Eingabelänge'''
void input_line()
{
char line[1000];
if (fgets(line, sizeof(line), stdin)) // fgets überprüft die Länge
puts(line); // puts schreibt den Inhalt von line nach stdout
}
</syntaxhighlight>

: '''Gegenmaßnahme: fgets überprüft die Eingabelänge'''


=== Überprüfung des Programmcodes ===
=== Überprüfung des Programmcodes ===
Spezielle Überprüfungswerkzeuge erlauben die Analyse des Codes und entdecken gegebenenfalls mögliche Schwachstellen. Allerdings kann der Code zur Feldgrenzenüberprüfung fehlerhaft sein, was oftmals nicht getestet wird.
Spezielle Überprüfungswerkzeuge erlauben die Analyse des Codes und entdecken mögliche Schwachstellen. Allerdings kann der Code zur Feldgrenzenüberprüfung fehlerhaft sein, was oft nicht getestet wird.


=== Unterstützung durch Compiler ===
=== Unterstützung durch Compiler ===
In C und C++ steht eine sehr große Auswahl bestehender Programme zur Verfügung. Moderne [[Compiler]] wie neue Versionen des ''GNU C-Compilers'' erlauben die Aktivierung von Überprüfungscodeerzeugung bei der Übersetzung.
In C und C++ steht eine sehr große Auswahl bestehender Programme zur Verfügung. Moderne [[Compiler]] wie neue Versionen des ''GNU C-Compilers'' erlauben die Aktivierung von Überprüfungscode-Erzeugung bei der Übersetzung.


Sprachen wie C erlauben aufgrund ihres Designs nicht immer die Überprüfung der Feldgrenzen (Beispiel: ''gets''). Die Compiler müssen andere Wege gehen: Sie fügen zwischen der Rücksprungadresse und den lokalen Variablen Platz für eine Zufallszahl ein. Beim Programmstart wird diese Zahl ermittelt, wobei sie jedes Mal unterschiedliche Werte annimmt. Bei jedem Unterprogrammaufruf wird in den dafür vorgesehen Bereich die Zufallszahl geschrieben. Der erforderliche Code wird vom Compiler automatisch generiert. Vor dem Verlassen des Programms über die Rücksprungadresse fügt der Compiler Code ein, der die Zufallszahl auf den vorgesehenen Wert überprüft. Wurde sie geändert, ist auch der Rücksprungadresse nicht zu trauen. Das Programm wird mit einer entsprechenden Meldung abgebrochen.
Sprachen wie C erlauben aufgrund ihres Designs nicht immer die Überprüfung der Feldgrenzen (Beispiel: ''gets''). Die Compiler müssen andere Wege gehen: Sie fügen zwischen der Rücksprungadresse und den lokalen Variablen Platz für eine Zufallszahl (auch „Canary“ genannt) ein. Beim Programmstart wird diese Zahl ermittelt, wobei sie jedes Mal unterschiedliche Werte annimmt. Bei jedem Unterprogrammaufruf wird die Zufallszahl in den dafür vorgesehenen Bereich geschrieben. Der erforderliche Code wird vom Compiler automatisch generiert. Vor dem Verlassen des Programms über die Rücksprungadresse fügt der Compiler Code ein, der die Zufallszahl auf den vorgesehenen Wert überprüft. Wurde sie geändert, ist auch der Rücksprungadresse nicht zu trauen. Das Programm wird mit einer entsprechenden Meldung abgebrochen.


{| border="0"
{| border="0"
| bgcolor="9999FF" | Rücksprungadresse
|class="hintergrundfarbe6"| Rücksprungadresse
|
|
|-
|-
| bgcolor="FF6900" | Zufallszahlbarriere
|style="background:#FF6900"| Zufallszahlbarriere
|
|
|-
|-
| bgcolor="BBBBFF" | line, 1000. Zeichen
|class="hintergrundfarbe5"| line, 1000. Zeichen
|
|
|-
|-
|class="hintergrundfarbe5"| …
| bgcolor="BBBBFF" | ...
|
|
|-
|-
| bgcolor="BBBBFF" | line, 3. Zeichen
|class="hintergrundfarbe5"| line, 3. Zeichen
|
|
|-
|-
| bgcolor="BBBBFF" | line, 2. Zeichen
|class="hintergrundfarbe5"| line, 2. Zeichen
|
|
|-
|-
| bgcolor="BBBBFF" | line, 1. Zeichen
|class="hintergrundfarbe5"| line, 1. Zeichen
| ←Stackpointer
| <math>\leftarrow</math>Stackpointer
|}
|}


:'''Gegenmaßnahme: Zufallszahlbarriere '''
: '''Gegenmaßnahme: Zufallszahlbarriere '''


Daneben kann man manche Compiler auch veranlassen, beim Unterprogrammaufruf eine ''Kopie'' der Rücksprungadresse unterhalb der lokalen Felder zu erzeugen. Diese Kopie wird beim Rücksprung verwendet, die Ausnutzung von Pufferüberläufen ist dann wesentlich erschwert:
Daneben kann man manche Compiler auch veranlassen, beim Unterprogrammaufruf eine ''Kopie'' der Rücksprungadresse unterhalb der lokalen Felder zu erzeugen. Diese Kopie wird beim Rücksprung verwendet, die Ausnutzung von Pufferüberläufen ist dann wesentlich erschwert:


{| border="0"
{| border="0"
| bgcolor="9999FF" | Rücksprungadresse
|class="hintergrundfarbe6"| Rücksprungadresse
|
|
|-
|-
| bgcolor="BBBBFF" | line, 1000. Zeichen
|class="hintergrundfarbe5"| line, 1000. Zeichen
|
|
|-
|-
|class="hintergrundfarbe5"| …
| bgcolor="BBBBFF" | ...
|
|
|-
|-
| bgcolor="BBBBFF" | line, 3. Zeichen
|class="hintergrundfarbe5"| line, 3. Zeichen
|
|
|-
|-
| bgcolor="BBBBFF" | line, 2. Zeichen
|class="hintergrundfarbe5"| line, 2. Zeichen
|
|
|-
|-
| bgcolor="BBBBFF" | line, 1. Zeichen
|class="hintergrundfarbe5"| line, 1. Zeichen
|
|
|-
|-
| bgcolor="00FF00" | Kopie der Rücksprungadresse
|style="background:#00FF00"| Kopie der Rücksprungadresse
| ←Stackpointer
| <math>\leftarrow</math>Stackpointer
|}
|}


:'''Gegenmaßnahme: Kopie der Rücksprungadresse'''
: '''Gegenmaßnahme: Kopie der Rücksprungadresse'''


== Compiler und Compilererweiterungen ==
== Compiler und Compilererweiterungen ==
Für die [[GNU Compiler Collection]] existieren beispielsweise zwei verbreitete Erweiterungen, die Maßnahmen wie die oben beschriebenen implementieren:
Für die [[GNU Compiler Collection]] existieren beispielsweise zwei verbreitete Erweiterungen, die Maßnahmen wie die oben beschriebenen implementieren:
* Der ''Stack Smashing Protector'' von [[IBM]], ehemals als ''ProPolice'' bekannt ([http://www.trl.ibm.com/projects/security/ssp/ Homepage, englisch]).
* Der ''Stack Smashing Protector'' von [[IBM]], ehemals als ''ProPolice'' bekannt ([http://www.trl.ibm.com/projects/security/ssp/ Homepage, englisch]).
* Der ''Stack Guard'', entwickelt an der [[Oregon Health and Science University]], zwischenzeitlich bei der [[Linux-Distribution]] ''Immunix'', jetzt bei [[Novell]].
* Der ''Stack Guard'', entwickelt an der [[Oregon Health & Science University]], zwischenzeitlich bei der [[Linux-Distribution]] ''Immunix'', jetzt bei [[Novell]].


== Heap-Überlauf ==
== Heap-Überlauf ==
Ein ''Heap-Überlauf'' ist ein Pufferüberlauf, der auf dem [[Dynamischer Speicher|Heap]] stattfindet. Speicher auf dem Heap wird zugewiesen, wenn Programme dynamischen Speicher anfordern, etwa über malloc() oder den ''new''-Operator in [[C++]]. Werden in einen Puffer auf dem Heap Daten ohne Überprüfung der Länge geschrieben und ist die Datenmenge größer als die Größe des Puffers, so wird über das Ende des Puffers hinausgeschrieben und es kommt zu einem Speicherüberlauf.
Ein ''Heap-Überlauf'' ist ein Pufferüberlauf, der auf dem [[Dynamischer Speicher|Heap]] stattfindet. Speicher auf dem Heap wird zugewiesen, wenn Programme dynamischen Speicher anfordern, etwa über malloc() oder den ''new''-Operator in [[C++]]. Werden in einen Puffer auf dem Heap Daten ohne Überprüfung der Länge geschrieben und ist die Datenmenge größer als die Größe des Puffers, so wird über das Ende des Puffers hinausgeschrieben und es kommt zu einem Speicherüberlauf.


Durch ''Heap-Überläufe'' kann meist beliebiger Code auf dem Rechner ausgeführt werden, insbesondere wenn der Heap ausführbar ist. [[FreeBSD]] hat beispielsweise einen Heap-Schutz, hier ist dies nicht möglich. Sie können nur in [[Programmiersprachen]] auftreten, in denen bei Pufferzugriffen keine Längenüberprüfung stattfindet. [[C (Programmiersprache)|C]], [[C++]] oder [[Assemblersprache|Assembler]] sind anfällig, [[Java (Programmiersprache)|Java]] oder [[Perl (Programmiersprache)|Perl]] sind es nicht.
Durch ''Heap-Überläufe'' kann durch Überschreiben von Zeigern auf Funktionen beliebiger Code auf dem Rechner ausgeführt werden, insbesondere wenn der Heap ausführbar ist. [[FreeBSD]] hat beispielsweise einen Heap-Schutz, hier ist das nicht möglich. Sie können nur in [[Programmiersprache]]n auftreten, in denen bei Pufferzugriffen keine Längenüberprüfung stattfindet. [[C (Programmiersprache)|C]], [[C++]] oder [[Assemblersprache|Assembler]] sind anfällig, [[Java (Programmiersprache)|Java]] oder [[Perl (Programmiersprache)|Perl]] sind es nicht.


Zum Bsp. wurde am 23. Juni 2015 von [[Adobe Inc.|Adobe]] bekannt gegeben, dass durch solch einen Pufferüberlauf beliebiger [[Schadprogramm|Schadcode]] auf Systemen ausgeführt werden könne und so die Kontrolle über das System übernommen werden könnte, auf denen der [[Adobe Flash|Flash Player]] installiert sei.<ref>{{Internetquelle |autor=Dennis Schirrmacher |url=https://www.heise.de/security/meldung/Adobe-Notfall-Update-fuer-Flash-Player-2724360.html |titel=Adobe: Notfall-Update für Flash Player |werk=Heise Security |hrsg=[[Heise online]] |datum=2015-06-24 |abruf=2015-06-29}}</ref><ref>{{Internetquelle |url=https://helpx.adobe.com/security/products/flash-player/apsb15-14.html |titel=Security updates available for Adobe Flash Player – CVE number: CVE-2015-3113 |werk=Adobe Security Bulletin |hrsg=[[Adobe Inc.]] |datum=2015-06-23 |abruf=2015-06-29}}</ref>
''Siehe auch'': [[Shellcode]], [[Exploit]]

{{Siehe auch|Shellcode|Exploit}}


== Beispiel ==
== Beispiel ==
<syntaxhighlight lang="c">
#define BUFSIZE 128


char * copy_string(const char *s)
<code>
{
#define BUFSIZE 128
char * buf = malloc(BUFSIZE); // Annahme: Längere Strings kommen niemals vor
char * copy_string(const char *s)
{
char * buf = malloc(BUFSIZE); // Annahme: Längere Strings kommen niemals vor
strcpy(buf, s); // Heap-Überlauf, falls strlen(s) > 127
return buf;
}
</code>


if (buf)
Man sollte lieber folgendes verwenden:
strcpy(buf, s); // Heap-Überlauf, falls strlen(s) > 127


return buf;
<code>
}
char * buf;
</syntaxhighlight>
buf = malloc(1 + strlen(s)); // Plus 1 wegen des terminierenden NUL-Zeichens
strcpy(buf, s);
</code>


Da strcpy() die Größen von Quelle und Ziel nicht überprüft, sondern als Quelle einen null-terminierten ('\0') Speicherbereich erwartet, ist auch die folgende Variante unsicher (sie wird allerdings nicht über "buf" hinausschießen, sondern ggf. über das Ende des "s" zugewiesenen Speicherbereichs).
Alternativ hilft auch die Verwendung des strncpy-Befehls (Kopieren von maximal n Zeichen):


<syntaxhighlight lang="c">
<code>
char * buf;
char * buf;

if ((buf = (char *) malloc(BUFSIZE)) == NULL) // Überprüfung des Zeigers
buf = malloc(1 + strlen(s)); // Plus 1 wegen des terminierenden NUL-Zeichens
if (buf)
return (NULL); // Fehlerfall: Speicher konnte nicht reserviert werden
strcpy(buf, s);
</syntaxhighlight>
memset(buf, '\0', BUFSIZE) // kompletten Speicher mit '\0' auffüllen

Der strncpy-Befehl dagegen kopiert maximal n Zeichen von der Quelle zum Ziel und funktioniert somit, wenn s nullterminiert oder größer als BUFSIZE ist.
strncpy(buf, s, BUFSIZE-1); // Minus 1, da ein NUL-Zeichen in jedem Fall bleiben muss

</code>
<syntaxhighlight lang="c">
char *buf;

if ((buf = malloc(BUFSIZE)) != NULL) { // Überprüfung des Zeigers.
strncpy(buf, s, BUFSIZE - 1);
buf[BUFSIZE - 1] = '\0'; // Nachteil: Die Zeichenkette muss manuell terminiert werden.
}
return buf;
</syntaxhighlight>


Einige Betriebssysteme, z.&nbsp;B. [[OpenBSD]], bieten die Funktion ''strlcpy'' an, die ihrerseits sicherstellt, dass der Zielstring nullterminiert wird und das Erkennen eines abgeschnittenen Zielstrings vereinfacht.
Einige Betriebssysteme, z.&nbsp;B. [[OpenBSD]], bieten die Funktion ''strlcpy'' an, die ihrerseits sicherstellt, dass der Zielstring nullterminiert wird und das Erkennen eines abgeschnittenen Zielstrings vereinfacht.
Zeile 215: Zeile 229:
== Siehe auch ==
== Siehe auch ==
* [[Angriffsvektor]]
* [[Angriffsvektor]]
* [[Arithmetischer Überlauf]]
* [[NX-Bit]]


== Literatur ==
== Literatur ==
* Aleph One: ''[http://fringe.davesource.com/Fringe/Hacking/Documents/Smashing_The_Stack Smashing The Stack For Fun And Profit].'' In: ''[[Phrack-Magazin]].'' 7, Nr. 49 (Dieser Artikel veranschaulicht sehr gut die Wirkungsweise von Pufferüberläufen).
* Felix Lindner (2006): „Ein Haufen Risiko, Pufferüberläufe auf dem Heap und wie man sie ausnutzt“ in: c't, {{ISSN|0724-8679}}, 23. Jahrgang (2006), Nr. 9, [http://www.heise.de/security/artikel/72101 auch kostenlos online im heise Security].
* [[Tobias Klein]]: ''Buffer Overflows und Format-String-Schwachstellen'', Dpunkt Verlag, ISBN 3-89864-192-9.
* Jon Erickson: ''Forbidden Code.'' mitp, Bonn 2004, ISBN 3-8266-1457-7.
* Tobias Klein: ''Buffer Overflows und Format-String-Schwachstellen. Funktionsweisen, Exploits und Gegenmaßnahmen.'' Dpunkt, Heidelberg 2004, ISBN 3-89864-192-9.
* Aleph One [http://community.core-sdi.com/~juliano/smashing/P49-4-Smashing_the_stack.txt Smashing The Stack For Fun And Profit], [[Phrack-Magazin]] Nr. 49. Dieser Artikel veranschaulicht sehr gut die Wirkungsweise von Pufferüberläufen.
* Felix Lindner: ''Ein Haufen Risiko. Pufferüberläufe auf dem Heap und wie man sie ausnutzt.'' In: ''[[c’t]]. Magazin für Computer-Technik.'' 23, 9, 2006, S. 186–192, [https://www.heise.de/security/artikel/Ein-Haufen-Risiko-270800.html auch kostenlos online im heise Security].
* St. Kallnik, D. Pape, D. Schröter, St. Strobel, D. Bachfeld: [http://www.heise.de/security/artikel/37958 ''Eingelocht. Buffer-Overflows und andere Sollbruchstellen''] Einführungsartikel bei heise Security, mit einfachen Beispielen in C. auch in [http://www.heise.de/ct/01/23/216/ c't 23/2001, S.216]
* Oliver Müller: ''Überläufer. Systemeinbruch via Stack-Overflow.'' In: ''[[iX – Magazin für professionelle Informationstechnik]].'' Band 2, 2007, S. 100–105.
* Jon Erickson: ''Forbidden Code'' mitp, ISBN 3-8266-1457-7.
* Stephan Kallnik, Daniel Pape, Daniel Schröter, Stefan Strobel, Daniel Bachfeld: [https://www.heise.de/security/artikel/Eingelocht-270148.html ''Eingelocht. Buffer-Overflows und andere Sollbruchstellen''.] heise Security, auch in {{Webarchiv |url=http://www.heise.de/ct/01/23/216/ |text=''c’t'' 23/2001, S. 216 |wayback=20011114032312}}. Einführungsartikel mit einfachen Beispielen in C.


== Weblinks ==
== Weblinks ==
* [http://insecure.org/stf/smashstack.html Technische Details von Buffer-Overflow (und Exploits)]
* http://www.w00w00.org/files/articles/heaptut.txt
* [http://www.heise.de/security/artikel/72101/0 Artikel zur Ausnutzung eines Heap overflows]
* [https://www.heise.de/security/artikel/Ein-Haufen-Risiko-270800.html/0 Ausnutzung eines Heap overflow]
* {{Webarchiv |url=http://www.heise.de/ct/01/23/216/ |text=Buffer-Overflows und wie man sich davor schützt |wayback=20011114032312}}
* [http://www.buffershield.com/index10.htm „Buffershield“] – ist eine Sicherheitssoftware die Code auf dem „stack“ und dem „heap memory area“ entdeckt und dadurch die Ausbeutung von Pufferüberläufen verhindert.
* [https://www.heise.de/security/artikel/Eingelocht-270148.html Buffer-Overflows und andere Sollbruchstellen]


== Einzelnachweise ==
[[Kategorie:Sicherheitslücke]]
<references />


{{Normdaten|TYP=s|GND=4752450-9}}
[[en:Buffer overflow]]

[[es:Desbordamiento de búfer]]
{{SORTIERUNG:Pufferuberlauf}}
[[fi:Puskurin ylivuotovirhe]]
[[Kategorie:Programmfehler]]
[[fr:Dépassement de tampon]]
[[Kategorie:Sicherheitslücke]]
[[he:גלישת חוצץ]]
[[Kategorie:Hackertechnik (Computersicherheit)]]
[[it:Buffer overflow]]
[[ja:バッファオーバーラン]]
[[nl:Bufferoverloop]]
[[pl:Przepełnienie bufora]]
[[ru:Переполнение буфера]]
[[sv:Buffertöverskridning]]
[[tr:Arabellek aşımı]]

Aktuelle Version vom 27. August 2023, 19:16 Uhr

Pufferüberläufe (englisch buffer overflow), nicht zu verwechseln mit Stapelüberläufen (englisch stack overflows), gehören zu den häufigsten Sicherheitslücken in aktueller Software, die sich u. a. über das Internet ausnutzen lassen können. Im Wesentlichen werden bei einem Pufferüberlauf durch Fehler im Programm zu große Datenmengen in einen dafür zu kleinen reservierten Speicherbereich – den Puffer – geschrieben, wodurch nach dem Ziel-Speicherbereich liegende Speicherstellen überschrieben werden.

Ursache für unerwünschtes Schreiben außerhalb des Puffers kann nicht nur eine übergroße Datenmenge, sondern auch ein Überlauf (oder sonstige fehlerhafte Berechnung) der Zieladresse sein, die anzeigt, wo der Datensatz in den Puffer geschrieben werden soll. In diesem Fall wird von einem pointer overflow (vom englischen pointer, für „Zeiger“) gesprochen.

Gefahren durch Pufferüberläufe

[Bearbeiten | Quelltext bearbeiten]

Ein Pufferüberlauf kann zum Absturz des betreffenden Programms, zur Verfälschung von Daten oder zur Beschädigung von Datenstrukturen der Laufzeitumgebung des Programms führen. Durch Letzteres kann die Rücksprungadresse eines Unterprogramms mit beliebigen Daten überschrieben werden, wodurch ein Angreifer durch Übermittlung von beliebigem Maschinencode beliebige Befehle mit den Privilegien des für den Pufferüberlauf anfälligen Prozesses ausführen kann. Dieser Code hat in der Regel das Ziel, dem Angreifer einen komfortableren Zugang zum System zu verschaffen, damit dieser das System dann für seine Zwecke verwenden kann. Pufferüberläufe in verbreiteter Server- und Clientsoftware werden auch von Internetwürmern ausgenutzt.

Angriffe mit Pufferüberläufen sind ein wichtiges Thema in der Computersicherheit und Netzwerksicherheit. Sie können nicht nur über jegliche Art von Netzwerken, sondern auch lokal auf dem System versucht werden. Behoben werden sie in der Regel nur durch kurzfristig gelieferte Fehlerkorrekturen (Patches) der Hersteller.

Interpretierte Sprachen sind, abgesehen von Fehlern im Interpreter, in der Regel nicht anfällig, da der Speicher vom Interpreter, und nicht vom Programm selbst, verwaltet wird.

Programmiersprachen

[Bearbeiten | Quelltext bearbeiten]

Die wesentlichste Ursache für Pufferüberläufe ist die Verwendung von Programmiersprachen, die nicht die Möglichkeit bieten, Grenzen von Speicherbereichen automatisch zu überwachen, um eine Bereichsüberschreitung von Speicherbereichen zu verhindern. Dazu gehört besonders die Sprache C, die das Hauptgewicht auf Performance (und ursprünglich Einfachheit des Compilers) legt und auf eine Überwachung verzichtet, sowie die C-Weiterentwicklung C++. Hier ist ein Programmierer teilweise gezwungen, den entsprechenden Code von Hand zu generieren, wobei oft absichtlich oder aus Nachlässigkeit darauf verzichtet wird. Die Überprüfung ist häufig auch fehlerhaft implementiert, da während der Programmtests diese Programmteile meist nicht oder ungenügend getestet werden. Daneben stellen der (im Fall von C++) komplexe Sprachumfang und die Standardbibliothek sehr viele fehleranfällige Konstrukte zur Verfügung, zu denen es in vielen Fällen kaum eine Alternative gibt.

Die häufig verwendete Programmiersprache C++ bietet nur eingeschränkte Möglichkeiten zur automatischen Überprüfung von Feldgrenzen. Als Weiterentwicklung der Programmiersprache C übernimmt sie die meisten Eigenschaften von C, wobei sich aber das Risiko von Pufferüberläufen bei Benutzung von modernen Sprachmitteln (u. a. automatische Speicherverwaltung) weitestgehend vermeiden lässt. Aus Gewohnheit, Kompatibilitätsgründen zu vorhandenem C-Code, Systemaufrufen in C-Konvention sowie aus Performancegründen wird von diesen Möglichkeiten aber nicht immer Gebrauch gemacht. Laufzeitüberprüfungen sind im Gegensatz zu Sprachen wie beispielsweise Pascal oder Ada nicht Bestandteil der Sprache, lassen sich aber in einigen Anwendungsfällen (z. B. mit Smart Pointern) nachrüsten.

Da die meisten Programmiersprachen auch Standardbibliotheken definieren, bedeutet die Wahl einer Sprache meist auch die Verwendung der entsprechenden Standardbibliotheken. Im Fall von C und C++ enthält die Standardbibliothek eine Anzahl von gefährlichen Funktionen, die zum Teil gar keine sichere Verwendung zulassen und zu denen zum Teil keine Alternativen bestehen.

Auf Programmiersprachenebene kann die Gefahr von Pufferüberläufen durch die Verwendung von Programmiersprachen, die konzeptionell sicherer als C++ oder C sind, verringert oder ausgeschlossen werden. Ein sehr viel geringeres Risiko besteht zum Beispiel in Programmiersprachen wie Rust, Java, C# oder der Pascal-Familie Modula, Object Pascal oder Ada.

Fast ausgeschlossen sind Pufferüberläufe beispielsweise in der Programmiersprache Java, da die Ausführung im Bytecode überwacht wird. Aber auch in Java gibt es einerseits Pufferüberläufe, deren Ursache im Laufzeitsystem liegt und von denen mehrere JRE-Versionen betroffen sind.[1][2]

Prozessoren und Programmierstil

[Bearbeiten | Quelltext bearbeiten]

Weitere Eigentümlichkeiten von C und C++ sowie der am häufigsten eingesetzten Prozessoren machen das Auftreten von Pufferüberläufen wahrscheinlich. Die Programme in diesen Sprachen bestehen zum Teil aus Unterprogrammen. Diese besitzen lokale Variablen.

Bei modernen Prozessoren ist es üblich, die Rücksprungadresse eines Unterprogramms und dessen lokale Variablen auf einen als Stack bezeichneten Bereich zu legen. Dabei werden beim Unterprogrammaufruf zunächst die Rücksprungadresse und danach die lokalen Variablen auf den Stack gelegt. Bei modernen Prozessoren wie dem Intel Pentium wird der Stack durch eingebaute Prozessorbefehle verwaltet und wächst zwingend nach unten. Werden Felder oder Zeichenketten in den lokalen Variablen verwendet, werden diese meist nach oben beschrieben. Wird die Feldgrenze nicht geprüft, kann man damit durch Überschreiten des Feldes die Rückkehradresse auf dem Stack erreichen und gegebenenfalls absichtlich modifizieren.

Das folgende Programmstück in C, das in ähnlicher Form oft verwendet wird, zeigt einen solchen Pufferüberlauf:

void input_line()
{
    char line[1000];
    if (gets(line))     // gets erhält Zeiger auf das Array, keine Längeninformation
        puts(line);     // puts schreibt den Inhalt von line nach stdout
}

Bei Prozessoren, die den Stack nach unten beschreiben, sieht dieser vor dem Aufruf von gets (Funktion der Standard-Bibliothek von C) so aus (wenn man vom eventuell vorhandenen Base Pointer absieht[3]):

Rücksprungadresse
1000. Zeichen
… …
3. Zeichen
2. Zeichen
1. Zeichen ←Stackpointer
Der Stack wächst nach unten, die Variable wird nach oben überschrieben

gets liest eine Zeile von der Eingabe und schreibt die Zeichen ab line[0] in den Stack. Es überprüft die Länge der Zeile nicht. Gemäß der Semantik von C erhält gets nur die Speicheradresse als Pointer, jedoch keinerlei Information über die verfügbare Länge. Wenn man jetzt 1004 Zeichen eingibt, überschreiben die letzten 4 Bytes die Rücksprungadresse (unter der Annahme, dass eine Adresse hier 4 Bytes groß ist), die man auf ein Programmstück innerhalb des Stack richten kann. In den ersten 1000 Zeichen kann man gegebenenfalls ein geeignetes Programm eingeben.

00@45eA/%A@4 … ... … ... … ... … ... … ... … ... 0A&%
Eingabe, wird von gets in den Stack geschrieben (1004 Zeichen)
modifizierte Rücksprungadresse
line, 1000. Zeichen
line, 5. Zeichen drittes Byte im Code
line, 4. Zeichen zweites Byte im Code
line, 3. Zeichen Ziel der Rücksprungadresse, Programmcodestart
line, 2. Zeichen
line, 1. Zeichen ←Stackpointer
Überschreiben der Rücksprungadresse und Programmcode im Stack

Falls das Programm höhere Privilegien besitzt als der Benutzer, kann dieser unter Ausnutzung des Pufferüberlaufs durch eine spezielle Eingabe diese Privilegien erlangen.

Gegenmaßnahmen

[Bearbeiten | Quelltext bearbeiten]

Programmerstellung

[Bearbeiten | Quelltext bearbeiten]

Eine sehr nachhaltige Gegenmaßnahme besteht in der Verwendung typsicherer Programmiersprachen und -werkzeuge, wie zum Beispiel Java oder C#, bei denen die Einhaltung von zugewiesenen Speicherbereichen gegebenenfalls schon beim Übersetzen in Maschinensprache mit dem Compiler kontrolliert, aber spätestens zur Laufzeit mit entsprechendem Programmcode überwacht wird. Es ist hierbei unerlässlich, dass das Verändern von Zeigervariablen nur nach strengen, einschränkenden Regeln erfolgen darf, und es ist in diesem Zusammenhang auch hilfreich, wenn ausschließlich das Laufzeitsystem automatische Speicherbereinigungen durchführt.

Bei der Erstellung von Programmen muss also auf die Überprüfung aller Feldgrenzen geachtet werden. Dies liegt bei nicht-typsicheren Programmiersprachen in der Verantwortung des Programmierers. Allerdings sollte vorzugsweise die Verwendung von Programmiersprachen, die automatisch Feldgrenzen überwachen, in Erwägung gezogen werden, was jedoch nicht immer ohne weiteres möglich ist. Bei Verwendung von C++ sollte die Verwendung von Feldern im C-Stil so weit wie möglich vermieden werden.

void input_line()
{
    char line[1000];
    if (fgets(line, sizeof(line), stdin))   // fgets überprüft die Länge
        puts(line);  // puts schreibt den Inhalt von line nach stdout
}
Gegenmaßnahme: fgets überprüft die Eingabelänge

Überprüfung des Programmcodes

[Bearbeiten | Quelltext bearbeiten]

Spezielle Überprüfungswerkzeuge erlauben die Analyse des Codes und entdecken mögliche Schwachstellen. Allerdings kann der Code zur Feldgrenzenüberprüfung fehlerhaft sein, was oft nicht getestet wird.

Unterstützung durch Compiler

[Bearbeiten | Quelltext bearbeiten]

In C und C++ steht eine sehr große Auswahl bestehender Programme zur Verfügung. Moderne Compiler wie neue Versionen des GNU C-Compilers erlauben die Aktivierung von Überprüfungscode-Erzeugung bei der Übersetzung.

Sprachen wie C erlauben aufgrund ihres Designs nicht immer die Überprüfung der Feldgrenzen (Beispiel: gets). Die Compiler müssen andere Wege gehen: Sie fügen zwischen der Rücksprungadresse und den lokalen Variablen Platz für eine Zufallszahl (auch „Canary“ genannt) ein. Beim Programmstart wird diese Zahl ermittelt, wobei sie jedes Mal unterschiedliche Werte annimmt. Bei jedem Unterprogrammaufruf wird die Zufallszahl in den dafür vorgesehenen Bereich geschrieben. Der erforderliche Code wird vom Compiler automatisch generiert. Vor dem Verlassen des Programms über die Rücksprungadresse fügt der Compiler Code ein, der die Zufallszahl auf den vorgesehenen Wert überprüft. Wurde sie geändert, ist auch der Rücksprungadresse nicht zu trauen. Das Programm wird mit einer entsprechenden Meldung abgebrochen.

Rücksprungadresse
Zufallszahlbarriere
line, 1000. Zeichen
line, 3. Zeichen
line, 2. Zeichen
line, 1. Zeichen ←Stackpointer
Gegenmaßnahme: Zufallszahlbarriere

Daneben kann man manche Compiler auch veranlassen, beim Unterprogrammaufruf eine Kopie der Rücksprungadresse unterhalb der lokalen Felder zu erzeugen. Diese Kopie wird beim Rücksprung verwendet, die Ausnutzung von Pufferüberläufen ist dann wesentlich erschwert:

Rücksprungadresse
line, 1000. Zeichen
line, 3. Zeichen
line, 2. Zeichen
line, 1. Zeichen
Kopie der Rücksprungadresse ←Stackpointer
Gegenmaßnahme: Kopie der Rücksprungadresse

Compiler und Compilererweiterungen

[Bearbeiten | Quelltext bearbeiten]

Für die GNU Compiler Collection existieren beispielsweise zwei verbreitete Erweiterungen, die Maßnahmen wie die oben beschriebenen implementieren:

Ein Heap-Überlauf ist ein Pufferüberlauf, der auf dem Heap stattfindet. Speicher auf dem Heap wird zugewiesen, wenn Programme dynamischen Speicher anfordern, etwa über malloc() oder den new-Operator in C++. Werden in einen Puffer auf dem Heap Daten ohne Überprüfung der Länge geschrieben und ist die Datenmenge größer als die Größe des Puffers, so wird über das Ende des Puffers hinausgeschrieben und es kommt zu einem Speicherüberlauf.

Durch Heap-Überläufe kann durch Überschreiben von Zeigern auf Funktionen beliebiger Code auf dem Rechner ausgeführt werden, insbesondere wenn der Heap ausführbar ist. FreeBSD hat beispielsweise einen Heap-Schutz, hier ist das nicht möglich. Sie können nur in Programmiersprachen auftreten, in denen bei Pufferzugriffen keine Längenüberprüfung stattfindet. C, C++ oder Assembler sind anfällig, Java oder Perl sind es nicht.

Zum Bsp. wurde am 23. Juni 2015 von Adobe bekannt gegeben, dass durch solch einen Pufferüberlauf beliebiger Schadcode auf Systemen ausgeführt werden könne und so die Kontrolle über das System übernommen werden könnte, auf denen der Flash Player installiert sei.[4][5]

#define BUFSIZE 128

char * copy_string(const char *s)
{
    char * buf = malloc(BUFSIZE); // Annahme: Längere Strings kommen niemals vor

    if (buf)
        strcpy(buf, s); // Heap-Überlauf, falls strlen(s) > 127

    return buf;
}

Da strcpy() die Größen von Quelle und Ziel nicht überprüft, sondern als Quelle einen null-terminierten ('\0') Speicherbereich erwartet, ist auch die folgende Variante unsicher (sie wird allerdings nicht über "buf" hinausschießen, sondern ggf. über das Ende des "s" zugewiesenen Speicherbereichs).

char * buf;

buf = malloc(1 + strlen(s)); // Plus 1 wegen des terminierenden NUL-Zeichens
if (buf)
    strcpy(buf, s);

Der strncpy-Befehl dagegen kopiert maximal n Zeichen von der Quelle zum Ziel und funktioniert somit, wenn s nullterminiert oder größer als BUFSIZE ist.

char *buf;

if ((buf = malloc(BUFSIZE)) != NULL) { // Überprüfung des Zeigers.
    strncpy(buf, s, BUFSIZE - 1);
    buf[BUFSIZE - 1] = '\0';  // Nachteil: Die Zeichenkette muss manuell terminiert werden.
}
return buf;

Einige Betriebssysteme, z. B. OpenBSD, bieten die Funktion strlcpy an, die ihrerseits sicherstellt, dass der Zielstring nullterminiert wird und das Erkennen eines abgeschnittenen Zielstrings vereinfacht.

Einzelnachweise

[Bearbeiten | Quelltext bearbeiten]
  1. Schwachstelle im Sun Java Runtime EnvironmentLinuxCommunity, am 17. Januar 2007
  2. Sun Java JRE bis 1.5.x GIF Image Handler Pufferüberlaufvuldb.com, am 22. Januar 2007 (letzte Änderung am 7. Juli 2015)
  3. Was ist ein Stack?. 2. Juni 2012.
  4. Dennis Schirrmacher: Adobe: Notfall-Update für Flash Player. In: Heise Security. Heise online, 24. Juni 2015, abgerufen am 29. Juni 2015.
  5. Security updates available for Adobe Flash Player – CVE number: CVE-2015-3113. In: Adobe Security Bulletin. Adobe Inc., 23. Juni 2015, abgerufen am 29. Juni 2015.