Cours MIT "Sécurité des systèmes informatiques". Conférence 3: Buffer Overflows: Exploits and Protection, Part 1

Institut de technologie du Massachusetts. Cours magistral # 6.858. "Sécurité des systèmes informatiques." Nikolai Zeldovich, James Mickens. 2014 année


Computer Systems Security est un cours sur le développement et la mise en œuvre de systèmes informatiques sécurisés. Les conférences couvrent les modèles de menace, les attaques qui compromettent la sécurité et les techniques de sécurité basées sur des travaux scientifiques récents. Les sujets incluent la sécurité du système d'exploitation (OS), les fonctionnalités, la gestion du flux d'informations, la sécurité des langues, les protocoles réseau, la sécurité matérielle et la sécurité des applications Web.

Cours 1: «Introduction: modèles de menace» Partie 1 / Partie 2 / Partie 3
Conférence 2: «Contrôle des attaques de pirates» Partie 1 / Partie 2 / Partie 3
Conférence 3: «Débordements de tampon: exploits et protection» Partie 1 / Partie 2 / Partie 3

Bienvenue à la conférence sur les exploits pour les dépassements de tampon. Aujourd'hui, nous terminerons la discussion sur les limites de Baggy , puis passerons à d'autres méthodes de protection contre le débordement de la mémoire tampon.



Ensuite, nous parlerons des documents imprimés de la conférence d'aujourd'hui, qui sont consacrés à la programmation orientée retour aveugle (BROP) - la programmation orientée inverse aveugle. Il s'agit d'une technique d'exploitation qui peut être effectuée même si l'attaquant n'a pas le binaire cible. Ces exploits visent à détruire les "canaris" dans les piles de systèmes 64 bits. Donc, si vous étiez comme moi lorsque j'ai lu ces documents pour la première fois, vous auriez dû avoir envie de regarder le film de Christopher Nolan. Ce n'était qu'une explosion cérébrale!

Nous allons voir comment ces gadgets fonctionnent correctement. Par conséquent, j'espère qu'à la fin de la conférence, vous serez en mesure de comprendre toutes ces hautes technologies décrites dans le matériel de la conférence. Mais d'abord, comme je l'ai dit, nous terminerons la discussion sur les limites de Baggy . Prenons un exemple très simple.

Supposons que nous allons assigner un pointeur p et lui allouer une taille de 44 octets. Supposons également que la taille de l'emplacement est de 16 octets.

Que se passe-t-il lorsque nous attribuons la fonction malloc ? Vous savez déjà que dans ce cas le système de limites Baggy va essayer de compléter cette distribution avec un logarithme 2n . Donc pour notre pointeur de 44 octets, 64 octets de mémoire seront alloués. Mais la taille de l'emplacement est de 16 octets, nous allons donc créer 64/16 = 4 tables de limites de 16 octets chacune. Chacune de ces entrées sera placée dans le journal de distribution de taille.

Ensuite, affectez un autre caractère de pointeur * q = p + 60 . Nous voyons que cette valeur est hors limites car la taille de p est de 44 octets, et ici elle est de 60 octets. Mais Baggy bounds fonctionne de telle sorte que dans ce cas, rien de mauvais ne se produira, bien que le programmeur n'ait pas dû le faire.

Supposons maintenant que la prochaine chose que nous faisons est d'attribuer un autre pointeur, qui sera égal à char * r = q + 16 . Maintenant, cela provoquera une erreur, car la taille du décalage sera 60 + 16 = 76, ce qui est 12 octets de plus que les 4 emplacements (4x16 = 64 octets) alloués par le système Baggy . Et cet excédent représente vraiment plus de la moitié de la fente.



Si vous vous en souvenez, dans ce cas, le système Baggy bounds répondra immédiatement à une erreur de synchronisation critique, ce qui provoquera le plantage du programme et l'arrêtera.

Imaginons donc que nous ayons seulement deux lignes:

char * p = malloc (44)
char * q = p + 60

Et il n'y a pas de troisième ligne avec le code. Au lieu de cela, nous ferons ceci:

char * s = q + 8

Dans ce cas, le pointeur aura une valeur de 60 + 8 = 68 bits, ce qui correspondra à 4 octets de plus que les limites attribuées par les limites Baggy . En fait, cela ne provoquera pas d'erreur critique, bien que la valeur dépasse les limites. Ce que nous avons fait ici est de définir un bit d'ordre élevé pour le pointeur. Donc, si quelqu'un essaie par la suite de le déréférencer, cela entraînera une erreur critique à ce stade.



Et la dernière chose que nous ferons est d'attribuer un autre pointeur:

char * t = s - 32

En fait, nous l'avons fait - nous avons renvoyé le pointeur à la frontière. Donc, si initialement s allait au-delà, nous l'avons maintenant retourné au volume alloué à l'origine que nous avons créé pour le pointeur. Par conséquent, maintenant t n'aura pas de bit d'ordre élevé dans sa composition, et il peut être facilement déréférencé.



Public: comment le programme sait-il que r a un excès supérieur à la moitié de la pile?

Professeur Mickens: notez que lorsque nous avons créé r , nous avons obtenu un code d'outil qui fonctionnera dans toutes ces opérations avec des pointeurs. Nous pouvons donc dire où q sera situé, et nous savons qu'il est dans les limites de Baggy . Par conséquent, lorsque nous effectuons cette opération q + 16 , les outils de limites Baggy savent d'où vient cette valeur initiale. Et puis, si un décalage de cette taille d'origine se produit , les limites Baggy détermineront facilement que le décalage est supérieur à la moitié de la taille de la fente.

En principe, lorsque vous effectuez des opérations avec des pointeurs, vous devez vérifier s'ils dépassent ou non la taille allouée. À un moment donné, vous avez un pointeur situé dans les limites des limites de Baggy , puis quelque chose se produit qui le fait dépasser les limites. Donc, juste au moment où cela se produit, nous découvrirons qu'une sorte de crochet «sort» de notre code.

J'espère que c'est compréhensible. C'était un très bref aperçu des devoirs, mais j'espère que vous pourrez facilement le comprendre.

Nous avons donc un pointeur qui ressemble à ceci:

char * p = malloc (256) , puis nous ajoutons le pointeur char * q = p + 256 , après quoi nous allons essayer de déréférencer ce pointeur.

Alors que va-t-il se passer? Eh bien, notez que 256 est une séquence 2n , donc ce sera dans les limites de Baggy . Par conséquent, lorsque nous ajoutons 256 bits supplémentaires, cela signifie que nous effectuons un autre passage à la fin des frontières des limites Baggy . Comme dans l'exemple précédent, cette ligne est assez bonne, mais elle conduit au fait qu'un bit d'ordre supérieur sera défini pour q . Par conséquent, lorsque nous essayons de le déréférencer, tout explosera et devra appeler notre agent d'assurance. Est-ce clair?



À partir de ces 2 exemples, vous pouvez comprendre le fonctionnement du système de limites Baggy . Comme je l'ai mentionné dans la dernière conférence, vous n'avez pas vraiment besoin d'instrumenter chaque opération de pointeur si vous pouvez utiliser une analyse de code statique pour découvrir qu'un ensemble spécifique d'opérations de pointeur est sûr. Je vais reporter la discussion de certaines analyses statiques, mais il suffit de dire que vous n'avez pas toujours besoin d'effectuer ces actions mathématiques, nous l'avons déjà vérifié auparavant.

Une autre question qui est mentionnée sur Piazza: comment garantir la compatibilité des limites de Baggy avec les bibliothèques précédentes non liées aux outils. L'idée est que lorsque les limites Baggy initialisent les tables de bordure, elles établissent que tous les enregistrements doivent être dans les 31 bits. Par conséquent, lorsque nous lisons la table des limites, chaque enregistrement qu'il contient représente une valeur de la forme 2n + 31 . Ainsi, en initialisant les limites initiales de la taille 31 bits, nous supposons que chaque pointeur aura la taille maximale possible de 2n + 31 . Permettez-moi de vous donner un exemple très simple qui le clarifiera.

Supposons que nous ayons un espace mémoire que nous utilisons pour un tas. Cet espace mémoire est composé de deux composants. En haut, nous avons un tas qui a été alloué en utilisant du code non-outil, et ci-dessous est un tas qui a été alloué avec du code outil. Alors, que fera Baggy Bound ? Comme vous vous en souvenez, ce système a le concept d'un slot dont la taille est de 16 bits. Par conséquent, la table des limites se composera de 2 sections, initiées à partir de 31 bits.

Cependant, lors de l'exécution du code de l'outil, il utilisera en fait l'algorithme des limites Baggy pour définir les valeurs appropriées pour cette ligne du tableau.



Lorsqu'un pointeur provient du haut de l'espace mémoire, il est toujours défini sur les limites maximales possibles de 2n + 31 . Cela signifie que les limites de Baggy ne considéreront jamais qu'une opération de pointeur qui est «venue» d'une bibliothèque non-outil peut dépasser les limites.

L'idée est que dans le code de l'outil, nous effectuerons toujours ces comparaisons pour les pointeurs, mais si nous définissons les limites d'écriture du pointeur pour un code non-outil de la forme 2n + 31 , nous n'aurons jamais d'erreur de déréférence. Autrement dit, nous avons une bonne interaction entre les entrées de code de limites Baggy et les enregistrements non instrumentaux des bibliothèques précédentes.

Cela signifie que nous avons ce système, ce qui est bien, car il ne plante pas le programme lors de l'utilisation de bibliothèques non-outils, mais il a un problème. Le problème est que nous ne pouvons jamais déterminer les limites des pointeurs générés par du code non-outil. Parce que nous ne définirons jamais un bit d'ordre élevé lorsque, par exemple, ce pointeur obtient trop ou trop peu d'espace. Ainsi, nous ne pouvons pas réellement garantir la sécurité de la mémoire pour les opérations qui se produisent lors de l'utilisation de code non instrumental. Vous ne pouvez pas non plus déterminer quand nous passons un pointeur qui a dépassé les limites de taille du code instrumental au code non instrumental. Dans ce cas, quelque chose d'inimaginable peut se produire. Si vous avez un tel pointeur extrait du code de l'outil, alors il a un bit de poids fort défini sur 1. Il semble donc qu'il ait des dimensions gigantesques.

Nous savons que si nous venons de placer ce code dans le code de l'outil, nous pouvons effacer ce drapeau à certains moments lorsqu'il revient aux frontières. Mais si nous passons simplement cette énorme adresse au code non instrumental, cela peut faire quelque chose d'inimaginable. Il peut même renvoyer ce pointeur aux limites, mais nous n'aurons jamais l'occasion d'effacer ce bit d'ordre élevé. Nous pouvons donc toujours avoir des problèmes même lors de l'utilisation du circuit illustré ici.

Public: si nous avons du code outil pour allouer de la mémoire, utilise-t-il la même fonction malloc que le code attribut utilise?

Professeur: C'est une question un peu délicate. Si nous considérons le cas ici, cela est strictement observé, car nous avons deux zones de mémoire, chacune obéissant aux règles établies pour cela. Mais en principe, cela dépendra du code qui utilise le langage de programmation sélectionné. Imaginez qu'en C ++, par exemple, vous pouvez attribuer votre propre qualificatif. Cela dépend donc de certains détails du code.

Public: comment le qualificatif peut-il vérifier si la limite est fixée à 31 bits ou non?

Professeur: aux niveaux inférieurs, les algorithmes de distribution fonctionnent de sorte que lorsque vous appelez un système inconnu, le pointeur remonte. Donc, si vous avez plusieurs allocateurs, ils essaient tous d'allouer de la mémoire, chacun d'eux a sa propre mémoire, qu'ils se réservent, pour l'essentiel, correctement. Donc, dans la vraie vie, il peut être plus fragmenté qu'à un niveau élevé.

Donc, tout ce que nous avons examiné ci-dessus concernait le fonctionnement des limites Baggy dans les systèmes 32 bits. Considérez ce qui se passe lors de l'utilisation de systèmes 64 bits. Dans de tels systèmes, vous pouvez réellement vous débarrasser de la table des limites, car nous pouvons stocker des informations sur les limites dans le pointeur lui-même.

Considérez à quoi ressemble un pointeur normal dans les limites de Baggy. Il se compose de 3 parties. 21 bits sont alloués pour la première partie zéro, 5 autres bits sont alloués pour la taille, il s'agit de la taille principale du journal et 38 autres sont les bits de l'adresse habituelle.



La raison pour laquelle cela ne limite pas massivement la taille de l'adresse du programme que vous utilisez est que la plupart des bits de poids fort du système d'exploitation et / ou de l'équipement situés dans les 2 premières parties du pointeur ne permettent pas d'utiliser l'application pour diverses raisons. Ainsi, il s'est avéré que nous ne réduisons pas considérablement le nombre d'applications utilisées dans le système. Voici à quoi ressemble un pointeur normal.

Que se passe-t-il lorsque nous n'avons qu'un seul de ces pointeurs? Eh bien, sur un système 32 bits, tout ce que nous pouvons faire est simplement de définir un bit d'ordre élevé et espère que cette chose n'obtiendra jamais plus de la moitié de la taille de l'emplacement. Mais maintenant que nous avons tout cet espace d'adressage supplémentaire, vous pouvez placer le décalage en dehors des bordures OOB (hors limite) directement dans ce pointeur. Nous pouvons donc faire quelque chose comme celui montré dans la figure, diviser le pointeur en 4 parties et redistribuer sa taille.

Ainsi, nous pouvons obtenir 13 bits pour les limites de décalage, c'est-à-dire écrire à quelle distance ce pointeur OOB sera de l'endroit où il devrait être. Là encore, vous pouvez définir la taille réelle de l'objet indiqué ici à 5, et le reste de la partie zéro, qui sera désormais 21-13 = 8 bits. Et puis suit notre partie d'adresse de 38 bits. Dans cet exemple, vous voyez les avantages de l'utilisation de systèmes 64 bits.



Notez qu'ici nous avons la taille habituelle pour un pointeur régulier, dans les deux cas cette taille est de 64 bits, et sa description est élémentaire. Et c'est bien, car lors de l'utilisation de pointeurs "épais", nous aurions besoin de beaucoup de mots pour les décrire.
Je note également que le code non-outil peut être facilement appliqué ici, car il fonctionne et utilise la même taille que les pointeurs réguliers. Nous pouvons mettre ces choses dans une structure , par exemple, et la taille de cette structure restera inchangée. C'est donc très bien lorsque nous avons l'opportunité de travailler dans un monde 64 bits.

Public: pourquoi dans le second cas l'offset se situe devant la taille, et pas comme dans le cas précédent, et que se passera-t-il si la taille de l'offset est importante?

Professeur: Je pense que dans certains cas, nous avons certains problèmes limitatifs sur lesquels nous devrons travailler. Par exemple, un problème se produit s'il y a plus de bits. Mais au fond, je ne pense pas qu'il y ait une raison pour laquelle vous ne pourriez pas lire certaines de ces choses. Sauf si certaines conditions strictes, auxquelles je ne pense pas maintenant, auraient dû stipuler la taille de la pièce zéro, sinon il pourrait y avoir des problèmes avec le matériel.

Ainsi, vous pouvez toujours initier un débordement de tampon dans le système de limites Baggy , car l'application des approches ci-dessus ne résout pas tous les problèmes, non? Un autre problème que vous pouvez rencontrer si vous avez un code non instrumental, car nous ne pourrons détecter aucun problème dans le code non instrumental. Vous pouvez également rencontrer des vulnérabilités de mémoire résultant d'un système d'allocation dynamique de mémoire. Si vous vous souvenez, dans une conférence précédente, nous avons examiné cet étrange pointeur pour libérer le malloc , et les limites de Baggy ne pouvaient pas empêcher l'apparition de telles choses.

Nous avons également discuté lors de la dernière conférence du fait que les pointeurs de code n'ont pas de frontières qui leur seraient associées. Supposons que nous ayons une structure dans laquelle le tampon de mémoire est situé en bas, le pointeur en haut et le tampon déborde. Nous supposons que le débordement de tampon est toujours dans les limites de Baggy . Ensuite, vous devez redéfinir ce pointeur de fonction. Sinon, si nous essayons de l'utiliser, il peut être envoyé à du code malveillant pour attaquer une partie contrôlée de la mémoire. Et dans ce cas, les frontières ne nous aideront pas, car nous n'avons pas de frontières associées, associées à ces pointeurs de fonction.

Alors, quel est le prix de l'utilisation des bornes Baggy ? En fait, nous n'avons que 4 composants de ce prix.



Le premier est l'espace. Parce que si vous utilisez un pointeur «épais», il est évident que vous devez agrandir les pointeurs. Si vous utilisez le système de limites Baggy dont nous venons de parler, vous devez enregistrer une table de bordure. Et cette table a une taille de slot qui vous permet de contrôler la taille de cette table jusqu'à épuisement des possibilités de la mémoire qui lui est allouée.

De plus, vous avez également augmenté la charge du processeur, qui est obligé de faire toutes ces opérations instrumentales avec des pointeurs. Étant donné que pour chaque ou presque chaque pointeur, il est nécessaire de vérifier les limites en utilisant les mêmes modes de fonctionnement, ce qui ralentira l'exécution de votre programme.

Il y a aussi un problème avec les fausses alarmes. Nous avons déjà discuté de ce qui pourrait arriver qu'un programme génère des pointeurs qui dépassent les limites, mais n'essaye jamais de les déréférencer. À strictement parler, ce n'est pas un problème. Baggy bounds signalera ces drapeaux «hors limites » s'ils dépassent la moitié de la taille de l'emplacement, au moins dans la solution 32 bits.

Ce que vous verrez dans la plupart des outils de sécurité, c'est que les fausses alarmes réduisent la probabilité que les gens utilisent ces outils. Parce que dans la pratique, nous espérons tous que nous nous soucions de la sécurité, mais qu'est-ce qui excite vraiment les gens? Ils veulent pouvoir télécharger leurs stupides photos sur Facebook, ils veulent accélérer le processus de téléchargement et ainsi de suite. Ainsi, si vous voulez vraiment que vos outils de sécurité soient en demande, ils ne devraient avoir aucune fausse alarme. Tenter d'attraper toutes les vulnérabilités conduit généralement à de fausses alarmes, ce qui ennuiera les développeurs ou les utilisateurs.

Cela entraîne également des dépenses improductives dont vous avez besoin du support du compilateur. Parce que vous devez ajouter tous les outils au système, en contournant la vérification du pointeur, et ainsi de suite.

Donc, pour utiliser le système Baggy bounds , nous devons payer un prix, qui consiste en une utilisation excessive de l'espace, une augmentation de la charge du processeur, de fausses alarmes et la nécessité d'utiliser un compilateur.

Ceci conclut la discussion sur les limites de Baggy .

Nous pouvons maintenant penser à deux autres stratégies d'atténuation des dépassements de tampon. En fait, ils sont beaucoup plus faciles à expliquer et à comprendre.

L'une de ces approches est appelée mémoire non exécutable . Son idée principale est que le matériel d'échange indiquera 3 bits de R , W et X - lire, écrire et exécuter - pour chaque page que vous avez en mémoire. , , . 2 , , , , .

, . , , , , - . , . « W X » , , , , , . , , . . , , . ?

- , . , . , , .

, , , , . , , . .

, . – just-in-time , .

-, JavaScript . JavaScript, , - - «» , - «» , x86 . , .
. , , just-in-time W , X . , , .

— . , , , .



, , . GDB, , . , . , . .

, . , , , , , .

: , , — . , , , , , , , - . , , .

, , , GDB , , , , . .
, , , , . - , - , . , . , , .

, ? , . , , . , , , , - , .

27:10

:

Cours MIT "Sécurité des systèmes informatiques". 3: « : », 2


.

Merci de rester avec nous. Aimez-vous nos articles? Vous voulez voir des matériaux plus intéressants? Soutenez-nous en passant une commande ou en le recommandant à vos amis, une réduction de 30% pour les utilisateurs Habr sur un analogue unique de serveurs d'entrée de gamme que nous avons inventés pour vous: Toute la vérité sur VPS (KVM) E5-2650 v4 (6 cœurs) 10 Go DDR4 240 Go SSD 1 Gbps à partir de 20 $ ou comment diviser le serveur? (les options sont disponibles avec RAID1 et RAID10, jusqu'à 24 cœurs et jusqu'à 40 Go de DDR4).

Dell R730xd 2 fois moins cher? Nous avons seulement 2 x Intel Dodeca-Core Xeon E5-2650v4 128 Go DDR4 6x480 Go SSD 1 Gbps 100 TV à partir de 249 $ aux Pays-Bas et aux États-Unis! Pour en savoir plus sur la création d'un bâtiment d'infrastructure. classe utilisant des serveurs Dell R730xd E5-2650 v4 coûtant 9 000 euros pour un sou?

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


All Articles