ELF and the Beyond by Dobin Rutishauser (aka Anthraxx) for UNF, www.u-n-f.com Version 0.4.2 Eine Einführung zum Executable and Linkable Format. /* test reader for rc: Klaus mueller What to do: - string table */ Inhalt: 1) Einführung 2) Symbole + Relocation 2.1) Symbole 2.2) Relocation 2.3) Praxis 3) Der ELF Standard 3.0) Einführung 3.1) ELF Header 3.2) Sections 3.2.1) Section Header 3.2.2) Some Sections 3.3) Segmente 3.3.1) Programm Header 3.3.2) Segment Praxis 4) Prozesse 4.0) übersicht 4.1) Heap (.bss) 4.2) Code (.text) 4.3) Global Offset Table (.got) 4.4) Procedure Linkage Table (.plt) 4.5) !! .data 4.6) ELF Dynamic Linking 5) Elf loading 5.1) Der flow eines programmes im memory 5.2) Initializers und Finalizers 5.3) Konstruktoren und Desktruktoren 6.0) !! Schlusswort 6.1) Greets 6.2) References 1) Einführung ============= "ELF and the Beyond" soll die Zusammenhänge von ELF auf Unix Systemen aufzeigen. Die grösste Priorität beim schreiben dieses Textes war, möglichst alle nötigen Bereiche zu beschreiben, damit der Leser versteht, was genau geschieht wenn er zB ein Programm startet. Ausserdem werden in anderen Texten Ausdrücke wie die GOT Section an den Kopf geworfen, die man nur einzeln, aber selten im Zusammenhang mit dem ganzen sieht. ELF ist ein ziemlich komplexes und umfangreiches Thema, so glaub ich nicht das es jemand versteht, indem er nur diesen einen Text liest. Ich empfehle desshalb dem Leser, möglichst die Referenzen anzugucken und auch durchzulesen, vorallem die ELF Spezifikationen und das "Linkers & Loaders" Buch ist sehr zu empfehlen. Wenn was nicht klar ist, fehlt oder sogar falsch ist, mail mir: dobin.rutishauser@gmx.net Updates dieses Textes gibts immer auf meiner HP: http://home.datacomm.ch/prutishauser/other/elf.txt 2) Symbole + Relocation ======================= 2.0) Symbole ------------ Du wirst noch öfters auf den Begriff "Symbol" stossen. Damit sind aber nicht die klickbunti Teils von Windows gemeint, sondern eine Art Platzhalter. Ein Symbol kann man auch als Zeiger vorstellen: Es hat u.a. einen Namen und einen Wert. Es asoziiert also einen Namen (zb "puts") mit einer Adresse (die Startadresse der Funktion, zb 0x80238023). Symbol: |-----------------------------------------------| | symbol name (byte offset into .strtab) | |-----------------------------------------------| | value | |-----------------------------------------------| | size | |-----------------------------------------------| | info | 0 | st_shndx | |-----------------------------------------------| Der Name steht eigentlich in der String Table, es wird nur der Offset darauf angegeben. Value ist eine Adresse, welche relativ von einer Section oder absolut angegeben werden kann. Info beschreibt erstens einmal das "Binding" des Symbols, also die Sichtbarkeit. Das "Binding" kann entweder local, global oder weak sein. Lokale Symbole sind ausserhalb ihres Object Files nicht sichtbar (ähnlich wie eine lokale C Variable), Global definierte Symbole sind in allen Object Files sichtbar, dürfen also auch nicht in mehreren vorkommen. Weak Symbols sind in etwa gleich wie Globale, haben aber eine tiefere Priorität. Dh, wenn ein Weak und ein Globales Symbol definiert werden, wird das Weak'e Symbol ignoriert. Info gibt ausserdem noch die Klassifikation des Symbols an, zb Object (Variablen, Arrays etc), Function (Funktionen), Section oder File. Symbole werden immer in relation zu einer Section definiert (Adresse der Section + Value = Reelle Adresse), st_shndx ist also ein Section Header Index. [size??] Symbole werden in der sogenannten Symbol Table organisiert, welches einfach eine aneinandereihung von Symbolen ist. Mit dem tool nm(1) kann man die Symbole eines Binarys auflisten: unreal:~/tmp $ nm test 08049504 A _DYNAMIC 080494f0 A _GLOBAL_OFFSET_TABLE_ 080494e4 ? __CTOR_END__ 080494e0 ? __CTOR_LIST__ 080494ec ? __DTOR_END__ 080494e8 ? __DTOR_LIST__ [...] 080484c8 ? _fini 08048318 ? _init 08048354 T _start U atexit U exit [...] 08048460 T harakiri 08048458 t init_dummy 080484c0 t init_dummy 08048478 T main Wie wir hier sehen, haben ein paar Symbole kein Value, also keine Adresse. Diese werden beim laden des Binarys durch den Dynamic Linker eingesetzt. Die Bedeutung der Buchstaben lässt sich in der info page zu nm nachlesen. A bedeutet zb Absolut, also das sich diese Adresse auch bei weiterem Linken nicht ändert. T zeigt, dass das Symbol in der Text (Code) Section ist, und U undefinied, also wird die Adresse später noch eingesetzt. 2.2) Relocation --------------- Relocation ist der Prozess vom verbinden von Symbol Referenzen mit Symbol Definitionen. Auf Deutsch, wenn ein Programm eine Funktion aufruft, muss es an die Richtige Adresse (die der Funktion) springen. Die Funktion hat ein Symbol, das ihren Namen (zb "puts") und ihre Adresse beinhaltet. Das Programm hat ausserdem ein Relocation Entry, welcher genau auf die Adresse zeigt, wo das Symbol genutzt wird (zb genau nach einem call). Wer das jetzt noch nicht versteht sei beruhigt, im nächsten Kapitel wird noch genauer auf Relocation mit einem Beispiel eingegangen. Relocation Entry: |-----------------------------------------------| | offset | |-----------------------------------------------| | symbol table index | type | |-----------------------------------------------| Offset ist die genaue Position, wo das Relocation angewendet werden muss. Für relocatable Files wird Offset ab dem Anfang der Section gezählt. Bei executables oder shared objects ist es die Virtuelle Adresse. Symbol Table Index gibt an, welches Symbol sich an dieser Adresse befindet. Type beschreibt, wie die Adresse genau berechnet werden soll. 2.3) Praxis ----------- Symbol Table: ---------------------------------------------------------------- |Index | Name | value | info | shdndx | |------|--------|---------|-----------------------|------------| | 1 | puts | 0x0500 | STB_LOCAL, STB_FUNC | 5 (.code) | | 2 | ... | | | | | --------------------|------------------------------------------- | ---------------- Relocation Entrys: | ------------------------------- | | Offset | STI | Type | | |------------|-------|--------| | | 0x0050 | 1 | | | ---:--------------------------- | : | : |----------------- v v 0x0050: call 0x0500 Der Relocation Entry zeigt genau dorthin, wo die Adresse geändert werden muss, also gerade nach dem call. Auch wird der Symbol Table Index angegeben, der hier 1 ist. Wenn wir den das Symbol mit der Index Nr 1 ansehen, sehen wir, dass es eine (in diesem Beispiel) lokale Funktion namens "puts" beschreibt, welche die Adresse 0x0500 hat. Nun, warum so ein Aufwand? Wenn nun das Binary geladen wird, können (durch relocation) zB Adressen von Funktionen nicht mehr stimmen. Also geht der Linker durch die ganze Symbol Table und passt die Symbole an. Danach führt er die Relocation durch, passt also alle Stellen im Code den neuen Begebenheiten an. Nun lässt sich das Programm einwandfrei ausführen. [really??] Merke: Symbol, Section und String Tables sind _nicht_ das gleiche :) 3) Der ELF Standard =================== ELF ist im Prinzip ein Standard für ausführbare Dateien. Es wird vorallem auf Unix Systemen benutzt und ersetzt dort das ältere und unflexiblere a.out. ELF ist dazu da, Maschienencode so anzuordnen das es Leicht, Schnell + Effektiv von einem Massenspeicher in den RAM eines Computers geladen und ausgeführt werden kann. Dazu beschreibt es, wie die Befehle in einer Datei Organisiert werden, und hat zusätliche Informationen wie sie interpretiert und geladen werden. Zb wird angegeben, ob und wo sich ein Datensegment befindet, löst Abhängigkeiten im Zusammenhang mit Shared Libraries auf etc. Der primäre Vorteil von ELF gegenüber a.out ist, das es flexibler ist. Vorallem der Umgang mit Dynamic Librarys und die Unterstützung von C++ (zb mit seinen Konstructoren, die vor main() ausgeführt werden müssen) wurde erherblich verbessert. 3.0) ELF - Einführung --------------------- Es gibt 3 verschiedene ELF Typen: - relocatable file Beinhaltet Code und Daten, um ein executable File oder ein Shared Object File zu erstellen. Die besonderheit ist, das der Code Positionsunabhängig ist, also an einer beliebigen Adresse stehen kann. [really? only here?] - executable file Beinhaltet ein Programm, das bereit ist ausgeführt zu werden. - shared object file Beinhaltet code und data, die man gegen zwei Sachen linken kann: - mit einem anderem relocatable oder shared object file um ein anderes shared object file zu erstellen - oder der dynamic linker kombiniert es mit einem executable und anderen shared objects um ein process image zu erstellen. Ein Object File braucht man für zwei Sachen: - program linking (ein executable erstellen) - program executing (ein programm in den speicher laden und laufen lassen) Um diese zwei Sachen effektiv durchführen zu können, kann man ein ELF File auf 2 Arten anschauen: Als aneinandereihung von Sections oder Segmente. Man merke, Segmente und Sections beschreiben die gleiche Datei, bloss aus verschiedenen Ansichten. Zu jeder Ansichtsweise gibt es noch eine Tabelle, die die einzelnen Abschnitte beschreibt, genannt Section Header für die Sections, und Programm Header für die Segmente (welche nicht mit den Segmenten des x86 Protected Modus zu verwechseln sind). Grafisch dargestellt sieht das etwa so aus: Linkable Executable ___________________________ ¦ ELF Header ¦ =========================== (optional, ignored) ¦ Programm Header Table ¦ -> Describes Segments ¦ ¦ ¦ ¦ ->--| =========================== | ¦ ¦ | |----> ¦ ¦ <---| | ¦------------- ¦ | |----> ¦ ¦ | Sections | ¦-------------------------¦ | Segments |----> ¦ ¦ | | ¦------------- ¦ <---| |----> ¦ ¦ | | ¦-------------------------¦ | | ¦ ¦ | |----> ¦ ¦ <---| | ¦ ¦ | =========================== -----<-¦ ¦ ¦ ¦ Describes Sections <- ¦ Section Header Table ¦ (optional, ignored) --------------------------- Sections werden zb von Compilern oder Linker gebraucht, und beschreiben eher, wo welche Teile in der Datei gespeichert sind. Die einzelnen Segmente werden in den Speicher geladen, also meistens Data + Code Segment. Werden vom Program Loader gebraucht. 3.1) ELF Header --------------- Der ELF Header ist immer am Anfang der Datei. Er beschreibt, wie er und der Rest der Datei aufgebaut ist, wie er interpretiert werden soll und wo sich zusätzliche Informationen zur Datei befinden. Eine detailierte Beschreibung des ELF Headers findet sich unter [1] oder [2]. Eine grafische Darstellung des ELF Headers, Sections und mehr findet sich unter [5]. Hier die Kurzzusammenfassung: #define EI_NIDENT (16) typedef struct { unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */ Elf32_Half e_type; /* Object file type */ Elf32_Half e_machine; /* Architecture */ Elf32_Word e_version; /* Object file version */ Elf32_Addr e_entry; /* Entry point virtual address */ Elf32_Off e_phoff; /* Program header table file offset */ Elf32_Off e_shoff; /* Section header table file offset */ Elf32_Word e_flags; /* Processor-specific flags */ Elf32_Half e_ehsize; /* ELF header size in bytes */ Elf32_Half e_phentsize; /* Program header table entry size */ Elf32_Half e_phnum; /* Program header table entry count */ Elf32_Half e_shentsize; /* Section header table entry size */ Elf32_Half e_shnum; /* Section header table entry count */ Elf32_Half e_shstrndx; /* Section header string table index */ } Elf32_Ehdr; Wie wir hier sehen, spezifieziert der ELF Header zb, wie gross der ELF Header selber ist, wo die Program Header Table oder Section Header Table startet, wie gross sie sind etc. Dadurch sind sie nicht an irgendeine Position gebunden und können irgendwo in der Datei auftreten. Dadurch kann man das File nach eigenem gutdünken organisieren. Wichtig ist, das ELF sehr flexibel ist, da es bei sehr verschiedenen Systemen zum einsatz kommen kann. e_ident (ELF Identification) beschreibt zb ob 32 Bit oder 64 Bit benutzt wird, oder ob die Datei in Big Endian oder Little Endian gespeichert wurde. e_type beschreibt den Object File Type. Das könnte sein: ET_NONE Kein File Type ET_REL Relocatable file ET_EXEC Executable file (ausführbare Files) ET_DYN Shared object file (Shared Librarys) ET_CORE Core File e_machine spezifiziert, auf welcher Architektur das File läuft (zb EM_SPARC für Sparc's oder EM_386 für Intel 80386+ Compatible Systeme). ELF Identification: Schauen wir uns einmal die ersten 16 Bytes eines Binarys mit dem Hex Editor an. e_ident zeigt, wie man sie Interpretieren muss: e_ident[]: #define ELFMAG0 0 /* Magic number byte 0 */ #define ELFMAG1 1 /* Magic number byte 1 */ #define ELFMAG2 2 /* Magic number byte 2 */ #define ELFMAG3 3 /* Magic number byte 3 */ #define EI_CLASS 4 /* File class byte index */ #define EI_DATA 5 /* Data encoding byte index */ #define EI_VERSION 6 /* File version byte index */ #define EI_PAD 7 /* Start of padding Bytes */ Die ersten 16 Bytes von /bin/ls von FreeBSD auf einem AMD: Adresse Hexadizmaler Inhalt ASCII 00000000: 7F 45 4C 46 01 01 01 09 00 00 00 00 00 00 00 00 .ELF.... Die ersten 4 Bytes sind immer "0x7f E L F", die sogenannte Magic Number, an der das System ELF Dateien Identifizieren kann (bei Windows ist das zb "MZ"). Das nächste Byte EI_CLASS, ist "0x01": #define ELFCLASSNONE 0 /* Invalid class */ #define ELFCLASS32 1 /* 32-bit objects */ #define ELFCLASS64 2 /* 64-bit objects */ Hier haben wir also 32 Bit. Danach kommt EI_DATA, welches wieder "0x01" ist: #define ELFDATANONE 0 /* Invalid data encoding */ #define ELFDATA2LSB 1 /* 2's complement, little endian */ #define ELFDATA2MSB 2 /* 2's complement, big endian */ Also Little Endian. Die File Version ist 0x01 (wird afaik selten bis nie gebraucht). Danach kommt das Padding, damit e_ident auf 16 Bytes kommt. Hier kann alles mögliche stehen, bei ungestrippten Binarys steht bei mir zb "FreeBSD". Wie wir bis jetzt sahen, lässt sich noch viele Informationen aus dem Header herauslesen. Der geneigte Leser kann hier noch weiter gehen und die restlichen Bytes des Binaries mit dem struct Elf32_Ehdr interpretieren. Detaillierte Informationen über den Inhalt der einzelnen Felder gibts wieder unter [1] und [2]. Ein gutes Tool um ELF Header auszulesen, oder allgemein Informationen ueber ELF Dateien zu erhalten ist readelf(1) oder objdump(1). 3.2) Sections ------------- Alle relocatable oder shared-object Files werden als eine Sammlung von Sections angeschaut. Sections beinhalten alle Informationen von einem Object File, ausser natürlich dem ELF Header, dem Program Header und der Section Header Table. ELF Header Section Header Table ----------- -------------------- | | - - - - - - - - - > | Section Header 0 | | e_shoff | | Section Header 1 |.... | | --- | ... | : ----------- | -------------------- : | : | : | Section Header Table Sections | Index 1. ------------------------ | | [Symbol Table] | <--- | | | [String Table] | | | | [.code] | | | | [.data] | ------------------------ e_shoff ist der Section Header Table Offset im Elf Header. Also der Anfang der Section Header's, vom Anfang der Datei an gezaehlt. Die "Section Header Table" ist eine Einanderreihung von "Section Headers". Jeder Section Header wird durch den "Section Header Table Index" eindeutig identifiziert. Die Section Header beschreiben also Sections, welche eigentlich nur eine Aneinanderreihung von Bytes im Object File sind. Zb. koennte eine Section die Symbol Table, den Code oder Daten beinhalten. Eine andere Section koennte .bss sein. Diese hat als Typ SHT_NOBITS (sie belegt keinen Speicherplatz im Object File) und die Attribute SHF_ALLOC (die Section existiert im Memory beim ausfuehren des Programms) und SHF_WRITE (auf die Section kann geschrieben werden). Jede Section hat also noch Optionen, was genau mit ihr gemacht werden soll. Die Section Header Table ist meistens am Ende eines ELF Files. Sections werden zb von Compilern, Assembler und Linker genutzt. Also werden Sections für das Linking benutzt (das Zusammenführen von mehreren Object Files), nicht aber für das Loading (das Ausführen eines Binarys, bzw kopieren dessen Inhaltes in den Speicher). Sections beschreiben zb, wo der Code Anfängt und aufhört, oder wo sich die statische Daten des Programmes befinden. Sections beinhalten aber vorallem Informationen für Compiler und Linker, zb Linking/Relocation Informationen, Symbole etc. 3.2.1) Section Header --------------------- typedef struct { Elf32_Word sh_name; /* Section name (string tbl index) */ Elf32_Word sh_type; /* Section type */ Elf32_Word sh_flags; /* Section flags */ Elf32_Addr sh_addr; /* Section virtual addr at execution */ Elf32_Off sh_offset; /* Section file offset */ Elf32_Word sh_size; /* Section size in bytes */ Elf32_Word sh_link; /* Link to another section */ Elf32_Word sh_info; /* Additional section information */ Elf32_Word sh_addralign; /* Section alignment */ Elf32_Word sh_entsize; /* Entry size if section holds table */ } Elf32_Shdr; Zuerst ist da der Name der Section. sh_type sagt ein wenig über den Inhalt aus, ist aber meistens "PROGBITS". Dh, das der Inhalt der Section vom Program vorgegeben sind. Andere Typen wären zb: SHT_SYMTAB Symbol Table SHT_STRTAB String Table SHT_DYNAMIC Informationen über Dynamic Linking SHT_REL relocation Informationen SHT_NOBITS die Section beantsprucht keinen Platz im File, zb .bss Die Flags geben vor, ob auf die Section geschrieben werden kann, ob sie in den Speicher geladen wird etc. Gültige Werte sind hier: SHF_WRITE 0x01 SHF_ALLOC 0x02 SHF_EXECINSTR 0x04 sh_addr ist die Position der Section bei der Ausführung, während sh_offset angiebt wo sich die Section im File befindet, und sh_size deren Grösse. Der Rest wird nicht oft genutzt. 3.2.2) Some Sections -------------------- Ein paar Sections: .text Ausführbare Daten (Code) .data Daten .rodata Read Only Data .bss Heap .rel.* Relocation Informationen der spezifischen Section .init ähnlich wie .text. Code der vor/nach dem eigentlichen Programm .fini ausgeführt werden soll. Zb. initialisatoren in C++ .got Global Offset Table .plt Procedure Linkage Table Mit dem Programm readelf(1) kann man die verschiedenen Sections anzeigen lassen: <--snip--> unreal:~ $ readelf -S /bin/ls There are 15 section headers, starting at offset 0x48ca0: Section Headers: [Nr] Name Type Addr Off Size ES Flg Lk Inf Al [ 0] NULL 00000000 000000 000000 00 0 0 0 [ 1] .init PROGBITS 080480ac 0000ac 00000b 00 AX 0 0 4 [ 2] .text PROGBITS 080480b8 0000b8 032c2a 00 AX 0 0 4 [ 3] .fini PROGBITS 0807ace4 032ce4 000006 00 AX 0 0 4 [ 4] .rodata PROGBITS 0807ad00 032d00 010d2c 00 A 0 0 32 [ 5] .data PROGBITS 0808ca40 043a40 00174c 00 WA 0 0 32 [...] [10] .bss NOBITS 0808e1a0 0451a0 00c1f0 00 WA 0 0 32 [11] .comment PROGBITS 00000000 0451a0 0029cc 00 0 0 1 [12] .note NOTE 00000000 047b6c 0010b8 00 0 0 1 [13] .note.ABI-tag NOTE 08048094 000094 000018 00 A 0 0 4 [14] .shstrtab STRTAB 00000000 048c24 00007b 00 0 0 1 Key to Flags: W (write), A (alloc), X (execute), M (merge), S (strings) [...] <--snap--> Alle Sections, die bei den Flags ein "A" haben, werden beim laden in den Speicher kopiert (mit dem Flag "X" haben sie ausführrechte, mit "W" darf der Kernel auf sie schreiben). Diese Sections werden dann auch als Segmente betrachtet (siehe weiter unten). 3.3) Segmente ------------- Die Program Header Table beinhaltet Program Segment Header, beschreibt also die Segmente des Objects. Segmente bestehen aus einer oder mehreren Sections, die so zusammengefasst werden. Die Segmente werden beim ausführen eines Programmes als ganzes in den Speicher geladen. 3.3.1) Programm Header ---------------------- typedef struct { Elf32_Word p_type; /* Segment type */ Elf32_Off p_offset; /* Segment file offset */ Elf32_Addr p_vaddr; /* Segment virtual address */ Elf32_Addr p_paddr; /* Segment physical address */ Elf32_Word p_filesz; /* Segment size in file */ Elf32_Word p_memsz; /* Segment size in memory */ Elf32_Word p_flags; /* Segment flags */ Elf32_Word p_align; /* Segment alignment */ } Elf32_Phdr; Die verschiednen Program Header beschreiben also exakt, wo die Segmente im ELF File (offset) und nachher im RAM (vaddr/paddr) existieren, und wie gross sie sind (filesz, memsz). Ausserdem haben Segmente, die in den Speicher geladen werden den p_type LOAD. Die Flags beschreiben Zugriffsrechte (r, rw etc) und p_align die Ausrichtung (zb 8 Bits). 3.3.2) Segment Praxis --------------------- Wir schauen uns mal mit hilfe von readelf(1) unser gutes altes ls binary an: unreal:~ $ readelf -S /bin/ls [...] Section Headers: [Nr] Name Type Addr Off Size ES Flg Lk Inf Al [ 0] NULL 00000000 000000 000000 00 0 0 0 [ 1] .init PROGBITS 080480ac 0000ac 00000b 00 AX 0 0 4 [ 2] .text PROGBITS 080480b8 0000b8 032f02 00 AX 0 0 4 [ 3] .fini PROGBITS 0807afbc 032fbc 000006 00 AX 0 0 4 [ 4] .rodata PROGBITS 0807afe0 032fe0 010c0c 00 A 0 0 32 [ 5] .data PROGBITS 0808cc00 043c00 00172c 00 WA 0 0 32 [ 6] .eh_frame PROGBITS 0808e32c 04532c 000004 00 WA 0 0 4 [ 7] .ctors PROGBITS 0808e330 045330 000008 00 WA 0 0 4 [ 8] .dtors PROGBITS 0808e338 045338 000008 00 WA 0 0 4 [ 9] .sbss PROGBITS 0808e340 045340 000000 00 W 0 0 1 [10] .bss NOBITS 0808e340 045340 00c1f0 00 WA 0 0 32 [11] .comment PROGBITS 00000000 045340 002281 00 0 0 1 [12] .note NOTE 00000000 0475c1 0010b8 00 0 0 1 [13] .note.ABI-tag NOTE 08048094 000094 000018 00 A 0 0 4 [14] .shstrtab STRTAB 00000000 048679 00007b 00 0 0 1 [...] Das sind also nochmal die verschiedenen Sections. Nun schauen wir mal die Segmente an: unreal:~ $ readelf -l /bin/ls Elf file type is EXEC (Executable file) Entry point 0x80480b8 There are 3 program headers, starting at offset 52 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align LOAD 0x000000 0x08048000 0x08048000 0x43bec 0x43bec R E 0x1000 LOAD 0x043c00 0x0808cc00 0x0808cc00 0x01740 0x0d930 RW 0x1000 NOTE 0x000094 0x08048094 0x08048094 0x00018 0x00018 R 0x4 Section to Segment mapping: Segment Sections... 00 .init .text .fini .rodata .note.ABI-tag 01 .data .eh_frame .ctors .dtors .bss 02 .note.ABI-tag unreal:~ $ Hier sieht man klar, das viele verschiedene Sections zu zwei unterschiedlichen Segmente zusammengefasst wurden. Eines (00) ist Readable und Executable (beinhaltet .text, .init etc, also Code). Das andere (01) beinhaltet Daten, ist also nur Readable und Writable (obwohl bei den Flags nur Writable richig bindend ist). Die Segmente mit dem Typ LOAD werden beim ausführen des Binaries in den Speicher geladen, un zwar an die "Virtuelle" Adresse des Segmentes. Man beachte, das die Mem Size des Segments 01 sehr viel grösser ist als die File Size. Das desswegen, weil es die Section .bss, also der Heap, beinhaltet und dadurch ein bisschen Platz für ihn reserviert. 4) Prozesse =========== 4.1) Übersicht -------------- In Linux sieht ein Prozess im Speicher etwa so aus: ¦-----------------------------------------------¦ ¦ stack stack ¦ ¦-----------------------------------------------¦ ¦ ... free space ¦ ¦-----------------------------------------------¦ ¦ bss .bss section (heap) ¦ ¦-----------------------------------------------¦ ¦ data .data section ¦ ¦-----------------------------------------------¦ 0x08048000 ¦ code .text section ¦ ¦-----------------------------------------------¦ ¦ ... free space ¦ ¦-----------------------------------------------¦ 0x0000000 Diese aufstellung ist keineswegs zwingend, und kann je nach Betriebsystem verschieden sein (zb ist kann der Stack unter der Code Section sein). 4.1) Heap (.bss) ---------------- Der Heap ist zusammenhängender Speicherbereich für dynamischen Speicher. Auf den Heap lässt sich also Speicher anfordern und wieder freigeben, damit wächst und schrumpft auch die grösse des Heap. Im gegensatz zum Stack wächst der Heap nach oben, also zu grösseren Speicheradressen. In C lässt sich dynamischer Speicher durch malloc(3) und co anfordern. Hier ein beispiel: ptr = (int *) (malloc (sizeof (int) )); Malloc reserviert hier einen Speicherbereich der grösse "sizeof(int)" und gibt einen Zeiger auf dessen Anfang zurück. Mit brk(2) und sbrk(2) kann man die grösse des Heap verändern. Der Heap ist genauso anfällig für Buffer Overflows wie der Stack, blos lässt sich hier nicht die Return Adresse verändern. Man kann aber andere Daten verändern, zb nachträglich Identifikationen oder Zugriffsrechte. Mehr Informationen über den Heap gibts unter [4] und [6]. 4.2) Code (.text) ----------------- Die .text Section beinhaltet den ausführbaren Programmcode (opcode). Die gesamte .text Section wird in den Virtuellen Addressraum des Prozesses gemmap't. Nach dem Ausführen der Initialisierungs und Startroutinen wird an die erste Adresse in dieser Section gejumpt, das Program/der Prozess startet also seine Ausführung. 4.3) Global Offset Table (.got) ------------------------------- Die GOT beinhaltet eine Tabelle von absoluten Adressen, welche man verändern kann (sie sind also im .data Segment). Wie wir beim Dynamic Linking Kapitel noch sehen werden, enthält Position Independant Code (PIC) keine Absoluten Adressen. Um aber trotzdem veränderbare, absolute Adressen zu gebrauchen, wird die GOT benutzt. Der Dynamic Linker wird alle GOT Einträge anpassen, für welche er Symbol Einträge besitzt, wenn er das Data Segment lädt. Beispiel: [GOT]: [00] 0x8049470 <.dynamic> [01] (nil) [Code, um die verwendete Library zu identifizieren] [02] (nil) [Adresse der Symbol Resolution Routine des Dynamic Linkers] [03] 0x8048332 [04] 0x8048342 [00] Adresse der "Dynamic Struktur", referenziert über das Symbol _DYNAMIC. Diese wird von Programmen gebraucht, um ihre eigene Dynamic Struktur zu finden, ohne vorher die Relocation Entrys abgearbeitet zu haben. Dies ist insbesondere für den Dynamic Linker wichtig, weil er sich initialisieren muss, ohne das andere Programme sein Memory Image relocat'en (das wäre ja wieder der Dynamic Linker). [01] und [02] werden vom Dynamic Linker eingetragen bevor das Code Segment startet. 4.4) Procedure Linkage Table (.plt) ----------------------------------- Die .plt besteht Hauptsächlich aus kleinen Code Fragmenten, nämlich immer ein "jmp *", "push" und wieder ein "jmp". Diese werden gebraucht, um externe Funktionen (zb aus der libc) aufzurufen. Wie das genau funktioniert wird im Abschnitt "ELF Dynamic Linking" genauer beschrieben. Die .plt sieht zb so aus: 080482c8 <.plt>: 80482c8: ff 35 54 94 04 08 pushl 0x8049454 80482ce: ff 25 58 94 04 08 jmp *0x8049458 80482d4: 00 00 add %al,(%eax) 80482d6: 00 00 add %al,(%eax) 80482d8: ff 25 5c 94 04 08 jmp *0x804945c 80482de: 68 00 00 00 00 push $0x0 80482e3: e9 e0 ff ff ff jmp 80482c8 <_init+0x30> 80482e8: ff 25 60 94 04 08 jmp *0x8049460 80482ee: 68 08 00 00 00 push $0x8 80482f3: e9 d0 ff ff ff jmp 80482c8 <_init+0x30> 80482f8: ff 25 64 94 04 08 jmp *0x8049464 80482fe: 68 10 00 00 00 push $0x10 8048303: e9 c0 ff ff ff jmp 80482c8 <_init+0x30> 8048308: ff 25 68 94 04 08 jmp *0x8049468 804830e: 68 18 00 00 00 push $0x18 8048313: e9 b0 ff ff ff jmp 80482c8 <_init+0x30> Der erste eintrag der PLT, ist dazu da den Dynamic Linker zu starten. Dieser trägt automatisch zwei Einträge in die GOT ein (siehe vorheriges und nächstes Kapitel). 4.5) .data ----------- FIXME Die .data Section enthält alle Statischen Daten eines programmes. In C Programmen zb Globake Variablen, static Variablen etc. 4.6) ELF Dynamic Linking ------------------------ Funktionen von Shared Lybraries sind ja PIC, dh, sie können irgendwo in den Speicher geladen werden. Damit sind die Adressen der Funktionen der Shared Lybraries zur Compile-Time nicht bekannt, und dh, man muss sie zur Run-Time herausfinden. Um das zu bewerkstelligen muss im Prinzip irgendwo eine Dummy Sprungaddresse existieren, wohin der Code springen kann (ein call muss immer eine gültige Adresse haben). Diese Dummy Adresse steht in der .plt Section (Procedue Linkage Table). Sobald also eine Funktion aufgerufen wird, die in einer Shared Library ist, wird zuerst an diese Dummy Adresse gesprungen. Die .plt gehört aber zum .data Segment, ist also Write-Only. Desshalb könnte man dort die Sprungaddresse nicht verändern, also lässt man sie auf einen Eintrag in .got zeigen. Da man aber die Adresse der aufzurufenden Funktion nicht weiss, springt man von der .got zu einem Programm, das dass herausfindet, nämlich dem RTLD (RunTime Dynamic Linker). Dieses Programm (meist "ld.so") löst dann das Symbol auf und schreibt die Adresse der Funktion in die .got Section. Von nun an wird jedesmal, wenn die Funktion aufgerufen wird, zuerst nach .plt gesprungen, und von dort aus nach .got, und von dort direkt zur Funktion (man muss dass Dynamic Linking also nur noch einmal machen). Da das ganze am Anfang ziemlich kompliziert zum verstehen ist, habe ich eine Art Schritt-für-Schritt Anleitung geschrieben, plus dazugehöriger Grafik. Ich hoffe das diese zum Verständniss beihilft. 1) Im Code soll die Funktion "puts" aufgerufen werden, zur zur Libc gehört. Der dazugehörige Befehl ist zb "call puts". 2) "puts" ist ein Symbol, welches zu einem Eintrag in der .plt Section aufgelöst wird. Dann wird zum dazugehörigen PLT Eintrag gesprungen. 3) Der erste Befehl vom PLT Eintrag ist ein "jmp" Befehl in die .got Section des dazugehörigen Symbols (hier puts) springt. 4) in der .got steht die Addresse des nächsten Befehles in der .plt Section von vorher. Wichtig ist hier zu wissen, dass dann dieser jmp Befehl abgeändert wird, und nach der Auflösung des Symbols auf die dazugehörige Funktion der Dynamic Library zeigt. Das wird hier abgeändert, weil die .plt Section read-only ist. 5) Wir sind also wieder im .plt. Jetzt wird ein Relocation Offset auf den Stack gepusht, damit nachher der RTLD das Symbol zur Funktion identifizieren kann. Genauer gesagt, kann man durch den Offset das Symbol, welches zu resolven ist, und gleichzeitig den GOT Eintrag identifizieren. Der Relocation Offset hat den Typ R_386_JMP_SLOT, und zeigt in die Relocation Table. Er spezifiziert den GOT Entry vom vorherigem jmp Befehl, und ausserdem noch den Symbol Table Index, also welches Symbol genau benutzt wird. FIXME [ein bisschen aufräumen in diesem abschnitt] 6) Wir springen zum plt Eintrag 0. Dieser ist speziell, da er den RTLD aufruft. Es wird auch noch der Wert des Zweiten GOT Eintrages auf den Stack gepusht, für den RTDL. 7) Und dann wird zum GOT Eintrag Nr 2 gesprungen, der die Adresse des RTLD beinhaltet. 8) Der RTLD macht dann folgende Sachen: 1) Relocation Table Entry aus dem Stack suchen 2) Das Symbol resolven 9) 3) Die Adresse des resolvten Symboles an die Addresse in der GOT schreiben Beim nächsten aufruf der Library Funktion steht in der GOT Section direkt die Adresse der Dynamik Library Funktion, der aufruf des RTDL entfällt. .code --------------- (2) ¦ call ¦ ---------------------- ¦ ¦ | --------------- | | .plt | ---------------------------------- | ¦PLT0: ¦ <-|-------- ¦ push GOT[1] ¦ | ¦ ¦ jmp GOT[2] ¦ --|-------¦---- ¦ ¦ | ¦ ¦(7) ¦PLTn: ¦ | ¦ ¦ ¦ jmp GOT[x+n] ¦ | ¦ ¦ ¦ push n ¦ ¦ ¦ ¦ ¦ jmp PLT0 ¦ ¦ (6)¦ ¦ ¦ ¦ ¦ ¦ ¦ ¦PLTn + 1: ¦ <-- ¦ ¦ ¦ jmp GOT[x+n+1] ¦ ----- ¦ ¦ ¦ push n+1 [5] ¦ <---¦--- ¦ ¦ ¦ jmp PLT0 ¦ ----¦--|--- ¦ ---------------------------------- ¦ | ¦ ¦ | ¦ .got (3)¦ | ¦ -------------------------- ¦ | ¦ ¦[00] ¦ ¦ ¦ ¦ ¦[01] ¦ ¦ ¦ ¦ ¦[02] ¦ <-----------¦--|------- ¦ ¦ ------------¦--|---¦ ¦ ¦ ¦ ¦ ¦ ¦[n+1]: ¦ <------------ | ¦ ¦ addr: PLTn + 1 ¦ ---------------- ¦ (8) ¦ : ¦ (4) ¦ --------------:----------- ¦ : v RTLD : ------------------ ....................... ¦ ¦ (9) ------------------ Mehr Informationen zu Dynamic Linking gibts unter [7], [8], [9] und [10]. 5) Elf loading ============== 5.1) Der flow eines programmes im memory ---------------------------------------- [ Oder: Vor main(), das unfassbare ] [ Oder: there's live after main()! ] Sobald alle Segmente in den Speicher gemmap't sind, wird nicht einfach die main() Funktion aufgerufen, bzw mit dem Ausführen des Hauptprogrammes begonnen. ELF unterstützt sogennante Initializers und Konstruktoren. 5.2) Initializers und Finalizers -------------------------------- Manchmal ist es nützlich, ein paar Sachen vor main() auszuführen, zb initialisierer für (statische) globale Variablen. Dafür erstellt man eine Routine, und erstellt einen Zeiger auf diese Routine in der .init Section. Beim Linken werden alle .init Section zusammengefasst, und somit hat man eine Liste aller Routinen, die vorher aufgerufen werden sollen. Das gleiche gilt für Finalizers und die .fini Section. 5.3) Konstruktoren und Desktruktoren ------------------------------------ Der Nachteil bei Initializers ist, das alle Routinen in unverhersagbarer Reihenfolge gestartet werden. Manchmal ist es aber nötig, das Code früher als anderer Code ausgeführt werden muss. Zb System Library Konstrukturen müssen früher ausgeführt werden als C++ Application Konstruktoren. Beim Starten des Executables werden erst .init Calls für Library Level initialisation gestartet, und danach Konstruktoren des Programmen von der .ctor. Das gleiche bei .fini und .dtor. 6.0) Schlusswort ---------------- 6.1) Greets ----------- socma, pr1, bf, svoern 6.2) References --------------- [1] "Armouring the ELF: Binary encryption on the Unix Platform", von grugq und scut, Phrack Volume 0x0b, Issue 0x3a, Phile #0x05 of 0x0e [2] GABI - generic Application Binary Interface http://www.caldera.com/developers/gabi/latest [3] System V Application Binary Interface, Intel386 Architecture Processor Supplemente, Fourth Edition http://www.caldera.com/developers/devspecs/ [4] "w00w00 on Heap Overflows", by Matt Conover (Shok) & w00w00 http://www.w00w00.org/files/articles/heaptut.txt [5] ELF Files (elf-info.txt), Author unknown http://home.datacomm.ch/prutishauser/elf/elf-info.txt [6] Vudo - An object superstitiously believed to embody magical powers by Michael "Maxx" Kaempf, Synnergy Networks [7] Reversing the ELF - Stepping with GDB during PLT uses and .GOT fixup by mayhem [8] Cheating the ELF - Subversive Dynamic Linking to Libraries by grugq [9] Shared Library Call Redirection vie ELF PLT Infection by Silvio Cesare, Phrack Volume 0xa, Issue 0x38, File 0x07 of 0x10 [10] "Linkers & Loaders", von John R. Levins, Morgan Kaufmann Publishers