Aller au contenu principal
own2pwn
elf/elf-segsec.tsx

.text, .data, .got, .plt : la carto d'un ELF

Lecture des segments et sections d'un ELF (Executable and Linkable Format) chargé en mémoire : PT_LOAD, .text, .plt, .got, .data, stack, heap, et résolution dynamique d'un appel via la PLT (Procedure Linkage Table) et la GOT (Global Offset Table).

Maxime Jérôme··6 min de lecture

Prérequis

Hello ! o/

Dans cet article, vous allez comprendre comment la mémoire est segmentée et sectionnée lorsqu'un programme ELF (Executable and Linkable Format) est chargé. Chouette, non ? Cela vous permettra de mieux comprendre l'organisation de votre programme en mémoire et à quoi sert chaque partie.

Segments

Les segments sont des regroupements de sections qui ont leurs propres flags, adresses de base, etc. Pour obtenir les segments d'un programme, vous pouvez utiliser la commande suivante :

bash
$ readelf -l ./program
readelf-segments.txt
Elf file type is EXEC (Executable file)
Entry point 0x401050
There are 13 program headers, starting at offset 64

Program Headers:
  Type           Offset   VirtAddr           PhysAddr           FileSiz  MemSiz   Flg Align
  PHDR           0x000040 0x0000000000400040 0x0000000000400040 0x0002d8 0x0002d8 R   0x8
  INTERP         0x000318 0x0000000000400318 0x0000000000400318 0x00001c 0x00001c R   0x1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x000000 0x0000000000400000 0x0000000000400000 0x000508 0x000508 R   0x1000
  LOAD           0x001000 0x0000000000401000 0x0000000000401000 0x0001b5 0x0001b5 R E 0x1000
  LOAD           0x002000 0x0000000000402000 0x0000000000402000 0x000120 0x000120 R   0x1000
  LOAD           0x002e10 0x0000000000403e10 0x0000000000403e10 0x000220 0x000228 RW  0x1000
  DYNAMIC        0x002e20 0x0000000000403e20 0x0000000000403e20 0x0001d0 0x0001d0 RW  0x8
  NOTE           0x000338 0x0000000000400338 0x0000000000400338 0x000020 0x000020 R   0x8
  NOTE           0x000358 0x0000000000400358 0x0000000000400358 0x000044 0x000044 R   0x4
  GNU_PROPERTY   0x000338 0x0000000000400338 0x0000000000400338 0x000020 0x000020 R   0x8
  GNU_EH_FRAME   0x002004 0x0000000000402004 0x0000000000402004 0x000044 0x000044 R   0x4
  GNU_STACK      0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW  0x10
  GNU_RELRO      0x002e10 0x0000000000403e10 0x0000000000403e10 0x0001f0 0x0001f0 R   0x1

 Section to Segment mapping:
  Segment Sections...
   00
   01     .interp
   02     .interp .note.gnu.property .note.gnu.build-id .note.ABI-tag .gnu.hash
          .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt
   03     .init .plt .text .fini
   04     .rodata .eh_frame_hdr .eh_frame
   05     .init_array .fini_array .dynamic .got .got.plt .data .bss
   06     .dynamic
   07     .note.gnu.property
   08     .note.gnu.build-id .note.ABI-tag
   09     .note.gnu.property
   10     .eh_frame_hdr
   11
   12     .init_array .fini_array .dynamic .got
Sortie typique de readelf -l : liste des segments ELF avec leurs types, flags et sections associées.

Voici les différents segments (liste complète ici) :

  • PT_PHDR : Contient l'adresse et la taille de la table des segments.
  • PT_INTERP : Contient le chemin vers ld (ex. /lib64/ld-linux-x86-64.so.2).
  • PT_LOAD : Segment pouvant être chargé en mémoire ; il est normal d'en avoir plusieurs, triés par adresse virtuelle.
  • PT_DYNAMIC : Informations sur le linkage dynamique.
  • PT_NOTE : Contient des informations auxiliaires comme :
    • OS/ABI requis.
    • Version minimale du kernel.
  • GNU_EH_FRAME : Gestionnaire des exceptions.
  • GNU_STACK : Contient la pile d'exécution (stack).
  • GNU_RELRO : Sections à mettre en RO (Read-Only) après les relocations dynamiques. RELRO = RELocation Read-Only.

Sections

Les sections sont les composantes des segments. Voici un aperçu des sections principales, triées des adresses basses (0x0) vers les adresses hautes (0xf) :

AdressesSegmentSectionDescription
0x0LOAD #2.pltPLT (Procedure Linkage Table), utilisée pour appeler les fonctions/procédures externes, comme printf@plt.
.textContient les opcodes (Operations Codes) de l'architecture CPU, c'est-a-dire le code source compilé du programme.
LOAD #3.rodataConstantes en RO (Read-Only).
LOAD #4.ctors / init_arrayListe des constructeurs (pointeurs sur fonction), appelés avant l'appel de main (définis avec __attribute__((constructor))).
.dtors / fini_arrayListe des destructeurs (pointeurs sur fonction), appelés après l'appel de main, comme atexit(3) (définis avec __attribute__((destructor))).
.gotGOT (Global Offset Table), utilisée pour pointer sur des variables globales importées de bibliotheques (ex. EXIT_SUCCESS de stdlib.h).
.got.pltGOT de la PLT, utilisée pour les fonctions/procedures externes, comme printf@libc.
.dataComme .rodata, mais en RW (Read-Write).
.bssComme .data, mais pour les variables globales non initialisées (initialisées a 0).
N/A[heap]Sur le DS (Data Segment), espace mémoire réservé aux allocations dynamiques. Elle grandit vers les adresses hautes et est utilisée par malloc(3), brk(2), etc.
N/A[libs]Apres la heap, les bibliotheques (libc, OpenCL, GTK, ...) sont chargées. Elles ont leurs propres segments/sections et sont des programmes a part entiere.
GNU_STACK[stack]Allocation statique, grandit vers les adresses basses. Contient les variables locales, les retours d'adresses et les sauvegardes des registres de stack comme RBP (EBP en Intel x86).
0xfN/A[kernel]Le kernel est chargé dans les adresses les plus hautes (non chargé lors du lancement d'un programme utilisateur).

Appel d'une Fonction

Imaginons que nous soyons dans .text, et que EIP (le pointeur sur l'instruction courante) pointe vers une instruction call sur printf@plt. Nous savons qu'en résultat, printf(3) sera appelée. Voici ce qu'il se passe réellement :

  • printf@plt appelle printf@got.plt.
  • printf@got.plt appelle printf@libc.
plt-got-libc-chain.txt
  .text
  ┌─────────────────────────────────────────────────────────┐
  │  ...                                                    │
  │  call  printf@plt          ; appel depuis le code       │
  └───────────────┬─────────────────────────────────────────┘
                  │
                  ▼
  .plt  (Procedure Linkage Table)
  ┌─────────────────────────────────────────────────────────┐
  │  printf@plt:                                            │
  │    jmp  *printf@got.plt    ; saut indirect via GOT      │
  │    push  <reloc_index>     ; 1er appel : lazy binding   │
  │    jmp   PLT[0]            ; résolution via ld.so       │
  └───────────────┬─────────────────────────────────────────┘
                  │  (apres résolution : adresse réelle)
                  ▼
  .got.plt  (Global Offset Table pour la PLT)
  ┌─────────────────────────────────────────────────────────┐
  │  printf@got.plt:  0x00007f... → printf@libc             │
  └───────────────┬─────────────────────────────────────────┘
                  │
                  ▼
  libc.so chargée en mémoire
  ┌─────────────────────────────────────────────────────────┐
  │  printf:  implémentation réelle dans la libc            │
  └─────────────────────────────────────────────────────────┘

  Note : la résolution est faite une seule fois (lazy binding).
  Les appels suivants passent directement de .plt vers libc.
Chaine de résolution d'un appel printf en linkage dynamique : PLT -> GOT -> libc.
Lazy binding et RELRO
Par défaut, la résolution GOT se fait en lazy binding : l'adresse réelle de printf@libc n'est écrite dans la GOT qu'au premier appel. La protection RELRO (RELocation Read-Only) en mode Full RELRO résout toutes les fonctions au démarrage et passe la GOT en lecture seule, bloquant ainsi les attaques de type GOT overwrite.

Dans le cas où l'on compile avec le flag -static, voici un exemple d'appel sur printf :

printf-static.txt
  Compilation dynamique (par défaut)        Compilation statique (-static)
  ──────────────────────────────────        ─────────────────────────────────────
  .text                                     .text
  │  call  printf@plt                       │  call  printf          ; appel direct
  │                                         │                        ; printf est
  .plt                                      │  printf:               ; dans .text !
  │  jmp *printf@got.plt                    │    ...implémentation...
  │                                         │    ret
  .got.plt
  │  → printf@libc (résolue au runtime)

  binaire léger, libc partagée               binaire lourd, libc embarquée
  dépend de la libc installée                100% autonome, pas de dépendance

  Taille typique : ~16 Ko                   Taille typique : ~800 Ko+
Appel printf avec compilation statique : la libc est embarquée, pas de PLT ni de GOT.