Dans cet article on verra comment ELF lie les différentes bibliothèques qu’un programme importe. Pour commencer il faut bien différencier 2 types de linkage: statique et dynamique, le linkage statique se fait à partir du flag GCC -static alors que le dynamique est par défaut. La différence entre ces 2 méthodes réside dans la taille de l’exécutable, en effet lors d’un linkage statique vous allez importer le code source de toutes les fonctions/constantes externes utilisées dans votre exécutable, dans le linkage dynamique, vous devrez juste faire passerelle avec la PLT et la GOT pour accéder à ces fonctions/constantes. Alors ça peut aussi jouer sur la performance de vos programmes, en effet comme vous accéderez à la même page mémoire (ou aux mêmes), les instructions seront sûrement déjà en cache, alors que les instructions sur une bibliothèque externe qui est chargée sûrement à des adresses différentes sera donc plus lente à charger car pas en cache.
Il n’y a pas grand chose à dire sur le linkage statique, il faut juste comprendre que tout est mis dans la section .text. Dans “tout” j’entends constantes globales et fonctions / procédures externes utilisées.
Là par contre c’est un chouilla plus compliqué… :). Je vais essayer d’être le plus simple possible. On va prendre quelques structures de données de elf.h
typedef uint32_t Elf32_Addr;
typedef uint32_t Elf32_Word;
typedef struct elf32_rel {
Elf32_Addr r_offset; /* Adresse */
Elf32_Word r_info; /* Type de relocation et index du symbole */
} Elf32_Rel;
typedef struct elf64_rel {
Elf64_Addr r_offset; /* Adresse */
Elf64_Xword r_info; /* Type de relocation et index du symbole */
} Elf64_Rel;
/* How to extract and insert information held in the r_info field. */
#define ELF32_R_SYM(val) ((val) >> 8)
#define ELF32_R_TYPE(val) ((val) & 0xff)
typedef struct elf32_sym
{
Elf32_Word st_name; /* Nom du symbole (Index dans la table des strings (DT_STRTAB)) */
Elf32_Addr st_value; /* Valeur du symbole */
Elf32_Word st_size; /* Taille du symbole */
unsigned char st_info; /* Binding et type du symbole */
unsigned char st_other; /* Visibilité du symbole sous glibc >= 2.2 */
Elf32_Section st_shndx; /* Index de la section */
} Elf32_Sym;
typedef struct elf64_sym
{
Elf64_Word st_name; /* Nom du symbole (Index dans la table des strings (DT_STRTAB)) */
unsigned char st_info; /* Binding et type du symbole */
unsigned char st_other; /* Aucune definition, 0 */
Elf64_Half st_shndx; /* Index de la section */
Elf64_Addr st_value; /* Valeur du symbole */
Elf64_Xword st_size; /* Taille du symbole */
} Elf64_Sym;
Il y a quelques sections dynamiques à connaître sur ELF:
Tag de Section Dynamique | Description |
---|---|
DT_STRTAB | Pointe sur la table des strings. Cette table contient simplement des strings séparées par un null byte ('\x00'). |
DT_SYMTAB | Pointe sur la table des symboles. Cette table contient les informations pour louer et relouer les définition et références symboliques du programme définies par la structure Elf32_Sym et Elf64_Sym décrites au dessus. |
DT_JMPREL | Pointe sur les entrées de relocation associées à la PLT. |
DT_VERSYM | Pointe sur l'adresse de la section .gnu.version qui contient les indices des versions des symboles |
Une entrée sur la PLT peut ressembler à ceci (x86):
0x804xxx4 <some_func>: jmp *some_func_dyn_reloc_entry ; Entrée de la relocation
0x804xxxa <some_func+6>: push $reloc_offset ; Offet de relocation
0x804xxxf <some_func+11>: jmp beginning_of_.plt_section ; Debut de la PLT
Exemple pour puts@plt:
0x0804xxx0 <+0>: jmp DWORD PTR ds:0x804xxxc ; puts@got.plt
0x0804xxx6 <+6>: push 0x0 ; Offset
0x0804xxxb <+11>: jmp 0x804xxx0 ; Debut de la PLT
Génériquement, on peut définir cet algorithme:
.plt:
push .got.plt[link_map] ; == .got.plt[1]
jmp .got.plt[_dl_runtime_resolve] ; == .got.plt[2]
<func>@plt:
jmp .got.plt[x + n]
push n
jmp .plt
Ce qui veut dire en bon français: 1- Jump sur la section .got.plt pour y trouver la func@got.plt 2- Si .got.plt n’a pas encore loué l’adresse sur la bibliothèque 1- L’adresse à .got.plt[x + n] renvoie sur func@plt 2- Mettre l’offset en argument 3- Mettre link_map de .got.plt en argument 4- Appeler _dl_runtime_resolve() pour louer l’adresse la fonction 3- Sinon, appeler la fonction sur la lib
Donc maintenant si on veut être encore plus précis.. il faut comprendre _dl_runtime_resolve() :)
_dl_runtime_resolve() est une fonction faite en assembleur (oui oui)
Dump of assembler code for function _dl_runtime_resolve_xsave:
0x00007ffff7fea430 <+0>: push rbx
0x00007ffff7fea431 <+1>: mov rbx,rsp
0x00007ffff7fea434 <+4>: and rsp,0xffffffffffffffc0
0x00007ffff7fea438 <+8>: sub rsp,QWORD PTR [rip+0x12389] # 0x7ffff7ffc7c8 <_rtld_global_ro+168>
0x00007ffff7fea43f <+15>: mov QWORD PTR [rsp],rax
0x00007ffff7fea443 <+19>: mov QWORD PTR [rsp+0x8],rcx
0x00007ffff7fea448 <+24>: mov QWORD PTR [rsp+0x10],rdx
0x00007ffff7fea44d <+29>: mov QWORD PTR [rsp+0x18],rsi
0x00007ffff7fea452 <+34>: mov QWORD PTR [rsp+0x20],rdi
0x00007ffff7fea457 <+39>: mov QWORD PTR [rsp+0x28],r8
0x00007ffff7fea45c <+44>: mov QWORD PTR [rsp+0x30],r9
0x00007ffff7fea461 <+49>: mov eax,0xee
0x00007ffff7fea466 <+54>: xor edx,edx
0x00007ffff7fea468 <+56>: mov QWORD PTR [rsp+0x240],rdx
0x00007ffff7fea470 <+64>: mov QWORD PTR [rsp+0x248],rdx
0x00007ffff7fea478 <+72>: mov QWORD PTR [rsp+0x250],rdx
0x00007ffff7fea480 <+80>: mov QWORD PTR [rsp+0x258],rdx
0x00007ffff7fea488 <+88>: mov QWORD PTR [rsp+0x260],rdx
0x00007ffff7fea490 <+96>: mov QWORD PTR [rsp+0x268],rdx
0x00007ffff7fea498 <+104>: mov QWORD PTR [rsp+0x270],rdx
0x00007ffff7fea4a0 <+112>: mov QWORD PTR [rsp+0x278],rdx
0x00007ffff7fea4a8 <+120>: xsave [rsp+0x40]
0x00007ffff7fea4ad <+125>: mov rsi,QWORD PTR [rbx+0x10]
0x00007ffff7fea4b1 <+129>: mov rdi,QWORD PTR [rbx+0x8]
0x00007ffff7fea4b5 <+133>: call 0x7ffff7fe3a30 <_dl_fixup>
0x00007ffff7fea4ba <+138>: mov r11,rax
0x00007ffff7fea4bd <+141>: mov eax,0xee
0x00007ffff7fea4c2 <+146>: xor edx,edx
0x00007ffff7fea4c4 <+148>: xrstor [rsp+0x40]
0x00007ffff7fea4c9 <+153>: mov r9,QWORD PTR [rsp+0x30]
0x00007ffff7fea4ce <+158>: mov r8,QWORD PTR [rsp+0x28]
0x00007ffff7fea4d3 <+163>: mov rdi,QWORD PTR [rsp+0x20]
0x00007ffff7fea4d8 <+168>: mov rsi,QWORD PTR [rsp+0x18]
0x00007ffff7fea4dd <+173>: mov rdx,QWORD PTR [rsp+0x10]
0x00007ffff7fea4e2 <+178>: mov rcx,QWORD PTR [rsp+0x8]
0x00007ffff7fea4e7 <+183>: mov rax,QWORD PTR [rsp]
0x00007ffff7fea4eb <+187>: mov rsp,rbx
0x00007ffff7fea4ee <+190>: mov rbx,QWORD PTR [rsp]
0x00007ffff7fea4f2 <+194>: add rsp,0x18
0x00007ffff7fea4f6 <+198>: bnd jmp r11
Tout ce qu’il faut comprendre c’est qu’en gros, ça sauvagarde les registres, appelle _dl_fixup() et enfin jump sur la valeur de retour de _dl_fixup().
Au plus simple, _dl_fixup() fait: 1- Trouver le nom de la fonction dans la section dynamique adéquate 2- Chercher dans toutes les libs chargées et trouver son adresse 3- Ecrire son adresse dans la GOT.PLT
Maintenant, un peu plus précis:
_dl_fixup (link_map, relocation_offset)
{
// Calculer l'entrée de la relocation
Elf32_Rel * relocation_entry = JMPREL + relocation_offset ;
// Calculer l'entrée du symbole
Elf32_Sym * symbol_entry = &SYMTAB [ ELF32_R_SYM ( relocation_entry -> r_info )];
// Adresse de la relocation à remplacer (.got.plt)
void *const relocation_addr = (void *)(link_map->l_addr + relocation_entry->r_offset);
// Check sanitaire
assert (ELF32_R_TYPE(reloc->r_info) == R_386_JMP_SLOT);
if (symbol_versionning == enabled)
{
// Déterminer l'index de la table des versions
// index doit avoir une valeur légale et de préférence 0
// qui signifie "symbol local"
uint16_t index = VERSYM[ ELF32_R_SYM (reloc->r_info) ];
// Trouver les informations sur la version
const struct r_found_version *version = &link_map->l_versions[index];
}
char * symbol_name = STRTAB + symbol_entry->st_name ;
// Trouver le link_map de l'objet ou son adresse de base de chargement
// qui définie symbol_entry
lookup_t result = _dl_lookup_symbol_x (strtab + symbol_entry->st_name,
link_map,
&symbol_entry,
link_map->l_scope,
version,
ELF_RTYPE_CLASS_PLT,
flags,
NULL);
// Ajouter l'offset du symbole
value = DL_FIXUP_MAKE_VALUE (result,
SYMBOL_ADDRESS (result, symbol_entry, false));
// Fixer la PLT
if (__glibc_unlikely (GLRO(dl_bind_not)))
return value;
return *relocation_addr = value;
}
Je vais essayer de pas vous perdre plus loin que ça mais NORMALEMENT, vous devriez comprendre le principe :)
Pour du linkage dynamique la procédure est la suivante: 1- On charge le programme sur le CPU avec ses segments et sections en mémoire 2- Lors d’un appel de fonction: 1- Si c’est la première fois: 1- On appelle _dl_runtime_resolve() 2- On trouve le nom de la fonction dans notre programme 3- On cherche dans les libs chargées la fonction 4- On réécrit la valeur pour les prochaines fois 5- On appelle la fonction 2- Sinon: 1- La valeur sur laquelle on est redirigée pointe sur la fonction qu’on appelle