kiddie
.:: @OREL ::.

1] - Structure d'un fichier ELF 

Linux et la majorité des systèmes d'exploitation *NIX utilisent le format de fichier
Executable and Linking Format(ELF) pour les fichiers binaires exécutables. Celui-ci 
est définit par 3 structures principales qui sont le ELF Header(Elf(32|64)_Ehdr) le 
Program header(Elf(32|64)_Phdr) et le Section header(Elf(32|64)_Shdr). La disposition
et définition de ces headers est la suivante pour une architecture 64 bit : 


                        +-------------------------------------------+
                        |                                           |
                        |            ELF header (Ehdr)              |
                        |                                           |
                        |                                           |
                        | typedef struct {                          |
                        |                unsigned char e_ident[16]  |
                        |                uint16_t      e_type;      |
                        |                uint16_t      e_machine;   |
                        |                uint32_t      e_version;   |
                        |                Elf64_Addr    e_entry;     |
                        |                Elf64_Off     e_phoff;     |
                        |                ElfN_Off      e_shoff;     |
                        |                uint32_t      e_flags;     |
                        |                uint16_t      e_ehsize;    |
                        |                uint16_t      e_phentsize  |
                        |                uint16_t      e_phnum;     |
                        |                uint16_t      e_shentsize  |
                        |                uint16_t      e_shnum;     |
                        |                uint16_t      e_shstrndx;  |
                        |            } Elf64_Ehdr;                  |
                        |                                           |
                        +-------------------------------------------+
                        |                                           |
                        |           Program header (Phdr)           |
                        |                                           |
                        |                                           |
+--+   phdr[0].p_offset |    typedef struct {                       |
|                       |                   uint32_t   p_type;      |
|                       |                   uint32_t   p_flags;     |
|  +   phdr[1].p_offset |                   Elf64_Off  p_offset;    |
|  |                    |                   Elf64_Addr p_vaddr;     |
|  |           .        |                   Elf64_Addr p_paddr;     |
|  |           .        |                   uint64_t   p_filesz;    |
|  |           .        |                   uint64_t   p_memsz;     |
|  |                    |                   uint64_t   p_align;     |
|  |   phdr[n].p_offset |               } Elf64_Phdr;               |
|  |                    |                                           |
|  |                    +-------------------------------------------+ <-+
|  |                +-> |                                           |   |
|  |  segment n°0   |   |              section .foo                 |   +--------------------+
+-------------------+   |                                           |   |                    |
   |                |   +-------------------------------------------+ <-+                    |
   |                +-> |                                           |   |                    |
   |                +-> |              section .bar                 |   +----------------+   |
   |  segment n°1   |   |                                           |   |                |   |
   +----------------+   +-------------------------------------------+ <-+                |   |
                    |   |                                           |                    |   |
                    |   |                   ...                     |                    |   |
                    |   |                                           |                    |   |
                    |   +-------------------------------------------+ <-+                |   |
                    |   |                                           |   |                |   |
                    +-> |              section .ack                 |   +------------------+ |
                        |                                           |   |                | | |
                        +-------------------------------------------+ <-+                | | |
                        |                                           |                    | | |
                        |           Section header (Shdr)           |                    | | |
                        |                                           |                    | | |
                        |  typedef struct {                         | shdr[0].sh_offset +----+
                        |                 uint32_t   sh_name;       |                    | |
                        |                 uint32_t   sh_type;       |                    | |
                        |                 uint64_t   sh_flags;      | shdr[1].sh_offset  + |
                        |                 Elf64_Addr sh_addr;       |                      |
                        |                 Elf64_Off  sh_offset;     |         .            |
                        |                 uint64_t   sh_size;       |         .            |
                        |                 uint32_t   sh_link;       |         .            |
                        |                 uint32_t   sh_info;       |                      |
                        |                 uint64_t   sh_addralign;  | shdr[n].sh_offset +--+
                        |                 uint64_t   sh_entsize;    |
                        |             } Elf64_Shdr;                 |
                        |                                           |
                        +-------------------------------------------+

L'en-tête unique ELF header(Edhr) est disposée en début de fichier et apporte notamment 
des renseignements sur le décalage(par rapport au début du fichier) des autres en-têtes 
ainsi que leurs tailles. 

Les en-têtes Program Headers(Phdr) définissent des segments. Les segments sont 
nécessaires à l'exécution du programme, ils peuvent contenir une ou plusieurs sections.
Sur notre schéma, les relations segments et sections sont les suivantes : 

			       Segment 0       Segment 1

			      /         \      /        \

			Section .foo  Section .bar  Section .ack

Les en-têtes Section Headers(Shdr) définissent des sections. Les sections quant à elles 
sont utilisées à l'édition de liens(linking) du programme. Elles ne jouent aucun rôle 
dans l'exécution de celui-ci.


2 ] Segments et sections, deux visions du format

Les segments et sections sont deux notions essentielles au format ELF et à ne pas confondre.
Elles ont toutes deux un rôle bien définit et sont indépendantes l'une de l'autre, gardez
en tête ces deux visions du format : 

		  statique                                    dynamique

	+---------------------------+                +---------------------------+
	|                           |                |                           |
	|     ELF Header (Ehdr)     |                |     ELF Header (Ehdr)     |
	|                           |                |                           |
	+---------------------------+                +---------------------------+
	|                           |                |                           |
	|   Program header (Phdr)   |                |   Program header (Phdr)   |
	|                           |                |                           |
	+---------------------------+                +---------------------------+
	|                           |                |                           |
	|       section .foo        |                |       segment n°0         |
	|                           |                |                           |
	+---------------------------+                +---------------------------+
	|                           |                |                           |
	|       section .bar        |                |       segment n°1         |
	|                           |                |                           |
	+---------------------------+                +---------------------------+
	|                           |                |                           |
	|       section .ack        |                |       segment n°2         |
	|                           |                |                           |
	+---------------------------+                +---------------------------+
	|                           |                |                           |
	|   Section header (Shdr)   |                |       segment n°3         |
	|                           |                |                           |
	+---------------------------+                +---------------------------+


Pour appuyer mes propos, je vous propose de supprimer le Section header(Shdr) d'un
programme et d'observer un quelconque changement à l'exécution de celui-ci : 

voidboy@voidsys:~/fun/assembly$ cat foo.c #include void main(void) { printf("still learning\n"); } voidboy@voidsys:~/fun/assembly$ gcc foo.c voidboy@voidsys:~/fun/assembly$ readelf -h a.out En-tête ELF: Magique: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 Classe: ELF64 Données: complément à 2, système à octets de poids faible [..] Version: 1 (actuelle) OS/ABI: UNIX - System V Version ABI: 0 Type: DYN (fichier objet partagé) Machine: Advanced Micro Devices X86-64 Version: 0x1 Adresse du point d'entrée: 0x1050 Début des en-têtes de programme : 64 (octets dans le fichier) Début des en-têtes de section : 14624 (octets dans le fichier) Fanions: 0x0 Taille de cet en-tête: 64 (octets) Taille de l'en-tête du programme: 56 (octets) Nombre d'en-tête du programme: 11 Taille des en-têtes de section: 64 (octets) Nombre d'en-têtes de section: 29 Table d'index des chaînes d'en-tête de section: 28 voidboy@voidsys:~/fun/assembly$ dd if=a.out of=b.out count=14623 bs=1 14623+0 enregistrements lus 14623+0 enregistrements écrits 14623 octets (15 kB, 14 KiB) copiés, 0,0374341 s, 391 kB/s voidboy@voidsys:~/fun/assembly$ chmod +x b.out voidboy@voidsys:~/fun/assembly$ ./b.out still learning
3] - Segments et chargement d'un binaire en mémoire Les segments d'un binaire permettent de renseigner le loader sur la manière de charger le binaire en mémoire, le couple load_elf_binary()/elf_map() assure ce rôle de loader et est défini dans fs/binfmt_elf.c ligne 681 et 348(kernel v5.5.9) respectivement :
/* * These are the functions used to load ELF style executables and shared * libraries. There is no binary dependent code anywhere else. */ static int load_elf_binary(struct linux_binprm *bprm) { [...] /* Now we do a little grungy work by mmapping the ELF image into the correct location in memory. */ for(i = 0, elf_ppnt = elf_phdata; i < loc->elf_ex.e_phnum; i++, elf_ppnt++) { int elf_prot, elf_flags; unsigned long k, vaddr; unsigned long total_size = 0; if (elf_ppnt->p_type != PT_LOAD) continue; [...] } error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt, elf_prot, elf_flags, total_size); } static unsigned long elf_map(struct file *filep, unsigned long addr, const struct elf_phdr *eppnt, int prot, int type, unsigned long total_size) { unsigned long map_addr; unsigned long size = eppnt->p_filesz + ELF_PAGEOFFSET(eppnt->p_vaddr); unsigned long off = eppnt->p_offset - ELF_PAGEOFFSET(eppnt->p_vaddr); addr = ELF_PAGESTART(addr); size = ELF_PAGEALIGN(size); /* mmap() will return -EINVAL if given a zero size, but a * segment with zero filesize is perfectly valid */ if (!size) return addr; /* * total_size is the size of the ELF (interpreter) image. * The _first_ mmap needs to know the full size, otherwise * randomization might put this image into an overlapping * position with the ELF binary image. (since size < total_size) * So we first map the 'big' image - and unmap the remainder at * the end. (which unmap is needed for ELF images with holes.) */ if (total_size) { total_size = ELF_PAGEALIGN(total_size); map_addr = vm_mmap(filep, addr, total_size, prot, type, off); if (!BAD_ADDR(map_addr)) vm_munmap(map_addr+size, total_size-size); } else map_addr = vm_mmap(filep, addr, size, prot, type, off); if ((type & MAP_FIXED_NOREPLACE) && PTR_ERR((void *)map_addr) == -EEXIST) pr_info("%d (%s): Uhuuh, elf segment at %px requested but [..]\n", task_pid_nr(current), current->comm, (void *)addr); return(map_addr); }
Seuls les segments de p_type PT_LOAD se verront mapper en mémoire via l'appel système mmap(), celui-ci est au coeur du processus de mappage, il permet entre autres de fixer la taille, les permissions et l'adresse de départ de la zone à inscrire en mémoire. On peut afficher tous les segments d'un fichier ELF à l'aide de la commande readelf -l, si on l'exécute sur notre binaire a.out utilisé plus haut, voici la sortie obtenue :
voidboy@voidsys:~/fun/assembly$ readelf -l a.out Type de fichier ELF est DYN (fichier objet partagé) Point d'entrée 0x1050 Il y a 11 en-têtes de programme, débutant à l'adresse de décalage 64 [...] Type Décalage Adr.virt Adr.phys. Taille fichier Taille mémoire Fanion Alignement LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000560 0x0000000000000560 R 0x1000 LOAD 0x0000000000001000 0x0000000000001000 0x0000000000001000 0x00000000000001bd 0x00000000000001bd R E 0x1000 LOAD 0x0000000000002000 0x0000000000002000 0x0000000000002000 0x0000000000000158 0x0000000000000158 R 0x1000 LOAD 0x0000000000002db8 0x0000000000003db8 0x0000000000003db8 0x0000000000000258 0x0000000000000260 RW 0x1000 [...]
On dénombre 4 entrées de p_type PT_LOAD, chaque entrée dispose de valeurs pour faire la relation fichier <=> mémoire, d'où les deux visions évoquées plus tôt. Ces valeurs ne sont pas choisies au hasard et respecte une règle bien précise qu'on peut retrouver dans le man elf : "Loadable process segments must have congruent values for p_vaddr and p_offset modulo the page size. Values of zero and one mean no alignment is required. Otherwise, p_align should be a positive, integral power of two, and p_vaddr should equal p_offset, modulo p_align" Autrement dit : p_vaddr % page_size == p_offset % page_size Mais pourquoi cette restriction ? Là encore on trouve la réponse dans la doc' : "As the system creates or augments a process image, it logically copies a file’s segment to a virtualmemory segment. When—and if—the system physically reads the file depends on the program’s execu-tion behavior, system load, etc. A process does not require a physical page unless it references the logicalpage during execution and processes commonly leave many pages unreferenced. Therefore delayingphysical reads frequently obviates them, improving system performance. To obtain this efficiency inpractice, executable and shared object files must have segment images whose file offsets and virtualaddresses are congruent, modulo the page size." Source : https://www.cs.yale.edu/flint/cs422/doc/ELF_Format.pdf Si maintenant on schématise les parties de notre ELF référencées dans les 4 segments PT_LOAD, on obtient les 4 zones suivantes : Segment n°0 [ R ] Segment n°2 [ R ] 0x0000 +---------------------+ 0x0000 +-----------------------+ 0x2000 +-----------------------+ | | | | | | | Segment n°0 [ R ] | | ELF Header (Ehdr) | | | | | | | | | 0x0560 +---------------------+ 0x0064 +-----------------------+ | | | | | | | "still learning\n" | | | | Program header (Phdr) | | | | | | | | | 0x1000 +---------------------+ 0x009c +-----------------------+ | | | | | . | | | | Segment n°1 [ R E ] | | . | | | | | 0x0560 +-----------------------+ 0x2158 +-----------------------+ 0x11bd +---------------------+ | | Segment n°1 [ R E ] Segment n°3 [ R W ] | | | | 0x1000 +-----------------------+ 0x2db8 +-----------------------+ 0x2000 +---------------------+ | | | | | | | push %rbp | | | | Segment n°2 [ R ] | | mov %rsp,%rbp | | | | | | . | | | 0x2158 +---------------------+ | . | | variables statiques | | | | . | | non initialisées | 0x2db8 +---------------------+ | call <puts@plt> | | | | | | pop %rbp | | | | Segment n°3 [ R W ] | | ret | | | | | | | | | 0x3010 +---------------------+ 0x11bd +-----------------------+ 0x3010 +-----------------------+ Seuls 2765 octets sont mappés en mémoire soit à peu près 10% du fichier, évidement c'est un cas extrême où le fichier n'est composé que d'un printf mais on notera tout de même que, cette contrainte d'avoir l'offset du fichier congru à l'adresse virtuelle modulo la taille d'une page engendre un grand nombre de remplissage par des 0 à l'intérieur du fichier. On connaît désormais l'agencement de nos segments et leurs contenus, il serait intéressant de voir le tout prendre forme sous gdb :
voidboy@voidsys:~/fun/assembly$ gdb -q a.out Reading symbols from a.out... (No debugging symbols found in a.out) (gdb) b main Breakpoint 1 at 0x1139 (gdb) r Starting program: /home/voidboy/fun/assembly/a.out Breakpoint 1, 0x0000555555555139 in main () (gdb) info proc mappings process 9518 Mapped address spaces: Start Addr End Addr Size Offset objfile 0x555555554000 0x555555555000 0x1000 0x0 [..]/a.out 0x555555555000 0x555555556000 0x1000 0x1000 [..]/a.out 0x555555556000 0x555555557000 0x1000 0x2000 [..]/a.out 0x555555557000 0x555555558000 0x1000 0x2000 [..]/a.out 0x555555558000 0x555555559000 0x1000 0x3000 [..]/a.out 0x7ffff7dca000 0x7ffff7def000 0x25000 0x0 [..]/libc-2.29.so 0x7ffff7def000 0x7ffff7f62000 0x173000 0x25000 [..]/libc-2.29.so 0x7ffff7f62000 0x7ffff7fab000 0x49000 0x198000 [..]/libc-2.29.so 0x7ffff7fab000 0x7ffff7fae000 0x3000 0x1e0000 [..]/libc-2.29.so 0x7ffff7fae000 0x7ffff7fb1000 0x3000 0x1e3000 [..]/libc-2.29.so 0x7ffff7fb1000 0x7ffff7fb7000 0x6000 0x0 0x7ffff7fce000 0x7ffff7fd1000 0x3000 0x0 [vvar] 0x7ffff7fd1000 0x7ffff7fd2000 0x1000 0x0 [vdso] 0x7ffff7fd2000 0x7ffff7fd3000 0x1000 0x0 [..]/ld-2.29.so 0x7ffff7fd3000 0x7ffff7ff4000 0x21000 0x1000 [..]/ld-2.29.so 0x7ffff7ff4000 0x7ffff7ffc000 0x8000 0x22000 [..]/ld-2.29.so 0x7ffff7ffc000 0x7ffff7ffd000 0x1000 0x29000 [..]/ld-2.29.so 0x7ffff7ffd000 0x7ffff7ffe000 0x1000 0x2a000 [..]/ld-2.29.so 0x7ffff7ffe000 0x7ffff7fff000 0x1000 0x0 0x7ffffffde000 0x7ffffffff000 0x21000 0x0 [stack] 0xffffffffff600000 0xffffffffff601000 0x1000 0x0 [vsyscall] (gdb) shell head -n 5 /proc/9518/maps 555555554000-555555555000 r--p 00000000 08:02 1445169 [..]/a.out 555555555000-555555556000 r-xp 00001000 08:02 1445169 [..]/a.out 555555556000-555555557000 r--p 00002000 08:02 1445169 [..]/a.out 555555557000-555555558000 r--p 00002000 08:02 1445169 [..]/a.out 555555558000-555555559000 rw-p 00003000 08:02 1445169 [..]/a.out
Il y a bien nos 4 segments listés dans le Program header, cependant, il semblerait qu'un soit en doublon, l'offset 0x2000 et présent 2 fois en mémoire avec les mêmes permissions, à savoir la lecture. Si on fait une analyse de la mémoire du segment on obtient la sortie suivante :
(gdb) disas 0x555555557000,0x555555558000 [...] 0x0000555555557fd0 <puts@got.plt+0>: rcr $0xe4,%ah 0x0000555555557fd3 <puts@got.plt+3>: idiv %edi 0x0000555555557fd5 <puts@got.plt+5>: jg 0x555555557fd7 <puts@got.plt+7> 0x0000555555557fd7 <puts@got.plt+7>: add %al,(%rax) [...]
En fait, le binaire a été compilé avec l'option RELRO, pour RELocation Read Only c'est le comportement par défaut sur les Ubuntu depuis la 8.10 et sur la majorité des dernières distributions, cette option permet la résolution de tous les symboles au début de l'exécution. Cela comble les faiblesses du Lazy binding qui obligé à avoir un segment RW pour la table .got.plt, grâce à cette option, il n'est plus possible d'écraser les entrées de la table .got.plt pour modifier le flux d'exécution d'un programme. Notre programme compte donc 5 segments au final, tous aligné sur la taille d'une page mémoire, ici, 4096 octets soit 4KB.

Tout est faux tout est conforme.