- Sometimes the biggest jerks are the ones in the right.
- -- Arlo & Janis by Jimmy Johnson, September, 9th 1999
Seit dem ich einen
Artikel in
fefes Blog, bin ich was den Einsatz von Linux-Systemen angeht
etwas um einiges unruhiger geworden. Ich werde im Folgenden versuchen, die Problematik mal so zu erläutern, dass sie auch ein nicht-Programmierer versteht.
Es jetzt um ein Programm, welches für viele Programmierer unerläßlich ist, einen sogenannten Compiler. Dieser übersetzt ein Programm, welches ich mir ausdenke, in Anweisungen, die ein Computer versteht. (Leider macht er dabei im besten Fall genau das, was ich ihm sage, aber nie das, was ich wirklich meine, nur so am Rande.)
Ein Großteil der Angriffe auf Computersystem fußt darauf, dass der Computer keine beliebig großen Zahlen darstellen kann. Da man dies auch selten braucht, ist es im Moment üblich bei ganzen Zahlen mit einem Zahlenbereich von 32Bit zu arbeiten, das bedeutet mit Zahlen von 0 bis 4294967295 (=2
32-1) zu arbeiten. Wenn man auch noch negative Zahlen braucht, verschiebt man das Ganze auf den Bereich -2147483648 bis +2147483647. Wo das her kommt ist in Wikipedia etwas
genauer erklärt.
Da ich nicht mit so großen Zahlen jonglieren möchte wähle ich im Folgenden einen Zahlenbereich von -128 bis +127, um die Sache etwas einfacher zu veranschaulichen. Eine wichtige Frage ist nun, was passiert, wenn ich den Zahlenbereich verlasse, beziehungsweise mit Absicht verlassen will. Was passiert also, wenn ich innerhalb dieses Zahlenbereiches 127 + 1 errechnen lasse? Es kommt -128 raus. Warum? Nun ja, bildlich gesprochen kann man sich vorstellen, dass die Zahlen in einem Ring angeordnet sind, und man so wenn man an einem Ende raus geht, kommt man am anderen wieder rein. Das gleiche passiert auch bei 2147483647 + 1, da kommt dann -2147483648 raus.
Dies ist eine Hälfte eines klassischen Angriffs auf Computersoftware. Nehmen wir einmal an, ich habe ein Programm geschrieben mit der ich eine Textzeile übertrage. Ich lege fest, dass eine Textzeile nicht mehr als 114 Zeichen enthalten darf, weil ich bevor ich den eigentlichen Text sende die Information verschicken will, wie groß der versendete Text sein wird. Warum will ich so etwas machen? Nun ja, damit der Empfänger weiß, wieviel Speicher er vom Betriebssystem anfordern muss, um die Textzeile zu verwalten. Und 114 deshalb, weil ich vor den gesendeten Text noch den Text "SvOlli sagt: " ausgeben will. Das sind insgesamt 13 Zeichen und liegt mit den 114 Zeichen vom Text noch genau im Zahlenbereich, der bis +127 geht.
Jetzt nehmen wir mal an, jemand möchte mein Empfängerprogramm zum Absturz bringen, dann versucht er als erstes mir eine Text zu schicken, der mehr als 114 Zeichen hat. Nehmen wir mal an er schickt mir 125 Zeichen, ich addiere meine 13 Zeichen drauf, und wir erhalten 138. Nach den Regeln der Mathematik jedenfalls. Da wir aber den Zahlenbereich verlassen läuft das Ganze etwas anders. Nachdem wir die ersten 3 von den 13 drauf addiert haben, sind wir nicht bei +128, sondern bei -128, plus die restlichen 10 macht -118. Nun versuche ich also vom Betzriebssystem Platz für -118 Zeichen anzufordern. Das denkt sich seinen Teil und lacht mich aus, oder so. Und ab hier passiert es, dass mein Programm nicht mehr so läuft, wie ich es eigentlich gemeint hatte. Diesen Teil einer Attacke nennt man "integer overflow". 'Integer', weil halt die Zahlendarstellung so heißt, und 'overflow', weil der Bereich quasi überläuft wie ein Eimer, der unter einem laufenden Wasserhahn steht. Wie der Rest einer solchen Attacke aussieht schenke ich mir mal hier, weil ich sonst zu stark vom eigentlichen Thema abschweife.
Um einen solchen Angriff zu entgehen frage ich einfach folgendes ab:
ist 'a' + 13 größer als 'a'?
Dabei steht 'a' für die Anzahl der übermittelten Zeichen. Mathematisch ist diese Aussage immer wahr, im Computerbereich nur solange, wie ich den Zahlenbereich nicht verlasse. Wenn ich auf mein Bespiel von eben zurückkomme, stellt sich die Frage:
ist -118 größer als 125?
Und das ist natürlich falsch. Und so habe ich den Angreifer entlarvt. Das hat auch bisher immer recht gut funktioniert.
Nun steht aber in der offiziellen Definition der Programmiersprache, dass das Verhalten in einem solchen Fall "undefiniert" ist. Das bedeutet, der Hersteller des Compilers (beziehungsweise Programmierer, denn das ist schließlich auch nur ein Computerprogramm) selbst entscheiden kann, was er in einem solchen Fall macht, und keine Regeln vorgegeben bekommt. Nun hat sich aber das Verhalten, wie ich es beschrieben habe als sogenannter Defacto-Standard durchgesetzt. Das bedeutet soviel wie, es steht nirgendwo geschrieben, aber alle machen es so.
Ein findiger Programmierer hat nun aber erkannt, dass er den Compiler "optimieren" kann, wenn er die ganze Abfrage "ist 'a' + 13 größer als 'a'?" einfach immer mit "ja" beantwortet. Laut offiziellen Standard darf er das, schließlich ist ein solcher Fall ja "undefiniert". Nun funktioniert aber meine Abfrage gegen den bösen Angreifer nicht mehr, und mein Programm ist wieder genauso verwundbar wie am Anfang. Der Compiler ist übrigens der GCC ab Version 4.1.0, der bei so ziemlich jeder aktuellen Linux-Distribution eingesetzt wird. Was mich noch mehr erschreckt ist die Ignoranz, mit der der Programmierer seinen Entschluss für diese "Optimierung" verteidigt. Der Standard gibt ihm Recht, und das reicht. Dass auf diese Weise eine ganz wichtige Methode zur Erkennung von Angriffen auf Computer ausgehebelt wird interessiert ihn nicht.
Nun überlege ich im Moment, was ich tun kann, damit meine Linux-Rechner von diesem Problem nicht betroffen sind. Die traurige Antwort ist: wenn ich mich nicht mit einem riesigen Haufen Arbeit überwerfen will, gar nichts. Und das trifft gleich doppelt zu. Es hilft nicht, einfach nur den Compiler auszutauschen, sondern ich muss auch noch die gesamte Distribution mit diesem Compiler neu übersetzen, und von dem Trip bin ich gerade erst vor etwas über einem Jahr runtergekommen, weil ich halt meine Zeit weniger mit informatischer Onanie verbringen wollte. Allerdings setzten die Linux-Distributionen, die ich im Moment verwende alle einen Compiler ein, die älter sind: GCC 4.0.3 und GCC 3.3.5. Solange ich also die Distributionen nicht gegen neuere austausche, bin ich erstmal in dieser Hinsicht sicher. Wohl ist mir bei den Gedanken trotzdem nicht.
Kommentare
Meine 2cents nun: Meines Erachtens kann der Programmierer diese Änderung ohne Probleme durchführen, denn nach den Gesetzen der Mathematik ist a+13 > a. Und auf die beruft man sich im Bereich der Informatik letztendlich immer.
Warum fragst Du es nicht (in Deinem Spezialfall) ab mit der Zeile:
if ($a < 114) { [OK] } else { [NANA] }
Die kannst Du zwar nicht so einfach für andere Probleme übernehmen wie die von Dir verwendete, aber schließlich musst Du nur einen Wert verändern. Diese Zeile fände ich als Fremd-Programmierer auch intuitiver und sofort zu verstehen. Würde ich in fremdem Source-Code unkommentiert irgendwo lesen:
if ($a+13 > $a) ....
wäre ich erst einmal komplett verwirrt und würde geistig den Augenwischer machen und denken "Das is doch immer so, oder wat?" - Denn Du denkst nicht sofort an die von Dir genannten Basics "Ach ja, Integer-Darstellung... Ring... Überlauf... schon klar."
if ($a < 114 && $a >= 0) { [OK] } else { [NANA] }
Und jetzt nehmen wir mal an, dass ich nicht mit einem Zahlenbereich von -128 bis +127 arbeite, sondern mit -32768 bis +32767, und nicht eine Zeichenkette ausgebe, sondern ein paar hundert. Dann greift Deine Abfrage nicht mehr.
Ich habe ganz bewusst ein Beispiel gewählt, welches sehr simpel und bestimmt auch geschickter zu lösen ist, als ich es in dem Artikel getan habe. Es ging mir ja eher darum, das Problem als solches zu schildern, und nicht die optimale Lösung für ein Problem vorzustellen, welches sich so im Programmieralltag nicht einmal stellen wird.
Eine ähnlich elegante Lösung wie Deine vorherige, ohne den Min- und Max-Wert mit anzugeben, fällt mir ansonsten ad hoc auch nicht ein.
Und es geht ja auch gar um eine Lösung für das Problem, "wie vermeide ich einen Überlauf?", sondern um die Fragestellung "wie verbohrt kann ein Compiler-Programmierer sein, dass er wegen einer 'Optimierung' ein solches Sicherheitsloch reißt, mit der Begründung 'der Standard gibt mir Recht'?"