Prérequis

Hello ! o/

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.

Static Linkage

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.

Dynamic Linkage

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;

Sections Dynamiques

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

Entrée PLT et GOT.PLT

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_*()

_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 :)

Conclusion

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

Merci pour votre lecture :)