Fragen? Antworten! Siehe auch: Alternativlos
Eine wichtige Sicherheitstechnologie ist, dass man dafür sorgt, dass die Shared Libraries nicht immer alle an der selben Adresse geladen werden. Es geht darum, Angreifern das Ausnutzen von Sicherheitslücken zu erschweren. Dieses Verfahren heißt ASLR.
Unter Linux ist das umgesetzt, indem mmap die Speicherbereiche randomisiert, d.h. ld.so muss gar nicht viel dafür machen. Jetzt gibt es zwei Probleme. Erstens ist ld.so ein Executable, keine Shared Library, d.h. es wird nicht an eine zufällige Adresse geladen, sondern an eine statische. Zweitens ist das Hauptprogramm ein Executable, keine Shared Library. Der ld.so von glibc ist deshalb jetzt doch eine Shared Library, um dieses Einfallstor zu schließen. Aber das Hauptprogramm landet doch immer noch immer an der selben Adresse, außer — ja außer, man kompiliert es mit -pie. Im Grunde gibt es gar keinen echten Unterschied zwischen Hauptprogramm und Shared Library in ELF, außer dass das Hauptprogramm im Header eine Adresse stehen hat, an die es geladen werden will, und die Shared Library kann irgendwohin geladen werden. Wenn man also ASLR in allen Komponenten haben will, dann müssen alle Komponenten ELF-technisch Shared Libraries werden.
So, was ist jetzt mit dietlibc. dietlibc erzeugt statische Binaries, d.h. ohne ld.so und ohne libc.so. Das ist ja gerade die Idee bei der dietlibc. Ich fragte mich jetzt, ob ich es nicht hinkriegen kann, ein statisches Binary zu erzeugen, dass der Kernel hinladen kann, wo er will. Erstmal geht es mir nur um meine Entwicklungsplattform, AMD64. Dort und bei x86 insgesamt ist es so, dass ein Funktionsaufruf zu "call 1234" wird, aber 1234 ist nicht die absolute Adresse, an die man hinspringen will, sondern relativ zur aktuellen Position des Instruction Pointer. Wenn die Calls alle relativ innerhalb des Code-Moduls sind, dann ist es auch egal, an welcher Stelle im Speicher das liegt.
Speicherzugriffe auf Variablen laufen bei x86 aber über absolute Adressen. Auf dem 32-bit x86 hat man dann ein Problem, denn man kann keine Adressen in Relation zum aktuellen Instruction Pointer konstruieren. Bei AMD64 ist das anders, daher dachte ich, wenn ich mich erstmal darauf konzentrieren, dann ist das ein Selbstläufer. Ist es leider nicht.
Das Problem ist, dass ich anscheinend der erste bin, der das machen will. Alle anderen wollen normale dynamische Binaries erzeugen, und da laufen alle Zugriffe über die Glue-Datenstrukturen. Mein erstes Problem ist, gcc zu sagen, dass er diese Datenstrukturen bitte nicht benutzen soll. Mein aktueller Ansatz dafür ist -fvisibility=hidden, aber das hilft leider nur ein bisschen. Für in Headern als "extern" deklarierte Symbole hilft es nicht.
Gut, dachte ich mir laut seufzend. Dann muss mein Startup-Code halt ran. Ich kompiliere also meinen Code mit -fpic -fvisibility=hidden und linke daraus erfolgreich eine Shared Library. Wenn ich die aufrufe, wird mein Startup-Code ausgeführt. GOT und PLT sind eh schon in den Speicher gemappt (das sind diese Glue-Datenstrukturen). Da stehen nur noch falsche Dinge drin. So schwer kann das ja wohl nicht sein. Ich fange also an, da ein bisschen Code zu schreiben, und scheitere gerade an trivial anmutenden Details.
Und zwar habe ich in meiner "shared library" ja keinen ELF Interpreter ("ld.so") drin. Woher weiß denn mein Code, wo die GOT ist? Das steht in ELF-Datenstrukturen, die Teil der Shared Library sind. Die mappt der Kernel in den Speicher. Ich weiß nur nicht, wo die genau sind. Der Mechanismus dafür heißt AUXVEC. In C ist das Hauptprogramm ja die Funktion main(), und die kriegt die Kommandozeilenargument und das Environment übergeben. Das Environment ist ein Array von char*, und NULL markiert das Ende der Liste. AUXVEC ist einfach dahinter im Speicher. Wer das mal sehen will:
% LD_SHOW_AUXV=1 /usr/bin/trueDa steht bei mir sowas hier:
AT_SYSINFO_EHDR: 0x7ffc0ab78000 AT_HWCAP: bfebfbff AT_PAGESZ: 4096 AT_CLKTCK: 100 AT_PHDR: 0x400040 AT_PHENT: 56 AT_PHNUM: 8 AT_BASE: 0x7fa6e6d65000 AT_FLAGS: 0x0 AT_ENTRY: 0x401460 AT_UID: 1000 AT_EUID: 1000 AT_GID: 100 AT_EGID: 100 AT_SECURE: 0 AT_RANDOM: 0x7ffc0aadb909 AT_EXECFN: /usr/bin/true AT_PLATFORM: x86_64Hier kann man ganz schön sehen, dass AT_PHDR (ein Zeiger auf den ELF Program Header) nicht an ASLR teilgenommen hat, während AT_BASE (die Basisadresse von ld.so) randomisiert wurde. AT_ENTRY ist die Adresse im Speicher, bei dem die Ausführung beginnt. Das ist nicht main() sondern ein Symbol namens "_start", in dem die libc ihren Kram macht und dann main() aufruft.
Gut, dachte ich mir, das ist ja weitgehend was ich haben will. Alles super. Von AT_BASE aus kann ich mich durch die ELF-Strukturen hangeln (und die Werte darin sind alle relativ zur Ladeadresse des Binaries). Da finde ich die GOT und kann mal schauen, was da so drin steht, und was ich relozieren muss. Viel mehr als AT_BASE draufaddieren wird das nicht sein, wenn ich Glück habe.
Und was stelle ich jetzt fest? AT_BASE wird nicht übergeben. AT_BASE ist nämlich die Ladeadresse des ELF Interpreters, also ld.so, und den habe ich ja nicht. Meine eigene Base-Adresse sagt mir der Kernel nicht. Immerhin sagt mir der Kernel immer noch AT_PHDR, und das ist auch schon mal was, aber von da aus finde ich die Basis-Adresse nicht, zu der mein Binary geladen wurde. Also jedenfalls nicht offiziell. In der Praxis runde ich von da auf den Page-Anfang ab und hab es, aber das ist ja iih-bäh. Ich fürchte, ich bin schlicht der Erste, der gerne statische PIE-Binaries haben will.
Seufz.
In den gemappten Datenstrukturen stehen leider überall nur Angaben relativ zum Beginn der Datei drin. Und wo das Mapping losgeht, sagt mir der Kernel nicht.
Sachdienliche Hinweise werden dankend entgegen genommen!