Les fichiers ELF exécutables

Linux vient de dépasser 2% des installations sur les ordinateurs domestiques!

Mais, alors, pourquoi la presse spécialisée clame sans cesse que GNU/Linux est le système le plus utilisé du monde?

Et bien si l'on se réfère au marché des serveurs, on se rend compte que GNU/Linux représente plus de 60% des parts. Et plus de 90% dans les systèmes d'exploitation Cloud!

Il est bien plus présent sur internet, dans le cloud que dans les foyers... En apparence... En fait, Linux est aussi très utilisé dans les systèmes embarqués. Si l'on prend par exemple les télévisions début 2019, 71% sont sous GNU/Linux et le reste sous Androïd ou FireFox OS, qui émanent de Linux...

Et ce n'est qu'un exemple... Les appareils ménager se sont modernisés et se voient aussi équipés de systèmes embarqués similaires à celui de votre télévision... Et votre routeur internet est certainement aussi animé par un noyau Linux...

Ces quelques mots pour vous faire prendre conscience ou plutôt vous motiver à vous intéresser à ce système.

Réputé pour sa robustesse et son invulnérabilité, personne ne semble s'en inquiéter... Pourtant, il est parfaitement possible de prendre la main sur ce système. Et cet article, ou cette suite d'articles, va tenter vous l'expliquer en vous apportant les connaissances nécessaires pour en comprendre les rouages.

Nous allons commencer par nous intéresser à ELF, un format de fichier particulier.

ELF est le format de fichier dominant pour Linux. Il est en concurrence avec Mach-O pour OS X et PE pour Windows. Linux est aussi très présent sur le marché des systèmes d'exploitations. D'après NetMarket on trouve Linux de moins en moins rarement installé sur un ordinateur de bureau, le plus souvent en dual-boot avec Windows et/ou Mac OS, comme système d'exploitation secondaire.

Avant de commencer

Je vous invite à installer un système GNU/Linux, si possible une distribution populaire comme Debian ou Ubuntu. Une simple recherche sur internet vous permettra de télécharger une image et de l'installer sur un ordinateur ou sur une machine virtuelle. Une fois cela, fait, vous aurez la possibilité de suivre les exemples par la pratique, afin de vous apporter une meilleure compréhension des principes et termes utilisés dans ces articles.

Créer un exécutable ELF avec gcc

Commençons par créer un fichier source en C, avec le fameux "Hello World!"):

1
2
3
4
5
6
7
#include <stdio.h>

int main(void)
{
    puts("Hello, World!");
    return(0);
}

Ensuite, il suffit de le compiler avec gcc:

gcc hello.c -o hello

Nous avons maintenant un exécutable d'une taille de 8640 octets:

$ls -l
total 16
-rwxr-xr-x 1 marc marc 8640 May 25 12:46 hello
-rw-r--r-- 1 marc marc   81 May 25 12:45 hello.c

Et, nous pouvons même l'exécuter !

>./hello 
Hello, World!

Voilà, vous avez votre fichier ELF exécutable ! Maintenant, nous allons pouvoir l'examiner et tenter d'en apprendre un peu plus. Pour information ELF signifie Executable and Linkable Format.

la commande readelf

Cette commande permet d'obtenir des informations sur un fichier ELF. Commençons par utiliser cette commande pour afficher l'en-tête de notre ficher:

>readelf -h hello
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x400400
  Start of program headers:          64 (bytes into file)
  Start of section headers:          6432 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         9
  Size of section headers:           64 (bytes)
  Number of section headers:         30
  Section header string table index: 29

Selon la configuration par défaut de votre compilateur, il se peut que le fichier généré ne soit pas exactement un exécutable typé comme EXEC (Executable file). Dans ce cas, la commande readelf -h retourne des informations comme ci-dessous.

>readelf -h hello
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              DYN (Shared object file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x580
  Start of program headers:          64 (bytes into file)
  Start of section headers:          6656 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         9
  Size of section headers:           64 (bytes)
  Number of section headers:         31
  Section header string table index: 30

Si vous souhaitez produire un exécutable au lieu d'une librairie dynamique exécutable, il suffit d'ajouter l'argument -no-pie à la commande de compilation:

>gcc -no-pie hello.c -o hello

Dans les deux cas, comme on peut le voir dans les deux exemples ci-dessus, l'en-tête ELF commence par un nombre magique.

Magic

  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 

Ce nombre magique présent au début du fichier ELF fournit des informations sur le fichier.

Octets 0, 1, 2 et 3

signature

Ils indiquent qu'il s'agit d'un fichier ELF (45 = 'E', 4c = 'L', 46 = 'F'), préfixés par la valeur 7f. Ainsi, ces 4 premiers octets du fichier (7f 45 4c 46) signifient qu'il s'agit bien d'un fichier ELF.

L'octet 4

architecture

Il peut utiliser une architecture 32 bits (= 01) ou 64 bits (= 02). Le nombre 02 ici, signifie ELF64. En d'autres termes, ce fichier ELF est construit pour une architecture 64 bits.

L'octet 5

bit numbering

Ou encore bit endianess ou encore byte order. Plus communément, on parle de la classe (Class). Cet octet peut avoir deux valeurs:

Cette valeur particulière aide à interpréter correctement les objets restants dans le fichier.

Ceci est important, car différents types de processeurs traitent différemment les instructions entrantes et les structures de données. Dans le cas ci-dessus, c'est LSB qui est utilisé, ce qui est commun pour les processeurs de type AMD64.

L'effet de LSB devient visible lorsque hexdump est utilisée sur un fichier binaire. Les détails de l'en-tête ELF du fichier hello avec cette commande donnent ceci:

>hexdump -n 16 /bin/ps
0000000 457f 464c 0102 0001 0000 0000 0000 0000
0000010

si la classe avait été MSB, cela donnerait:

>hexdump -n 16 /bin/ps
0000000 7f45 4c46 0201 0100 0000 0000 0000 0000
0000010

Vous saisissez la différence. Si ce point n'est pas clair pour vous n'hésitez pas à faire des recherches sur internet pour bien comprendre l'impact sur la manière de stocker les informations de la machine sur laquelle vous travaillez.

L'octet 6

numéro de version ELF

Actuellement, il n'existe que la version 1 qui correspond à la valeur «01».

L'octet 7

OS ABI

Généralement on trouvera la valeur 00 pour System V.

Selon l'ABI (Application Binary Interface) pour l'architecture cible, on peut avoir les valeurs:

Chaque système d'exploitation a des spécificités, ou du moins de légères différences lorsqu'il est comparé aux autres notemment sur la manière dont les appels de fonctions et procédures se fait. La définition du bon ensemble se fait avec une interface ABI (Application Binary Interface) qui décrit précisément ces mécanismes d'appels. De cette façon, le système d'exploitation et les applications savent à quoi s'attendre et les fonctions sont correctement transmises. Ce champ décrit quelle ABI est utilisée et le champ suivant donne la version associée.

L'octet 8

version de l'ABI

C'est le lien entre une architecture, un langage de programmation et un compilateur.

L'Application Binary Interface (ABI, interface binaire-programme), est la description de l'interface de bas niveau entre:

Elle définit les conventions d'appel des fonctions pour une architecture donnée. C'est l'ABI qui définit le rôle précis des registres généraux du processeur (passage de paramètres aux fonctions, retours des résultats des fonctions...) et la responsabilité de leur intégrité (appelant ou appelé).

L'ABI définit aussi la structure de la pile, notamment l'organisation des emplacements réservés aux paramètres supplémentaires d'appel d'une fonction, à la sauvegarde de certains registres, à l'allocation de mémoire dynamiquement sur la pile (taille connue à la compilation) selon la portée de l'identifiant.

Ici, la valeur de la version est 00, ce qui signifie qu'aucune extension spécifique n'est utilisée.

Notre fichier hello utilise l'ABI UNIX - System V sans extension spécifique.

Les octets 9 à 15

non utilisés

C'est tout ce que l'on peut obtenir des 16 premiers octets. Les autres sont toujours égals à 00. Ils ne semblent pas utilisés. On a ici affaire à un bourrage. Ces octets sont peut être réservés à un usage futur.

La suite

Au delà des 16 premiers octets

Pour voir les octets suivants, le plus approprié est d'utiliser la commande hexdump. Ci dessous un exemple pour afficher les 64 premiers octets de notre fichier exécutable ELF:

$ hexdump -C -n 64 hello
00000000  7f 45 4c 46 02 01 01 00  00 00 00 00 00 00 00 00  |.ELF............|
00000010  02 00 3e 00 01 00 00 00  00 04 40 00 00 00 00 00  |..>.......@.....|
00000020  40 00 00 00 00 00 00 00  20 19 00 00 00 00 00 00  |@....... .......|
00000030  00 00 00 00 40 00 38 00  09 00 40 00 1e 00 1d 00  |....@.8...@.....|
00000040
Octet 16 et 17

pour quel usage

Ce champ sur 2 octets nous dit quel est le but du fichier et permet de distinguer à quelle finalité est destiné ce fichier. Parmi les types de fichiers courants on notera:

Ici, notre champ a la valeur 02 00,et étant en LSB il faut comprendre 00 02, ce qui signifie EXEC (Executable file).

Octets 18, 19

type de machine

Ici, la valeur 3e 00 (soit 0x003E en LSB) indique le type de machine (62=AMD64). Ce qui signifie qu'il peut s'agir d'un processeur AMD64 ou Intel x86_64.

Pour voir les valeurs possibles de ce champs, je vous invite à consulter sur votre installation Linux, le fichier /usr/include/elf.h. Vous y trouverez la définition:

#define EM_X86_64       62      /* AMD x86-64 architecture */
Octets 20,21,22 et 23

version

Le champ version est un champ de quatre octets, qui nous confie à nouveau la version ELF. Et, comme il n'a a actuellement qu'une seule version, il est égal à 1. Ici la valeur 01 00 00 00 en LSB signifie 0x00000001.

Octets 24, 25, 26, 27, 28, 29, 30, 31

Addresse de début

Un champ de 8 octets. Et, il commence à l'index 24 qui est un multiple de 8. Dans notre fichier, cela ressemble à ceci:

>hexdump -C -s 24 -n 8 hello
00000018  00 04 40 00 00 00 00 00                           |..@.....|
00000020

Le fichier ELF indique à Linux comment configurer un processus et le démarrer. Ce champ indique par où commence le code. C'est l'adresse à laquelle l'exécution commencera lorsque le processus démarrera.

Le champ est de 8 octets ou 64 bits, car il s'agit d'une adresse mémoire pour une plate-forme 64 bits. Pour un ancien ancien x86, ce champ est de 4 octets (soit 32 bits). Notre programme commence à l’adresse 0x400400 (4195328 en décimal).

Les champs suivants traitent principalement des segments de programme (program segment) et des sections. Les sections contiennent des métadonnées utiles pour la liaison, le débogage, etc., mais ne sont pas nécessaires pour exécuter le programme.

Comme nous ne sommes intéressés que par l’exécution du programme, plutôt que par la liaison ou le débogage, notre fichier ELF ne contient aucune section et nous ne couvrirons rien à leur sujet ici.

Les segments de programme, en revanche, sont importants. Ils ont de multiples utilisations, mais notre programme ne les utilise que dans un sens: spécifier les données à charger en mémoire avant de démarrer le processus.

Si cela vous semble un peu obscure, ne vous inquiétez pas, les articles suivants, vous donneront plus d'explications.

**Octets 32,33,34,35,36,37,38,39

program header table offset

Un autre champs codé sur 8 octets. Sa valeur est 0x40 = 64. Ce décalage de la table d'en-tête du programme (program header table offset) nous indique où, dans le fichier, nous allons trouver la table d'en-tête du programme. Les champs précédents avaient tous des positions fixes dans le fichier, mais pour la table d'en-tête du programme c'est différent. Son emplacement est indiqué par un pointeur.

Ce champ est précisément ce pointeur et la valeur 64 nous indique que la table d’en-tête du programme commence à l’index 64.

Ces champs de type «pointeur» ou «décalage» peuvent ne pas vous être familiers si vous êtes habitué à des formats tels que XML ou JSON, qui peuvent être définis par des grammaires sans contexte. Vous devriez considérer ces champs de décalage comme analogues aux pointeurs de C. Ils sont utilisés à plusieurs reprises dans le format ELF et sont nécessaires au fonctionnement des programmes.

La suite...

Cet article sera suivi d'autres articles, afin de vous donner le plus d'information possibles sur le format ELF.

Il vous permet déjà d'identifier un fichier ELF. De prendre conscience qu'il peut exister des fichiers ELF pour différentes architectures et bien d'autres choses comme notemment la possibilité d'avoir des librairie dynamiques exécutables ou des exécutables.

Avant de passer aux articles suivant, je vous invite à pratiquer et à tester les différents outils disponibles pour réaliser une analyse de fichier ELF.

La suite vous permettra de comprendre les mécanismes d'injection de code en espérant que cela vous permettra de mieux savoir comment vous en prémunir.