• Navigation überspringen
  • Zur Navigation
  • Zum Seitenende
Organisationsmenü öffnen Organisationsmenü schließen
Friedrich-Alexander-Universität Lehrstuhl für Informatik 4 (Systemsoftware)
  • FAUZur zentralen FAU Website
  1. Friedrich-Alexander-Universität
  2. Technische Fakultät
  3. Department Informatik
Suche öffnen
  • English
  • Campo
  • StudOn
  • FAUdir
  • Stellenangebote
  • Lageplan
  • Hilfe im Notfall
  1. Friedrich-Alexander-Universität
  2. Technische Fakultät
  3. Department Informatik
Friedrich-Alexander-Universität Lehrstuhl für Informatik 4 (Systemsoftware)
Menu Menu schließen
  • Lehrstuhl
    • Team
    • Aktuelles
    • Kontakt und Anfahrt
    • Leitbild
    • 50-jähriges Jubiläum
    Portal Lehrstuhl
  • Forschung
    • Forschungsbereiche
      • Betriebssysteme
      • Confidential Computing
      • Eingebettete Systemsoftware
      • Verteilte Systeme
    • Projekte
      • AIMBOS
      • BALu
      • BFT2Chain
      • DOSS
      • Mirador
      • NEON
      • PAVE
      • ResPECT
      • Watwa
    • Projektkampagnen
      • maRE
    • Seminar
      • Systemsoftware
    Portal Forschung
  • Publikationen
  • Lehre
    • Sommersemester 2025
      • Applied Software Architecture
      • Ausgewählte Kapitel der Systemsoftware
      • Betriebssystemtechnik
      • Projekt angewandte Systemsoftwaretechnik
      • System-Level Programming
      • Systemnahe Programmierung in C
      • Systemprogrammierung 1
      • Verteilte Systeme
    • Wintersemester 2024/25
      • Betriebssysteme
      • Middleware – Cloud Computing
      • Systemprogrammierung 2
      • Verlässliche Echtzeitsysteme
      • Virtuelle Maschinen
      • Web-basierte Systeme
    Portal Lehre
  • Examensarbeiten
  1. Startseite
  2. Extern

Extern

Bereichsnavigation: Lehre
  • Betriebssystemtechnik
    • Vorlesung
      • Folien
      • Glossar
    • Übung
      • Aufgaben
      • Dokumentation
        • Blog
          • Entwicklungsumgebung
            • Assembler Crashkurs
              • C++ Crashkurs
                • 🔗 Testrechnerverwaltung
                • 🔗 Adora-Belle (Helpdesk)
                • Kontakt
              • Evaluation

              Blog

              Fallstricke in Benutzeranwendungen

              Bernhard Heinloth

              2022-06-26

              Nur weil Benutzeranwendungen in StuBS starten, heißt dies leider noch nicht, dass alles korrekt implementiert wurde.

              Deswegen gibt es hier eine (unvollständige!) Liste an kleinen Beispielanwendungen, welche mehr oder weniger offensichtliche Randfälle prüfen.

              • Ein return in main() darf keinen Fehler werfen:

                int main() {
                  cout << "bye!" << endl;
                  return 0;
                }

                Die init.cc muss sich darum kümmern, zum Beispiel mit einer kurzen Ausgabe, dass die Anwendung sich beendet hat und (derzeit mangels exit) Endlossschleife.

              • Benutzeranwendungen sollen nicht den Kernel auslesen können:

                int main() {
                  cout << "First Kernel bytes: " << hex 
                       <<  *reinterpret_cast<volatile unsigned long *>(0x1000000) << endl;
                  cout << "You shouldn't be here!" << endl;
                  return 1;
                }
              • Ein Page Fault sollte nicht vom Benutzer gefaked werden dürfen:

                int main() {
                  // Cause a real page fault (to set cr2)
                  volatile char tmp[9001] = {};
                  (void) tmp;
                  // Fake Page Fault
                  asm volatile("int $14\n\t");
                  // a GPF would stop this thread
                  cout << "You shouldn't be here!" << endl;
                  return 0;
                }

                Dies sollte einen General Protection Fault (13) auslösen – denn die Page Fault Exception (14) darf nur vom System selbst (MMU) ausgelöst werden. Entsprechend muss IDT::handle für den Page Fault mit DPL_KERNEL (eigentlich Standard) konfiguriert werden

              • Flüchtige (scratch) Register dürfen sich während eines Page Faults nicht ändern:

                int main() {
                  long rax, rcx, rdx, rsi, rdi;
                  register long r8  asm("r8");
                  register long r9  asm("r9");
                  register long r11 asm("r11");
                
                  asm volatile("mov $0xa000f000a, %%rax\n\t"
                               "mov $0xc000f000c, %%rcx\n\t"
                               "mov $0xd000f000d, %%rdx\n\t"
                               "mov $0x100000001, %%rsi\n\t"
                               "mov $0xd0000000d, %%rdi\n\t"
                               "mov $0x8000a0008, %%r8\n\t"
                               "mov $0x9000a0009, %%r9\n\t"
                               "mov $0x0100a0010, %%r10\n\t"
                               "mov $0x1100a0011, %%r11\n\t"
                               // Cause Pagefault
                               "mov -9001(%%rsp), %%rbx\n\t"
                                : "=a"(rax), "=c"(rcx), "=d"(rdx), "=S"(rsi), "=D"(rdi) :: "rbx");
                
                  if (rax != 0xa000f000a) cout << "rax was not preserved!" << endl;
                  if (rcx != 0xc000f000c) cout << "rcx was not preserved!" << endl;
                  if (rdx != 0xd000f000d) cout << "rdx was not preserved!" << endl;
                  if (rsi != 0x100000001) cout << "rsi was not preserved!" << endl;
                  if (rdi != 0xd0000000d) cout << "rdi was not preserved!" << endl;
                  if (r8 != 0x8000a0008) cout << "r8 was not preserved!" << endl;
                  if (r9 != 0x9000a0009) cout << "r9 was not preserved!" << endl;
                  if (r10 != 0x0100a0010) cout << "r10 was not preserved!" << endl;
                  if (r11 != 0x1100a0011) cout << "r11 was not preserved!" << endl;
                
                  return 0;
                }

              Initialisierung

              Unter Umständen ist es notwendig, vor dem Sprung in die eigentliche Anwendung (main) noch weitere Initialisierungen durchzuführen – so muss zum Beispiel bei einem globalen C++ Objekten der (nicht constexpr) Konstruktor ausgeführt werden.

              • Initialisierung zum Programmstart über .preinit_array wird nicht ausgeführt:

                int i = 23;
                
                void init_func_x() {
                  i *= 2;
                }
                
                void init_func_y() {
                  i -= 2;
                }
                
                __attribute__((section(".preinit_array"))) 
                void (*preinit_array[])(void) = { &init_func_y, &init_func_x };
                
                int main() {
                  if (i == 23)
                      cout << "No preinit_array executed!" << endl;   
                  else if (i != 42)
                      cout << "preinit_array executed in wrong order!" << endl;   
                  return 0;
                }
              • Die (veraltete) .init wird nicht ausgeführt:

                int i = 23;
                
                extern "C" void init_func_x() {
                  i *= 2;
                }
                
                extern "C" void init_func_y() {
                  i -= 2;
                }
                
                asm(".section .init\n\t"
                    "call init_func_y\n\t"
                  "call init_func_x\n\t"
                  ".section .text\n\t");
                
                int main() {
                  if (i != 42)
                      cout << "No .init!" << endl;    
                }

                Im Linkerskript wurde vermutlich nicht die Reihenfolge der C-Runtime-Objekte korrekt eingehalten, Details dazu im Blogbeitrag Das Elend mit dem Linker im Abschnitt Binden

              • Initialisierung zum Programmstart über init_array wird nicht ausgeführt:

                int i = 23;
                
                __attribute__((constructor(142))) void init_func_x() {
                  i *= 2;
                }
                
                __attribute__((constructor(123))) void init_func_y() {
                  i -= 2;
                }
                
                int main() {
                  if (i == 23)
                      cout << "No init_array executed!" << endl;  
                  else if (i != 42)
                      cout << "init_array executed in wrong order!" << endl;  
                  return 0;
                }

                Die Initialisierungsreihenfolge muss .preinit_array, .init, .init_array sein.

              Und ebenfalls gibt es eine Deinitialisierung (.fini und .fini_array), welche analog funktionieren muss.

              Stackzugriff

              Der Stack soll dynamisch bei Bedarf wachsen – aber nur bis zu einer maximalen Grenze (z.B. 1 MB).

              • Es ist nicht zwingend immer so, dass der Stack schrittweise Page für Page in Richtung kleinerer Adressen wächst – es kann auch vorkommen, dass der erste Zugriff gleich einige Seiten überspringt:

                int main() {
                  char buf[31337];
                  buf[0] = 0x23;
                
                  cout << "&buf = " << reinterpret_cast<void*>(&buf) << endl;
                  return 0;
                }
              • Schreiben jenseits der maximalen Stackgrenze (Beispiel: 64MB unterhalb dem Stack) sollte fehlschlagen:

                int main() {
                  char buf[0x4000000];
                  buf[0] = 0x23;
                
                  cout << "&buf = " << reinterpret_cast<void*>(&buf) << endl;
                  cout << "You shouldn't be here!" << endl;
                  return 1;
                }

              Abhängigkeiten durch den Compiler

              Der Compiler kann (relativ unerwartet) Funktionsaufrufe einbauen, Details dazu im Blogbeitrag Das Elend mit dem Linker im Abschnitt Die libgcc und weitere Abhängigkeiten.

              • Der Compiler wirft unter Umständen ein undefined reference to 'memcpy':

                struct Foo {
                  char bar[9001];
                } foo;
                
                int main() {
                  Foo baz = foo;  // memcpy
                  cout << "baz: " << (void*)&baz << endl;
                  return 0;
                }
              • Der Compiler wirft unter Umständen ein undefined reference to 'memset':

                int main() {
                  char zeroed[9001] = {};  // memset
                  cout << "zeroed: " << (void*)zeroed << endl;
                  return 0;
                }

              Die entsprechenden beiden Funktionen müssen also von der libsys bereit gestellt werden!

              Zeiger als Systemaufrufparameter

              Der Kernel sollte immer kritisch gegenüber Zeigern sein, die er von einer Benutzeranwendung bekommt. Ein einfaches Beispiel ist der write Syscall – gefährlich wird es aber natürlich mit dem read Syscall, mit welchem es ohne richtiger Prüfung passieren kann, dass wichtige interne Daten (oder gar Code) überschrieben werden!

              Wie ein solcher invalider Zugriff bei der Systemaufrufschnittstelle behandelt werden soll ist Implementierungssache, eine Möglichkeit wäre bei ungültigen Zeigern den Systemaufruf direkt mit 0 (= keine Ausgabe) zurückkehren zu lassen.

              • Benutzeranwendungen können via Syscall den Kernel auslesen

                int main() {
                  if (sys_write(0, reinterpret_cast<char*>(0x1000000), 42) != 0)
                      sys_write(1, "You shouldn't be here!\n", 23);
                  return 0;
                }
              • Benutzeranwendungen können via Syscall auf den APIC zugreifen

                int main() {
                  if (sys_write(0, reinterpret_cast<char*>(0xfec00000), 42) != 0)
                      sys_write(1, "You shouldn't be here!\n", 23);
                  return 0;
                }
              • Benutzeranwendungen können schlicht ungültigen Speicher adressieren

                int main() {
                  if (sys_write(0, reinterpret_cast<char*>(0x5000000), 42) != 0)
                      sys_write(1, "You shouldn't be here!\n", 23);
                  return 0;
                }

              Wie sollte der Zeiger geprüft werden

              Zum Beispiel mit Hilfe einer resolve Funktion, welche die (virtuelle) Adresse des Zeigers auflöst und dabei auch prüft, ob das Usermode-Bit gesetzt ist.

              Sofern an diese Adresse auch geschrieben werden soll (read Systemaufruf), so muss entsprechend zusätzlich auch das Writable-Bit gesetzt sein.

              Und dieser Check muss für jede Seite gemacht werden – ein Zeiger auf 0x4002f50 mit Länge 5000 muss entsprechend die Berechtigungen der Seiten 0x4002000, 0x4003000 sowie 0x4004000 im virtuellen Adressraum überprüfen, bevor er mit dem eigentlichen Systemaufruf fortfahren kann.

              Fehler beim ELF Laden

              Die extra 2.5 ECTS kommen nicht umsonst 😀, dafür müssen auch noch die Fallstricke von ELF-Dateien überwunden werden.

              • BSS ist nicht genullt:

                char zeroed[9001];
                int main() {
                  for (uint32_t i = 0; i < 9001; i++)
                      if (zeroed[i] != '\0')
                          cout << "zeroed[" << i << "] = " << hex << (int)zeroed[i] << endl;
                  while(1);
                }

                Falls die Speichergröße eines ELF-Segments größer als die Dateigröße ist, muss die Differenz mit 0 initialisiert werden (z.B. via memset)

              Fehlerhafte Berechtigungen

              Die Segmente in ELFs ermöglichen uns die Vorteile der Berechtigungen im x64 Paging voll auszuspielen und dadurch frühzeitige fehlerhaft Userspace Apps zu erkennen – solange korrekt implementiert!

              • Schreiben auf nur lesbares Datensegment (.rodata) möglich:

                const int i = 23;
                
                // prevent compiler optimization
                volatile int * iptr = reinterpret_cast<volatile int*>(const_cast<int*>(&i));
                
                int main() {
                  *iptr = 42;
                  if (*iptr != 23)
                      cout << "Test failed: " << iptr << " (.rodata) = " << *iptr << " (not 23!)"<< endl;
                  cout << "Test failed: Write to " << iptr << " (.rodata) didn't stop app!"<< endl;
                  return 1;
                }
              • Schreiben in Codesegment (.text) möglich

                int main() {
                  // try to change data in .text
                  volatile void ** ptr = reinterpret_cast<volatile void**>(main);
                  *ptr = reinterpret_cast<void*>(0xbadbeef);
                  if (*ptr == reinterpret_cast<void*>(0xbadbeef))
                      cout << "Test failed: main @ " << ptr << " (.text) = " << *ptr << endl;
                  cout << "Test failed: Write to " << ptr << " (.text) didn't stop app!"<< endl;
                  return 1;
                }
              • Man kann Code auf dem Stack ausführen: ```{.cpp} void fail() { sys_write(1, “You shouldn’t be here!”, 23); while(1); }

                int main() { char exec[3]; // Machine code bytes for call *%rdi exec[0] = ‘’; exec[1] = ‘’; // Machine code bytes for ret exec[2] = ‘’; auto fp = reinterpret_cast<void()(void()())>(exec); fp(fail); cout << “Test failed: Executing code from stack didn’t stop app!”<< endl; return 1; }

              • Man kann Code im Datensegment (.data) ausführen:

                // Machine code bytes for `call *%rdi; nop; ret;`
                char exec_data[]  = {'\xff', '\xd7', '\xc3', '\x90', '\0' };
                
                void fail() {
                  sys_write(1, "You shouldn't be here!\n", 23);
                  while(1);
                }
                
                int main() {
                  auto fp = reinterpret_cast<void(*)(void(*)())>(exec_data);
                  fp(fail);
                  cout << "Test failed: Executing code from .data didn't stop app!"<< endl;
                  return 1;
                }
              • Man kann Code im nur lesbaren Datensegment (.rodata) ausführen:

                // Bytes für Maschinencode `call *%rdi; ret;`
                const char exec_rodata[] = {'\xff', '\xd7', '\xc3','\0' };
                
                void fail() {
                  sys_write(1, "You shouldn't be here!\n", 23);
                  while(1);
                }
                
                int main() {
                  auto fp = reinterpret_cast<void(*)(void(*)())>(exec_rodata);
                  fp(fail);
                  cout << "Test failed: Executing code from .rodata didn't stop app!"<< endl;
                  return 1;
                }

              Obige Fehler resultieren meist aus entweder einem Nichtberücksichtigen der Berechtigungsflags im ELF Program Header beim Laden der Apps und dem Mappen der entsprechenden Seiten – oder aber einem fehlerhaften Linkerskript für die Anwendungen (user/sections.ld).

              Mittel dem Kommandozeilenwerkzeug readelf kann in der App-Binärdatei geprüft werden, ob die Segmente die korrekten Berechtigungen haben (die Parameter -Wl sind hierbei nützlich). Üblicherweise hat man drei LOAD-Segmente mit Berechtigungen R X (für Code in .text), R (für Konstanten in .rodata) und RW (für Daten in .data). Sollte jedoch ein Segment gleichzeitig als schreib- und ausführbar (RWE) markiert sein, so fehlt vermutlich mindestens ein . = ALIGN(0x1000); im Linkerskript: dadurch kann der Linker die Berechtigungen feingranular setzen, ohne die 4K Ausrichtung klebt er .text und .data unter Umständen zusammen…

              Zurück zur Übersicht

              Friedrich-Alexander-Universität
              Erlangen-Nürnberg

              Schlossplatz 4
              91054 Erlangen
              • Impressum
              • Datenschutz
              • Barrierefreiheit
              • Facebook
              • RSS Feed
              • Xing
              Nach oben