Das NX Bit - AMDs Anti-Viren Feature im Detail

i_hasser

Grand Admiral Special
Mitglied seit
06.06.2002
Beiträge
18.964
Renomée
85
Standort
IO 0x60
header.png

Nachdem es jetzt im Forum schon mehrere technische Diskussionen um das NX Bit gab und ich in der letzten davon auf einen Artikel angesprochen wurde hab ich endlich mal zu den Tasten, ein ein bisschen Zeit und einer(vielen) Teekanne(n) gegriffen und hier ist er - der NX Artikel.

Das NX Bit - technischer Hintergrund​

Momenten kreisen die Marketingabteilungen von AMD, Intel und Microsoft wie die Aasgeier um das NX-Bit (Transmeta kommt demnächst sicher auch dazu).
Alle sagen das NX-Bit mache den Computer sicherer und würde vor Viren schützen, also höchste Zeit sich das mal etwas genauer anzusehen.

Genauer werden wir uns auf das Wie und das Wieso stürzen - also wieso brauchen wir das NX-Bit und wie genau funktioniert es, und dabei wollen wir nicht nur ein bisschen an der Oberfläche kratzen.
Wichtig sind in dem Zusammenhang auch Sicherheitslücken allgemein, also machen wir da keinen Bogen drumherum sondern fangen gleich damit an.
[BREAK=Sicherheitslücken]
Jedes Betriebssystem und jedes Programm hat mit ziemlich hoher Wahrscheinlichkeit irgendwo eine Sicherheitslücke. Das betrifft nicht nur die aus dem Hause Microsoft, sondern auch andere Betriebssysteme, wie zB. Linux (und natürlich auch die Programme dafür).
Selbst BSD, das wohl sicherste Betriebssystem für x86 hat auch damit zu kämpfen.

Allgemein gesagt ist eine Sicherheitslücke nicht anderes, als dass sich ein Teil Programmcode anders verhält als erwartet. Das kann schon dann passieren, wenn der Programmierer nicht alle Eventualitäten vorhergesehen hat, zum Beispiel wenn man einem Browser ein defektes Bild gibt und der Programmierer es vor dem Anzeigen nicht darauf hin überprüfen lässt.
Das häufigste 'Fehlverhalten' ist wohl der Buffer Overflow/Underrun (bzw. Stack Overflow/Unterrun). Das lässt sich auch am leichtesten zum Einschleusen von irgendwelchen Viren benutzen, und deswegen schauen wir uns das auch genauer an.

Viele Daten, die eine Teilroutine von einem Programm gerade braucht, liegen auf dem Stack. Der Stack wurde genau für diese Aufgabe konzipiert - unter anderem lagern hier auch die Rücksprungadressen von den Teil- bzw. Coderoutinen zum Hauptprogramm (die Rücksprungadresse gibt eine eindeutige Position im Speicher an, in dem Fall von wo aus die Coderoutine aufgerufen wurde).
Wird also eine Coderoutine gestartet, wird die Adresse des Aufrufbefehls (auch Rückkehradresse genannt) auf dem Stack abgelegt. Die Coderoutine wird ausgeführt und zum Schluss wird die Rückkehradresse wieder vom Stack gelesen und die Ausführung geht nach dem Aufrufbefehl weiter.

pic1.png


Die ganze Sache hat leider nur einen kleinen Haken - bringt irgendetwas den Stack durcheinander, wird am Ende der Coderoutine die falsche Rücksprungadresse benutzt. Der Stack bietet zwar viel Funktionalität, aber der Aufbau ist recht einfach gehalten - wie bei einem Stapel werden einfach Werte draufgelegt und wieder heruntergenommen. Nimmt die Coderoutine genausoviele Werte herunter wie sie draufgelegt hat ist alles in Ordnung - nimmt sie aber mehr oder weniger Werte herunter wird zum Schluss die falsche Rücksprungadresse benutzt.

Normalerweise geht's dann ab ins Nirvana - die benutzte Rücksprungadresse ergibt selten auch nur irgend einen Sinn und das Betriebssystem schießt das ganze Programm ab (unter Windows gibts dann normalerweise irgendeine Fehlermeldung dazu, in der Art wie 'Programm xyz hat einen Fehler in abc verursacht und wurde geschlossen') .
Schwerwiegend wird die ganze Sache, wenn wir es schaffen die Rücksprungadresse gezielt zu manipulieren. Wissen wir nun auch noch wo Daten liegen, die wir dem Programm z.B. vorher geschickt haben (um beim Browser-Beispiel zu bleiben - wissen wir, wo das Bild im Speicher liegt), können wir dem Programm statt der Bilddaten bösartigen Programmcode übermitteln - getarnt als Bild.
Dann manipulieren wir die Rücksprungadresse so, dass wir zu dem Programmcode im Bild zurückkehren und fertig ist eine Sicherheitslücke ala Blaster - nun wird unser eingeschleuster Programmcode ausgeführt und im schlimmsten Fall können wir mit dem System machen was wir wollen (besonders krittisch wenn das Betriebssystem selbst eine Sicherheitslücke hat).

pic11.png


So könnte das dann z.B. aussehen. Statt zum eigentlichen Hauptprogramm kehrt die Coderoutine fälschlicherweise zu unserem eingeschleusten Code zurück.

Der Programmierer hätte aber vorhersehen können, dass es sich bei den Daten vom Bild nie und nimmer um Code handeln kann - und hier setzt NX an.
Derartige Sicherheitslücken sind vor allem x86 speziefisch, und deswegen müssen wir uns vorher erstmal die Speicherverwaltung von x86 anschauen.
[BREAK=Speicherverwaltung]
Die x86 Architektur kann 4 GB physikalischen Speicher adressieren. In der CPU wird dabei jedes Byte einzeln adressiert (also jedes Byte physikalischen Speichers bekommt eine eindeutige Zahl zugewiesen), und das nennt sich dann physikalische Adresse. Damit wir auf 4 GB Speicher kommen (ich vermeide hier bewusst das Wort RAM, weil im physikalischen Speicher auch noch andere Sachen liegen - z.B. der PCI Adressraum, oder verschiedene BIOSse) brauchen wir ca. 4 Milliarden verschiedene Adressen (genau genommen 4'294'967'296 - 1 kB = 1024 Byte usw.). Dank Rainer Zufall lassen sich mit 32 Bits genau diese 4'294'967'296 verschiedenen Möglichkeiten bilden. 32 Bit sind 4 Byte, also ist eine physikalische Adresse bei x86 immer 4 Byte lang.
Der Einfachheit zählt man einfach durch - 0 bis 4'294'967'295, macht genau 4'294'967'296 verschiedene Adressen (0 ist ja die 1. Adresse, bei praktisch allen Sachen um/in der CPU fängt man mit der 0 an zu zählen). Der Verständlichkeit halber nennt man das jetzt Adressraum (und da es der Adressraum der physikalischen Adresse ist nennen wir es den physikalischen Adressraum).

pic2.png


Die ganzen Programme arbeiten aber nicht mit der physikalischen Adresse. Das liegt einerseits daran, dass wie schon erwähnt im physikalischen Adressraum auch diverse BIOSse (mindestens das System BIOS und das VGA BIOS) und andere Sachen liegen, und wenn der Rechner keine 4 GB RAM besitzt sind die oberen Adressen sowieso ungültig.
Außerdem müsste jedes Programm selbst drauf achten wo es im RAM liegt und aufpassen, dass es anderen Programmen nicht ausversehen in den Speicher schreibt. Der letzte Punkt ist besonders wichtig, sonst könnte irgend ein Programm einfach mal das Betriebssystem überschreiben.

Aus dem Grund gibt es 2 Mechanismen, die die Adressen mit denen Programme arbeiten umrechnen.
Das Programm selbst arbeitet mit Segment/Offset Adressen. Die Segmentierung errechnet daraus eine lineare Adresse, und das Paging errechnet aus der linearen Adresse die physikalische Adresse.

pic3.png


Beide Prozesse kann das Betriebssystem steuern, und im Endeffekt so jede Segment/Offset Adresse auf eine beliebige physikalische Adresse umleiten.
[BREAK=Segmentierung]
Die Segmentierung stellt den 1. Teil der Adressumwandlung dar. Die Segment/Offset Adressen, mit denen Programme arbeiten, setzen sich aus einer 16 Bit Segmentnummer (65536 Möglichkeiten) und einem 32 Bit Offset (4 GB) zusammen. Von der Sache her stellt jedes Segment einen eigenen Adressraum dar, in dem wir über das Offset einzelne Bytes adressieren können.
Die Informationen zu den Segmenten werden in 2 Tabellen gespeichert, der GDT (Global Descriptor Table) und der LDT (Local Descriptor Table).
Da nur jede 8. Segmentnummer gültig ist (genauer gesagt gibt die Segmentnummer nur die Position des Segmenteintrages in der jeweiligen Tabelle wieder - da ein Eintrag 8 Byte lang ist...) können wir nur 8192 Segmente benutzen, da die Offsets 32 Bit lang sind macht das maximal 4 GB pro Segment und durch die 2 Tabellen kommen wir damit auf insgesammt 8192*2*4 GB=64TB theoretisch adressierbaren Speicher.
Deswegen werden x86 CPUs auch immer mit 64TB virtuellem Adressraum angegeben.

Schauen wir uns mal so einen Segmenteintrag (auch Segmentdeskriptor genannt) an:

pic4.png


Die Darstellung muss von rechts nach links gelesen werden, und von unten nach oben. Also in der unteren Zeile rechts ist das 1. Bit, unten links das 32., oben rechts das 33. und oben links das 64. Bit (8 Byte = 64 Bit).
Es kommt aber auch weniger darauf an wie die Sachen im Segmentdeskriptor drinnen stehen als was drinnen steht ;).

Wichtigster Eintrag ist Segment Base. Und weil dem so ist, hat Intel den gleich in 3 Teile zerlegt... (der Sinn der Sache ist bis heute niemandem klar :P).
Segment Base ist 32bit lang und gibt an wo das Offset 0 vom Segment im linearen Adressraum liegt. Da sind wir gleich wieder beim virtuellen Adressraum - da die lineare Adresse auch 32bit breit ist, und die Segment/Offset Adressen (mit denen wir theoretisch 64TB adressieren könnten) in eine lineare Adresse umgewandelt werden nutzen uns die 64TB eigentlich garnix, weil sich alle Segmente irgendwo innerhalb des linearen Adressraums befinden.

Aus den folgenden Offsets wird die lineare Adresse gebildet, indem der Wert des Offsets einfach auf Segment Base draufaddiert wird.
Die lineare Adresse vom Offset 1 wäre also (Segment Base)+1, für Offset 2 (Segment Base)+2 usw.

Der nächste Eintrag ist Segment Limit. Der Eintrag ist 20 Bit lang (diesmal nur 2 Teile :P) und gibt die Länge vom Segment in (für gewöhnlich) 4kB Schritten an (man kann die Segmentlänge auch auf das Byte genau angeben, da Segment Limit aber nur 20 Bit breit ist, kann ein solches Segment nicht größer 1 MB sein).
Mit den 20 Bit lassen sich Zahlen von 0 bis 1'048'575 darstellen, 1'048'575*4kB macht fast 4 GB - der Eintrag heißt nicht ohne Grund Limit und nicht Length, der gibt die größte Adresse an und damit kann ein Segment maximal ganz genau 4 GB lang sein (wir fangen ja bei 0 an zu zählen, wäre Limit=0 wäre das Segment 4kB groß).

Die Segmente liegen also verteilt im linearen Adressraum. Dabei können sich Segmente auch durchaus überlappen, dann ändert eben ein Zugriff auf ein Offset des eine Segments auch ein Offset im anderen Segment.

pic5.png


Es ist auch kein Problem ein Segment zu erstellen, das den gesamten linearen Adressraum umfasst (in der Abbildung zB. Segment 4328).

Die weiteren Eigenschaften im Segmentdeskriptor regeln vor allem den Zugriff.
A steht für Accessed und wird von der CPU immer dann auf 1 gesetzt (ist ja nur ein Bit, also 0 oder 1) wenn auf das Segment zugegriffen wird.
Das Betriebssystem könnte so zB. mitzählen wie oft ein Segment genutzt wird und wenn der RAM knapp wird eben das geringst genutzte Segment in die Auslagerungsdatei verschieben.

Das nächste ist TYPE, hauptsächlich wird hier festgelegt ob Daten in das Segment geschrieben und/oder gelesen werden dürfen.
Der drauf folgende Eintrag (DPL) ist wohl eine der wichtigsten Eigenschaften und steht für Descriptor Privilege Level. Der Eintrag ist 2 Bit breit, lässt also 4 Werte (0 bis 3) zu.
Jeder Code liegt ja in irgend einem Segment, da Programme nur die Segment/Offset Adressen nutzen. Das Segment von einem Programm in dem der Code liegt hat nun auch ein bestimmtes DPL - das Programm kann nur auf Segmente zugreifen, die ein DPL größer oder gleich dem des eignenen Segmentes haben.
Normalerweise werden nur die Zustände 0 und 3 benutzt, das Betriebssystem liegt in einem Segment mit DPL 0 und Programme in einem Segment mit DPL 3. So können Programme nicht das Betriebssystem manipulieren.

AVL sagt aus, ob der Segmentdeskriptor überhaupt gültig ist. Man braucht ja nicht immer 8192 Segmente ;).

Die nächsten Einträge haben keine so große Bedeutung, zum TYPE Eintrag kommen wir jetzt nochmal.
Unter anderem legt der Eintrag auch fest ob in einem Segment Daten oder Code liegt. Wichtig ist dabei, dass entweder Daten, oder Code in einem Segment liegen können - beides geht nicht. In ein Codesegment kann keinesfalls geschrieben werden, nur Code auführen geht (unter Umständen kann davon gelesen werden). In einem Datensegment kann dagegen kein Code ausgeführt werden (und unter Umständen auch nicht hineingeschrieben werden).
Ein Programm braucht also immer mindestens 2 Segmente, eines für den Code und eines für die Daten.

Von der Sache her ließe sich damit schon ganz problemlos eine Trennung von Code und Daten vollziehen, so wie es das NX Bit tut. Die Praxis sieht leider anders aus, doch dazu nach dem Paging.
[BREAK=Paging]
Nun haben wir unsere lineare Adresse, bzw. wenn irgendwas beim Zugriff auf die Segment/Offset Adresse nicht gestimmt hat (falsches DPL, Code- statt Datensegment etc.) wurde bereits ein Fehler ausgelöst und das Betriebssystem hätte das Programm wahrscheinlich abgeschossen.
Der Paging Mechanismus ist deutlich einfacher aufgebaut, hier brauchen wir ja keine Zugriffsreglementierung (wie zB. DPL) mehr, weil das schon bei den Segmenten implementiert wurde. Zwischen Daten und Code wird auch nicht mehr unterschieden.

Der lineare Adressraum besteht eigentlich aus 1'048'576 (2 hoch 20) aneinander aufgereihten Pages (zu Deutsch: Seiten).
Jede Page ist genau 4kB groß, damit kommen wir wieder auf 4 GB. Für jede einzelne Page existiert ein Eintrag in der Page Table der angibt wo diese Page im physikalischen Speicher liegt. Die Einträge sehen so aus:

pic6.png


Ein Eintrag ist also 32 Bit oder 4 Byte lang. Bei 1'048'576 Pages macht das genau 4 MB, also der Speicherverbrauch für die Verwaltung der Pages hält sich in Grenzen (und es gibt Wege um auch einen größeren Bereich von Pages zu deaktivieren, also es werden kaum 4 MB für die Page Table verbraucht).
Fangen wir rechts an - P ist wieder genau 1 Bit, ist es auf 1 gesetzt ist die Page gültig (Present), wenn nicht gibt ein Zugriff darauf einen Fehler.
R/W sagt aus, ob von der Page nur gelesen oder auch darauf geschrieben werden darf (wieder nur 1 Bit). U/S steht für User/Superuser und stellt ein recht einfaches Rechtemanagement dar (ähnlich dem DPL, jedoch nur mit 2 Levels).

Die beiden folgenden 0en sind von Intel so vorgegeben und besitzen keine Eigenschaft (oder sie ist nicht dokumentiert). A und D geben darüber Auskunft ob schon von der Page gelesen oder darauf geschrieben wurde und die nächsten beiden Bits sind wieder mit 0 vorgegeben. AVAIL steht auch nur für Available, mit den Bits kann der Programmierer anfangen was er will (die CPU kümmert sich nicht darum welchen Wert die Bits haben).

Wichtig ist die Page Frame Address. In dem Eintrag sind nur die Bits 12 bis 31 (bzw. das 13. bis zum 32. Bit) vorgegeben - die andere Bits sind immer 0.
Die Page Frame Adress gibt an, wo die Page im physikalischen Speicher liegt. Da die Bits 0 bis 11 immer 0 sind liegt jede Page an einer Position, die ein Vielfaches von 4096 oder 4kB darstellt.

Erfolgt jetzt also ein Zugriff auf irgend eine lineare Adresse wird nachgesehen ob auf die entsprechende Page (lineare Adresse / 4096 = Pagenummer, Nachkommastellen lassen wir weg) zugegriffen werden darf und ob die Page überhaupt vorhanden ist (P).
Ist dem so wird berechnet wie viele Bytes vom Pageanfang aus der Zugriff stattgefunden hat (zwischen 0 und 4095 Bytes) - der Wert wird dann auf die PAGE FRAME ADRESS draufaddiert und wir haben endlich unsere physikalische Adresse.
Nun findet erst der eigentliche Zugriff statt. Das ganze nochmal als Graphik:

pic7.png


Es ist auch problemlos möglich 2 Pages auf den selben physikalischen Speicher zeigen zu lassen, und ab und zu gibt es dafür sogar eine Anwendungsmöglichkeit.
Von der Sache her ist das Paging der Segmentierung recht ähnlich, nur haben eben alle 'Segmente' (Pages) eine einheitliche Größe und weniger Eigenschaften.

Man kann Paging in der CPU sogar abschalten, aber jedes halbwegs moderne Betriebssystem macht davon gebrauch (eigentlich alles außer DOS und CP/M - mit EMM386 auch DOS).
Intel hat damals aus historischen Gründen Paging und Segmentierung implementiert, von der Sache her hätte man auch auf eines verzichten können (dann hätte man nur das jeweils andere um einige wenige Funktionen erweitern müssen, wobei die Segmentierung so wie sie ist eigentlich schon komplett ausreicht, wesswegen man Paging auch abschalten kann).
[BREAK=Die Praxis]
Tja, eigentlich bietet x86 völlig ausreichende Funktionen um Daten und Code voneinander zu trennen (hier übertrifft die Vielfalt sogar einige andere Architekturen).

Die Sache hat nur einen Haken. Keine andere auch halbwegs verbreitete Architektur benutzt Segmente. Ergo kann auch kein einziger Compiler wirklich was damit anfangen, wesswegen es auch kein Programmierer praktisch benutzen kann.
Paging kann man in der CPU abschalten, bei der Segmentierung geht das aber nicht - die ist immer aktiv und jedes Programm muss immer Segment/Offset Adressen benutzen.

Also 'umgeht' man die Segmentierung einfach.
Man definiert nur 2 Segmente - ein Datensegment und ein Codesegment. Die gibt man einem Programm von Anfang an mit, bzw. ändern sich diese Segment nicht und werden von jedem Programm benutzt.
Beide Segmente fangen im linearen Speicher an der Position 0 an und enden bei 4 GB. Das Offset der Segmente entspricht damit praktsich der linearen Adresse, und da wir ein Datensegment und ein Codesegment haben können wir sowohl Daten lesen/schreiben, als auch Code ausführen.
Und da die Segmente genau aufeinander liegen verändert ein Zugriff auf das Datensegment auch das Codesegment an genau dem selben Offset (da jetzt alle Programme die selben Segmente benutzen müssten die sich eigentlich untereinander sehen - jedes Programm wird aber mit einer anderen Page Table ausgeführt, so dass sich die Programme doch nicht ins Gehege kommen).

pic8.png


Und da sind wir wieder bei unserer Trennung von Daten und Code. Wenn ein Hacker weiß an welchem Offset im Datensegment ein Programm seine Daten ablegt weiß er auch, dass diese 'Daten' an genau der selben Stelle im Codesegment liegen. Und wenn er statt Daten Code übermittelt hat kann er diesen über das Codesegment und das Offset an dem die 'Daten' im Datensegment liegen ausführen - fertig ist der Virus.

Selbst wenn man unbedingt wollte, Code- und Datentrennung über die Segmente kann man auf Programmebene nicht mehr implementieren.
Das Betriebssystem teilt den Programmen jeweils die Segmente zu, und wenn das nur die beiden 4 GB Segmente übergibt muss man sich eben damit abfinden (und in der Realität werden praktisch nur diese beiden Segmente übergeben).
[BREAK=Lösungsmöglichkeiten]
Unser eigentliches Problem besteht ja darin, dass Code- und Datensegment aufeinanderliegen. Dadurch kann ein Hacker immer davon ausgehen, dass bestimmte 'Daten' an genau dem selben Offset im Codesegment liegen wie im Datensegment. Und entsprechend kann er die 'Daten' dann übers Codesegment ausführen.

Die einfachste Möglichkeit wäre also das zu unterbinden.

Trennung von Code- und Datensegment​

Realisieren könnte man das zB. dadurch, dass man die beiden Segmente leicht gegeneinander verschiebt. Hier reichen schon Werte von einigen kB im linearen Adressraum aus.

pic9.png


Hier haben wir das Codesegment einfach mal 0.1GB nach hinten geschoben. Wenn man die beiden Segmente immer um einen etwas anderen Wert verschiebt kann ein Hacker praktisch nicht erraten, wo sein 'Daten' nun im Codesegment liegen.

Der Nachteil der Sache ist aber, dass einige Programme streng davon ausgehen, dass Daten- und Codesegment aufeinander liegen. Im schlimmsten Fall müssten also einige Programme umgeschrieben werden, oder man müsste die Funktion abschaltbar machen. Der nächste (kleinere) Nachteil ist, dass das Betriebssystem ein bisschen mehr rechnen muss. Das würde sich aber in Grenzen halten.
Und es gibt noch einen letzten Nachteil: Mit ein bisschen Glück könnte ein Hacker herausbekommen um wieviel die Segmente gegeneinander verschoben sind.

Aus welchen Gründen auch immer - so ist man dem Problem nicht zuleibe gerückt, so hätte man es aber bis AMD64 durchaus machen können und einige Ansätze dazu existieren auch schon.

Denkbar wäre auch sowas gewesen:

pic10.png


Damit hätte man zwar nur 2GB für Daten und Code, aber selbst das braucht man auch heute praktisch nie (da man sowieso viel mehr Daten als Code braucht hätte man auch 1GB Code und 3GB Daten realisieren können).

Im IA32 Kompatiblitätsmodus hat AMD dem K8 aber noch eine wichtige Eigenschaft mitgegeben...
[BREAK=Das NX Bit]
Das Bit befindet sich im Page Table Eintrag der Pages, allerdings versteckt es sich ein bisschen.
In dem Eintrag ist nämlich kein Platz mehr für das NX Bit. Die als verfügbar markierten Bits kann man nicht mehr so einfach benutzen, weil die CPU dann nicht mehr zum 386 kompatibel wäre.

Die Lösung bringt der PAE (Physical Address Extension) Modus. Eigentlich war der vorgesehen um mit bis zu 64 GB Speicher umgehen zu können (der lineare Adressraum bleibt aber 4 GB groß, wesswegen Programme ohne Handstand trotzdem nur 4 GB Speicher nutzen können).
Als kleiner Nebeneffekt werden die Page Table Entries effektiv größer und da war dann auch noch Platz für das NX Bit. Für NX muss die CPU also stets im PAE Modus arbeiten.

Ist das NX Bit nun nicht gesetzt (also =0) verhält sich die Page ganz normal, wie wir es von Pages gewohnt sind. Ist das Bit dagegen gesetzt weigert sich die CPU in dieser Page (bzw. bei allen Segment/Offset Adressen die auf diese Page zeigen) Programmcode auszuführen - das wars auch schon.

Die Kontrolle über das Bit (wie auch überhaupt über die Page Table, und auch die Global und Local Descriptor Table) hat einzig und allein das Betriebssystem.
Wird ein Programm in den Speicher geladen setzt das Betriebssystem für alle Pages die Code enthalten das NX Bit auf 0, für alle anderen Pages auf 1. Was Code und was Daten sind steht in den ausführbaren Dateien (unter Windows .EXE, bedingt auch DLLs, unter Linux a.out, ELF usw.) - hier wird sowieso streng zwischen Daten und Code unterschieden (bisher ging die Trennung aber verloren wenn das Programm in den Speicher geladen wurde).

Um mal wieder auf den Hacker zurückzukommen - der weiß zwar jetzt wo das Programm die vorher übermittelten 'Daten' gespeichert hat, und er weiß auch wo diese im Codesegment liegen - aber wenn er versucht den enthaltenen Code auszuführen würgt die CPU ab. Die Daten liegen nämlich in einer Page bei der das NX Bit gesetzt ist (als sich das Programm diesen Speicher reserviert hat war ja klar, dass da nur Daten liegen), ergo darf hier kein Code ausgeführt werden.
Das Betriebssystem bekommt dann die Benachrichtigung und schießt das Programm normalerweise mit irgend einer passenden Meldung ab.

Die Sache hat natürlich auch ihre Grenzen. Wenn der Hacker weis, dass im Programmcode an der und der Stelle der und der Code steht kann er versuchen den Code dort auszuführen (da der ja eigentlich zum Programm gehört ist das NX Bit hier nicht gesetzt) und damit entweder für den eigentlichen Virus-Code das NX Bit deaktiveren oder auf andere Art und Weise Schaden anrichten.

Aber der Rechner könnte genausogut von einem Blitz getroffen werden, also es wird wirklich extrem schwierig und da müssen schon so einige Zufälle zusammenspielen.
So ganz unmöglich ist es aber eben trotzdem nicht...


Zum Schluss...​


So, das solls auch schon gewesen sein.
Intel kann man eigentlich nicht vorwerfen in der Hinsicht bei IA32 geschlampt zu haben (das haben sie schon an anderer Stelle). 1985 (als der 386er und damit IA32 eingeführt wurde) konnte man kaum vorhersehen wie sich x86 entwickeln würde (die Grundlagen zu IA32 wurden auch schon mit dem 286er 1982 eingeführt), damals war der PC (und eben x86) noch ein ziemlicher Außenseiter.
IBM und Microsoft (die mit OS/2 und Windows die Segmentierung erstmals ausgehebelt haben) kann man vielleicht die größten Vorwürfe machen. Andererseits: Programme, die wirklich aktiv Segmentierung benutzen, sind ohnehin nicht portabel, können also nicht so einfach auf andere Architekturen portiert werden (weil es da ja keine Segmente gibt).
IBM hat schon seit je her auch andere Architekturen im Angebot, und Windows NT gibt es auch für MIPS, PPC und den Alpha (und auf Windows NT basieren auch alle aktuellen Windows-Versionen). Linux gibt's sowieso für alles was rechnen kann. Hätte man die Segmentierung so benutzt wie sie gedacht war, wären die Portierungen nicht so ohne weiteres möglich gewesen.

PS: Übrigens hat AMD, da die Segmentierung praktisch sowieso nicht mehr benutzt wird (bzw. eigentlich überhaupt nie richtig benutzt wurde) AMD64 nur Paging mitgegeben - keine Segmente mehr. Im IA32 Kompatiblitätsmodus haben wir sie aber noch (sonst wäre die Sache ja nicht zum 386 kompatibel).

 
Zurück
Oben Unten