Skip to content
Snippets Groups Projects
Commit aca440b4 authored by Peter Gerwinski's avatar Peter Gerwinski
Browse files

Notizen und Beispiele 16.5.2022

parent 4c829569
No related branches found
No related tags found
No related merge requests found
Showing with 571 additions and 0 deletions
Verwaltung von Arbeitsspeicher (RAM), 16.05.2022, 11:38:16
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
8086: 16-Bit-Intel-Prozessor (Mitte der 1980er Jahre):
Speicheradresse: xxxx:xxxx, z.B. 31d6:0100 oder b800:0300
~~~~ ~~~~
Segment Offset <-- jeweils 16 Bit
Adressierbarer Speicher: 1 MiB = 1024 kiB = 2^20 Bytes
Register: nur 16 Bit, nicht 20
Speicheradresse = 16 · Segment + Offset
z.B.: 31d6:0100 = 16 · 31d6 + 0100 = 31d60 + 0100 = 31e60
Praktischer Sinn: Relokation
- 16-Bit-Programme können über 64 kiB (= 65536 Bytes) frei verfügen
- Ansprechen dieser 64 kiB nur über Offset-Adresse
- Das Betriebssystem teilt die Segment-Adresse zu
--> Das Anwendungsprogramm kann ohne Änderung
an jeder beliebigen Stelle im Speicher laufen ("Relokation").
--> Der Prozessor sieht für das Programm bis zu 4 Segmente vor:
Code, Daten, Stack, "Extra" --> zusammen 256 kiB
Sobald dies nicht mehr genügte, nahmen die Programme eigenständig
weitere Segmente hinzu. --> volle 1024 kiB ansprechbar
--> Unterlaufen der Zuweisung durch das Betriebssystem
--> instabil
Hätten sich die Programme an die Zuweisung durch das Betriebssystem gehalten,
wären sie voreinander abgeschottet gewesen.
80286: 16-Bit-Intel-Prozessor mit "Protected Mode" (Ende der 1980er Jahre):
Speicheradresse: xxxx:xxxx
~~~~ ~~~~
Selektor Offset <-- jeweils 16 Bit
Der Selektor ist ein Index für ein Array,
das sich im Prozessor befindet.
Darin enthalten:
- tatsächliche Speicheradresse des Segments (auch über 1 MiB hinaus)
- Größe des Segments
- Zugriffsrechte, z.B.: nur lesbar, ausführbar
--> Die Hardware kann die Programme voreinander abschotten (daher "protected").
Das Betriebssystem nutzt dies nicht, um sich gegen Programme abzuschotten.
Nachteil: Offset nur 16 Bit --> Beschränkung auf 256 kiB
Umgehung: Das Programm selbst verwaltet die Selektoren.
--> "protected" hinfällig
(Es gab verschiedene Standards, um auf mehr als 1024 kiB zuzugreifen:
HMA, XMS, EMS, DPMI)
Zum Gruseln: HMA
- setze das Segment auf 0xffff
- sorge per Selektor dafür, daß damit die ersten 64 kiB
oberhalb von 1024 kiB angesprochen werden können
--> zusätzliche 64 kiB an Speicher nutzbar
80386: 32-Bit-Intel-Prozessor mit "Protected Mode" (1990er Jahre):
Speicheradresse: xxxx:xxxxxxxx
~~~~ ~~~~~~~~
Selektor Offset <-- 16 bzw. 32 Bit
Der Selektor ist ein Index für ein Array,
das sich im Prozessor befindet (wie beim 80286).
Das Offset ist 32 Bit breit und kann daher bis zu 4 GiB ansprechen. :-)
--> Das Betriebssystem weist dem Programm ein Segment bis zu 4 GiB zu
und schottet es gegenüber sich selbst und anderen Programmen ab.
(Bei OS/2 und Unix: von Anfang an; bei MS Windows: ab NT 4.0 bzw. XP)
Im "Protected Mode" möglich: Speicher auf Datenträger auslagern
Die Tabelle vergibt Segmente auch in Speicherbereichen,
die physikalisch gar nicht vorhanden sind.
Sobald das Programm darauf zugreift, entsteht eine Ausnahmesituation (ähnlich Interrupt).
Das Betriebssystem kann darauf reagieren und das Segment physikalischem Speicher zuordnen.
Falls kein freier physikalischer Speicher mehr vorhanden ist, kann das Betriebssystem
ein anderes Segment auf Datenträger schreiben, dieses auf nicht vorhandenen Speicher
umlegen und den dadurch gewonnenen Speicher für das andere Programm nutzen ("Swappen").
Inhalt der Tabelle ("Deskriptoren"):
- Länge des Segments
- Start-Adresse
- Zugriffsrechte
- Nur Kernel? Benutzerprogramme?
- Leserecht
- Schreibrecht
- Ausführungsrecht
--> Schadensbegrenzung bei Abstürzen.
Wenn ein Benutzerprogramm gegen die Rechte verstößt, entsteht eine Ausnahmesituation.
Das Betriebssystem kann darauf reagieren und das Programm beenden.
- Bit: "vorhanden" (s.o.)
--> Das Betriebssystem kann Programme gegeneinander abschotten.
Damit wird Multitasking und Multi-User möglich.
Das o.a. Konzept zur Auslagerung von Speicher hat einen Nachteil.
Beispiel: Ein Programm A belegt 3/4 des Arbeitsspeichers.
Ein weiteres Programm B soll gestartet werden, das 1/3 des Arbeitsspeichers benötigt.
--> Der Kernel kann nun ein Segment auslagern, z.B. das Datensegment von Programm A.
Problem: Das Segment ist u.U. viel größer als der tatsächlich benötigte Zusatzspeicher.
Mit dieser Hardware kann man immer nur ganze Segmente auslagern.
Lösungsideen:
- mehrere kleinere Datensegmente
--> Dies muß man bei der Programmierung berücksichtigen.
- herunterbrechen auf Speicherzellenebene
--> Zusätzliche Hardware, die die Segmente weiter unterteilt: Memory Management Unit (MMU)
AMD-64-Bit-Prozessoren (AMD Opteron, 2003, Intel-kompatibel):
MMU unterteilt den Speicher in Seiten von jeweils 4 kiB.
Das Betriebssystem entscheidet über die Zuordnung zwischen logischen
und physikalischen Speicheradressen - einschließlich Auslagerungsspeicher.
Dieser Mechanismus greift erst _nach_ den Segmenten.
Dies bedeutet für die Interpretation eines Zeigers:
- Ein Zeiger enthält eine Zahl, die Speicheradresse (Offset).
- Das Programm kennt das ihm zugeordnete Segment.
Segment und Offset definieren gemeinsam eine logische Speicheradresse.
- Die MMU ordnet diese logische Speicheradresse einer physikalischen zu,
die auch ausgelagert sein darf.
Dadurch wird möglich:
- Gemeinsamer Speicher mehrerer Programme (Shared Memory).
Dadurch möglich: extrem schnelle Kommunikation zwischen Programmen
- Sicherheitslücken, z.B. Meltdown, Spectre
Die Meltdown-Sicherheitslücke, 16.05.2022, 12:28:15
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Situation: Ein Programm will Speicher auslesen, für den es keine Leseberechtigung hat,
z.B. Speicher des Kernels.
Vorgehensweise: siehe https://de.wikipedia.org/wiki/Meltdown_(Sicherheitsl%C3%BCcke)
Wir arbeiten mit einem Riesen-Array:
#define VIELE 0xffffffffffffffff // größtmögliche 64-Bit-Zahl
typedef uint8_t page[4096]; // Datentyp: Array von 4096 Bytes
page A[VIELE]; // Riesen-Array. Jedes einzelne Element ist 4096 Bytes groß,
// und es sind VIELE Elemente.
Bei Programmstart wird das Riesen-Array nicht sofort im physikalischen Speicher angelegt.
Erst dann, wenn das Programm darauf zugreift, reserviert die MMU tatsächlich physikalischen Speicher.
Schritt 1: Ein "Sender"-Thread greift mit einem "verbotenen" Index auf das Array zu und stürzt ab.
Schritt 2: Ein "Empfänger"-Thread geht das Array durch
und prüft, bei welchem Index der Zugriff ungewöhnlich schnell erfolgt
(weil darauf vorher schon einmal zugegriffen wurde).
"Sender"-Thread:
- Greife "einfach so" auf die Speicherzelle zu.
Dies bewirkt einen Absturz (Prozessor-Ausnahme; Betriebssystem beendet Programm)
- Danach (nach dem Absturz!) multiplizieren wir den gelesenen Wert mit 4096
(entsprechend der Größe einer Speicherseite, 4 kiB)
- Das Ergebnis benutzen wir als Index für ein Array,
das für den Hauptspeicher viel zu groß ist.
- Wäre das Programm nicht bereits abgestürzt, würde nun der Prozessor versuchen,
auf diese Stelle im Array zuzugreifen. Zu diesem Zweck macht er die entsprechende
Speicherseite zugänglich.
"Empfänger"-Thread:
- Warte, bis der Sender-Thread fertig ist.
- Danach greife auf das Riesen-Array zu.
- Dort, wo die Speicher-Seite bereits zugänglich gemacht wurde, ist der Zugriff schneller.
Mit Hilfe einer Zeitmessung kann der Empfänger-Thread daraus schließen,
auf welche Speicher-Seite der Sender-Thread zuzugreifen versucht hat.
Wieso funktioniert dies, wenn doch der Sender-Thread bereits vorher abgestürzt ist?
- Vorauslesen des Prozessors zur Erhöhung der Effizienz ("Instruction Pipeline").
Bis der Prozessor den Absturz erkennt, hat er bereits das Riesen-Array für
den Speicherzugriff vorbereitet.
--> Hardware-Sicherheitsloch
Gegenmaßnahme: KPTI (siehe: https://de.wikipedia.org/wiki/Kernel_page-table_isolation)
- Vorab: Kernel Address Space Layout Randomization (KASLR):
Die Segmente, die das Programm zugeteilt bekommt, sind jedesmal zufällig
und damit nicht vorhersagbar.
- Vorgänger: Kernel Address Isolation to have Side-channels Efficiently Removed (KAISER)
- Kernel Page Table Isolation (KPTI)
Illustration: https://en.wikipedia.org/wiki/Kernel_page-table_isolation#/media/File:Kernel_page-table_isolation.svg
Page Table = Seitentabelle
= Tabelle, die einen Zeiger (auf logischen Speicher)
auf eine physikalische Speicheradresse abbildet
Das Programm arbeitet nur mit Zeigern.
Der Kernel kann auch die Tabelle(n) beeinflussen.
Es ist insbesondere möglich, dieselbe physikalische Speicheradresse
unter verschiedenen logischen Speicheradressen anzusprechen.
KPTI: Verwende verschiedene Seitentabellen im Kernel-Modus und im User-Modus.
Meltdown-Code:
- Sender und Empfänger arbeiten im User-Modus
und wollen die (logische) Speicherzelle Nr. rcx auslesen.
- Da der Kernel eine eigene Seitentabelle verwendet,
ist nicht klar, logische Speiherzelle im Kernel dafür steht
(oder umgekehrt: ..., welche Bedeutung diese Speicherzelle
im Kernel hat).
--> Zeiger im Kernel und im Benutzerprogramm haben völlig unterschiedliche Bedeutungen.
Man weiß gar nicht mehr, welche Speicherzelle man da eigentlich ausliest.
Ein Nachtrag zu SPECTRE/Meltdown: Mit dem Skript unter
https://github.com/speed47/spectre-meltdown-checker/
(oder gleichnamiges Debianpaket aus einer möglichst aktuellen Distribution)
können Sie Ihr Linux-System gegen entsprechende Sicherheitslücken überprüfen.
Ähnliches Konzept:
Ausnutzen von Pufferüberläufen und Maßnahmen dagegen
--> Nächste Woche
Gemeinsamer Speicher (Shared Memory), 16.05.2022, 14:10:56
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
shm-1: Ein Programm kann einen gemeinsamen Speicherbereich wie eine Datei öffnen.
Es gibt einen ("Datei-") Namen und einen Modus (rwxrwxrwx) wie bei Dateien.
Siehe: man shm_open
shm-2: Man muß die "Datei" auch schließen, sonst verliert man die Daten.
shm-3: Die Vorgehensweise mit SHM als "Datei" ist nahezu identisch dazu,
daß ein Programm die Daten in eine Datei schreibt
und ein anderes sie daraus wieder liest.
shm-4: Alternative: Ich kann die "Datei" in einen Zeiger umwandeln
und damit direkt auf den gemeinsamen Speicherbereich zugreifen.
shm-5: Umgekehrt kann man übrigens auch eine echte(!) Datei in einen Zeiger umwandeln
und auf die Datei zugreifen, als wäre sie ein Speicherbereich.
shm-7: Wo liegt der Speicherbereich? --> Das wissen wir nicht.
Jedes Programm, das auf den Speicherbereich zugreift, bekommt einen anderen(!)
Zeiger, obwohl alle diese Zeiger auf denselben physikalischen Speicherbereich zeigen.
shm-8: Nach Gebrauch sollte man den gemeinsamen Speicherbereich wieder freigeben
(analog zum Löschen einer temporären Datei).
Ergänzung: Mit der Funktion mmap() kann man auch eine Pseudo-Datei im /proc-Verzeichnis
öffnen (/proc/kcore?), über die der Kernel den gesamten Speicher als Datei
zur Verfügung stellt. (Diese Datei ist nur für den Administrator freigegeben.)
--> Man kann mit einem Zeiger auf beliebige Speicherzellen zugreifen.
Anwendung: GPIOs auf Einplatinen-Computern ansprechen.
#include <stdio.h>
int main (void)
{
char buffer[20];
gets (buffer);
printf ("Guten Tag, %s!\n", buffer);
return 0;
}
#include <stdio.h>
int main (void)
{
char buffer[20];
gets (buffer);
printf ("Guten Tag, %s!\n", buffer);
return 0;
}
cassini/home/peter/bo/2022ss/bs/20220516> gcc -Wall -O buffer-overflow-01.c -o buffer-overflow-01
buffer-overflow-01.c: In function ‘main’:
buffer-overflow-01.c:6:3: warning: implicit declaration of function ‘gets’; did you mean ‘fgets’? [-Wimplicit-function-declaration]
gets (buffer);
^~~~
fgets
/usr/bin/ld: /tmp/cc8eISnL.o: in function `main':
buffer-overflow-01.c:(.text+0x11): Warnung:the `gets' function is dangerous and should not be used.
cassini/home/peter/bo/2022ss/bs/20220516> ./buffer-overflow-01
Peter
Guten Tag, Peter!
cassini/home/peter/bo/2022ss/bs/20220516> ./buffer-overflow-01
Prof. Dr. rer. nat. Peter Gerwinski
Guten Tag, Prof. Dr. rer. nat. Peter Gerwinski!
cassini/home/peter/bo/2022ss/bs/20220516> ./buffer-overflow-01
Prof. Dr. rer. nat. Dipl.-Phys. Peter Gerwinski
Guten Tag, Prof. Dr. rer. nat. Dipl.-Phys. Peter Gerwinski!
Speicherzugriffsfehler
cassini/home/peter/bo/2022ss/bs/20220516>
#include <stdio.h>
int main (void)
{
char *hello = "Hello!";
printf ("%s\n", hello);
return 0;
}
#include <stdio.h>
int main (void)
{
char *hello = "Hello!";
hello[1] = 'a';
printf ("%s\n", hello);
return 0;
}
.file "hello-02.c"
.text
.section .rodata.str1.1,"aMS",@progbits,1
.LC0:
.string "Hello!"
.text
.globl main
.type main, @function
main:
.LFB11:
.cfi_startproc
subq $8, %rsp
.cfi_def_cfa_offset 16
movb $97, 1+.LC0(%rip)
leaq .LC0(%rip), %rdi
call puts@PLT
movl $0, %eax
addq $8, %rsp
.cfi_def_cfa_offset 8
ret
.cfi_endproc
.LFE11:
.size main, .-main
.ident "GCC: (Debian 8.3.0-6) 8.3.0"
.section .note.GNU-stack,"",@progbits
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
int main (void)
{
int shm = shm_open ("test", O_RDWR, 0666);
write (shm, "Hello, world!", 13);
return 0;
}
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main (void)
{
char buffer[42];
int shm = shm_open ("test", O_RDONLY, 0444);
read (shm, buffer, 42);
printf ("%s\n", buffer);
return 0;
}
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main (void)
{
char buffer[42];
int shm = shm_open ("test", O_RDONLY, 0444);
lseek (shm, 0, SEEK_SET);
read (shm, buffer, 42);
printf ("%s\n", buffer);
return 0;
}
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
int main (void)
{
int shm = shm_open ("test", O_CREAT | O_RDWR, 0666);
write (shm, "Hello, world!", 14);
close (shm);
return 0;
}
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main (void)
{
char buffer[42];
int shm = shm_open ("test", O_RDONLY, 0444);
read (shm, buffer, 42);
close (shm);
printf ("%s\n", buffer);
return 0;
}
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main (void)
{
int file = open ("test.txt", O_CREAT | O_RDWR, 0666);
printf ("file = %d\n", file);
lseek (file, 0, SEEK_SET);
write (file, "Hello, world!", 14);
close (file);
return 0;
}
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main (void)
{
char buffer[42];
int file = open ("test.txt", O_RDONLY, 0444);
lseek (file, 0, SEEK_SET);
read (file, buffer, 42);
close (file);
printf ("%s\n", buffer);
return 0;
}
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main (void)
{
int shm = shm_open ("test", O_CREAT | O_RDWR, 0666);
char *buffer = mmap (NULL, 42, PROT_READ | PROT_WRITE, MAP_SHARED, shm, 0);
strcpy (buffer, "Hello, world!");
munmap (buffer, 42);
close (shm);
return 0;
}
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main (void)
{
int shm = shm_open ("test", O_RDONLY, 0444);
char *buffer = mmap (NULL, 42, PROT_READ, MAP_SHARED, shm, 0);
printf ("%s\n", buffer);
munmap (buffer, 42);
close (shm);
return 0;
}
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main (void)
{
int file = open ("test.txt", O_CREAT | O_RDWR, 0666);
char *buffer = mmap (NULL, 42, PROT_READ | PROT_WRITE, MAP_SHARED, file, 0);
strcpy (buffer, "Hello, world!");
munmap (buffer, 42);
close (file);
return 0;
}
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main (void)
{
int file = open ("test.txt", O_RDONLY, 0444);
char *buffer = mmap (NULL, 42, PROT_READ, MAP_SHARED, file, 0);
printf ("%s\n", buffer);
munmap (buffer, 42);
close (file);
return 0;
}
#include <stdio.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main (void)
{
int file = open ("test.txt", O_CREAT | O_RDWR, 0666);
fprintf (stderr, "0\n");
char *buffer = mmap (NULL, 42, PROT_READ | PROT_WRITE, MAP_SHARED, file, 0);
fprintf (stderr, "1\n");
strcpy (buffer, "Hello, world!");
fprintf (stderr, "2\n");
munmap (buffer, 42);
fprintf (stderr, "3\n");
close (file);
return 0;
}
#include <stdio.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main (void)
{
int file = open ("test.txt", O_CREAT | O_RDWR, 0666);
fprintf (stderr, "0\n");
char *buffer = mmap (NULL, 42, PROT_READ | PROT_WRITE, MAP_SHARED, file, 0);
fprintf (stderr, "1\n");
strcpy (buffer, "Dies ist ein weiterer Test.");
fprintf (stderr, "2\n");
munmap (buffer, 42);
fprintf (stderr, "3\n");
close (file);
return 0;
}
cassini/home/peter/bo/2022ss/bs/20220516> cat shm-7a.c
#include <stdio.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main (void)
{
int shm = shm_open ("test", O_CREAT | O_RDWR, 0666);
char *buffer = mmap (NULL, 42, PROT_READ | PROT_WRITE, MAP_SHARED, shm, 0);
printf ("%016zx\n", buffer);
strcpy (buffer, "Hello, world!");
munmap (buffer, 42);
close (shm);
return 0;
}
cassini/home/peter/bo/2022ss/bs/20220516> gcc -Wall -O shm-7a.c -lrt -o shm-7a
shm-7a.c: In function ‘main’:
shm-7a.c:11:17: warning: format ‘%zx’ expects argument of type ‘size_t’, but argument 2 has type ‘char *’ [-Wformat=]
printf ("%016zx\n", buffer);
~~~~~^ ~~~~~~
%016s
cassini/home/peter/bo/2022ss/bs/20220516> ./shm-7a
00007f40c3ceb000
cassini/home/peter/bo/2022ss/bs/20220516> cat shm-7b.c
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main (void)
{
int shm = shm_open ("test", O_RDONLY, 0444);
char *buffer = mmap (NULL, 42, PROT_READ, MAP_SHARED, shm, 0);
printf ("%016zx\n", buffer);
printf ("%s\n", buffer);
munmap (buffer, 42);
close (shm);
return 0;
}
cassini/home/peter/bo/2022ss/bs/20220516> gcc -Wall -O shm-7b.c -lrt -o shm-7b
shm-7b.c: In function ‘main’:
shm-7b.c:10:17: warning: format ‘%zx’ expects argument of type ‘size_t’, but argument 2 has type ‘char *’ [-Wformat=]
printf ("%016zx\n", buffer);
~~~~~^ ~~~~~~
%016s
cassini/home/peter/bo/2022ss/bs/20220516> ./shm-7b
00007fd7e2c36000
Hello, world!
cassini/home/peter/bo/2022ss/bs/20220516>
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment