Je ne comprends pas le concept de Jump-Oriented-Programming. Quelqu'un peut-il essayer de m'expliquer cela d'une manière facile à comprendre?
Je vois que JOP est un concept qui a évolué en raison des mesures de sécurité qui ont été mises en œuvre pour protéger les logiciels contre les attaques par dépassement de tampon:
Dans un logiciel d'attaque par dépassement de tampon, il utilise une fonction qui n'effectue pas de vérification des limites sur les entrées. Par conséquent, un tableau peut se voir attribuer bien plus de valeurs qu'il ne devrait réellement en stocker et ainsi la pile de mémoire peut être écrasée.
Au début, le code Shell était écrit sur la pile. J'imagine par exemple l'adresse de retour de main () a été remplacée par un pointeur vers un autre emplacement de la pile. À cet emplacement particulier sur la pile, le code Shell a été inséré. Cependant, la prévention de l'exécution des données a été introduite, ce qui rend en quelque sorte impossible l'exécution du code à partir de la pile. De plus, ASLR a été introduit pour randomiser la mémoire. Je ne suis pas sûr ici, quelle mémoire ASLR randomise? Peut-il s'agir d'adresses de pile?
Des canaris ont également été introduits. Il s'agit d'un emplacement de mémoire sur la pile, qui est connu pour avoir une certaine valeur. Avant de revenir d'une fonction, la pile vérifie que le canari n'a pas été changé.
La programmation orientée retour est un concept qui permet d'exploiter les vulnérabilités de saturation de tampon même en présence de DEP, ASLR et de canaris. Comment ça marche? Dans ROP, le code Shell se compose uniquement d'appels de fonction de bibliothèque système. Ou quelque chose comme ça (veuillez me pardonner, je ne suis pas si bon avec tout cela. C'est pourquoi j'ai vraiment besoin de votre aide!).
Ainsi, dans ROP, une vulnérabilité de saturation de tampon peut être exploitée de telle manière, par exemple l'adresse de retour de main () est remplacée par un pointeur vers libc. En libc, il y a des fonctions système (je n'en connais vraiment pas, sauf execve). Ces fonctions sont certainement exécutables. Et ils peuvent être utilisés pour créer un code Shell.
Maintenant, comment protéger un système contre le ROP? Et comment cette protection devient-elle invalide via JOP? Quelqu'un peut-il m'aider à comprendre JOP?
Je suis reconnaissant pour n'importe quoi!
Beaucoup de vos questions sont en double ici, donc je vais les couvrir très brièvement. Il suffit de dire que tout cela a été une course aux armements de plusieurs décennies entre les développeurs d'exploits et les développeurs de systèmes d'exploitation/compilateurs.
Au départ, il n'y avait aucune protection contre les exploits. Vous pouvez trouver un bogue de dépassement de tampon de pile, l'utiliser pour remplacer l'adresse de retour du cadre d'appel actuel sur la pile, attendre que la fonction exploitée atteigne sa fin et revenir, prenant ainsi le contrôle du pointeur d'instruction (IP), que vous pourriez pointer revenir aux instructions que vous mettez dans le tampon de pile dans le cadre de votre charge utile d'exploit (shellcode), et ainsi exécuter du code arbitraire.
Puis il y a eu des cookies de pile. Ce sont des valeurs générées aléatoirement qui sont placées juste avant le pointeur de retour. À la fin de chaque fonction (dans l'épilogue), la fonction vérifierait que la valeur du cookie était correcte avant de revenir. Si vous écrasiez aveuglément un tampon de pile pour écraser l'adresse de retour, vous finiriez par écraser le cookie de pile, qui serait ensuite vérifié à la fin de la fonction et vous n'obtiendriez pas l'exécution de code. Pour contourner ce problème, les attaquants ont plutôt commencé à cibler les structures de gestion des exceptions (SEH) qui apparaissaient généralement à la fin du cadre de pile, mais avant le pointeur de retour. En écrasant les structures SEH, vous pourriez inciter le système d'exploitation à exécuter un gestionnaire d'exceptions à une adresse que vous contrôliez, encore une fois généralement le tampon de pile que vous avez remplacé par shellcode.
Pour résoudre ce problème, les développeurs de systèmes d'exploitation ont adopté deux approches. L'un était SafeSEH, qui gère une table en lecture seule des adresses de gestionnaire d'exceptions valides dans une table dans le cadre de la structure exécutable, et vérifie que l'adresse du gestionnaire d'exceptions est l'une de celles de cette table avant de rediriger le flux de contrôle vers elle. Sur les systèmes Windows x86-64, la pile n'est plus du tout utilisée pour stocker les structures SEH, ce qui atténue complètement cette attaque, mais je me devance un peu ici.
Entre-temps, certains développeurs d'entreprises entreprenants avaient commencé à examiner les débordements de mémoire tampon (c'est-à-dire la mémoire dynamique qui n'est pas la pile) et ce qui pouvait être fait avec ceux-ci. Les débordements de tampon de tas vous permettent parfois d'écraser des pointeurs (par exemple vers des gestionnaires d'événements, des rappels, etc.) qui ont conduit à contrôler le pointeur d'instruction et donc l'exécution de code. Encore une fois, les attaquants mettaient généralement leur shellcode dans le même tampon qu'ils débordaient.
L'autre approche que j'ai mentionnée ci-dessus était la prévention de l'exécution des données (DEP). C'est un peu déroutant car au début Microsoft a appelé SafeSEH "Software DEP" bien qu'il ne soit pas vraiment DEP. Donc, si vous voyez "logiciel DEP" écrit quelque part, cela signifie SafeSEH. Le DEP matériel, généralement appelé DEP ces jours-ci, est une fonctionnalité construite autour d'une extension matérielle qui applique des indicateurs d'accès aux pages dans le processeur. La fonctionnalité est appelée NX (No Execute) par la plupart des gens, mais elle est aussi parfois appelée XD (eXecute Disable); Je vais juste l'appeler NX. NX empêche le processeur d'exécuter des pages marquées comme non exécutables. Si vous essayez de rediriger le pointeur d'instruction vers une page non exécutable, le processeur lance simplement un défaut de protection. Les systèmes d'exploitation ont utilisé NX pour implémenter DEP, qui marque la pile et les segments de données comme non exécutables. Cela a rendu impossible de simplement déposer le shellcode dans une pile ou un tampon de tas et de l'exécuter là-bas - ces pages étaient désormais non exécutables et le processeur refuserait d'exécuter le code.
C'est là que ROP, autrement connu sous le nom de ret2libc (un Linuxisme pour la même chose), est entré en jeu. Au lieu d'essayer de mettre le shellcode dans la pile ou le tas et de l'exécuter, ROP a plutôt trouvé de petits morceaux de code de bibliothèque qui pourraient être chaînés ensemble dans afin d'obtenir l'exécution du code. La partie initiale de l'exploit gagnerait le contrôle du pointeur d'instruction d'une manière ou d'une autre (par exemple via une corruption de tas, UAF, etc.) et remplirait la pile avec les données de la chaîne ROP ou écraserait le pointeur de pile de manière à ce qu'il pointe vers le tampon de tas contenant le Données de chaîne ROP (c'est ce qu'on appelle le pivotement de pile). Dans les deux cas, le pointeur de pile finit par pointer vers le haut de la chaîne ROP. Chaque "gadget" de la chaîne ROP est un petit extrait de code au format "faire quelque chose, retourner". En plaçant les adresses de ces gadgets les uns après les autres sur la pile, l'application passe au premier gadget, exécute quelques instructions, frappe une instruction de retour, qui lit ensuite l'adresse du gadget suivant dans la pile et transfère l'exécution à celle-ci, en répétant jusqu'à la fin de la chaîne ROP. Habituellement, la chaîne appelle une fonction comme exec
sous Linux ou CreateProcess
sous Windows. Une autre façon de procéder consiste à appeler la fonction de protection de la mémoire pour le système d'exploitation (par exemple VirtualProtect
sous Windows) et à l'utiliser pour marquer une section de mémoire que vous contrôlez comme exécutable, puis avoir le dernier élément de votre redirection de chaîne ROP à cette mémoire afin que vous puissiez exécuter le shellcode. Neat, non?
La partie clé de cette attaque est que vous devez savoir où ces gadgets sont en mémoire. Afin de rendre cela difficile (ou impossible à la limite), les fournisseurs de systèmes d'exploitation ont introduit la randomisation de la disposition de l'espace d'adressage (ASLR). Cela randomise les adresses de base de divers bits de mémoire importants comme la pile et les modules exécutables. De plus en plus de choses ont été randomisées et la randomisation est devenue moins prévisible à mesure que les implémentations ASLR se sont améliorées. L'idée est que si l'attaquant ne peut pas savoir où se trouvent les modules en mémoire pour le processus cible, il ne peut pas construire une chaîne ROP. Malheureusement, cependant, il suffit d'un seul module non ASLR pour être chargé dans un processus afin de rompre ASLR. Dans les systèmes d'exploitation plus récents (par exemple, Win10 entièrement corrigé), vous pouvez configurer une politique qui force l'ASLR pour tous les modules, mais ce n'est pas la valeur par défaut (pour autant que je sache). ASLR peut également être contourné à l'aide d'une fuite de pointeur. Ceux-ci se produisent lorsqu'une adresse d'une partie de la mémoire exécutable d'un processus est divulguée à un attaquant (par exemple par un bogue UAF ou par une mauvaise conception de l'API). L'attaquant peut alors calculer la base du module à partir de ce pointeur qui a fui et l'utiliser pour construire une chaîne ROP.
JOP est extrêmement similaire à ROP. Il est utile lorsque des protections de pile sont utilisées, empêchant ainsi les remplacements de tampon de pile, le pivotement de pile ou le filtrage d'adresse de retour (une forme d'application partielle du flux de contrôle). Cela permet une exploitation en tas uniquement via la corruption de tas, UAF, etc.
Au lieu d'allouer un groupe d'adresses de gadgets ROP sur la pile et d'utiliser un RET de la fin de chaque gadget pour passer au suivant, JOP utilise une table de saut dans le tas (identique à une chaîne ROP mais avec des gadgets JOP à la place) et un répartiteur. Chaque gadget JOP est un morceau de code de bibliothèque sous la forme "faire quelque chose; saut indirect", par exemple add rcx, 4; jmp [rdi]
. Au lieu que chaque gadget passe directement au suivant, comme dans ROP, chaque gadget revient à la place à un gadget "répartiteur".
Par exemple, considérez les deux instructions suivantes:
dispatch:
add rax, 8
jmp [rax]
Cette paire d'instructions (ou une paire compatible) devrait être assez facile à trouver quelque part dans le code de la bibliothèque. Si un attaquant peut définir rax
à l'adresse de sa table de répartition (par exemple, un tampon de tas qu'il a rempli) et définir le pointeur d'instruction à l'adresse dispatch
, alors il peut enchaîner Gadgets JOP pour obtenir l'exécution de code comme suit:
rax
, rdx
et rip
afin que rax
pointe vers la table de répartition allouée à l'étape 1, rdx
pointe vers le gadget de table de répartition comme décrit ci-dessus et rip
est également redirigé vers le gadget de table de répartition.rax
, donc rax
pointe maintenant vers l'entrée suivante dans la table de répartition.rax
, qui y exécute le gadget.jmp [rdx]
. Cela redirige le contrôle vers le gadget de répartition.Les registres ici (rax
et rdx
) ne sont que des exemples, ils pourraient être échangés pour n'importe quel registre à usage général.
Vous pouvez lire une description plus complète de JOP dans l'article "Programmation orientée vers le saut: une nouvelle classe d'attaque de réutilisation de code" .
En bref, cependant, JOP n'est vraiment utile que lorsque la pile ne peut pas être utilisée abusivement en raison des protections de pile en place.
Le JOP peut être atténué à l'aide de l'application du flux de contrôle, comme Control Flow Guard (CFG). Des protections telles que CFG identifient les sites d'appels indirects dans le programme et construisent un tableau d'adresses cibles valides vers lesquelles ils peuvent pointer. Par exemple, si vous avez un rappel en C++ défini comme typedef void (*SomeCallback)(int, int);
le compilateur sait que seules les fonctions avec cette signature (par exemple void MyCallbackImplementation(int foo, int bar) { ... }
) sont des cibles valides. Le compilateur insère ensuite une vérification avant chaque instruction de saut indirect qui garantit que l'adresse de saut cible est valide. Sinon, il met fin au programme.
Une autre astuce ROP/JOP consiste à utiliser des gadgets non alignés, c'est-à-dire des gadgets qui ne sont pas constitués d'instructions car ils étaient destinés à être interprétés lorsque le compilateur les a générés, mais à la place sont des modèles d'instructions valides qui apparaissent lorsque vous sautez dans le milieu d'une instruction. Celles-ci peuvent également être atténuées dans une certaine mesure par le compilateur, mais je ne sais pas à quel point ce type de protection est courant.
J'espère que cela vous mettra au courant. J'ai passé sous silence certaines choses car cette réponse est déjà très longue et couvre très rapidement de nombreux sujets anti-exploitation. Certains détails manquent donc je vous suggère d'essayer de contourner chaque protection vous-même une par une afin que vous puissiez apprécier toutes les petites nuances.