Annonce

Réduire
Aucune annonce.

Reversing tutorials - level 2 - Le format ELF

Réduire
X
 
  • Filtre
  • Heure
  • Afficher
Tout nettoyer
nouveaux messages

  • Tutoriel Reversing tutorials - level 2 - Le format ELF

    Avant propos

    À consulter (au moins pour les infos dans l'intro) :
    Reversing tutorials - level 1 - Quelques outils avant de démarrer
    Il faut également noter qu'il ne s'agit pas d’une retranscription de la page de manuel ELF donc certaines infos ne seront pas citées et d'autres seront approfondies.
    Nous utiliserons souvent readelf (man readelf) à titre d'exemple, il ne faut pas hésiter à lire le manuel de cette commande (un simple readlf sans options vous aidera déjà).


    Introduction
    Le format ELF (Executable and Linking Format) est un format de fichier binaire exécutable principalement utilise sur Linux mais aussi sur d'autres plateformes. Etant donné qu'il s'agit d'un format ouvert, d’autres systèmes l'on adopté (BSD, Solaris ...).
    Sous Windows le format utilise est le format PE (Portable Executable).
    Tous les deux sont gérés par leur système respectif qui se chargent de leur mise en mémoire et de retrouver et exécuter les données selon l'organisation des fichiers.

    Un binaire ELF est compose de divers informations et données que le compilateur met en place directement pour vous sans que vous ayez à vous en soucier. Ainsi on va retrouver dans un fichier binaire ELF toute les informations dont il a besoin pour pouvoir fonctionner et être exécuté si besoin.
    Tel que :
    • Des espaces réservés au stockage de données (chaines de caractères, valeurs constantes ...)
    • Des espaces réservés au stockage du code interprétable par le processeur
    • Des espaces d'informations sur les librairies utilisées pour pouvoir y accéder
    • Des espaces d'information de debug
    • ...


    Connaitre le format ELF est nécessaire pour faire du reversing (sous Linux). Le contraire serait comme demander de développer un programme en C# a quelqu'un qui ne connait pas ce langage.

    Un format pour différents types de fichiers
    Le format ELF est utilisé pour plusieurs types de fichiers sous Linux permettant en un seul format de garantir plusieurs utilisations. Ainsi le processus de mise en mémoire ... reste le même pour le système (ou presque).
    Les fichiers exécutables
    Ce sont les fichiers exécutables classiques tel que les programmes se trouvant dans /bin /usr/bin. Un fichier exécutable contient tout le nécessaire pour permettre son exécution (liaisons avec les librairies partagées ...). Il peut être lance directement depuis la ligne de commande.

    Vous pouvez les différencier facilement avec la commande file (man file).

    Un exemple de sortie avec la commande ls :
    Code:
    $ file /bin/ls
    /bin/ls: ELF 64-bit LSB  executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.26, BuildID[sha1]=05e0f155082725dfdc56c4d4cedcc7c2536500a1, stripped
    $
    Les fichiers objets repositionnables
    Ce sont des fichiers objet souvent lies avec d'autres fichiers de ce type pour former ensuite un exécutable dynamique. L'option -c de gcc permet de créer ce genre de fichier.

    Un exemple de sortie de la commande file :
    Code:
    $ gcc -o main.o -c main.c
    $ file main.o 
    main.o: ELF 64-bit LSB  relocatable, x86-64, version 1 (SYSV), not stripped
    $
    Les fichiers objets partagés
    Comme son nom l'indique ce sont des fichiers objet partages. On les retrouve principalement comme librairies dynamiques (.so) qui seront liées dynamiquement a un exécutable lors de son exécution.

    Exemple de sortie de la commande file :
    Code:
    $ file /usr/lib/liba52-0.7.4.so 
    /usr/lib/liba52-0.7.4.so: ELF 64-bit LSB  shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=f6ec1f8e50e403dc0d8861b753fdbc5c867bd899, stripped
    $
    Les fichiers cores
    Souvent générés par gdb, ils représentent un état du binaire a un moment précis. On ne les utilise quasiment jamais.

    Chaîne de production d'un binaire
    gcc est magique gcc fait tout !
    En effet, gcc se charge d'appeler tous les programmes qui feront d'un fichier source un binaire exécutable ELF. Cependant, lorsque vous lancez gcc avec votre fichier source, que se passe-t-il ?

    Etape 1 : le préprocesseur.
    Lors de cette phase, le préprocesseur va résoudre et faire l'inclusion des header utilisés dans votre programme. Autrement dit, les structures et fonctions externes ... seront prototypées.
    Pour cela on utilisera la commande cpp (C Pre-Processor) (man cpp).

    Exemple :
    Code:
    $ cat main.c
    #include <stdio.h>
    
    int main()
    {
      printf("Hello World !\n");
      return (0);
    }
    
    $ cpp main.c -o main_cpp.c
    $ cat main_cpp.c
    ...
    Etape 2 : la compilation
    Il s'agit dans cette étape de transformer des fichiers source en code assembleur. Lors de cette étape, gcc fait des optimisations sur le code, c'est pourquoi faire de l’ASM aujourd'hui sert principalement à le comprendre mais pas pour de réels projets ni pour des optimisations (gcc est suffisamment performant). C'est ici que l'on utilise gcc.

    Exemple :
    Code:
    $ gcc -S main_cpp.c -o main.S
    $ cat main.S
      .file	"main_cpp.c"
      .section	.rodata
    .LC0:
    	.string	"Hello World !"
    	.text
    	.globl	main
    	.type	main, @function
    main:
    .LFB0:
    	.cfi_startproc
    	pushq	%rbp
    ...
    $
    Etape 3 : l'assemblage
    Nous avons maintenant nos fichiers assembleur, il faut ensuite les transformer en fichiers binaires, dans notre cas en fichiers ELF repositionnables. Pour ce faire, l'assembleur GNU as fera l'affaire (man as).

    Exemple :
    Code:
    $ as main.S -o main.o
    $ file main.o
    main.o: ELF 64-bit LSB  relocatable, x86-64, version 1 (SYSV), not stripped
    $
    Etape 4: la liaison (linking)
    La dernière étape du processus, il s'agit de rassembler tous les fichiers objets repositionnables de votre programme pour en faire un binaire. C'est aussi lors de cette étape que le le programme appelle "le linker" va stocker des informations dans le fichier final pour lui permettre ensuite de retrouver les différentes librairies dynamiques utilisées. La commande est ld (man ld).

    Warning, ld ne fonctionnera pas, du moins pas de façon classique. Pour ne pas vous embrouiller, nous verrons plus tard pourquoi.
    Il nous faudra utiliser gcc qui va faire la liaison comme il se doit (même si lui utilise ld en interne finalement).

    Exemple :
    Code:
    $ gcc -o main main.o
    $ ./main 
    Hello World !
    $ file main
    main: ELF 64-bit LSB  executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=ba3de236fa8a892bc6124a5e70c6b9b4ee1f449b, not stripped
    $

    Le header ELF
    Le header ELF est la première chose que l'on trouve dans un fichier binaire ELF. Celui-ci contient les informations primaires sur le reste du fichier et sa structure. En effet, les données sont stockées dans des endroits différents du fichier et donc contrairement à un fichier texte ascii, une lecture ligne par ligne n'est pas possible (ne servirait à rien en tout cas).
    Ce header principale est positionne à l'offset 0 (le tout début du fichier). La structure ElfN_Ehdr définie dans <elf.h> nous permettra d'accéder à ces informations. Le N correspond a l'architecture qui peut varier en fonction du système et de l'architecture du binaire. Etant donné que ce binaire sera un binaire ELF 64bit, on va directement utiliser Elf64_Ehdr. (Elf32_Ehdr pour du 32bit par exemple).

    Soit le code suivant :
    Code:
    #include <stdio.h>
    #include <unistd.h>
    #include <fcntl.h>
    #include <sys/types.h>
    #include <sys/mman.h>
    #include <elf.h>
    
    int elf_check_header(Elf64_Ehdr * data)
    {
      // Si les premiers bytes sont differents de ceux du format ELF
      if (data->e_ident[EI_MAG0] != ELFMAG0 ||
          data->e_ident[EI_MAG1] != ELFMAG1 ||
          data->e_ident[EI_MAG2] != ELFMAG2 ||
          data->e_ident[EI_MAG3] != ELFMAG3)
        return (-1);
    
      // Si l'architecture du binaire est bien 64bits
      if (data->e_ident[EI_CLASS] != ELFCLASS64)
        return (-1);
    
      return (0);
    }
    
    void elf_print_header(Elf64_Ehdr * data)
    {
      printf("Point d'entree                               : %p\n", data->e_entry);
      printf("Adresse de la table des segments (relative)  : + %4d bytes\n", data->e_phoff);
      printf("Nombre d'elements dans la table des segments : %d\n", data->e_phnum);
      printf("Adresse de la table des segments (relative)  : + %4d bytes\n", data->e_shoff);
      printf("Nombre d'elements dans la table des sections : %d\n", data->e_shnum);
    }
    
    int main(int argc, char *argv[])
    {
      int fd;
      void *data;
      struct stat file_infos;
    
      if (argc == 2)
        {
          if ((fd = open(argv[1], O_RDONLY)) > 0)
          {
            if (fstat(fd, &file_infos))
    	    fprintf(stderr, "Recuperation des informations du fichier impossible\n");
    	      else if ((data = mmap(0, file_infos.st_size, PROT_READ, MAP_PRIVATE, fd, 0)) == MAP_FAILED)
    	          fprintf(stderr, "Chargement du fichier en memoire impossible\n");
    		    else
    		        {
    			      if (!elf_check_header((Elf64_Ehdr*)data))
    			      	 {
    					  elf_print_header((Elf64_Ehdr*)data);
    
    						}
    						      munmap(data, file_infos.st_size);	      
    						          }
    							    close(fd);
    							    }
          else
          fprintf(stderr, "Ouverture du fichier impossible\n");
        }
      else
        fprintf(stderr, "USAGE: %s <binary_file>\n", argv[0]);
      return (0);
    }
    Si vous n'êtes pas familier avec mmap (man 2 mmap), open (man 2 open) ou fstat (man 2 fstat) je vous conseille de lire les pages de manuel associées a ces syscall.

    Dans notre fonction principale (main) nous allons "mapper", mettre en mémoire notre fichier en prenant le soin de faire les vérifications nécessaires d'usage (erreurs ...).
    Le syscall mmap nous renvoi un void*, un pointeur générique. Etant donné que notre header se trouve à l'offset 0, on peut directement transformer ce pointeur générique en ElfN_Ehdr* pour accéder aux informations.

    La structure Elf64_Ehdr est représentée comme cela :
    Code:
    typedef struct {
        unsigned char e_ident[EI_NIDENT];
        uint16_t      e_type;
        uint16_t      e_machine;
        uint32_t      e_version;
        Elf64_Addr     e_entry;
        Elf64_Off      e_phoff;
        Elf64_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;
    Le premier champ e_ident va nous permettre d'identifier le binaire et vérifier qu'il s'agisse bien d'un binaire ELF 64bit.
    Le manuel ELF nous informe que ce champ est un tableau de bytes et qu’on y accède par les macros suivantes :
    • EI_MAG0 : le premier byte du nombre magique (servant à identifier le fichier comme un fichier ELF). Sa valeur doit être ELFMAG0.
    • EI_MAG1 : le second byte du nombre magique (servant à identifier le fichier comme un fichier ELF). Sa valeur doit être ELFMAG1.
    • EI_MAG2 : le troisième byte du nombre magique (servant à identifier le fichier comme un fichier ELF). Sa valeur doit être ELFMAG2.
    • EI_MAG3 : le quatrième byte du nombre magique (servant à identifier le fichier comme un fichier ELF). Sa valeur doit être ELFMAG3.
    • EI_CLASS : le cinquième byte, permettant d'identifier l'architecture du binaire. On nous propose les valeurs suivantes :
      • ELFCLASS32 : le binaire est un binaire ELF 32bit
      • ELFCLASS64 : le binaire est un binaire ELF 64bit


    Voilà l'intérêt de la fonction elf_check_header, on vérifie que ces valeurs sont bien celles attendues, que le fichier est valide et qu'il s'agisse bien d’un binaire ELF 64bit.

    On peut ensuite extraire du header les informations plus utiles qui suivent.
    • Le point d'entrée (entry point) :
      Le point d'entrée d'un programme est un adresse à l'laquelle le processus va commencer son exécution. A cette adresse on y trouve généralement du bytecode (code binaire directement compréhensible par le processeur). Si vous avez fait un peu d'assembleur ou que vous vous y êtres intéressé un minimum, vous devez savoir qu'il s'agit d'un langage impératif, il est exécuté ligne par ligne et comprenant des sauts pour utiliser d'autres portions de code. Le processeur (à quelques différences faites par le système qui gère un tas d'autres choses en plus) va faire de même, il va interpréter chaque instruction à la suite dans la mémoire selon leur emplacement et sera conditionne par des instructions de saut.
      Le point d'entre sert à savoir ou commencer l'exécution de ce code.
    • Les informations sur les segments (on y reviendra)
    • Les informations sur les sections (on y reviendra)
    • D'autres informations plus ou moins utiles


    Nous avons donc notre fonction elf_print_header qui se charge de nous afficher ces informations. Un petit test s'impose :
    Code:
    $ gcc -o main main.c
    $ cp main main2
    `main' -> `main2'
    $ ./main main2
    Point d'entree                               : 0x400640
    Adresse de la table des segments (relative)  : +   64 bytes
    Nombre d'elements dans la table des segments : 8
    Adresse de la table des segments (relative)  : + 4312 bytes
    Nombre d'elements dans la table des sections : 30
    $ readelf -h main2
    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:               0x400640
      Start of program headers:          64 (bytes into file)
      Start of section headers:          4312 (bytes into file)
      Flags:                             0x0
      Size of this header:               64 (bytes)
      Size of program headers:           56 (bytes)
      Number of program headers:         8
      Size of section headers:           64 (bytes)
      Number of section headers:         30
      Section header string table index: 27
    $
    Les headers de programme (program header)
    Les segments
    Un segment est une portion de mémoire qui est réservé pour accueil des données. Il est défini par deux valeurs : son adresse et sa taille. Votre système utilise un mécanisme appelé segmentation permettant de découper la mémoire et gérée au plus bas niveau.
    Ainsi quand vous utiliser une adresse dans un programme C, celle-ci est dite virtuelle, le système se chargera à partir de celle-ci de retrouver l'adresse physique par le mécanisme de segmentation (en bref). Je ne vais pas vous faire un cours sur la segmentation, ca nécessiterai d'appréhender pas mal de concepts et ce n’est pas le but ici (pas pour ce tutoriel).

    Le principale est de savoir que votre binaire sera reparti généralement sur plusieurs segments et que chaque segment a ses propres propriétés. Chaque segment a des droits et des propriétés qui lui sont propre. Ainsi on peut retrouver le système de droits sur les fichiers (read, write, execute) sur les segments. Un segment avec le droit d'exécution accueillera la plus part du temps du code interprétable par le processeur...


    Les program headers
    Dans un fichier ELF, on y trouve une table de "program header" représentés par la structure suivante :
    Code:
    typedef struct {
        uint32_t   p_type;
        uint32_t   p_flags;
        Elf64_Off  p_offset;
        Elf64_Addr p_vaddr;
        Elf64_Addr p_paddr;
        uint64_t   p_filesz;
        uint64_t   p_memsz;
        uint64_t   p_align;
    } Elf64_Phdr;
    Un program header contient des informations sur le programme en fonction de son type définie par p_type. La plus part du temps il s'agit d'informations sur les segments. Voici les différents types (utiles) représentés par des macros et les informations associées :
    • PT_NULL : le program header n'est pas défini (pas très utile dans ce cas)
    • PT_LOAD : le program header recense des informations sur un segment chargeable (le plus intéressant)
    • PT_DYNAMIC : le program header contient des informations de liaison dynamique (pour la gestion des librairies ...)
    • PT_INTERP : le program header nous donne des informations sur l'interpréteur, notamment son chemin pour pouvoir l'invoquer
    • PT_PHDR : si présent, le header contient la taille et l'entête de programme elle-même dans le fichier et dans l'image mémoire du binaire (moins important)


    D'autres existent qui ne nous serviront probablement jamais, jetez un œil sur le man ELF pour une liste plus exhaustive. Si vous ne devez en retenir qu'un, alors ce sera PT_LOAD, c'est dans celui-ci ou l'on retrouvera la plus part des donnes du binaire (code exécutable, données de votre programme ...).

    Exemple:
    Code:
    $ readelf -l main2
    Elf file type is EXEC (Executable file)
    Entry point 0x400640
    There are 8 program headers, starting at offset 64
    
    Program Headers:
      Type           Offset             VirtAddr           PhysAddr
                     FileSiz            MemSiz              Flags  Align
      PHDR           0x0000000000000040 0x0000000000400040 0x0000000000400040
                     0x00000000000001c0 0x00000000000001c0  R E    8
      INTERP         0x0000000000000200 0x0000000000400200 0x0000000000400200
                     0x000000000000001c 0x000000000000001c  R      1
          [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
      LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
                     0x0000000000000d2c 0x0000000000000d2c  R E    200000
      LOAD           0x0000000000000d30 0x0000000000600d30 0x0000000000600d30
                     0x0000000000000268 0x0000000000000278  RW     200000
      DYNAMIC        0x0000000000000d48 0x0000000000600d48 0x0000000000600d48
                     0x00000000000001d0 0x00000000000001d0  RW     8
      NOTE           0x000000000000021c 0x000000000040021c 0x000000000040021c
                     0x0000000000000044 0x0000000000000044  R      4
      GNU_EH_FRAME   0x0000000000000be4 0x0000000000400be4 0x0000000000400be4
                     0x0000000000000044 0x0000000000000044  R      4
      GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                     0x0000000000000000 0x0000000000000000  RW     8
    
     Section to Segment mapping:
      Segment Sections...
       00     
       01     .interp 
       02     .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .text .fini .rodata .eh_frame_hdr .eh_frame 
       03     .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss 
       04     .dynamic 
       05     .note.ABI-tag .note.gnu.build-id 
       06     .eh_frame_hdr 
       07     
    $
    Ici ce sont les deux segments LOAD qui vont nous intéresser (et le DYNAMIC ensuite) car on constate que c’est dans ces segments que ce trouvent les principales sections de notre programme (.text, .init, .data, .bss ...) que l'on verra juste maintenant.

    Les sections
    Comme son nom l'indique il s'agit d'une section du binaire dans laquelle sont regroupées différentes données. Comme déjà énoncé, un binaire contient plusieurs types de données, du code exécutable, des informations diverses pour la liaison de librairies ... C'est au travers ces différentes sections que l'on va pouvoir retrouver toute ces informations.

    Le format ELF contient une table de headers de section nous donnant les informations sur chaque section, son emplacement dans le fichier, sa taille ... Un header est représenté avec la structure suivante :
    Code:
    typedef struct {
        uint32_t   sh_name;
        uint32_t   sh_type;
        uint64_t   sh_flags;
        Elf64_Addr sh_addr;
        Elf64_Off  sh_offset;
        uint64_t   sh_size;
        uint32_t   sh_link;
        uint32_t   sh_info;
        uint64_t   sh_addralign;
        uint64_t   sh_entsize;
    } Elf64_Shdr;
    Dans un binaire il existe plusieurs types de section, pour pouvoir les différencier, sh_type nous indiquera le type de la section (section contenant des chaines, des informations de liaison dynamique, ...).
    sh_offset nous donnera la position de la section à partir du début du fichier et sh_size la taille de la section pour pouvoir retrouver ces données.

    Ce qu'il faut également savoir, c'est que dans le format ELF on retrouve des sections spécifiques pour le stockage des noms de sections, le nom des symboles ... Ce sont les strings tables (tables de chaines de caractères).
    Ainsi, dans le header principale, on dispose du champ e_shstrndx qui correspond à l'index dans la table des headers de section correspondant à la section contenant les noms des différentes sections. On va pouvoir ensuite retrouver chaque nom à l'aide du champ sh_name (de la structure précédente) qui définit un index dans cette section.

    Dans ces sections les chaines sont représentées sous la forme suivante :

    "Une chaine\0une seconde chaine\0une troisième chaine\0"

    Ce n'est pas un tableau de chaines de caractères mais une simple chaine dont chaque sous-chaine est terminée par un null byte (\0).
    Lorsque sh_name nous donne l'index 11, on se retrouve donc sur "une seconde ...\0une troi...\0". Lorsque l'on va utiliser des fonctions tel que printf, strlen ... on va donc automatiquement s'arrêter au null byte.

    Voyons comment récupérer les noms des différentes sections grâce à cette section :
    Code:
    #include <stdio.h>
    #include <unistd.h>
    #include <fcntl.h>
    #include <sys/types.h>
    #include <sys/mman.h>
    #include <elf.h>
    
    int elf_check_header(Elf64_Ehdr * data)
    {
      // Si les premiers bytes sont differents de ceux du format ELF
      if (data->e_ident[EI_MAG0] != ELFMAG0 ||
          data->e_ident[EI_MAG1] != ELFMAG1 ||
          data->e_ident[EI_MAG2] != ELFMAG2 ||
          data->e_ident[EI_MAG3] != ELFMAG3)
        return (-1);
    
      // Si l'architecture du binaire est bien 64bits
      if (data->e_ident[EI_CLASS] != ELFCLASS64)
        return (-1);
    
      return (0);
    }
    
    void elf_print_sections_name(Elf64_Ehdr * data)
    {
      int i;
    
      // on recupere un pointeur sur le premier header de section
      // ne as oublier de caster en void*, autrement l'arithmetique entre les pointeurs ne sera pas bon
      Elf64_Shdr *section_header_start = (Elf64_Shdr*)((void*)data + data->e_shoff);
    
      // on retrouve la string table avec le nom des sections ..
      Elf64_Shdr sections_string = section_header_start[data->e_shstrndx];
      
      // on recupere un pointeur sur le debut de la section et donc sur la premiere chaine
      char *strings = (char*)((void*)data + sections_string.sh_offset);
    
      // pour stoquer chaque section
      Elf64_Shdr section_header;
    
      for (i = 0; i < data->e_shnum; i++)
        {
          // on recupere le header de section courrant
          section_header = section_header_start[i];
    
          // on affiche son nom a partir de la section contenant les strings et son index
          printf("%s\n", strings + section_header.sh_name);
        }
    }
    
    int main(int argc, char *argv[])
    {
      int fd;
      void *data;
      struct stat file_infos;
    
      if (argc == 2)
        {
          if ((fd = open(argv[1], O_RDONLY)) > 0)
    	{
    	  if (fstat(fd, &file_infos))
    	    fprintf(stderr, "Recuperation des informations du fichier impossible\n");
    	  else if ((data = mmap(0, file_infos.st_size, PROT_READ, MAP_PRIVATE, fd, 0)) == MAP_FAILED)
    	    fprintf(stderr, "Chargement du fichier en memoire impossible\n");
    	  else
    	    {
    	      if (!elf_check_header((Elf64_Ehdr*)data))
    		{
    		  elf_print_sections_name((Elf64_Ehdr*)data);
    		}
    	      munmap(data, file_infos.st_size);	      
    	    }
    	  close(fd);
    	}
          else
    	fprintf(stderr, "Ouverture du fichier impossible\n");
        }
      else
        fprintf(stderr, "USAGE: %s <binary_file>\n", argv[0]);
      return (0);
    }
    Pas si complique si ? Si vous avez du mal avec ce code je vous conseille de lire : Arithmetique des pointeurs qui vous aidera certainement !


    La section .text
    La section .text, probablement la plus importante est une section qui va contenir le code exécutable du binaire. La suite de bytecode interprétable par le processeur se trouve ici.
    Ci-dessous on désassemble la section .text avec objdump (man objdump) :
    Code:
    $ objdump -j.text -d main2
    main2:     file format elf64-x86-64
    
    Disassembly of section .text:
    
    0000000000400640 <_start>:
      400640:	31 ed                	xor    %ebp,%ebp
      400642:	49 89 d1             	mov    %rdx,%r9
      400645:	5e                   	pop    %rsi
      400646:	48 89 e2             	mov    %rsp,%rdx
      400649:	48 83 e4 f0          	and    $0xfffffffffffffff0,%rsp
      40064d:	50                   	push   %rax
      40064e:	54                   	push   %rsp
      40064f:	49 c7 c0 20 0a 40 00 	mov    $0x400a20,%r8
      400656:	48 c7 c1 90 09 40 00 	mov    $0x400990,%rcx
      40065d:	48 c7 c7 18 08 40 00 	mov    $0x400818,%rdi
      400664:	e8 67 ff ff ff       	callq  4005d0 <[email protected]>
      400669:	f4                   	hlt    
      40066a:	90                   	nop
      40066b:	90                   	nop
      40066c:	90                   	nop
      40066d:	90                   	nop
      40066e:	90                   	nop
      40066f:	90                   	nop
    ...
    0000000000400818 <main>:
      400818:	55                   	push   %rbp
      400819:	48 89 e5             	mov    %rsp,%rbp
      40081c:	48 81 ec b0 00 00 00 	sub    $0xb0,%rsp
      400823:	89 bd 5c ff ff ff    	mov    %edi,-0xa4(%rbp)
      400829:	48 89 b5 50 ff ff ff 	mov    %rsi,-0xb0(%rbp)
      400830:	83 bd 5c ff ff ff 02 	cmpl   $0x2,-0xa4(%rbp)
      400837:	0f 85 1d 01 00 00    	jne    40095a <main+0x142>
      40083d:	48 8b 85 50 ff ff ff 	mov    -0xb0(%rbp),%rax
      400844:	48 83 c0 08          	add    $0x8,%rax
      400848:	48 8b 00             	mov    (%rax),%rax
      40084b:	be 00 00 00 00       	mov    $0x0,%esi
      400850:	48 89 c7             	mov    %rax,%rdi
      400853:	b8 00 00 00 00       	mov    $0x0,%eax
      400858:	e8 c3 fd ff ff       	callq  400620 <[email protected]>
      40085d:	89 45 fc             	mov    %eax,-0x4(%rbp)
      400860:	83 7d fc 00          	cmpl   $0x0,-0x4(%rbp)
      400864:	0f 8e d0 00 00 00    	jle    40093a <main+0x122>
      40086a:	48 8d 95 60 ff ff ff 	lea    -0xa0(%rbp),%rdx
      400871:	8b 45 fc             	mov    -0x4(%rbp),%eax
      400874:	48 89 d6             	mov    %rdx,%rsi
      400877:	89 c7                	mov    %eax,%edi
      400879:	b8 00 00 00 00       	mov    $0x0,%eax
    ...
      400964:	48 8b 05 2d 06 20 00 	mov    0x20062d(%rip),%rax        # 600f98 <__bss_start>
      40096b:	be c9 0b 40 00       	mov    $0x400bc9,%esi
      400970:	48 89 c7             	mov    %rax,%rdi
      400973:	b8 00 00 00 00       	mov    $0x0,%eax
      400978:	e8 63 fc ff ff       	callq  4005e0 <[email protected]>
      40097d:	b8 00 00 00 00       	mov    $0x0,%eax
      400982:	c9                   	leaveq 
      400983:	c3                   	retq   
      400984:	90                   	nop
      40098f:	90                   	nop
    ...
    Ce que va contenir cette section sera les bytecodes que l'on peut voir ici par exemple :
    Code:
    55
    48 89 e5
    48 81 ec b0 00 00 00
    89 bd 5c ff ff ff
    Essayons de désassembler tout ça avec notre propre code (et une lib). Pour la suite j'utiliserai la lib udis86 : http://udis86.sourceforge.net/ . Il s'agit d'une librairie qui nous permettra de convertir du bytecode en code assembleur. Cela évite de réinventer la roue quand on sait le nombre de bytecode immense existants. Si vous voulez un aperçu :

    Soit le code suivant :
    Code:
    #include <stdio.h>
    #include <unistd.h>
    #include <fcntl.h>
    #include <sys/types.h>
    #include <sys/mman.h>
    #include <elf.h>
    #include <udis86.h>
    
    int elf_check_header(Elf64_Ehdr * data)
    {
      // Si les premiers bytes sont differents de ceux du format ELF
      if (data->e_ident[EI_MAG0] != ELFMAG0 ||
          data->e_ident[EI_MAG1] != ELFMAG1 ||
          data->e_ident[EI_MAG2] != ELFMAG2 ||
          data->e_ident[EI_MAG3] != ELFMAG3)
        return (-1);
    
      // Si l'architecture du binaire est bien 64bits
      if (data->e_ident[EI_CLASS] != ELFCLASS64)
        return (-1);
    
      return (0);
    }
    
    void elf_disass_text(Elf64_Ehdr * data, Elf64_Shdr * section_header)
    {
      // on recupere le contenu de la section text sous forme d'un tableau de unsigned char(representation d'un bytecode)
      unsigned char *buffer = (unsigned char*)((void*)data + section_header->sh_offset);
      ud_t ud_obj;
    
      // initialisation de udis86
      ud_init(&ud_obj);
      ud_set_input_buffer(&ud_obj, buffer, section_header->sh_size);
      ud_set_mode(&ud_obj, 64);
      ud_set_syntax(&ud_obj, UD_SYN_ATT);
    
      // affichage de chaque instruction (le pointeur est incremente automatquement pour passer a la suivante)
      while (ud_disassemble(&ud_obj))
        printf("\t%s\n", ud_insn_asm(&ud_obj));
    }
    
    void elf_print_sections_name(Elf64_Ehdr * data)
    {
      int i;
    
      // on recupere un pointeur sur le premier header de section
      // ne as oublier de caster en void*, autrement l'arithmetique entre les pointeurs ne sera pas bon
      Elf64_Shdr *section_header_start = (Elf64_Shdr*)((void*)data + data->e_shoff);
    
      // on retrouve la string table avec le nom des sections ..
      Elf64_Shdr sections_string = section_header_start[data->e_shstrndx];
      
      // on recupere un pointeur sur le debut de la section et donc sur la premiere chaine
      char *strings = (char*)((void*)data + sections_string.sh_offset);
    
      // pour stoquer chaque section
      Elf64_Shdr section_header;
    
      for (i = 0; i < data->e_shnum; i++)
        {
          // on recupere le header de section courrant
          section_header = section_header_start[i];
    
          if (!strcmp(".text", strings + section_header.sh_name))
    	elf_disass_text(data, &section_header);
        }
    }
    
    int main(int argc, char *argv[])
    {
      int fd;
      void *data;
      struct stat file_infos;
    
      if (argc == 2)
        {
          if ((fd = open(argv[1], O_RDONLY)) > 0)
    	{
    	  if (fstat(fd, &file_infos))
    	    fprintf(stderr, "Recuperation des informations du fichier impossible\n");
    	  else if ((data = mmap(0, file_infos.st_size, PROT_READ, MAP_PRIVATE, fd, 0)) == MAP_FAILED)
    	    fprintf(stderr, "Chargement du fichier en memoire impossible\n");
    	  else
    	    {
    	      if (!elf_check_header((Elf64_Ehdr*)data))
    		{
    		  elf_print_sections_name((Elf64_Ehdr*)data);
    		}
    	      munmap(data, file_infos.st_size);	      
    	    }
    	  close(fd);
    	}
          else
    	fprintf(stderr, "Ouverture du fichier impossible\n");
        }
      else
        fprintf(stderr, "USAGE: %s <binary_file>\n", argv[0]);
      return (0);
    }
    Je vous laisse le soin d'essayer. Voilà les instructions de votre code sous forme d'assembleur. (objdump fait la même chose en ajoutant des informations en plus).

    La section .data .rodata
    Dans la section .data on va retrouver les variables globales et statiques initialisées explicitement. La section .rodata est identique mais pour des valeurs constantes. L'écriture en section .rodata n'est pas permise.

    Exemple :
    Code:
    $ cat test.c
    
    int toto = 42;
    const int tata = 8;
    
    int main()
    {
      static char maman[8] = "coucou!";
    
      return (0);
    }
    
    $ gcc -o test test.c
    $ objdump -d -j.data test
    
    test:     file format elf64-x86-64
    
    
    Disassembly of section .data:
    
    0000000000600860 <__data_start>:
    	...
    
    0000000000600868 <__dso_handle>:
    	...
    
    0000000000600870 <toto>:
      600870:	2a 00 00 00                                         *...
    
    0000000000600874 <maman.1707>:
      600874:	63 6f 75 63 6f 75 21 00                             coucou!.
    
    $ objdump -d -j.rodata test
    
    test:     file format elf64-x86-64
    
    
    Disassembly of section .rodata:
    
    0000000000400560 <_IO_stdin_used>:
      400560:	01 00 02 00                                         ....
    
    0000000000400564 <tata>:
      400564:	08 00 00 00                                         ....
    
    $
    On retrouve bien nos variables dans les sections adequates.


    La section .bss
    La section .bss contient elle aussi vos variables mais non-initialisées cette fois.

    Exemple :
    Code:
    $ cat test.c
    
    int toto;
    
    int main()
    {
      static char maman[8];
    
      return (0);
    }
    
    $ objdump -d -j.bss test
    
    test:     file format elf64-x86-64
    
    
    Disassembly of section .bss:
    
    0000000000600868 <completed.6109>:
      600868:	00 00 00 00                                         ....
    
    000000000060086c <maman.1706>:
    	...
    
    0000000000600874 <toto>:
      600874:	00 00 00 00                                         ....
    
    $
    On retrouve la également nos variables cette fois avec la valeur 0 étant donne quelle ne sont pas initialisées.


    La section .plt .got
    Etant donné que ces sections nécessiteraient un tutoriel complet, je vous renvoie vers un article déjà rédigé : http://www.segmentationfault.fr/linu...plt-got-ld-so/


    Les autres sections
    Parmi les autres sections, on retrouve la section .dynamic contenant des informations sur les liaisons dynamiques. On retrouvera également d'autres sections contenant des chaines de caractères pour les symboles ...
    Si vous voulez approfondir ces autres sections -> man elf.

    Essayez d'utiliser la commande ldd (man ldd), les informations viennent de quelle section à votre avis ?


    Les symboles
    Un symbole est une étiquette qui identifie certains éléments du programme. Les symboles vont permettre de référencer les noms de vos fonctions, des fonctions des librairies dynamiques et statiques, des fichiers, des sections ...
    Les symboles sont principalement utilisés pour le debug et permettent (entre autres) d'utiliser ces noms comme référence dans gdb.

    Ex: lorsque l'on pose un break point sur main, main est un symbole, si l'on supprime les symboles du binaire, alors on ne pourra plus poser un break point sur main, il faudra spécifier une adresse à la place.

    En effet, les symboles gonflent la taille du binaire, c'est pourquoi en production on supprime souvent ceux-là pour gagner de la place. Cela n'empêche pas le debug mais ça le complique légèrement (man strip). En version de développement, on gardera les symboles pour pouvoir faire du debug plus simplement.

    Une commande utile pour lister les symboles d'un binaire ELF est nm (man nm) :
    Code:
    $ nm main2
    0000000000600d48 d _DYNAMIC
    0000000000600f20 d _GLOBAL_OFFSET_TABLE_
    0000000000400a50 R _IO_stdin_used
                     w _ITM_deregisterTMCloneTable
                     w _ITM_registerTMCloneTable
                     w _Jv_RegisterClasses
    0000000000400d28 r __FRAME_END__
    0000000000600d40 d __JCR_END__
    0000000000600d40 d __JCR_LIST__
    0000000000600f98 D __TMC_END__
    0000000000600f98 A __bss_start
    0000000000600f88 D __data_start
    00000000004006e0 t __do_global_dtors_aux
    0000000000600d38 t __do_global_dtors_aux_fini_array_entry
    0000000000600f90 D __dso_handle
    0000000000600d30 t __frame_dummy_init_array_entry
    0000000000400a30 T __fstat
                     U [email protected]@GLIBC_2.2.5
                     w __gmon_start__
    0000000000600d38 t __init_array_end
    0000000000600d30 t __init_array_start
    0000000000400a20 T __libc_csu_fini
    0000000000400990 T __libc_csu_init
                     U [email protected]@GLIBC_2.2.5
    0000000000600f98 A _edata
    0000000000600fa8 A _end
    0000000000400a40 T _fini
    0000000000400570 T _init
    0000000000400640 T _start
                     U [email protected]@GLIBC_2.2.5
    0000000000600fa0 b completed.6109
    0000000000600f88 W data_start
    0000000000400670 t deregister_tm_clones
    000000000040072c T elf_check_header
    0000000000400784 T elf_print_header
                     U [email protected]@GLIBC_2.2.5
    0000000000400700 t frame_dummy
    0000000000400a30 W fstat
                     U [email protected]@GLIBC_2.2.5
    0000000000400818 T main
                     U [email protected]@GLIBC_2.2.5
                     U [email protected]@GLIBC_2.2.5
                     U [email protected]@GLIBC_2.2.5
                     U [email protected]@GLIBC_2.2.5
    00000000004006a0 t register_tm_clones
    0000000000600f98 B [email protected]@GLIBC_2.2.5
    $
    Les symboles ont différents types selon a quoi ils font référence. Plus de détails ici : http://www.thegeekstuff.com/2012/03/linux-nm-command/

    Si vous voulez approfondir certains points faites le moi savoir, ici le but est de faire du reversing ne l'oubliez pas et pas de faire un interpréteur de binaire ELF ...


    La prochaine étape ... surprise.
Chargement...
X