Wie bestimmt der ELF-Loader die anfängliche Stapelgröße?

9

Ich studiere die ELF-Spezifikation ( http://www.skyfree.org/linux/references/ELF_Format.pdf ) und ein Punkt, der mir über den Programmladeprozess nicht klar ist, ist, wie und was der Stapel initialisiert wird Die anfängliche Seitengröße ist. Hier ist der Test (unter Ubuntu x86-64):

$ cat test.s
.text
  .global _start
_start:
  mov $0x3c,%eax
  mov $0,%edi
  syscall
$ as test.s -o test.o && ld test.o
$ gdb a.out -q
Reading symbols from a.out...(no debugging symbols found)...done.
(gdb) b _start
Breakpoint 1 at 0x400078
(gdb) run
Starting program: ~/a.out 

Breakpoint 1, 0x0000000000400078 in _start ()
(gdb) print $sp
$1 = (void *) 0x7fffffffdf00
(gdb) info proc map
process 20062
Mapped address spaces:

          Start Addr           End Addr       Size     Offset objfile
            0x400000           0x401000     0x1000        0x0 ~/a.out
      0x7ffff7ffa000     0x7ffff7ffd000     0x3000        0x0 [vvar]
      0x7ffff7ffd000     0x7ffff7fff000     0x2000        0x0 [vdso]
      0x7ffffffde000     0x7ffffffff000    0x21000        0x0 [stack]
  0xffffffffff600000 0xffffffffff601000     0x1000        0x0 [vsyscall]

Die ELF-Spezifikation hat sehr wenig darüber zu sagen, wie oder warum diese Stapelseite überhaupt existiert, aber ich kann Referenzen finden, die besagen, dass der Stapel mit SP initialisiert werden sollte, das auf argc zeigt, mit argv, envp und dem Hilfsvektor direkt darüber das, und ich habe dies bestätigt. Aber wie viel Platz ist unter SP verfügbar? Auf meinem System sind 0x1FF00Bytes unterhalb von SP zugeordnet, aber vermutlich zählt dies von der Oberseite des Stapels bei herunter 0x7ffffffff000, und es gibt 0x21000Bytes in der vollständigen Zuordnung. Was beeinflusst diese Zahl?

Mir ist bewusst, dass die Seite direkt unter dem Stapel eine "Schutzseite" ist, die automatisch beschreibbar wird und "den Stapel verkleinert", wenn ich darauf schreibe (vermutlich, damit die naive Stapelbehandlung "nur funktioniert"), aber wenn ich eine zuweise Ein riesiger Stapelrahmen, dann könnte ich die Schutzseite und den Segfault überschreiten. Daher möchte ich feststellen, wie viel Speicherplatz mir bereits zu Beginn des Prozesses ordnungsgemäß zugewiesen wurde.

EDIT : Einige weitere Daten machen mich noch unsicherer, was los ist. Der Test ist der folgende:

.text
  .global _start
_start:
  subq $0x7fe000,%rsp
  movq $1,(%rsp)
  mov $0x3c,%eax
  mov $0,%edi
  syscall

Ich habe hier mit verschiedenen Werten der Konstante gespielt 0x7fe000, um zu sehen, was passiert, und für diesen Wert ist es nicht deterministisch, ob ich einen Segfault bekomme oder nicht. Laut GDB wird die subqAnweisung allein die Größe der mmap erweitern, was für mich mysteriös ist (woher weiß Linux, was sich in meinem Register befindet?), Aber dieses Programm stürzt GDB normalerweise beim Beenden aus irgendeinem Grund ab. Es kann nicht ASLR sein, das den Nichtdeterminismus verursacht, weil ich keinen GOT- oder PLT-Abschnitt verwende. Die ausführbare Datei wird jedes Mal an denselben Stellen im virtuellen Speicher geladen. Ist dies also eine Zufälligkeit der PID oder des physischen Gedächtnisses? Alles in allem bin ich sehr verwirrt darüber, wie viel Stapel tatsächlich legal für den wahlfreien Zugriff verfügbar ist und wie viel beim Ändern des RSP oder beim Schreiben in Bereiche "nur außerhalb des Bereichs" angefordert wird.

Mario Carneiro
quelle
siehe gist.githubusercontent.com/slmingol/... und diese für mehr Ideen auf Werkzeuge unix.stackexchange.com/questions/418354/...
Rui F Ribeiro
@ Rui Danke für die Referenzen. Ich sollte klarstellen, dass ich beim Verständnis des ELF-Standards eigentlich ziemlich weit bin. Ich versuche, eine formale Spezifikation des Prozessladeverhaltens angesichts der Dateieingabe zu erstellen, und ich habe das meistens heruntergekommen. Werkzeuge wie readelfund objdumpwaren zu diesem Zweck sehr nützlich. Dies ist nur ein letztes Stück unterbestimmtes Verhalten, das ich hoffentlich regeln kann. (Anders ausgedrückt, mein ultimatives Ziel ist nicht "Ich frage mich, was unter der Haube vor sich geht", sondern "Ich möchte eine genaue Beschreibung, welche Bytes bei der folgenden Eingabe wohin gehen")
Mario Carneiro
Ich nehme an, Sie sind mit Assembly / M / C ziemlich vertraut und wissen, was ein SP ist und wofür der Stapel (Bereich) ist.
Rui F Ribeiro
Ich habe bereits eine formale Spezifikation der x86-Semantik, aber ich brauche einige Linux-Sachen für IO. Ich weiß, wofür ein Stack ist und wie er normalerweise verwendet wird, aber ich versuche festzustellen, wie der Linux-Kernel den anfänglichen Maschinenstatus bei einer ELF-Datei definiert.
Mario Carneiro
Ich weiß, dass dies für FreeBSD ist, aber eines der besten Bücher darüber. Es könnte Ihnen weitere Hinweise zu diesem Thema geben. amazon.com/Design-Implementation-FreeBSD-Operating-System/dp/…
Rui F Ribeiro

Antworten:

4

Ich glaube nicht, dass diese Frage wirklich mit ELF zu tun hat. Soweit ich weiß, definiert ELF eine Möglichkeit, ein Programmabbild in Dateien " flach zu packen " und es dann für die erste Ausführung wieder zusammenzusetzen. Die Definition des Stacks und seiner Implementierung liegt irgendwo zwischen CPU-spezifisch und OS-spezifisch, wenn das OS-Verhalten nicht auf POSIX erhöht wurde. Zweifellos stellt die ELF-Spezifikation einige Anforderungen an die Anforderungen an den Stapel.

Minimale Stapelzuordnung

Aus Ihrer Frage:

Mir ist bewusst, dass die Seite direkt unter dem Stapel eine "Schutzseite" ist, die automatisch beschreibbar wird und "den Stapel verkleinert", wenn ich darauf schreibe (vermutlich, damit die naive Stapelbehandlung "nur funktioniert"), aber wenn ich eine zuweise Ein riesiger Stapelrahmen, dann könnte ich die Schutzseite und den Segfault überschreiten. Daher möchte ich feststellen, wie viel Speicherplatz mir bereits zu Beginn des Prozesses ordnungsgemäß zugewiesen wurde.

Ich kämpfe darum, eine maßgebliche Referenz dafür zu finden. Ich habe jedoch eine ausreichende Anzahl nicht autorisierender Referenzen gefunden, um darauf hinzuweisen, dass dies falsch ist.

Nach dem, was ich gelesen habe, wird die Schutzseite verwendet, um Zugriff außerhalb der maximalen Stapelzuweisung zu erhalten, und nicht für "normales" Stapelwachstum. Die eigentliche Speicherzuordnung (Zuordnung von Seiten zu Speicheradressen) erfolgt nach Bedarf. Dh: Wenn auf nicht zugeordnete Adressen im Speicher zugegriffen wird, die zwischen Stapelbasis und Stapelbasis liegen - maximale Stapelgröße + 1, wird möglicherweise eine Ausnahme von der CPU ausgelöst, aber der Kernel behandelt die Ausnahme durch Zuordnung einer Seite Speicher, ohne einen Segmentierungsfehler zu kaskadieren.

Der Zugriff auf den Stapel innerhalb der maximalen Zuordnung sollte daher keinen Segmentierungsfehler verursachen. Wie du entdeckt hast

Maximale Stapelzuordnung

Die Untersuchung der Dokumentation sollte den Zeilen der Linux-Dokumentation zur Thread-Erstellung und zum Laden von Images folgen ( Fork (2) , Clone (2) , Execve (2) ). Die Dokumentation von execve erwähnt etwas Interessantes:

Begrenzung der Größe von Argumenten und Umgebung

... schnipsen ...

In Kernel 2.6.23 und höher unterstützen die meisten Architekturen eine Größenbeschränkung, die von der Soft- RLIMIT_STACK- Ressourcenbeschränkung abgeleitet ist (siehe getrlimit (2) ).

... schnipsen ...

Dies bestätigt, dass das Limit erfordert, dass die Architektur es unterstützt, und verweist auch, wo es begrenzt ist ( getrlimit (2) ).

RLIMIT_STACK

Dies ist die maximale Größe des Prozessstapels in Byte. Bei Erreichen dieser Grenze wird ein SIGSEGV-Signal erzeugt. Um dieses Signal zu verarbeiten, muss ein Prozess einen alternativen Signalstapel (Sigaltstack (2)) verwenden.

Seit Linux 2.6.23 bestimmt diese Begrenzung auch den Speicherplatz, der für die Befehlszeilenargumente und Umgebungsvariablen des Prozesses verwendet wird. Einzelheiten finden Sie unter execve (2).

Vergrößern des Stapels durch Ändern des RSP-Registers

Ich kenne keinen x86-Assembler. Ich werde Sie jedoch auf die "Stack Fault Exception" aufmerksam machen, die von x86-CPUs ausgelöst werden kann, wenn das SS-Register geändert wird. Bitte korrigieren Sie mich, wenn ich falsch liege , aber ich glaube auf x86-64 SS: SP ist gerade zu "RSP" geworden. Wenn ich das richtig verstehe, kann eine Stapelfehlerausnahme durch dekrementiertes RSP ( subq $0x7fe000,%rsp) ausgelöst werden .

Siehe Seite 222 hier: https://xem.github.io/minix86/manual/intel-x86-and-64-manual-vol3/o_fe12b1e2a880e0ce.html

Philip Couling
quelle
Das alte SPRegister ist gerecht geworden RSPund SSpraktisch verschwunden. Der x86-64 im Long-Modus, der unter 64-Bit-Linux der "normale" Modus ist, verwendet keine Segmentierung mehr. Nur "die FSund GS-Segmente werden in Restform zur Verwendung als zusätzliche Basiszeiger auf Betriebssystemstrukturen beibehalten". Wikipedia . Das Laden der rspmit einer nicht-kanonischen Adresse kann eine Ausnahme verursachen, wobei eine nicht-kanonische Adresse eine Adresse bedeutet, die nicht alle Einsen oder Nullen in (normalerweise) den oberen 16 Bits der virtuellen 64-Bit-Adresse enthält.
Johan Myréen
@ JohanMyréen Danke, das kommt dem, was ich dachte, ziemlich nahe. Das einzige Detail, das ich nicht finden konnte, war, was diese Änderung an der Stapelfehlerausnahme bewirkt hat. Hat der Verlust SPdie Ausnahme vollständig entfernt oder kann sie jetzt durch ausgelöst werden rsp?
Philip Couling
Ich denke, sie nennen es immer noch Stack Fault Exception, da sie die Violation Exception im Handbuch erwähnen. Eines ist sicher: Sie erhalten eine Ausnahme, wenn Sie außerhalb des zulässigen Speicherbereichs auftreten.
Johan Myréen
Ich konnte keine Nebenwirkungen nachweisen, R10 <- RSP, RSP <- 0xbababa, RSP <- R10wenn ich beispielsweise festlegte, wo der schlechte Wert von RSP niemals verwendet wird, bevor er auf einen vernünftigen Wert zurückgesetzt wurde. Dies ist wahrscheinlich kein sehr guter Test, aber es fällt mir schwer zu glauben, dass dies jemals einen eigenen Fehler verursachen würde, ohne dass ein erheblicher Leistungsaufwand in der Hardware entsteht.
Mario Carneiro
@MarioCarneiro Ja, Sie haben Recht, auch das Handbuch sagt es. Es ist kein Fehler, nur die nicht-kanonische Adresse zu speichern. rspSie müssen den Speicher mit der ungültigen Adresse referenzieren, um die Ausnahme auszulösen. Ich weiß nicht, warum sie die nicht-kanonischen Adressen separat erwähnen, weil sie sowieso illegal sind.
Johan Myréen
3

Jeder Prozessspeicherbereich (z. B. Code, statische Daten, Heap, Stapel usw.) hat Grenzen, und ein Speicherzugriff außerhalb eines Bereichs oder ein Schreibzugriff auf einen schreibgeschützten Bereich erzeugt eine CPU-Ausnahme. Der Kernel verwaltet diese Speicherbereiche. Ein Zugriff außerhalb eines Bereichs breitet sich in Form eines Segmentierungsfehlersignals bis zum Benutzerraum aus.

Nicht alle Ausnahmen werden durch Zugriff auf Speicher außerhalb der Regionen generiert. Ein regionaler Zugriff kann auch eine Ausnahme erzeugen. Wenn die Seite beispielsweise nicht dem physischen Speicher zugeordnet ist, behandelt der Seitenfehler-Handler dies transparent für den laufenden Prozess.

Dem Prozesshauptstapelbereich ist anfangs nur eine geringe Anzahl von Seitenrahmen zugeordnet, wächst jedoch automatisch, wenn mehr Daten über den Stapelzeiger auf ihn übertragen werden. Der Ausnahmebehandler überprüft, ob sich der Zugriff noch in dem für den Stapel reservierten Bereich befindet, und weist gegebenenfalls einen neuen Seitenrahmen zu. Dies geschieht automatisch aus Sicht des Codes auf Benutzerebene.

Eine Schutzseite wird direkt nach dem Ende des Stapelbereichs platziert, um einen Überlauf des Stapelbereichs zu erkennen. Kürzlich (im Jahr 2017) haben einige Leute erkannt, dass eine einzelne Schutzseite nicht ausreicht, da ein Programm möglicherweise dazu gebracht werden kann, den Stapelzeiger um einen großen Betrag zu dekrementieren, wodurch der Stapelzeiger möglicherweise auf einen anderen Bereich zeigt, der Schreibvorgänge zulässt. Die "Lösung" für dieses Problem bestand darin, die 4-kB-Schutzseite durch eine 1-MB-Schutzregion zu ersetzen. Siehe diesen LWN-Artikel .

Es sollte beachtet werden, dass diese Sicherheitsanfälligkeit nicht ganz trivial auszunutzen ist. Sie erfordert beispielsweise, dass der Benutzer die Speichermenge steuern kann, die ein Programm über einen Aufruf zuweist alloca. Robuste Programme sollten den übergebenen Parameter überprüfen alloca, insbesondere wenn er aus Benutzereingaben abgeleitet wird.

Johan Myréen
quelle
Angenommen, ich möchte sichergehen, dass ich den Stapelbereich nicht falsch behandle. Wie kann ich wissen, welcher Speicher zur Verfügung steht? Sollte ich einfach davon ausgehen, dass nach RSP 0 Bytes verfügbar sind, und immer alle Bytes zuordnen, die ich berühre?
Mario Carneiro
Die einfache Antwort lautet, dass Sie davon ausgehen können, dass der Stapel groß genug werden kann, wenn Sie keine großen Objekte auf den Stapel legen. Der Stapel ist für kleine Objekte wie skalare lokale Variablen, Rücksprungadressen usw. vorgesehen. Sie können Zeiger auf große Objekte auf dem Stapel speichern, die Objekte selbst sollten jedoch auf dem Heap abgelegt werden. Wenn Ihnen ohnehin der Stapelspeicherplatz ausgeht, können Sie sich nicht auf mmap verlassen, da Sie nicht wissen, ob die Adressen verfügbar sind oder von einer anderen Region belegt werden, und diese tatsächlich von der Schutzregion belegt wird.
Johan Myréen
Ich bekomme das als allgemeinen Rat, aber mein Ziel ist eine formale Spezifikation, und dafür brauche ich tatsächliche Zahlen darüber, wie groß groß ist. (Anders ausgedrückt, ich führe beliebigen x86-Code von einem böswilligen Benutzer aus und möchte sichergehen, dass ich sie vollständig in einer Sandbox gespeichert habe.) Zumindest mit mmap weiß ich, dass der Systemaufruf einen Fehler zurückgibt und der Speicher nicht zugewiesen; Was passiert mit der automatischen Zuweisung der Schutzseite? Muss ich Segfaults fangen, denn das ist wirklich eklig.
Mario Carneiro
Wenn Sie beliebigen Schadcode ausführen, benötigen Sie einen anderen Ansatz für das Sandboxing. Sie sollten sich Sorgen über Systemaufrufe, den Zugriff auf (Geräte-) Dateien usw. machen. Der Prozess externer Ressourcen ist gefährdet. Sie müssen sich keine Sorgen um den Stapel machen, der Angreifer kann auf andere Weise auf den Prozessspeicherplatz zugreifen oder den Wert des Stapelzeigers auf einen beliebigen Wert ändern.
Johan Myréen
Es ist ein bisschen abseits des Themas für die vorliegende Frage, aber ich habe tatsächlich (fast) alle Systemaufrufe gesperrt. Es kann Dateien lesen und mmap verwenden, aber das war es auch schon. Es ist ein guter Punkt, dass Datei-E / A ein Angriffsvektor ist, aber wie kann eine eigenständige ELF-Datei sonst Informationen abrufen? Der Angreifer kann den Stapelzeiger tatsächlich auf alles setzen. Reicht diese Tatsache allein aus, um eine Systeminstabilität zu verursachen? Meine Untersuchungen haben ergeben, dass RSP ein bisschen magisch ist - ich habe angenommen, dass willkürliche Änderungen an Regs in Ordnung sind, aber der Speicherzugriff muss auf einer vorhandenen Seite erfolgen - daher der Q.
Mario Carneiro