Blog
Fallstricke in Benutzeranwendungen
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
inmain()
darf keinen Fehler werfen:int main() { "bye!" << endl; cout << return 0; }
Die
init.cc
muss sich darum kümmern, zum Beispiel mit einer kurzen Ausgabe, dass die Anwendung sich beendet hat und (derzeit mangelsexit
) Endlossschleife.Benutzeranwendungen sollen nicht den Kernel auslesen können:
int main() { "First Kernel bytes: " << hex cout << reinterpret_cast<volatile unsigned long *>(0x1000000) << endl; << *"You shouldn't be here!" << endl; cout << 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 "You shouldn't be here!" << endl; cout << 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 mitDPL_KERNEL
(eigentlich Standard) konfiguriert werdenFlü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() { 2; i *= } void init_func_y() { 2; i -= } ".preinit_array"))) __attribute__((section(void (*preinit_array[])(void) = { &init_func_y, &init_func_x }; int main() { if (i == 23) "No preinit_array executed!" << endl; cout << else if (i != 42) "preinit_array executed in wrong order!" << endl; cout << return 0; }
Die (veraltete)
.init
wird nicht ausgeführt:int i = 23; extern "C" void init_func_x() { 2; i *= } extern "C" void init_func_y() { 2; i -= } 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) "No .init!" << endl; cout << }
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; 142))) void init_func_x() { __attribute__((constructor(2; i *= } 123))) void init_func_y() { __attribute__((constructor(2; i -= } int main() { if (i == 23) "No init_array executed!" << endl; cout << else if (i != 42) "init_array executed in wrong order!" << endl; cout << 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]; 0] = 0x23; buf[ "&buf = " << reinterpret_cast<void*>(&buf) << endl; cout << return 0; }
Schreiben jenseits der maximalen Stackgrenze (Beispiel: 64MB unterhalb dem Stack) sollte fehlschlagen:
int main() { char buf[0x4000000]; 0] = 0x23; buf[ "&buf = " << reinterpret_cast<void*>(&buf) << endl; cout << "You shouldn't be here!" << endl; cout << 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() { // memcpy Foo baz = foo; "baz: " << (void*)&baz << endl; cout << return 0; }
Der Compiler wirft unter Umständen ein
undefined reference to 'memset'
:int main() { char zeroed[9001] = {}; // memset "zeroed: " << (void*)zeroed << endl; cout << 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) 1, "You shouldn't be here!\n", 23); sys_write(return 0; }
Benutzeranwendungen können via Syscall auf den APIC zugreifen
int main() { if (sys_write(0, reinterpret_cast<char*>(0xfec00000), 42) != 0) 1, "You shouldn't be here!\n", 23); sys_write(return 0; }
Benutzeranwendungen können schlicht ungültigen Speicher adressieren
int main() { if (sys_write(0, reinterpret_cast<char*>(0x5000000), 42) != 0) 1, "You shouldn't be here!\n", 23); sys_write(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') "zeroed[" << i << "] = " << hex << (int)zeroed[i] << endl; cout << 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. viamemset
)
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() { 42; *iptr = if (*iptr != 23) "Test failed: " << iptr << " (.rodata) = " << *iptr << " (not 23!)"<< endl; cout << "Test failed: Write to " << iptr << " (.rodata) didn't stop app!"<< endl; cout << return 1; }
Schreiben in Codesegment (
.text
) möglichint main() { // try to change data in .text volatile void ** ptr = reinterpret_cast<volatile void**>(main); reinterpret_cast<void*>(0xbadbeef); *ptr = if (*ptr == reinterpret_cast<void*>(0xbadbeef)) "Test failed: main @ " << ptr << " (.text) = " << *ptr << endl; cout << "Test failed: Write to " << ptr << " (.text) didn't stop app!"<< endl; cout << return 1; }
Man kann Code auf dem Stack ausführen:
void fail() { 1, "You shouldn't be here!\n", 23); sys_write(while(1); } int main() { char exec[3]; // Machine code bytes for `call *%rdi` 0] = '\xff'; exec[1] = '\xd7'; exec[// Machine code bytes for `ret` 2] = '\xc3'; exec[auto fp = reinterpret_cast<void(*)(void(*)())>(exec); fp(fail);"Test failed: Executing code from stack didn't stop app!"<< endl; cout << 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() { 1, "You shouldn't be here!\n", 23); sys_write(while(1); } int main() { auto fp = reinterpret_cast<void(*)(void(*)())>(exec_data); fp(fail);"Test failed: Executing code from .data didn't stop app!"<< endl; cout << 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() { 1, "You shouldn't be here!\n", 23); sys_write(while(1); } int main() { auto fp = reinterpret_cast<void(*)(void(*)())>(exec_rodata); fp(fail);"Test failed: Executing code from .rodata didn't stop app!"<< endl; cout << 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…