_ .-') _ ('-. ('-. _ .-') _ .-. .-') .-') _ ('-. .-') ( ( OO) ) _( OO) ( OO ).-.( ( OO) ) \ ( OO ) ( OO) ) _( OO) ( OO ). \ .'_ (,------./ . --. / \ .'_ ;-----.\ ,--. ,--./ '._(,------.(_)---\_) ,`'--..._) | .---'| \-. \ ,`'--..._) | .-. | \ `.' / |'--...__)| .---'/ _ | | | \ ' | | .-'-' | | | | \ ' | '-' /_).-') / '--. .--'| | \ :` `. | | ' |(| '--.\| |_.' | | | ' | | .-. `.(OO \ / | | (| '--. '..`''.) | | / : | .--' | .-. | | | / : | | \ || / /\_ | | | .--' .-._) \ | '--' / | `---.| | | | | '--' / | '--' /`-./ /.__) | | | `---.\ / `-------' `------'`--' `--' `-------' `------' `--' `--' `------' `-----' ~ xcellerator [ Übersetzung von dash ] Ahoy, ihr ELF Anhänger! In diesem Artikel möchte ich euch eine kleine Software-Bibliothek, an der ich arbeite, vorstellen. Die Bibliothek heißt: ‚LibGolf‘. Angefangen hatte alles damit, dass ich das ELF-Format und die Programm-Header verstehen wollte. Doch seitdem hat es sich zu etwas ziemlich Praktischem entwickelt. Mein Programm macht es sehr leicht eine ladbare Binär-Datei mit ELF Kopfzeile, gefolgt von einer einzigen Programm-Kopfzeile sowie einem einzelnen Segment. Standardmäßig werden alle Werte in den Kopfzeilen auf sinnvolle Werte gesetzt, es gibt jedoch einen einfachen Weg mit diesen Standards zu spielen – darum soll es in diesem Artikel gehen. Ich werde demonstrieren wie ich ‚LibGolf‘ genutzt habe um herauszufinden welche Bytes wirklich notwendig sind und welche vom Linux Programmlader (Linux Loader) ignoriert werden. Zum Glück hat sich herausgestellt, dass der Linux Loader einer der Parser ist, der im Vergleich zu anderen Linux Tools, nicht sonderlich oft nörgelt. Bevor wir fertig sind, werden wir feststellen, dass mehrere beliebte statische Analysetools an unserer korrupten ELF-Datei zerbrechen, während der Programmlader fröhlich unseren Programmcode lädt und zu den ausgewählten Bytes springt. +----------------------------+ |--[ Einführung in LibGolf]--| +----------------------------+ Vor einiger Zeit schrieb ich ELF-Dateien händisch mit NASM(Anm.d.Ü: Ein Tool um Assembler-Code zu kompilieren). Obgleich dies eine Zeitlang Spaß gemacht hat (und sicherlich auch seine Vorteile hatte), musste ich feststellen, dass ich den ganzen Spaß verpasse den C Strukturen bieten. Insbesondere und ich bin sicher vielen Lesern ist dies bekannt, dass die Datei <linux/elf.h> voll ist mit lustigen Dingen wie den Elf64_Ehdr-Headern und dem Elf32_Phdr die nur darauf warten genutzt zu werden. Im Interesse, dass diese Header nicht ungenutzt bleiben hatte ich mich entschieden sie mir zu schnappen und loszulegen. Aus diesen Bemühungen ist libgolf.h entstanden. Eine Software-Bibliothek die es erlaubt aus Shellcode eine funktionierende ELF-Datei zu machen. Ich weiß was du jetzt denkst – „Das klingt wie ein furchtbarer Linker!“ und vielleicht ist dem auch so. Jedoch, der Vorteil ist, dass es möglich ist die Header der Datei *vor* der Dateierstellung zu bestimmen. Lass uns ansehen wie das ganze funktioniert. Wenn du ebenfalls mit dem Programmcode zuhause spielen willst, kannst du diesen hier finden [0] oder hier ‚examples/01_dead_bytes‘. Für einen einfachen Test brauchen wir zwei Dateien: eine C Quelldatei und eine Shellcodedatei (shellcode.h). Bezüglich des Shellcodes möchte ich mit dem guten alten ‚b0 3c 48 31 ff 0f 05‘ beginnen. Disassembliert sieht dieser wie folgt aus: mov al, 0x3c @ b0 3c xor rdi, rdi @ 48 31 ff syscall @ 0f 05 (Ja, ok, *dass* shellcode zu nennen ist jetzt etwas übertrieben!) Die obigen drei Zeilen rufen einfach nur den Syscall (Anm.d.Ü: Üblicher Systemaufruf unter Linux vgl. asm/syscall.h) exit mit dem Rückgabewert ‚0‘ auf. Das ist hilfreich, denn wir können mit der Shell-Erweiterung ‚$?‘ den Rückgabewert überprüfen des Programms überprüfen. Es ist möglich diesen oder einen anderen Shellcode, der als PIC kompiliert worden ist (es gibt aktuell noch keine Unterstützung für ‚relocatable‘ kompilierte Programme) in der Datei shellcode.h in einen Puffer zu schreiben (buf[]). Wenn du nur eine Datei haben willst die deinen Shellcode ausführt, dann ist das hier alles was du brauchst: #include "libgolf.h" #include "shellcode.h" int main(int argc, char **argv) { INIT_ELF(X86_64,64); GEN_ELF(); return 0; } Wenn du das ganze kompilierst und die Datei ausführst erhältst du ein .bin File. Dies ist dein funkelnagelneues ELF! Ziemlich einfach, oder? Einfachheit geht auch mit Langeweile einher, dies ist auch hier der Fall. Also lass uns etwas interessanteres machen! Vorher macht es jedoch Sinn zu erklären, was diese Makros (INIT_ELF(), GEN_ELF()) eigentlich machen. INIT_ELF() erwartet zwei Argumente: den ISA und die Architektur. Aktuell unterstützte Instruktion-Sets von ‚LibGolf‘ sind X86_64, ARM32 und AARCH64. Und entweder 32 oder 64Bit für die Architektur selber. Zu aller Anfang werden einige interne Strukturen festgelegt, die für die spätere Verarbeitung notwendig sind. Danach wird entschieden ob Elf32_* oder Elf64_* Objekt-Header genutzt werden sollen. Die ELF Programm-Kopfzeilen „ehdr“ und „phdr“ und entsprechenden Verweise (Pointer) werden ebenfalls automatisiert berechnet und gesetzt. Und genau diese werden wir einfach nutzen um die Felder zu manipulieren. Davon abgesehen, kopiert ‚LibGolf‘ ebenfalls den Puffer vom Shellcode, berechnet und befüllt die ELF Programm-Kopfdateien und erstellt einen sinnvollen Einstiegspunkt (Entry Point). Danach kommt GEN_ELF() zur Ausführung. GEN_ELF() schreibt lediglich ein paar schöne Statistiken nach STDOUT und danach die korrekten Strukturen für die .bin Datei. Der Name für die .bin Datei wird von dem ersten Argument, argv[0], genommen. Ok. Nachdem wir das INIT_ELF() Makro benutzt haben, können wir ‚ehdr‘ und ‚phdr‘ dereferenzieren. Angenommen wir wollen e_version von der ELF-Kopfzeile modifizieren, müssen wir lediglich eine Zeile zu unserem Code hinzufügen: #include "libgolf.h" #include "shellcode.h" int main(int argc, char **argv) { INIT_ELF(X86_64); // Set e_version to 12345678 ehdr->e_version = 0x78563412; GEN_ELF(); return 0; } Jetzt noch schnell kompilieren, ausführen und eine neue .bin Datei wartet auf dich. Wenn man sich die Datei mit ‚xxd‘, ‚hexyl‘ oder deinem Lieblings-Hexeditor ansieht, wirst du ein paar hübsche kleine Werte, '12 34 56 78', ab Offset 0x14 sehen. Na war das nicht einfach? Damit das ganze etwas schneller geht nutze ich gerne das folgende Makefile: .PHONY golf clean CC=gcc CFLAGS=-I. PROG=golf golf: @$(CC) -o $(PROG) $(PROG).c @./$(PROG) @chmod +x $(PROG).bin @rm $(PROG) $(PROG).bin (Diese Datei findest du ebenfalls im Repo [0]) +---------------------------------+ |--[ Probleme direkt am Anfang ]--| +---------------------------------+ Wie viele bereits wissen, Fileparser sind schwierige Programme. Obgleich Spezifikationen meistens die besten Absichten haben, werden sie selten respektiert, auch von denen nicht, die es besser wissen sollten. Überraschenderweise ist gerade der Linux ELF Lader, derjenige der sich am wenigsten an die Vorgaben hält. Unsere Bibliothek ‚LibGolf‘ macht es einfach herauszufinden wie viele Verstöße gegen die Datei ‚elf.h‘ und ELF Spezifikation begangen werden. Ein guter Punkt um zu starten, ist immer der Anfang, in diesem Fall der ELF Header (Kopfzeile). Am Anfang einer jeden ELF Datei ist, wie wir wissen ‚0x7f‘ gefolgt von der Zeichenkette ‚ELF‘. Diese werden ebenfalls als EI_MAG0 bis EI_MAG3 bezeichnet. Wenig überraschend: wenn diese vier Byte manipuliert werden, wird die Datei vom Ladeprogramm zurückgewiesen. Gott-Sei-Dank! Was ist mit dem fünften Byte bzw. mit Offset 0x5? Unsere Spezifikation sagt uns, dass dies die EI_CLASS ist und die Zielarchitektur beschreibt. Werte die akzeptiert werden sind: 0x01 und 0x02, jeweils für 32 und 64 Bit. Ich wiederhole: „Nur die folgenden Werte sollen laut Spezifikation akzeptiert werden: 0x01 und 0x02“. Was passiert also, wenn wir einen Wert wie z.B. 0x58 (‚X‘ in der ASCII Tabelle) nutzen? Wir machen dies in dem wir dem Programmcode das folgende hinzufügen: (ehdr->e_ident)[EI_CLASS] = 0x58; Du fragst warum wir ein ‚X‘ nehmen? Nun ja, man sieht es ziemlich deutlich in der Ausgabe von Tools wie z.B. xxd oder hexyl. Bevor wir jetzt anfangen mit unserer neuen Datei zu spielen, können wir ja noch ein paar weitere ELF Parser testen um andere Missetäter zu finden (Anm. d. Übersetzers: die die ELF Spezifikation nicht respektieren). Das erste Programm auf unserer Liste ist GDB. Los geht’s, schauen wir was passiert. "not in executable format: file format not recognized" Ebenso verhält sich auch ‚objdump’. Die Datei wird nicht akzeptiert. Es scheint also, dass diese Parser ihren Job gut machen. Ok, nun versuchen wir einmal die Binärdatei ganz normal zu starten. <Spoiler>Es funktioniert einwandfrei! </Spoiler> Falls du meinen Beispiel-Shellcode benutzt, kannst du mittels der Abfrage von $? (Anm.d.Ü: Bash-Shell Return Variable) herausfinden, dass das Programm erfolgreich beendet worden ist. Verstöße gegen die Spezifikation sind ebenfalls für die Felder EI_DATA und EI_VERSION möglich. +------------------------------------------+ |--[ Drehen wir die Manipulation bis 11 ]--| +------------------------------------------+ Wie weit können wir gehen? Wie viele der ELF und der Programm-Kopfzeilen wird der Linux Programmlader einfach ignorieren? Wir haben bereits EI_CLASS, EI_DATA und EI_VERSION betrachtet. Es stellt sich heraus, dass ebenfalls EI_OSABI vom Programmlader sicherheitshalber *komplett* ignoriert wird. Und schon sind wir bei Offset 0x8. Anhand der Spezifikation sehen wir, dass das nächste Feld EI_ABIVERSION und EI_PAD sind und uns bis zu Offset 0xf bringen. Auch hier scheint sich niemand für die Werte zu interessieren, also setzen wir sie auch einfach auf ‚X‘. Und weiter geht es. Wir kommen zu einem Feld, dass sich jedoch wehrt manipuliert zu werden: e_type. Das ergibt Sinn. Der ELF Programmlader mag es eben nicht, wenn wir ihm nicht sagen um welchen Programmtyp es sich handelt. (Gut zu wissen, dass der Programmlader wenigsten ein *paar* Standards hat – Wortspiel gewollt). Auch ET_EXEC muss bei 0x0002 bleiben. Das nun kommende Feld ist ebenfalls Wählerisch. Wir sind bei Offset 0x12 und das Feld heißt: e_machine. Dieses Feld ist wichtig für das Ziel-Instruktions-Set der CPU (ISA). Soweit es uns betrifft, wurde das Feld von ‚LibGolf‘ bereits mit 0x3e beschrieben, da wir X86_64 als erstes Argument an unser Makro INIT_ELF() übergeben haben. Und damit hat sich das Thema erledigt. Und dann DAS! Eine unangepasste Version von e_version taucht auf der Bildfläche auf! Ein weiterer Dissident. Eigentlich sollte dieser immer die Bytes 0x00000001 haben. In der Realität jedoch, scheint dies niemanden zu interessieren. Also füllen wir diese ebenso mit: 0x58585858 auf. Nach dieser Menge an Ketzern haben wir ein paar wichtige Felder, die tatsächlich immun gegen Missbrauch scheinen: e_entry und e_phoff. Ich bin sicher ich muss nicht zu sehr ins Detail gehen was e_entry betrifft – es ist der Programmcode-Einstiegspunkt der Binärdatei. Die dort hinterlegte Adresse bestimmt was ausgeführt wird, sobald alle ladbaren Teile im Speicher vorhanden sind. Obgleich man annehmen könnte, dass der Loader in der Lage ist ohne den Offset zum Programmheader zu kennen(e_phoff) diesen selber zu berechnen, scheint es ganz so, als wenn dieser nicht clever genug ist dies zu tun. Daher muss auch hier nachgeholfen werden. Wir fassen diese beiden Felder also nicht weiter an. ‚LibGolf‘ unterstützt aktuell keine ‚Section Headers‘ (und da der Fokus der Bibliothek darauf liegt *kleine* Binaries zu erstellen, kommt dieses Feature wahrscheinlich nicht). Daraus ergibt sich, dass jede Art von Headern, die sich auf die verschiedenen möglichen Sektionen beziehen, von uns nach Herzenslust manipuliert werden können. Dazu gehören, e_shoff, e_shentsize, eh_shnum and sogar e_shstrndx. Wenn wir keine Sektionen haben, können wir auch nicht verantwortlich gemacht werden, wenn wir die dazugehörigen Kopfzeilen einfach verändern. Die restlichen Felder, die für den Programmlader wichtig zu sein scheinen sind: e_ehsize, e_phentsize und e_phnum. Auch das ist nicht sehr überraschend, da diese notwendig sind um das einzige ladbare Segment in den Speicher zu bringen, bevor die Kontrolle an das Programm übergeben wird. Falls du eine kleine Erinnerung brauchst: e_ehsize ist die Größe des ELF Headers (diese ist entweder 0x34 für 32-Bit oder 0x40 für 64-Bit). Eh_phentsize ist die Größe des folgenden Headers (auch dieser ist hart-kodiert mit 0x20 oder 0x38 für 32 oder für eine 64 Bit Architektur). Wenn der Programmlader etwas mehr achtgeben würde bei dem Feld EI_CLASS, würde er diese beiden letztgenannten nicht brauchen. Und zum Schluss: e_phnum, hierbei handelt es sich lediglich um die Anzahl der Einträge im Programmheader. In unserem Fall ist diese immer auf 0x1 gesetzt. Ohne Frage, dies wird für irgendeine Kontrollschleife in den Speicherladeroutinen genutzt, ich habe das ganze jedoch nicht weiter untersucht. Es ist ein letzter Bereich im ELF Header übrig den ich nicht angefasst habe. Dabei handelt es sich um e_flags. Der Grund hierfür ist relativ simpel: er ist abhängig von der Architektur. Für ein X86_64 System spielt er gar keine Rolle, da es undefiniert ist (Auf der anderen Seite ist es wichtig für einige ARM Systeme, am besten du schaust dir einmal das ARM32-Beispiel bei [0] an). Und damit sind wir am Ende des ELF Headers. Für alle die nicht mitgezählt haben, knapp über 50% des Headers wird vom Programmlader ignoriert. Aber wie sieht es mit dem Programm Header aus? Es stellt sich heraus, dass dieser weitaus besser aufgestellt ist. Tatsächlich jedoch nicht aus den Gründen die man erwarten sollte. Denn jegliche Art von korrumpierten Programm Header wird vom Programmlader ignoriert. Wir könnten den gesamten Programm-Header mit unseren X’en füllen und dem Lader wäre es komplett egal. Aber Achtung mutiger Padavan, spielst du mit den falschen Bytes herum wirst du schnell in den Katakomben der Abstürze landen. Gibt es denn nun irgendetwas im Programm Header? Es ergibt sich, dass ohne Selbstverschulden zwei Felder genutzt werden können, da sie einfach nicht mehr gebraucht werden. Dabei handelt es sich um: p_addr und p_align. Ersterer war notwendig, als es noch keine virtuelle Speicheraddressierung gab. Zu Zeiten als 4GB Systemspeicher nichts anderes war als ein Traum von kleinen Kindern. Daher war es damals notwendig dem Lader mitzuteilen wo im physischen Speicherbereich das Segment vom Programm geladen werden sollte. Das Feld für die Anpassung des Speichers (p_align), ist wiederrum lustig. Normallerweise sollte p_vaddr gleich dem Ergebnis von p_offset modulus (%) von p_align sein. “Gute“ ELF Dateien, zumindest die, die mit GCC kompiliert worden sind, setzen einfach einen gleichen Wert bei p_offset und p_vaddr und kümmern sich nicht weiter darum. Das macht übrigens auch LibGolf und lässt p_align überflüssig zurück. Alles in allem nicht so spaßig wie der ELF Header, aber immerhin ein paar kleine Vorteile. Um die Binärdatei in C zu generieren gibt es den folgenden Code: #include "libgolf.h" #include "shellcode.h" int main(int argc, char **argv) { INIT_ELF(X86_64,64); /* * Damit kommen statische Analysetools wie gdb und objdump nicht klar */ (ehdr->e_ident)[EI_CLASS] = 0x58; // Architektur (ehdr->e_ident)[EI_DATA] = 0x58; // Endianness (ehdr->e_ident)[EI_VERSION] = 0x58; // Sollte immer 0x1, machen wir aber nicht (ehdr->e_ident)[EI_OSABI] = 0x58; // Ziel Betriebssystem // Kontrollschleife für den Rest von e_ident int i; for ( i = 0 ; i < 0x10 ; i++ ) (ehdr->e_ident)[i] = 0x58; ehdr->e_version = 0x58585858; // Sollte immer 0x00000001 sein, machen wir aber nicht // Section headers? Wir brauchen keine stinkenden Section Headers! ehdr->e_shoff = 0x5858585858585858; ehdr->e_shentsize = 0x5858; ehdr->e_shnum = 0x5858; ehdr->e_shstrndx = 0x5858; ehdr->e_flags = 0x58585858; // x86_64 hat keine Flags definiert phdr->p_paddr = 0x5858585858585858; // Physikalische Adresse wird ignoriert phdr->p_align = 0x5858585858585858; // p_vaddr = p_offset, daher obsolet GEN_ELF(); return 0; } Sobald du das ganze kompiliert hast, bekommst du das folgende Binary: 00000000: 7f45 4c46 5858 5858 5858 5858 5858 5858 .ELFXXXXXXXXXXXX 00000010: 0200 3e00 5858 5858 7800 4000 0000 0000 ..>.XXXXx.@..... 00000020: 4000 0000 0000 0000 5858 5858 5858 5858 @.......XXXXXXXX 00000030: 5858 5858 4000 3800 0100 5858 5858 5858 XXXX@.8...XXXXXX 00000040: 0100 0000 0500 0000 0000 0000 0000 0000 ................ 00000050: 0000 4000 0000 0000 5858 5858 5858 5858 ..@.....XXXXXXXX 00000060: 0700 0000 0000 0000 0700 0000 0000 0000 ................ 00000070: 5858 5858 5858 5858 b03c 4831 ff0f 05 XXXXXXXX.<H1... Diese Datei ist 127 byte groß, außerdem waren wir in der Lage 50Byte davon mit ‚X‘ zu ersetzen. Das bedeutet, dass knapp 40% der Datei vom ELF Programmlader ignoriert werden. Und wer weiß was man mit diesen 50 Byte alles machen könnte? Es stellt sich heraus – eine ganze Menge! Vor einigen Jahren hatte ‚netspooky‘ durch eigene Forschung aufgezeigt, dass man einige Teile des Programmheaders in den ELF Header verlagern kann. Wenn man das ganze jetzt kombiniert, indem man seinen Shellcode in den Regionen der ‚‘Toten Bytes‘ speichert, sowie einige weitere coole Tricks, ist es möglich die Datei auf 84 Byte zu begrenzen. Dies ist eine weitere Verkleinerung und zwar von 34%. Hier kannst du die Ergebnisse seiner unglaublich coolen ‚ELF Mangling‘ Serie nachlesen [1]. Ein weiterer interessanter Aspekt dieser Techniken wird oft einfach übersehen. Während dem Linux Programmlader teilweise die Struktur der ELF Datei Schnuppe ist, ist dies anderen Tools bei weitem nicht egal. Wir hatten uns ja schon ‚objdump‘ und ‚gdb‘ angesehen. Jede Menge AV (Antivirus) Programme zerbröseln ebenfalls wenn sie mit missgebildeten ELF Dateien zu tun haben. Während meiner Erforschung dessen, stellte ich fest, dass die einzige Lösung, die es halbwegs hinbekommt derartige Dateien zu lesen ClamAV ist. ClamAV erkennt diese ELF Dateien als ‚Heuristics.Broken.Executable‘. Die besten Ergebnisse bekommt man noch immer mit dynamischer Analyse. +--------------------------+ |--[ Und weiter Geht es ]--| +--------------------------+ X86_64 ist nicht die einzige ISA die von ‚LibGolf‘ unterstützt wird. Es ist ebenfalls möglich kleine ausführbare Dateien für ARM32 and AARCH64 zu bauen. Im Github-Repository, kannst du Beispiele für beide ARM-Architekturen finden. (Sowie auch die toten Bytes aus diesem Artikel) Aber verflucht sein sollen all‘ die Beispiele! Hoffentlich habt ihr es bis hier geschafft und wollt ebenfalls einen Blick in ‚libgolf.h‘ selber werfen. Wie ich ja bereits am Anfang gesagt hatte, dass ganze ging Los, weil ich etwas über ELF lernen wollte. Daher habe ich mich besonders bemüht den Programmcode sauber zu kommentieren. +--------------------------+ |--[ Reproduzierbarkeit ]--| +--------------------------+ Ein Großteil der Recherche, fand unter Ubuntu 20.04 mit Kernel 5.4.0-65-generic statt. Außerdem habe ich verifiziert, dass die gleichen Ergebnisse unter Archlinux mit Kernel 5.11.11-arch1-1 erzielt werden konnten. Ich habe gehört, dass komisches Verhalten auf den WSL (Anm.d.Ü: Linux-Erweiterung von Windows) beobachtet werden kann, dies habe ich jedoch nicht weiter verfolgt – vielleicht willst du das ja machen! +--------------+ |--[ Grüsse ]--| +--------------+ Ein besonderes „ahoy“ geht an jeden in Thugcrowd, Symbolcrash und die ‚‘Mental ELF Support Group‘! +------------------+ |--[ Referenzen ]--| +------------------+ [0] https://www.github.com/xcellerator/libgolf [1] https://n0.lol/ebm/1.html Glossar ------- ISA – Instruction Set Architecture Syscall – Unter Linux ein Aufruf an den Kernel, z.B. zum öffnen von Dateien zum lesen oder schreiben Linker – Programm, das ausführbare Dateien und Bibliotheken miteinander verbindet C Structs – C Strukturen Linux Loader - Programmlader ELF – Executable and Linkable Format ELF-Header - ELF-Kopfzeile Programm-Header - Programm-Kopfzeile Kopfzeilen Headers, Beispiel: Programm-Kopfzeilen im Englischen program headers