Warum verwendet Linux on X86 verschiedene Segmente für Benutzerprozesse und den Kernel?
-
14-10-2019 - |
Frage
Ich weiß also, dass Linux vier Standardsegmente für einen X86 -Prozessor (Kernelcode, Kerneldaten, Benutzercode, Benutzerdaten) verwendet, aber alle haben dieselbe Basis und Grenze (0x00000000 und 0xfffff), was bedeutet Satz linearer Adressen.
Warum sollten Sie auch Benutzer-/Kernel -Segmente haben? Ich verstehe, warum es separate Segmente für Code und Daten geben sollte (nur weil der X86 -Prozessor mit den CS- und DS -Registern umgeht), aber warum nicht ein einzelnes Codesegment und ein einzelnes Datensegment haben? Der Speicherschutz erfolgt durch Paging, und die Segmente des Benutzer- und Kernel -Segmente sind ohnehin auf dieselben linearen Adressen geordnet.
Lösung
Die X86 -Architektur assoziiert mit jedem Segmentdeskriptor einen Typ und eine Privilegie. Der Typ eines Deskriptors ermöglicht es Segmenten, nur Lesen, Lesen/Schreiben, ausführbare Dateien usw. zu erstellen, aber der Hauptgrund für verschiedene Segmente mit derselben Basis und Grenze besteht darin, eine andere Deskriptor -Privilegie (DPL) zu ermöglichen.
Das DPL besteht aus zwei Bits, sodass die Werte 0 bis 3 codiert werden können. Wenn die Privilegienniveau 0 beträgt, dann soll es sein Ring 0, was am privilegiertesten ist. Die Segmentdeskriptoren für den Linux -Kernel sind Ring 0, während die Segmentdeskriptoren für den Benutzerraum Ring 3 sind (am wenigsten privilegiert). Dies gilt für die meisten segmentierten Betriebssysteme. Der Kern des Betriebssystems ist Ring 0 und der Rest Ring 3.
Der Linux -Kernel setzt, wie Sie bereits erwähnt haben, vier Segmente ein:
- __Kernel_cs (Kernel -Code -Segment, Base = 0, Limit = 4GB, Typ = 10, dpl = 0)
- __Kernel_ds (Kernel -Datensegment, Base = 0, Limit = 4GB, Typ = 2, dpl = 0)
- __User_cs (Benutzercode -Segment, Base = 0, Limit = 4GB, Typ = 10, dpl = 3)
- __User_ds (Benutzerdatensegment, Base = 0, Limit = 4GB, Typ = 2, dpl = 3)
Die Basis und die Grenze aller vier sind gleich, aber die Kernelsegmente sind dpl 0, die Benutzersegmente sind dpl 3, die Codesegmente sind ausführbar und lesbar (nicht beschreibbar), und die Datensegmente sind lesbar und beschreibbar (nicht ausführbar). .
Siehe auch:
Andere Tipps
Die X86 -Speichermanagementarchitektur verwendet sowohl Segmentierung als auch Paging. Sehr ungefähr ist ein Segment eine Aufteilung des Adressraums eines Prozesses, der eine eigene Schutzrichtlinie hat. In der X86 -Architektur ist es also möglich, den Speicherbereich zu teilen, den ein Prozess in mehrere zusammenhängende Segmente sieht, und jeweils unterschiedliche Schutzmodi zuzuweisen. Paging ist eine Technik zum Abbau kleiner (normalerweise 4 KB) Regionen des Adressraums eines Prozesses zu Stücken des realen, physischen Gedächtnisses. Paging steuert somit, wie Regionen in einem Segment auf physischen RAM zugeordnet werden.
Alle Prozesse haben zwei Segmente:
Ein Segment (adressiert 0x00000000 bis 0xBffffffff) für Benutzerebene, prozessspezifische Daten wie den Code des Programms, statische Daten, Heap und Stack. Jeder Prozess hat ein eigenes, unabhängiges Benutzersegment.
Ein Segment (Adressen 0xc0000000 bis 0xffffffffffsfff), das Kernelspezifische Daten wie die Kernel-Anweisungen, Daten und einige Stapel enthält, auf denen Kernelcode ausgeführt werden kann, und interessanterweise ist ein Region in diesem Segment direkt dem physischen Speicher zugeordnet, so dass Der Kernel kann direkt auf physische Speicherorte zugreifen, ohne sich um die Adressübersetzung zu kümmern. Das gleiche Kernelsegment wird in jeden Prozess zugeordnet, aber Prozesse können nur bei der Ausführung im geschützten Kernelmodus darauf zugreifen.
Im Benutzer-Modus kann der Prozess also nur auf Adressen von weniger als 0xC0000000 zugreifen. Jeder Zugriff auf eine höhere Adresse als dies führt zu einem Fehler. Wenn jedoch ein Benutzermodusprozess im Kernel ausgeführt wird (zum Beispiel nach dem Aufrufen eines Systems), wird das Schutzbit in der CPU in den Supervisor-Modus geändert (und einige Segmentierungsregister werden geändert), was bedeutet, dass der Prozess ist, dass der Prozess ist Dadurch können Sie über 0xC0000000 auf Adressen zugreifen.
Beziehen sich aus: HIER
In x86 - Linux -Segmentregister werden für die Überprüfung des Pufferüberlaufs verwendet [siehe unten, die einige Zeichen -Arrays in Stack definiert haben]:
static void
printint(int xx, int base, int sgn)
{
char digits[] = "0123456789ABCDEF";
char buf[16];
int i, neg;
uint x;
neg = 0;
if(sgn && xx < 0){
neg = 1;
x = -xx;
} else {
x = xx;
}
i = 0;
do{
buf[i++] = digits[x % base];
}while((x /= base) != 0);
if(neg)
buf[i++] = '-';
while(--i >= 0)
my_putc(buf[i]);
}
Wenn wir nun die Entmischung des Code GCC-generierten Code sehen.
Dump des Assembler -Codes für Funktionsdrucke:
0x00000000004005a6 <+0>: push %rbp
0x00000000004005a7 <+1>: mov %rsp,%rbp
0x00000000004005aa <+4>: sub $0x50,%rsp
0x00000000004005ae <+8>: mov %edi,-0x44(%rbp)
0x00000000004005b1 <+11>: mov %esi,-0x48(%rbp)
0x00000000004005b4 <+14>: mov %edx,-0x4c(%rbp)
0x00000000004005b7 <+17>: mov %fs:0x28,%rax ------> obtaining an 8 byte guard from based on a fixed offset from fs segment register [from the descriptor base in the corresponding gdt entry]
0x00000000004005c0 <+26>: mov %rax,-0x8(%rbp) -----> pushing it as the first local variable on to stack
0x00000000004005c4 <+30>: xor %eax,%eax
0x00000000004005c6 <+32>: movl $0x33323130,-0x20(%rbp)
0x00000000004005cd <+39>: movl $0x37363534,-0x1c(%rbp)
0x00000000004005d4 <+46>: movl $0x42413938,-0x18(%rbp)
0x00000000004005db <+53>: movl $0x46454443,-0x14(%rbp)
...
...
// function end
0x0000000000400686 <+224>: jns 0x40066a <printint+196>
0x0000000000400688 <+226>: mov -0x8(%rbp),%rax -------> verifying if the stack was smashed
0x000000000040068c <+230>: xor %fs:0x28,%rax --> checking the value on stack is matching the original one based on fs
0x0000000000400695 <+239>: je 0x40069c <printint+246>
0x0000000000400697 <+241>: callq 0x400460 <__stack_chk_fail@plt>
0x000000000040069c <+246>: leaveq
0x000000000040069d <+247>: retq
Wenn wir nun die stapelbasierten Zeichen -Arrays aus dieser Funktion entfernen, erzeugt GCC diese Wachprüfung nicht.
Ich habe dasselbe von GCC selbst für Kernel -Module gesehen. Grundsätzlich habe ich einen Absturz gesehen, als ich einen Kernelcode botte, und er fuhr mit virtueller Adresse 0x28. Später stellte ich fest, dass ich dachte, ich hätte den Stapelzeiger korrekt initialisiert und das Programm richtig geladen. Ich habe nicht die richtigen Einträge in GDT, was den FS basierend in eine gültige virtuelle Adresse übersetzen würde.
Im Falle eines Kernel -Codes ignorierte er jedoch einfach den Fehler, anstatt zu etwas wie __stack_chk_fail@plt> zu springen.
Die relevante Compiler -Option, die diese Wache in GCC hinzufügt, ist der Fundprotektor. Ich denke, dies ist standardmäßig aktiviert, um eine Benutzer -App zu erstellen.
Für Kernel können wir dieses GCC -Flag über die Option CONFOR CC_StackProtector aktivieren.
config CC_STACKPROTECTOR 699 bool "Enable -fstack-protector buffer overflow detection (EXPERIMENTAL)" 700 depends on SUPERH32 701 help 702 This option turns on the -fstack-protector GCC feature. This 703 feature puts, at the beginning of functions, a canary value on 704 the stack just before the return address, and validates 705 the value just before actually returning. Stack based buffer 706 overflows (that need to overwrite this return address) now also 707 overwrite the canary, which gets detected and the attack is then 708 neutralized via a kernel panic. 709 710 This feature requires gcc version 4.2 or above.
Die relevante Kerneldatei, in der dieses GS/FS Linux/Arch/x86/include/asm/stackProtector ist.h ist
Der Kernel -Speicher sollte nicht aus Programmen lesbar sein, die im Benutzerbereich ausgeführt werden.
Programmdaten sind häufig nicht ausführbar (DEP, eine Prozessorfunktion, mit der es hilft, einen überflüssigen Puffer und andere böswillige Angriffe auszuführen).
Es geht um Zugangskontrolle - verschiedene Segmente haben unterschiedliche Rechte. Aus diesem Grund gibt Ihnen der Zugriff auf das falsche Segment einen "Segmentierungsfehler".