ld -z code séparé


Cet article se concentrera sur une petite fonctionnalité de sécurité ajoutée dans GNU ld à la version 2.30 de décembre 2018. En russe, cette amélioration a été mentionnée sur opennet avec l'annotation suivante:


Mode "-z code séparé", qui augmente la sécurité des fichiers exécutables au prix d'une petite augmentation de la taille et de la consommation de mémoire

Voyons cela. Pour expliquer de quel type de problème de sécurité nous parlons et quelle est la solution, commençons par les caractéristiques générales des exploits de vulnérabilité binaire.


Exploiter les problèmes de flux de contrôle


Un attaquant peut transférer des données vers le programme et les manipuler de cette manière à l'aide de diverses vulnérabilités: écriture par index au-delà des limites d'un tableau, copie non sécurisée de chaînes, utilisation d'objets après publication. Ces erreurs sont typiques du code de programme C et C ++ et peuvent entraîner une corruption de la mémoire avec certaines données d'entrée pour le programme.


Vulnérabilités de corruption de mémoire

CWE-20: Validation d'entrée incorrecte
CWE-118: accès incorrect à la ressource indexable («erreur de plage»)
CWE-119: Restriction incorrecte des opérations dans les limites d'un tampon mémoire
CWE-120: Copie de tampon sans vérifier la taille de l'entrée («débordement de tampon classique»)
CWE-121: Dépassement de tampon basé sur la pile
CWE-122: Débordement de tampon basé sur le tas
CWE-123: Condition d'écriture où-où
CWE-124: Buffer Underwrite ('Buffer Underflow')
CWE-125: Lecture en dehors des limites
CWE-126: Tampon en sur-lecture
CWE-127: Tampon sous-lu
CWE-128: Erreur de bouclage
CWE-129: Validation incorrecte de l'index de tableau
CWE-130: Traitement incorrect de l'incohérence des paramètres de longueur
CWE-131: calcul incorrect de la taille de la mémoire tampon
CWE-134: Utilisation d'une chaîne de format à contrôle externe
CWE-135: calcul incorrect de la longueur de chaîne multi-octets
CWE-170: Résiliation nulle incorrecte
CWE-190: débordement d'entier ou enveloppe
CWE-415: Double gratuit
CWE-416: utiliser après la version gratuite
CWE-476: Déréférencement de pointeur NULL
CWE-787: Écriture hors limites
CWE-824: Accès au pointeur non initialisé
...


L'élément exploit classique des vulnérabilités de type corruption de mémoire écrase un pointeur en mémoire. Le pointeur sera ensuite utilisé par le programme pour transférer le contrôle vers un autre code: pour appeler une méthode ou une fonction de classe à partir d'un autre module, pour revenir d'une fonction. Et puisque le pointeur a été écrasé, le contrôle sera intercepté par l'attaquant - c'est-à-dire que le code préparé par lui sera exécuté. Si vous êtes intéressé par les variations et les détails de ces techniques, nous vous recommandons de lire le document .


Ce moment commun de l'opération de tels exploits est connu, et ici pour l'attaquant les barrières sont depuis longtemps placées:


  1. Vérification de l'intégrité des pointeurs avant de passer le contrôle: empiler les cookies, contrôler la protection du flux, authentification du pointeur
  2. Randomisation des adresses de segment avec code et données: randomisation de la disposition de l'espace d'adressage
  3. Empêcher le code d'exécuter des segments de code extérieurs: protection de l'espace exécutable

Ensuite, nous nous concentrerons sur la protection de ce dernier type.


protection de l'espace exécutable


La mémoire du programme est hétérogène et divisée en segments avec des droits différents: lire, écrire et exécuter. Ceci est assuré par la capacité du processeur à marquer les pages mémoire avec des drapeaux d'accès dans les tables de pages. L'idée de protection est basée sur une séparation stricte du code et des données: les données reçues de l'attaquant lors de leur traitement doivent être placées dans des segments non exécutables (pile, tas), et le code du programme lui-même - dans des segments inchangeables séparés. Ainsi, cela devrait priver l'attaquant de la possibilité de placer et d'exécuter du code superflu en mémoire.


Pour contourner l'interdiction de l'exécution de code dans les segments de données, des techniques de réutilisation de code sont utilisées. Autrement dit, l'attaquant transfère le contrôle aux fragments de code (ci-après dénommés gadgets) situés sur les pages exécutables. Les techniques de ce type sont de difficulté variable, en ordre croissant:


  • transfert du contrôle à une fonction qui fait ce qui est suffisant pour l'attaquant: à la fonction system () avec un argument contrôlé pour exécuter des commandes shell arbitraires (ret2libc)
  • transfert du contrôle à une fonction ou une chaîne de gadgets qui désactivera la protection ou rendra exécutable une partie de la mémoire (par exemple, appeler mprotect() ), suivi de l'exécution de code arbitraire
  • exécution de toutes les actions souhaitées à l'aide d'une longue chaîne de gadgets

Ainsi, l'attaquant est confronté à la tâche de réutiliser le code existant dans un volume ou un autre. Si c'est quelque chose de plus compliqué que de revenir à une seule fonction, alors une chaîne de gadgets sera nécessaire. Pour rechercher des gadgets par segments exécutables, il existe des outils: ropper , ropgadget .


Trou READ_IMPLIES_EXEC


Cependant, des zones de mémoire contenant des données peuvent parfois être exécutables et les principes de séparation du code et des données décrits ci-dessus sont clairement violés. Dans de tels cas, l'attaquant n'a pas la peine de trouver des gadgets ou des fonctions pour réutiliser le code. Une découverte intéressante de ce type a été la pile exécutable et tous les segments de données sur le même «pare-feu industriel».


Liste /proc/$pid/maps :


 00008000-00009000 r-xp 00000000 08:01 10 /var/flash/dmt/nx_test/a.out 00010000-00011000 rwxp 00000000 08:01 10 /var/flash/dmt/nx_test/a.out 00011000-00032000 rwxp 00000000 00:00 0 [heap] 40000000-4001f000 r-xp 00000000 1f:02 429 /lib/ld-linux.so.2 4001f000-40022000 rwxp 00000000 00:00 0 40027000-40028000 r-xp 0001f000 1f:02 429 /lib/ld-linux.so.2 40028000-40029000 rwxp 00020000 1f:02 429 /lib/ld-linux.so.2 4002c000-40172000 r-xp 00000000 1f:02 430 /lib/libc.so.6 40172000-40179000 ---p 00146000 1f:02 430 /lib/libc.so.6 40179000-4017b000 r-xp 00145000 1f:02 430 /lib/libc.so.6 4017b000-4017c000 rwxp 00147000 1f:02 430 /lib/libc.so.6 4017c000-40b80000 rwxp 00000000 00:00 0 be8c2000-be8d7000 rwxp 00000000 00:00 0 [stack] 

Vous voyez ici la carte mémoire du processus de l'utilitaire de test. Une carte se compose de zones de mémoire - lignes de table. Tout d'abord, faites attention à la colonne de droite - elle explique le contenu de la zone (segments de code, données des bibliothèques de fonctions ou le programme lui-même) ou son type (tas, pile). Sur la gauche, dans l'ordre, se trouve la plage d'adresses que chaque zone de mémoire occupe et, en outre, les indicateurs de droits d'accès: r (lecture), w (écriture), x (exécution). Ces indicateurs déterminent le comportement du système lors de la tentative de lecture, d'écriture et d'exécution de la mémoire à ces adresses. Si le mode d'accès désigné est violé, une exception se produit.


Notez que presque toute la mémoire à l'intérieur du processus est exécutable: la pile, le tas et tous les segments de données. C'est un problème. De toute évidence, la présence de pages de mémoire rwx facilitera la vie d'un attaquant, car il pourra exécuter librement son code dans un tel processus à n'importe quel endroit où son code obtient lors du transfert de données (paquets, fichiers) vers un tel programme pour traitement.


Pourquoi une telle situation s'est-elle produite sur un appareil moderne qui prend en charge l'interdiction de l'exécution de code sur les pages de données avec du matériel, la sécurité des réseaux d'entreprise et industriels dépend-elle de l'appareil, et le problème sondé et sa solution sont connus depuis très longtemps?


Cette image est déterminée par le comportement du noyau lors de l'initialisation du processus (allocation d'une pile, tas, chargement de l'ELF principal, etc.) et lors de l'exécution des appels du processus nucléaire. L'attribut clé qui affecte cela est l'indicateur de personnalité READ_IMPLIES_EXEC . L'effet de cet indicateur est que toute mémoire lisible devient également exécutable. Un indicateur peut être défini pour votre processus pour plusieurs raisons:


  1. L'héritage peut être explicitement demandé par le drapeau logiciel dans l'en-tête ELF pour implémenter un mécanisme très intéressant: un tremplin sur la pile ( 1 , 2 , 3 )!
  2. Il peut être hérité par les processus enfants du parent.
  3. Il peut être installé par le noyau indépendamment pour tous les processus! Tout d'abord, si l'architecture ne prend pas en charge la mémoire non exécutable. Deuxièmement, juste au cas où, pour supporter d'autres anciennes béquilles . Ce code est dans le noyau 2.6.32 (ARM), qui avait une très longue durée de vie. C'était juste notre cas.

Espace pour trouver des gadgets dans une image ELF


Les bibliothèques de fonctions et les exécutables de programme sont au format ELF. Le compilateur gcc traduit les constructions de langage en code machine et les place dans une section, et les données que ce code opère dans d'autres sections. Il existe de nombreuses sections et elles sont regroupées par l'éditeur de liens ld en segments. Ainsi, ELF contient une image de programme qui a deux représentations: un tableau de sections et un tableau de segments.


 $ readelf -l /bin/ls Elf file type is EXEC (Executable file) Entry point 0x804bee9 There are 9 program headers, starting at offset 52 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align PHDR 0x000034 0x08048034 0x08048034 0x00120 0x00120 RE 0x4 INTERP 0x000154 0x08048154 0x08048154 0x00013 0x00013 R 0x1 [Requesting program interpreter: /lib/ld-linux.so.2] LOAD 0x000000 0x08048000 0x08048000 0x1e40c 0x1e40c RE 0x1000 LOAD 0x01ef00 0x08067f00 0x08067f00 0x00444 0x01078 RW 0x1000 DYNAMIC 0x01ef0c 0x08067f0c 0x08067f0c 0x000f0 0x000f0 RW 0x4 NOTE 0x000168 0x08048168 0x08048168 0x00044 0x00044 R 0x4 GNU_EH_FRAME 0x018b74 0x08060b74 0x08060b74 0x00814 0x00814 R 0x4 GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x10 GNU_RELRO 0x01ef00 0x08067f00 0x08067f00 0x00100 0x00100 R 0x1 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 .rel.dyn .rel.plt .init .plt .plt.got .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 08 .init_array .fini_array .jcr .dynamic .got 

Vous voyez ici le mappage des sections aux segments dans une image ELF.


Le tableau des sections est utilisé par les utilitaires pour analyser les programmes et les bibliothèques, mais il n'est pas utilisé par les chargeurs pour projeter les ELF dans la mémoire de processus. Le tableau des sections décrit la structure ELF plus en détail que le tableau des segments. Plusieurs sections peuvent être à l'intérieur d'un segment.


Une image ELF en mémoire est créée par les chargeurs ELF en fonction du contenu de la table de segments . La table de partition n'est plus utilisée pour charger ELF en mémoire.


Mais il y a des exceptions à cette règle.

Par exemple, dans la nature, il existe un correctif pour les développeurs Debian pour le chargeur ELF ld.so pour l'architecture ARM, qui recherche une section spéciale ".ARM.attributes" comme SHT_ARM_ATTRIBUTES et les binaires avec une table de section tronquée dans un tel système ne sont pas chargés ...


Un segment ELF a des indicateurs qui déterminent les autorisations que le segment aura en mémoire. Traditionnellement, la plupart des logiciels pour GNU / Linux étaient organisés de telle manière que deux PT_LOAD (chargeables en mémoire) étaient déclarés dans la table des segments - comme dans la liste ci-dessus:


  1. Segment avec drapeaux RE


    1.1. Code exécutable ELF: sections .init , .text , .fini


    1.2. Données immuables dans ELF: .rodata .symtab , .rodata


  2. Segment RW Flags


    2.1. Données variables dans ELF: sections .plt , .got , .data , .bss



Si vous faites attention à la composition du premier segment et à ses indicateurs d'accès, il devient clair qu'une telle disposition élargit l'espace de recherche de gadgets pour les techniques de réutilisation de code. Dans les grands ELF tels que libcrypto, les tables de service et autres données immuables peuvent occuper jusqu'à 40% du segment exécutable . La présence de quelque chose de similaire à des morceaux de code dans ces données est confirmée par les tentatives de démontage de tels fichiers binaires avec une grande quantité de données dans le segment exécutable sans tables de section et symboles. Chaque séquence d'octets dans ce segment exécutable unique peut être considérée comme utile pour le fragment d'attaque du code machine et du tremplin - être cette séquence d'octets avec au moins une partie de la ligne du message de débogage du programme, une partie du nom de la fonction dans la table des symboles ou le nombre constant de l'algorithme cryptographique ...


En-têtes exécutables PE

Les en-têtes et tableaux exécutables au début du premier segment de l'image ELF ressemblent à la situation avec Windows il y a environ 15 ans. Il y avait un certain nombre de virus infectant les fichiers, écrivant leur code dans leur en-tête PE, qui y était également exécutable. J'ai réussi à déterrer un tel échantillon dans les archives:


Virus.Win32.Haless.1127


Comme vous pouvez le voir, le corps du virus est pressé juste après le tableau des sections dans la zone des en-têtes PE. Dans une projection d'un fichier sur la mémoire virtuelle, il y a généralement environ 3 Ko d'espace libre ici. Après le corps du virus, il y a un espace vide, puis la première section commence par le code du programme.


Cependant, pour Linux, il y avait des œuvres beaucoup plus intéressantes de la scène VX: Retaliation .


Solution


  • Le problème décrit ci-dessus est connu depuis longtemps .
  • Correction du 12 janvier 2018 : la clé `ld -z code-séparé:" Créer un code séparé "PT_LOAD" en-tête de segment dans l'objet est ajoutée. Ceci spécifie un segment de mémoire qui ne doit contenir que des instructions et doit être dans des pages entièrement disjointes de toute autre donnée. Ne créez pas de segment de code "PT_LOAD" séparé si le code de nez est utilisé. "). La fonctionnalité a été publiée dans la version 2.30 .
  • De plus, cette fonctionnalité a été incluse par défaut dans la prochaine version 2.31 .
  • Présent dans les nouveaux packages binutils , par exemple, dans les référentiels Ubuntu 18.10. De nombreux packages ont déjà été assemblés avec cette nouvelle fonctionnalité, que le chercheur ElfMaster a rencontrée et documentée

À la suite des modifications apportées à l'algorithme de mise en page, une nouvelle image ELF est obtenue:


 $ readelf -l ls Elf file type is DYN (Shared object file) Entry point 0x41aa There are 11 program headers, starting at offset 52 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align PHDR 0x000034 0x00000034 0x00000034 0x00160 0x00160 R 0x4 INTERP 0x000194 0x00000194 0x00000194 0x00013 0x00013 R 0x1 [Requesting program interpreter: /lib/ld-linux.so.2] LOAD 0x000000 0x00000000 0x00000000 0x01e6c 0x01e6c R 0x1000 LOAD 0x002000 0x00002000 0x00002000 0x14bd8 0x14bd8 RE 0x1000 LOAD 0x017000 0x00017000 0x00017000 0x0bf80 0x0bf80 R 0x1000 LOAD 0x0237f8 0x000247f8 0x000247f8 0x0096c 0x01afc RW 0x1000 DYNAMIC 0x023cec 0x00024cec 0x00024cec 0x00100 0x00100 RW 0x4 NOTE 0x0001a8 0x000001a8 0x000001a8 0x00044 0x00044 R 0x4 GNU_EH_FRAME 0x01c3f8 0x0001c3f8 0x0001c3f8 0x0092c 0x0092c R 0x4 GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x10 GNU_RELRO 0x0237f8 0x000247f8 0x000247f8 0x00808 0x00808 R 0x1 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 .rel.dyn .rel.plt 03 .init .plt .plt.got .text .fini 04 .rodata .eh_frame_hdr .eh_frame 05 .init_array .fini_array .data.rel.ro .dynamic .got .data .bss 06 .dynamic 07 .note.ABI-tag .note.gnu.build-id 08 .eh_frame_hdr 09 10 .init_array .fini_array .data.rel.ro .dynamic .got 

La frontière entre le code et les données est désormais plus précise. Le seul segment exécutable ne contient vraiment que des sections de code: .init, .plt, .plt.got, .text, .fini.


Qu'est-ce qui a été changé exactement dans ld

Comme vous le savez, la structure du fichier ELF de sortie est décrite par le script de l' éditeur de liens . Vous pouvez voir le script par défaut comme ceci:


 $ ld --verbose GNU ld (GNU Binutils for Ubuntu) 2.26.1 * * * using internal linker script: ================================================== /* Script for -z combreloc: combine and sort reloc sections */ /* Copyright (C) 2014-2015 Free Software Foundation, Inc. * * * 

De nombreux autres scripts pour différentes plates-formes et combinaisons d'options se trouvent dans le répertoire ldscripts . De nouveaux scripts ont été créés pour l'option de separate-code .


 $ diff elf_x86_64.x elf_x86_64.xe 1c1 < /* Default linker script, for normal executables */ --- > /* Script for -z separate-code: generate normal executables with separate code segment */ 46a47 > . = ALIGN(CONSTANT (MAXPAGESIZE)); 70a72,75 > . = ALIGN(CONSTANT (MAXPAGESIZE)); > /* Adjust the address for the rodata segment. We want to adjust up to > the same address within the page on the next page up. */ > . = SEGMENT_START("rodata-segment", ALIGN(CONSTANT (MAXPAGESIZE)) + (. & (CONSTANT (MAXPAGESIZE) - 1))); 

Ici, vous pouvez voir qu'une directive a été ajoutée pour déclarer un nouveau segment avec des sections en lecture seule après le segment de code.


Cependant, en plus des scripts, des modifications ont été apportées aux sources de l'éditeur de liens. A savoir, dans la fonction _bfd_elf_map_sections_to_segments - voir commit . Désormais, lors de la sélection de segments pour les sections, un nouveau segment sera ajouté lorsque la section diffère par l'indicateur SEC_CODE de la section précédente.


Conclusion


Comme précédemment , nous recommandons aux développeurs de ne pas oublier d'utiliser les indicateurs de sécurité intégrés au compilateur et à l'éditeur de liens lors du développement de logiciels. Un tout petit changement peut compliquer considérablement la vie de l'attaquant et rendre la vôtre beaucoup plus calme.

Source: https://habr.com/ru/post/fr433108/


All Articles