web-dev-qa-db-fra.com

Comment fonctionnent l'ASLR et le DEP?

Comment fonctionnent la randomisation de la disposition de l'espace d'adressage (ASLR) et la prévention de l'exécution des données (DEP), en termes de prévention de l'exploitation des vulnérabilités? Peuvent-ils être contournés?

115
Polynomial

La randomisation de la mise en page de l'espace d'adressage (ASLR) est une technologie utilisée pour empêcher la réussite du shellcode. Il le fait en décalant aléatoirement l'emplacement des modules et de certaines structures en mémoire. La prévention de l'exécution des données (DEP) empêche certains secteurs de la mémoire, par exemple la pile, d'être exécuté. Une fois combinés, il devient extrêmement difficile d'exploiter les vulnérabilités des applications à l'aide de techniques de shellcode ou de programmation orientée retour (ROP).

Voyons d'abord comment une vulnérabilité normale pourrait être exploitée. Nous allons ignorer tous les détails, mais disons simplement que nous utilisons une vulnérabilité de débordement de tampon de pile. Nous avons chargé un gros bloc de valeurs 0x41414141 Dans notre charge utile et eip a été défini sur 0x41414141, Nous savons donc qu'il est exploitable. Nous sommes ensuite partis et avons utilisé un outil approprié (par exemple pattern_create.rb De Metasploit) pour découvrir le décalage de la valeur en cours de chargement dans eip. Il s'agit du décalage de démarrage de notre code d'exploitation. Pour vérifier, nous chargeons 0x41 Avant ce décalage, 0x42424242 Au décalage et 0x43 Après le décalage.

Dans un processus non ASLR et non DEP, l'adresse de pile est la même chaque fois que nous exécutons le processus. Nous savons exactement où il se trouve en mémoire. Voyons donc à quoi ressemble la pile avec les données de test que nous avons décrites ci-dessus:

stack addr | value
-----------+----------
 000ff6a0  | 41414141
 000ff6a4  | 41414141
 000ff6a8  | 41414141
 000ff6aa  | 41414141
>000ff6b0  | 42424242   > esp points here
 000ff6b4  | 43434343
 000ff6b8  | 43434343

Comme nous pouvons le voir, esp pointe vers 000ff6b0, Qui a été défini sur 0x42424242. Les valeurs précédentes sont 0x41 Et les valeurs suivantes sont 0x43, Comme nous l'avons dit. Nous savons maintenant que l'adresse stockée dans 000ff6b0 Sera sautée. Donc, nous le définissons à l'adresse d'une mémoire que nous pouvons contrôler:

stack addr | value
-----------+----------
 000ff6a0  | 41414141
 000ff6a4  | 41414141
 000ff6a8  | 41414141
 000ff6aa  | 41414141
>000ff6b0  | 000ff6b4
 000ff6b4  | cccccccc
 000ff6b8  | 43434343

Nous avons défini la valeur à 000ff6b0 De telle sorte que eip sera défini sur 000ff6b4 - le prochain décalage dans la pile. Cela entraînera l'exécution de 0xcc, Qui est une instruction int3. Puisque int3 Est un point d'arrêt d'interruption logicielle, il lèvera une exception et le débogueur s'arrêtera. Cela nous permet de vérifier que l'exploit a réussi.

> Break instruction exception - code 80000003 (first chance)
[snip]
eip=000ff6b4

Maintenant, nous pouvons remplacer la mémoire de 000ff6b4 Par shellcode, en modifiant notre charge utile. Ceci conclut notre exploit.

Afin d'empêcher ces exploits de réussir, Data Execution Prevention a été développé. DEP force certaines structures, y compris la pile, à être marquées comme non exécutables. Ceci est renforcé par la prise en charge du processeur avec le bit No-Execute (NX), également connu sous le nom de bit XD, EVP ou XN, ce qui permet au CPU d'appliquer des droits d'exécution au niveau matériel. DEP a été introduit dans Linux en 2004 (noyau 2.6.8), et Microsoft l'a introduit en 2004 dans le cadre de WinXP SP2. Apple a ajouté le support DEP quand ils sont passés à l'architecture x86 en 2006. Avec DEP activé, notre exploit précédent ne fonctionnera pas:

> Access violation - code c0000005 (!!! second chance !!!)
[snip]
eip=000ff6b4

Cela échoue car la pile est marquée comme non exécutable et nous avons essayé de l'exécuter. Pour contourner cela, une technique appelée Return-Oriented Programming (ROP) a été développée. Cela implique de rechercher de petits extraits de code, appelés gadgets ROP, dans des modules légitimes du processus. Ces gadgets consistent en une ou plusieurs instructions, suivies d'un retour. En les enchaînant avec les valeurs appropriées dans la pile, le code peut être exécuté.

Tout d'abord, regardons à quoi ressemble notre pile en ce moment:

stack addr | value
-----------+----------
 000ff6a0  | 41414141
 000ff6a4  | 41414141
 000ff6a8  | 41414141
 000ff6aa  | 41414141
>000ff6b0  | 000ff6b4
 000ff6b4  | cccccccc
 000ff6b8  | 43434343

Nous savons que nous ne pouvons pas exécuter le code à 000ff6b4, Nous devons donc trouver du code légitime que nous pouvons utiliser à la place. Imaginez que notre première tâche consiste à obtenir une valeur dans le registre eax. Nous recherchons une combinaison pop eax; ret Quelque part dans n'importe quel module du processus. Une fois que nous en avons trouvé un, disons à 00401f60, Nous mettons son adresse dans la pile:

stack addr | value
-----------+----------
 000ff6a0  | 41414141
 000ff6a4  | 41414141
 000ff6a8  | 41414141
 000ff6aa  | 41414141
>000ff6b0  | 00401f60
 000ff6b4  | cccccccc
 000ff6b8  | 43434343

Lorsque ce shellcode est exécuté, nous obtiendrons à nouveau une violation d'accès:

> Access violation - code c0000005 (!!! second chance !!!)
eax=cccccccc ebx=01020304 ecx=7abcdef0 edx=00000000 esi=7777f000 edi=0000f0f1
eip=43434343 esp=000ff6ba ebp=000ff6ff

Le CPU a maintenant fait ce qui suit:

  • Saut à l'instruction pop eax Dans 00401f60.
  • cccccccc extrait de la pile, dans eax.
  • Exécuté le ret, en insérant 43434343 Dans eip.
  • Jeté une violation d'accès car 43434343 N'est pas une adresse mémoire valide.

Maintenant, imaginez qu'au lieu de 43434343, La valeur de 000ff6b8 A été définie à l'adresse d'un autre gadget ROP. Cela signifierait que pop eax Est exécuté, puis notre prochain gadget. Nous pouvons enchaîner des gadgets comme ça. Notre objectif ultime est généralement de trouver l'adresse d'une API de protection de la mémoire, telle que VirtualProtect, et de marquer la pile comme exécutable. Nous inclurions ensuite un dernier gadget ROP pour exécuter une instruction équivalente jmp esp Et exécuter le shellcode. Nous avons réussi à contourner DEP!

Afin de lutter contre ces astuces, ASLR a été développé. ASLR implique un décalage aléatoire des structures de mémoire et des adresses de base des modules pour rendre très difficile la devinette de l'emplacement des gadgets ROP et des API.

Sur Windows Vista et 7, ASLR randomise l'emplacement des exécutables et des DLL en mémoire, ainsi que la pile et les tas. Lorsqu'un exécutable est chargé en mémoire, Windows obtient le compteur d'horodatage (TSC) du processeur, le décale de quatre emplacements, effectue le module de division 254, puis ajoute 1. Ce nombre est ensuite multiplié par 64 Ko, et l'image exécutable est chargée à ce décalage . Cela signifie qu'il existe 256 emplacements possibles pour l'exécutable. Étant donné que les DLL sont partagées en mémoire entre les processus, leurs décalages sont déterminés par une valeur de biais à l'échelle du système qui est calculée au démarrage. La valeur est calculée en tant que TSC du CPU lorsque la fonction MiInitializeRelocations est appelée, décalée et masquée pour la première fois sur une valeur de 8 bits. Cette valeur n'est calculée qu'une seule fois par démarrage.

Lorsque les DLL sont chargées, elles vont dans une région de mémoire partagée entre 0x50000000 Et 0x78000000. Le premier DLL à charger est toujours ntdll.dll, qui est chargé à 0x78000000 - bias * 0x100000, Où bias est la valeur de biais à l'échelle du système calculée au démarrage. Puisqu'il serait trivial de calculer l'offset d'un module si vous connaissez l'adresse de base de ntdll.dll, l'ordre dans lequel les modules sont chargés est également aléatoire.

Lorsque les threads sont créés, leur emplacement de base de pile est aléatoire. Cela se fait en trouvant 32 emplacements appropriés en mémoire, puis en choisissant un en fonction du TSC actuel décalé masqué en une valeur de 5 bits. Une fois l'adresse de base calculée, une autre valeur de 9 bits est dérivée du TSC pour calculer l'adresse de base finale de la pile. Cela fournit un degré théorique élevé de caractère aléatoire.

Enfin, l'emplacement des tas et des allocations de tas est aléatoire. Ceci est calculé comme une valeur dérivée de TSC 5 bits multipliée par 64 Ko, donnant une plage de tas possible de 00000000 À 001f0000.

Lorsque tous ces mécanismes sont combinés avec DEP, nous ne pouvons pas exécuter de shellcode. Cela est dû au fait que nous ne pouvons pas exécuter la pile, mais nous ne savons pas non plus où nos instructions ROP vont être en mémoire. Certaines astuces peuvent être effectuées avec des traîneaux nop pour créer un exploit probabiliste, mais elles ne sont pas entièrement réussies et ne sont pas toujours possibles à créer.

Le seul moyen de contourner de manière fiable DEP et ASLR est par une fuite de pointeur. Il s'agit d'une situation où une valeur sur la pile, à un emplacement fiable, peut être utilisée pour localiser un pointeur de fonction utilisable ou un gadget ROP. Une fois cela fait, il est parfois possible de créer une charge utile qui contourne de manière fiable les deux mécanismes de protection.

Sources:

Lectures complémentaires:

153
Polynomial

Pour compléter l'auto-réponse de @ Polynomial: DEP peut en fait être appliqué sur les anciennes machines x86 (qui sont antérieures au bit NX), mais à un prix.

Le moyen simple mais limité de faire DEP sur un ancien matériel x86 est d'utiliser des registres de segments. Avec les systèmes d'exploitation actuels sur ces systèmes, les adresses sont des valeurs 32 bits dans un espace d'adressage plat de 4 Go, mais en interne, chaque accès à la mémoire utilise implicitement une adresse 32 bits et un registre spécial 16 bits , appelé "registre de segment".

En mode dit protégé, les registres de segments pointent vers une table interne (la "table des descripteurs" - en fait, il y a deux de ces tables, mais c'est une technicité) et chaque entrée du tableau spécifie les caractéristiques du segment. En particulier, les types d'accès autorisés et la taille du segment. De plus, l'exécution de code utilise implicitement le registre de segment CS, tandis que l'accès aux données utilise principalement DS (et l'accès à la pile, par exemple avec les opcodes Push et pop, utilise SS). Cela permet au système d'exploitation de diviser l'espace d'adressage en deux parties; les adresses inférieures étant dans la plage pour CS et DS, tandis que les adresses supérieures sont hors plage pour CS. Par exemple, le segment décrit par CS est fait être de taille 512 Mo. Cela signifie que toute adresse au-delà de 0x20000000 sera accessible en tant que données (lues ou écrites à l'aide de DS comme registre de base) mais les tentatives d'exécution utiliseront CS, auquel cas le Le CPU lèvera une exception (que le noyau convertira en un signal approprié comme SIGILL ou SIGSEGV, impliquant généralement la mort du processus incriminé).

(Notez que les segments sont appliqués sur l'espace d'adressage; le MMU est toujours actif, sur une couche inférieure, donc l'astuce expliquée ci-dessus est par processus.)

C'est peu coûteux à faire: le matériel x86 le fait applique systématiquement les segments (et le premier 80386 le faisait déjà; en fait, le 80286 avait déjà de tels segments avec des limites, mais seulement des décalages de 16 bits ). Nous pouvons généralement les oublier parce que des systèmes d'exploitation sensés définissent les segments pour commencer à un décalage de zéro et une longueur de 4 Go, mais les définir autrement n'implique aucun frais généraux que nous n'avions pas déjà. Cependant, en tant que mécanisme DEP, il est inflexible: lorsqu'un certain bloc de données est demandé au noyau, le noyau doit décider si c'est pour le code ou non pour le code, car la frontière est fixe. Nous ne pouvons pas décider de convertir dynamiquement une page donnée entre le mode code et le mode données.

La façon amusante mais un peu plus chère de faire DEP utilise quelque chose appelé PaX . Pour comprendre ce qu'il fait, il faut entrer dans certains détails.

MMU sur le matériel x86 utilise des tables en mémoire, qui décrivent l'état de chaque page de 4 Ko dans l'espace d'adressage. L'espace d'adressage est de 4 Go, il y a donc 1048576 pages. Chaque page est décrite par une entrée 32 bits dans un sous-tableau; il y a 1024 sous-tables, chacune contenant 1024 entrées, et il y a une table principale, avec 1024 entrées qui pointent vers les 1024 sous-tables. Chaque entrée indique où l'objet pointé (une sous-table ou une page) se trouve dans la RAM, ou s'il y est, et quels sont ses droits d'accès. La racine du problème est que les droits d'accès concernent les niveaux de privilèges (code du noyau vs espace utilisateur) et un seul bit pour le type d'accès, permettant ainsi "lecture-écriture" ou "lecture seule". "Exécution" est considérée comme une sorte d'accès en lecture. Par conséquent, le MMU n'a aucune notion d '"exécution" distincte de l'accès aux données. Ce qui est lisible, est exécutable.

(Depuis le Pentium Pro, au siècle précédent, les processeurs x86 connaissent un autre format pour les tables, appelé PAE . Il double la taille des entrées, ce qui laisse de la place pour adresser plus de RAM physique, et aussi pour ajouter un bit NX - mais ce bit spécifique n'a été implémenté par le matériel que vers 2004.)

Cependant, il y a une astuce. RAM is slow. Pour effectuer un accès mémoire, le processeur doit d'abord lire la table principale pour localiser la sous-table qu'il doit consulter, puis faire une autre lecture dans cette sous-table, et seulement à ce stade, le processeur sait-il si l'accès à la mémoire doit être autorisé ou non, et où en physique RAM les données consultées sont réellement. Ce sont des accès en lecture avec une dépendance complète (chaque accès dépend de la valeur lue par la précédente), ce qui paie une latence complète, ce qui, sur un processeur moderne, peut représenter des centaines de cycles d'horloge. Par conséquent, le processeur inclut un cache spécifique qui contient le dernier accès MMU table Ce cache est le Translation Lookaside Buffer .

À partir du 80486, les processeurs x86 n'ont pas un TLB, mais deux. La mise en cache fonctionne sur l'heuristique, et l'heuristique dépend des modèles d'accès, et les modèles d'accès pour le code ont tendance à différer des modèles d'accès pour les données. Ainsi, les gens intelligents d'Intel/AMD/other ont trouvé utile d'avoir un TLB dédié à l'accès au code (exécution) et un autre pour l'accès aux données. De plus, le 80486 possède un opcode (invlpg) qui peut supprimer une entrée spécifique du TLB.

L'idée est donc la suivante: faire en sorte que les deux TLB aient des vues différentes de la même entrée. Toutes les pages sont marquées dans les tables (en RAM) comme "absentes", déclenchant ainsi une exception lors de l'accès. Le noyau intercepte l'exception, et l'exception inclut certaines données sur le type d'accès, en particulier si c'était pour l'exécution de code ou non. Le noyau invalide ensuite l'entrée TLB nouvellement lue (celle qui dit "absent"), puis remplit l'entrée dans RAM avec certains droits qui autorisent l'accès, puis force un accès du type requis ( soit lecture de données ou exécution de code), qui alimente l'entrée dans le TLB correspondant, et seulement celui-là. Le noyau remet ensuite rapidement l'entrée dans RAM de nouveau à absent, et revient finalement au processus (de nouveau à essayer l'opcode qui a déclenché l'exception).

L'effet net est que, lorsque l'exécution revient au code de processus, le TLB pour le code ou le TLB pour les données contient l'entrée appropriée, mais l'autre TLB ne le fait pas et ne sera pas puisque les tables de RAM disent toujours "absent". À ce stade, le noyau est en position de décider d'autoriser ou non l'exécution, indépendamment du fait qu'il autorise ou non l'accès aux données. Il peut ainsi appliquer une sémantique de type NX.

Le diable se cache dans les détails; dans ce cas, il y a de la place pour toute une légion de démons. Une telle danse avec le matériel n'est pas facile à mettre en œuvre correctement. Surtout sur les systèmes multicœurs.

La surcharge est la suivante: lorsqu'un accès est effectué et que le TLB ne contient pas l'entrée appropriée, les tables de RAM doivent être consultées, ce qui implique à lui seul la perte de quelques centaines de cycles. Pour cela coût, PaX ajoute les frais généraux de l'exception, et le code de gestion qui remplit le bon TLB, transformant ainsi les "quelques centaines de cycles" en "quelques milliers de cycles". Heureusement, TLB manque de raison. Les gens de PaX prétendent avoir mesuré un ralentissement d'aussi peu que 2,7% sur un gros travail de compilation (cela dépend cependant du type de CPU).

Le bit NX rend tout cela obsolète. Notez que le jeu de patchs PaX contient également d'autres fonctionnalités liées à la sécurité, comme ASLR, qui est redondant avec certains fonctionnalité des noyaux officiels plus récents.

40
Thomas Pornin