('-. .-') _( OO) ( OO ). ,--. (,------.(_)---\_) | |.-') | .---'/ _ | | | OO ) | | \ :` `. | |`-' |(| '--. '..`''.) (| '---.' | .--' .-._) \ | | | `---.\ / `------' `------' `-----' .-') _ ('-. .-') _ .-') ( OO) ) _( OO) ( OO) ) ( OO ). .-'),-----. .-----./ '._(,------./ '._ (_)---\_) ( OO' .-. ' ' .--./|'--...__)| .---'|'--...__)/ _ | / | | | | | |('-.'--. .--'| | '--. .--'\ :` `. \_) | |\| | /_) |OO ) | | (| '--. | | '..`''.) \ | | | | || |`-'| | | | .--' | | .-._) \ `' '-' '(_' '--'\ | | | `---. | | \ / `-----' `-----' `--' `------' `--' `-----' _ .-') _ .-') .-') _ .-') ( '.( OO )_ ( \( -O ) ( OO) ) ( OO ). ,--. ,--.).-'),-----. ,------. / '._ (_)---\_) | `.' |( OO' .-. '| /`. '|'--...__)/ _ | | |/ | | | || / | |'--. .--'\ :` `. | |'.'| |\_) | |\| || |_.' | | | '..`''.) | | | | \ | | | || . '.' | | .-._) \ | | | | `' '-' '| |\ \ | | \ / `--' `--' `-----' `--' '--' `--' `-----' ~ xcellerator [ Traduction par evilcel3ri ] Ahoy, chers camarades des ELF ! Dans cet article, nous aimerions présenter une petite librairie intitulée LibGolf. Elle a débuté comme méthode afin de mieux comprendre les ELF et les en-têtes de programmes, mais a évolué depuis vers quelque chose de vaguement utile. Cette librairie permet de facilement générer un binaire contenant un en-tête ELF suivi d'un unique en-tête de programme qui est lui-même suivi d'un segment chargeable. Par défaut, tous les champs des en-têtes sont définis avec des valeurs saines, mais il est possible de jouer aisément avec ces valeurs par défaut - c'est l'objet de cet article ! Nous démontrons comment utiliser LibGolf afin d'énumérer précisément quels octets sont nécessaires et lesquels sont ignorés par le chargeur Linux. Heureusement, il apparaît que le chargeur est d'un des analyseurs syntaxiques les moins pointilleux de la boîte à outils Linux. Avant de terminer, nous verrons quelques outils populaires d'analyse statique s'effondrer devant notre ELF corrompu, alors que le chargeur continue gaiement à charger et sauter aux octets que nous avons précédemment choisis. +-------------------------------+ |--[ Présentation de LibGolf ]--| +-------------------------------+ Il y a quelque temps, nous explorions l'écriture des ELFs à la main dans NASM. Cela était amusant pour le temps que ça a duré (et a certainement des avantages), nous réalisâmes que nous manquions complètement le plaisir que les structures C ont à offrir. En particulier, et certainement que beaucoup de personnes me rejoindront là-dessus,qui est rempli de choses amusantes comme `Elf64_Ehdr` et `Elf32_Phdr` prêtes à être déclarées. Ne voulant pas gaspiller des en-têtes aussi utiles, nous les avons recueillis et mis à bon usage. De tous ces efforts, est né libgolf.h, une librairie qui rend l'injection de shellcode dans un exécutable fonctionnel. Nous savons déjà ce que vous pensez : "cela ressemble à un terrible éditeur de liens !", et vous auriez peut-être raison. Cependant, ce qui est bien ici est que vous pouvez modifier les en-têtes *avant* que le binaire ne soit construit. Regardons comment cela fonctionne. Si vous voulez suivre chez vous, vous pouvez trouver le code source de tout ceci ici : [0]. Le code de cet article se trouve sous "exemples/01_dead_bytes". La mise en place basique requiert deux fichiers : un code source C et un shellcode.h. À propos du shellcode, nous aimerions continuer avec le fameux et classique "b0 3c 48 31 ff 0f 05" qui est déssassemblé vers : mov al, 0x3c @ b0 3c xor rdi, rdi @ 48 31 ff syscall @ 0f 05 (Certes - appeler ça "shellcode" est un peu exagéré) Essentiellement, il appelle simplement exit(0). Nous pouvons facilement vérifier que ces octets ont été exécutés avec succès avec l'expansion du shell $?. Ajoutons ceci ou un autre shellcode (mais soyez sûrs que c'est du code en position indépendante - PIC - car le support pour les symboles relocalisables n'est pas encore implémenté !) dans un buffer appelé buf[] dans shellcode.h et sautez à la fin du fichier C. Si vous voulez un binaire qui exécute votre shellcode, vous n'avez besoin que de : #include "libgolf.h" #include "shellcode.h" int main(int argc, char **argv) { INIT_ELF(X86_64,64); GEN_ELF(); return 0; } Après compilation et lancement de l'exécutable, vous aurez un fichier .bin - voici votre nouveau ELF ! Simple comme bonjour, non ? La simplicité est aussi être accompagnée par l'ennui, et c'est le cas ici ; faisons quelque chose de plus intéressant ! Avant de continuer, il est important d'expliquer ce qui est effectué par ces deux macros. D'abord, INIT_ELF() prends deux arguments, le jeu d'instructions (ISA) et l'architecture. Pour le moment, LibGolf supporte les architectures X86_64, ARM32 et AARCH64 comme ISA valides pour les architectures 32 et 64 bits. Elle définit plusieurs structures de gestion interne et décide si elle utilise les objets Elf32_* ou Elf64_* pour les en-têtes. Elle assigne aussi automatiquement les pointeurs à l'ELF et aux en-têtes du programme, appelés ehdr et phdr respectivement. C'est ces derniers que nous utiliserons pour modifier les champs. De plus, elle copie le buffer du shellcode et rempli l'ELF et les en-têtes du programme avant de calculer un point d'entrée fonctionnel. Ensuite, GEN_ELF() imprime simplement des statistiques au flux standard de sortie et écrit les structures appropriées au fichier .bin. Le nom de .bin est déterminé par argv[0]. Donc, après avoir utilisé la macro INIT_ELF(), nous avons ehdr et phdr disponibles pour déréférence. Supposons que nous voudrions modifier le champ e_version de l'en-tête ELF. Nous avons juste besoin d'ajouter une seule ligne: #include "libgolf.h" #include "shellcode.h" int main(int argc, char **argv) { INIT_ELF(X86_64); // Définit e_version à 12345678 ehdr->e_version = 0x78563412; GEN_ELF(); return 0; } Après une compilation et une exécution rapide, vous aurez un autre fichier .bin qui vous attendra. Examinez ce fichier avec xxd, hexyl ou votre manipulateur de binaire favori, vous verrez les petits "12 34 56 78" présents au offset 0x14. N'était-ce pas simple comme bonjour ? Afin de faire les choses plus rapidement, nous utiliserons le Makefile suivant : .PHONY golf clean CC=gcc CFLAGS=-I. PROG=golf golf: @$(CC) -o $(PROG) $(PROG).c @./$(PROG) @chmod +x $(PROG).bin @rm $(PROG) $(PROG).bin (Vous trouverez le Makefile dans le repo [0]) +-------------------------------------+ |-- [ Échouer au premier obstacle ]-- | +-------------------------------------+ Comme beaucoup le savent déjà, les analyseurs de fichiers sont des choses horribles. Alors que les spécifications ont généralement des objectifs sérieux, elles sont rarement respectées par ceux qui sont censés être mieux informés. Le principal de ces blasphémateurs est le chargeur ELF de Linux lui-même. LibGolf permet d'enquêter facilement sur l'étendue de ces crimes contre elf.h. Un bon endroit pour commencer est le début, c'est-à-dire l'en-tête ELF. Au début de tout fichier ELF se trouve bien sûr le fameux 0x7f suivi de ELF, connu de ses amis sous les noms de EI_MAG0 à EI_MAG3. Sans surprise, la modification de l'un de ces quatre octets entraîne le rejet du fichier par le chargeur Linux. Heureusement pour nous ! Qu'en est-il de l'octet 0x5 ? Notre fidèle spécification nous dit qu'il s'agit de l'octet EI_CLASS et qu'il indique l'architecture cible. Les valeurs acceptables sont 0x01 et 0x02, pour 32 et 64 bits respectivement. Je le répète : les valeurs acceptables sont 0x01 et 0x02. Et si nous lui donnions la valeur 0x58 (ou "X" pour les adeptes de l'ASCII) ? Nous pouvons le faire en ajoutant : (ehdr->e_ident)[EI_CLASS] = 0x58; à notre fichier C générateur. (Pourquoi 0x58 ? Il apparaît clairement dans la sortie de xxd/hexyl !) Une fois que nous avons obtenu notre .bin pour jouer avec, avant d'essayer de l'exécuter, essayons quelques autres outils d'analyse d'ELF communs dans la recherche d'autres coupables. Le premier sur la liste est gdb. Allez-y, nous attendons. Voyez vous ce qu'il se passe ? "not in executable format: file format not recognized" *NDLR : ("pas dans le format exécutable : le format du fichier n'est pas reconnu")* De même, objdump vous donnera une réponse similaire. Il semble que ces analyseurs font leur travail correctement. Maintenant, essayons d'exécuter le binaire normalement. Ça marche sans soucis. Si vous utilisez mon exemple de shellcode, une consultation avec $? vous informera à regret que le binaire s'est terminé avec succès. Les mêmes crimes sont commis lorsque vous définissez EI_DATA et EI_VERSION à des valeurs illégales. +---------------------------------------+ |--[ Corruption : niveau 11 ]--| +---------------------------------------+ Alors, jusqu'où pouvons nous aller ? Quelle quantité d'en-têtes ELF et de programmes le chargeur Linux peut-il ignorer ? Nous avons déjà couvert EI_CLASS, EI_DATA et EI_VERSION, mais il s'avère que EI_OSABI peut également être ignoré en toute sécurité. Cela nous amène à l'offset 0x8. Selon la spécification, le suivant est EI_ABIVERSION et EI_PAD qui, ensemble, nous amènent jusqu'à l'octet 0xf. Il semble que personne ne s'intéresse à eux, donc nous pouvons les mettre tous à 0x58 sans crainte. En allant toujours plus loin, nous tombons sur un champ qui semble résister aux manipulations : e_type. Il est compréhensible que le chargeur Linux apprécie peu que nous ne lui disions pas quel type de fichier ELF nous lui fournissons (il est bon de savoir qu'il a *quelques* normes !). Nous avons besoin que ces deux octets restent 0x0002 (ou ET_EXEC pour les acolytes de elf.h). Le suivant est un autre octet délicat, au offset trop familier de 0x12 : e_machine, qui désigne l'ISA cible. En ce qui nous concerne, en spécifiant X86_64 comme premier argument de INIT_ELF(), cet octet a déjà été rempli de 0x3e par LibGolf. Soudain, un e_version sauvage est apparu ! Nous sommes confrontés à un autre dissident, qui devrait soi-disant toujours être l'octet 0x00000001. Cependant, en pratique, personne ne semble être intéressé, alors remplissons le avec 0x58585858 à la place. Après cette série d'hérésies, nous avons deux champs importants qui semblent résister aux abus : e_entry et e_phoff. Nous sommes sûrs que les détails de e_entry sont triviaux : c'est le point d'entrée du binaire vers lequel l'exécution est finalement transférée une fois que les sections chargeables sont en mémoire. Alors que l'on pourrait s'attendre à ce que le chargeur puisse se débrouiller sans savoir quel est l'offset de l'en-tête du programme, il semble qu'il ne soit pas assez intelligent pour le savoir sans être être nourri à la petite cuillère. Mieux vaut laisser ces deux-là tranquilles. LibGolf ne prend pas encore en charge les en-têtes de section (et étant donné son objectif de produire des *petits* binaires il est aussi peu probable qu'il les prenne en charge à l'avenir). Cela signifie que face à n'importe quel en-tête les concernant, nous pouvons bricoler à notre guise. Cela inclut e_shoff, e_shentsize, eh_shnum et même e_shstrndx. Si nous n'avons pas d'en-tête de section, nous ne pourrions pas être responsables de leur corruption ! Les champs restants qui semblent avoir une certaine importance pour le chargeur Linux sont e_ehsize, e_phentsize, et e_phnum. Encore une fois, ce n'est pas trop surprenant, vu qu'ils sont concernés par le chargement du seul segment chargé en mémoire avant de céder le contrôle. Si vous avez besoin d'un rappel : e_ehsize est la taille de l'en-tête ELF (qui est soit 0x34 soit 0x40 pour les 32 et 64 bits respectivement), eh_phentsize est la taille de l'en-tête du programme à venir (encore une fois, codée en dur à 0x20 ou 0x38 pour les architectures 32 et 64 bits). Si le chargeur avait été un peu plus pointilleux sur EI_CLASS, il n'aurait pas eu besoin de ces deux champs. Enfin, e_phnum est simplement le nombre d'entrées dans l'en-tête du programme - pour nous, c'est toujours 0x1. Il ne fait aucun doute qu'il est utilisé pour une boucle dans les routines de chargement de la mémoire, mais nous n'avons pas exploré cette route. Il reste encore un champ dans l'en-tête ELF, il s'agit de e_flags. La raison est assez simple, car il dépend de l'architecture. Pour x86_64, il n'a aucune importance car il n'est pas défini (bien qu'il soit *important* pour certaines plateformes ARM ! Jetez un coup d'œil à l'exemple arm32 à [0]). Cela nous amène à la fin de l'en-tête ELF. En ce qui concerne le compte, c'est un peu plus de 50% de l'en-tête ELF qui est ignoré par le chargeur. Mais qu'en est-il de l'en-tête du programme ? Il s'avère que les en-têtes de programmes ont beaucoup moins de marge de manœuvre, mais pas pour la raison à laquelle on pourrait s'attendre. En effet, *toute* corruption de l'en-tête du programme n'affectera pas réellement le chargeur Linux. Nous pourrions remplir le tout avec notre fidèle 0x58, et le chargeur ne s'en souciera pas du tout. Mais attention, hacker téméraire, modifie le mauvais octet et dis bonjour au donjon de la segmentation défectueuse ! Alors, y a-t-il quoi que ce soit de coercitif dans l'en-tête du programme ? Il s'avère qu'il y a deux champs qui, sans que ce soit leur faute, ne sont tout simplement plus pertinents : p_paddr et p_align. Le premier était important à l'époque précédant la mémoire virtuelle, où 4 Go de RAM n'étaient rien de plus qu'un rêve d'enfant et où il était donc important d'informer le chargeur de l'endroit de la mémoire physique où le segment devait être chargé. L'alignement de la mémoire est un sujet amusant. Apparemment, p_vaddr est censé être égal à p_offset modulo p_align. Les fichiers ELF "corrects" (au moins ceux compilés avec GCC) semblent simplement définir p_offset égal à p_vaddr et continuer. C'est aussi ce que LibGolf fait par défaut et rend p_align totalement superflu ! Dans l'ensemble, ce n'est pas aussi amusant que l'en-tête ELF, mais il y a quand même quelques petits avantages. Le fichier C générant le binaire ressemble maintenant à ceci : #include "libgolf.h" #include "shellcode.h" int main(int argc, char **argv) { INIT_ELF(X86_64,64); /* * Casse les outils d'analyse statique comme gdb et objdump */ (ehdr->e_ident)[EI_CLASS] = 0x58; // Architecture (ehdr->e_ident)[EI_DATA] = 0x58; // Endianness (ehdr->e_ident)[EI_VERSION] = 0x58; // Supposément, toujours 0x01 (ehdr->e_ident)[EI_OSABI] = 0x58; // OS cible // Boucle autour du reste de e_ident int i; for ( i = 0 ; i < 0x10 ; i++ ) (ehdr->e_ident)[i] = 0x58; ehdr->e_version = 0x58585858; // Supposément, toujours 0x00000001 // En-tête de section ? Nous n'avons pas besoin d'en-tête de section ! ehdr->e_shoff = 0x5858585858585858; ehdr->e_shentsize = 0x5858; ehdr->e_shnum = 0x5858; ehdr->e_shstrndx = 0x5858; ehdr->e_flags = 0x58585858; // x86_64 n'a pas de flags définis phdr->p_paddr = 0x5858585858585858; // L'adresse physique est ignorée phdr->p_align = 0x5858585858585858; // p_vaddr = p_offset, tellement hors-sujet GEN_ELF(); return 0; } Si vous compilez et exécutez ce programme, vous aurez le binaire suivant : 00000000: 7f45 4c46 5858 5858 5858 5858 5858 5858 .ELFXXXXXXXXXXXX 00000010: 0200 3e00 5858 5858 7800 4000 0000 0000 ..>.XXXXx.@..... 00000020: 4000 0000 0000 0000 5858 5858 5858 5858 @.......XXXXXXXX 00000030: 5858 5858 4000 3800 0100 5858 5858 5858 XXXX@.8...XXXXXX 00000040: 0100 0000 0500 0000 0000 0000 0000 0000 ................ 00000050: 0000 4000 0000 0000 5858 5858 5858 5858 ..@.....XXXXXXXX 00000060: 0700 0000 0000 0000 0700 0000 0000 0000 ................ 00000070: 5858 5858 5858 5858 b03c 4831 ff0f 05 XXXXXXXX.