Cours MIT "Sécurité des systèmes informatiques". Conférence 2: «Contrôle des attaques de pirates», partie 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

James Mickens: Lors de la conférence précédente, nous avons tout appris sur les attaques par dépassement de tampon et aujourd'hui, nous continuerons à discuter de certaines méthodes de lancement de ces attaques. L'idée de base d'une attaque par dépassement de tampon est la suivante.



Tout d'abord, je note que ces attaques affectent plusieurs circonstances différentes. La première circonstance qu'ils utilisent est que le logiciel système est souvent écrit en C.

Par logiciel système, j'entends les bases de données, les compilateurs, les serveurs de réseau, etc. Vous pouvez rappeler une chose comme votre shell de commande préféré. Tout ce «logiciel» est généralement écrit en C. Pourquoi en C? Parce que, premièrement, il est plus rapide, et deuxièmement, C est considéré comme un assembleur de haut niveau qui convient le mieux aux besoins d'une variété de plates-formes matérielles. Par conséquent, tous les systèmes critiques sont écrits dans ce langage de programmation de bas niveau. Le problème avec un logiciel écrit en C est qu'il utilise vraiment des adresses mémoire brutes et n'a pas d'outils ou de modules logiciels pour les vérifier. Dans certains cas, cela peut entraîner des conséquences désastreuses.

Pourquoi n'y a-t-il pas de vérification d'index de tableau en C, c'est-à-dire pas de vérification de bordure? L'une des raisons est que le matériel ne fonctionne pas. Et les personnes qui écrivent en C veulent généralement la vitesse d'exécution de programme la plus rapide possible. Une autre raison est qu'en C, comme nous le verrons plus loin, il est en fait très difficile de définir la sémantique de ce qu'est un pointeur et dans quelle mesure il doit agir. Par conséquent, dans certains cas, il serait très difficile d'automatiser les processus logiciels en C.
Discutons de certaines technologies qui tentent de créer un type de gestion automatique de la mémoire. Mais, comme nous le verrons, aucune de ces méthodes n'est totalement «à l'épreuve des balles».

De plus, les attaques par dépassement de tampon utilisent une connaissance x86 de l'architecture, par exemple, dans quelle direction la pile se développe. Qu'est-ce qu'une convention d'appel pour les fonctions? Lorsque vous accédez à la fonction C, à quoi ressemble la pile? Et lorsque vous sélectionnez un objet sur le tas, à quoi ressemblent ces principales structures sélectionnées?

Regardons un exemple simple. C'est très similaire à ce que vous avez vu lors de la dernière conférence. Donc, ici, nous avons une demande de lecture standard, puis nous obtenons un tampon, ici, puis vient le canonique int i , suivi de la fameuse commande gets . Et ci-dessous, nous avons d'autres choses nécessaires.



Donc, comme nous en avons discuté lors de la conférence la semaine dernière, c'est problématique, non? Parce que cette opération obtient ne vérifie pas les limites du tampon. Si l'utilisateur remplit le tampon de données et que nous utilisons cette fonction non sécurisée ici, nous pouvons alors déborder le tampon. Nous pouvons réécrire tout le contenu de la pile. Permettez-moi de vous rappeler à quoi cela ressemble.

Tout en bas se trouve le tableau "i". Un tampon est situé au-dessus, il a la première adresse en bas et la dernière en haut. Dans tous les cas, au-dessus du tampon, nous avons la valeur enregistrée de l'indicateur d'écart - la valeur enregistrée EBP. Au-dessus, c'est l'adresse de retour de la fonction, et encore plus haut sont certaines choses de la trame précédente.

Et n'oubliez pas qu'ici en bas, à gauche de "i", nous avons un pointeur de pile ESP qui y va, et un nouveau pointeur de pause vient dans la section EBP enregistrée. L'adresse de retour comprend ESP et le reste de la trame précédente comprend un point d'arrêt.



Permettez-moi de vous rappeler que la façon dont la pile déborde est que les données sont accumulées vers le haut, dans le sens de cette flèche vers la droite. Lorsque l'opération get est lancée, nous commençons à écrire des octets dans le tampon, à la fin, elle commencera à écraser tout ce qui se trouve en amont. Fondamentalement, tout devrait vous être familier.

Que fait un attaquant pour en profiter? Fondamentalement, il entre une longue séquence de données. Par conséquent, l'idée clé est qu'une telle technique peut être utilisée pour attaquer.

Et si l'adresse de retour est capturée par l'attaquant, il peut déterminer où la fonction sautera après le débordement. Autrement dit, la seule chose qu'un pirate puisse faire est d'intercepter l'adresse de retour et de sauter où il veut. La plupart des attaquants exécutent du code avec des privilèges pour contrôler le processus d'interception.

Donc, si ce processus était une priorité élevée, par exemple, il a été exécuté en tant que root ou admin, peu importe ce que nous appelons le superutilisateur de votre système d'exploitation préféré, maintenant ce programme, qui est contrôlé par un attaquant, peut faire ce qu'il veut en utilisant les privilèges de cette priorité. Ainsi, un pirate peut lire des fichiers ou envoyer du spam s'il a endommagé un serveur de messagerie. Il peut même vaincre les pare-feu, car l'idée d'un pare-feu est qu'il y a de «bonnes» machines derrière lui et de «mauvaises» en dehors. En règle générale, les ordinateurs à l'intérieur du pare-feu «se font confiance» et si vous parvenez à pirater au moins un ordinateur à l'intérieur d'un réseau protégé par un pare-feu, ce sera parfait. Parce que maintenant, vous pouvez simplement ignorer les nombreuses vérifications que ces ordinateurs font habituellement concernant les machines «étrangères», car ils vous considéreront comme une personne de confiance.

Il y a une chose à laquelle vous devriez penser et à laquelle je pensais en tant qu'étudiant:

«Génial, ils nous ont montré comment déborder le tampon, mais pourquoi le système d'exploitation ne peut-il pas arrêter cela? N'agit-elle pas comme quelqu'un comme les Gardiens de la Galaxie, qui protège le bien des mauvaises choses qui se passent autour de vous? "

Il est important de noter que le système d'exploitation ne vous surveille pas en permanence. Et le matériel observe, il extrait des instructions et les déchiffre et fait beaucoup de choses similaires. Mais en première approximation, que fait l'OS? Il définit essentiellement des tables de pages qui permettent à l'application de fonctionner, et si vous demandez au système d'exploitation, par exemple, d'envoyer un paquet réseau, ou si vous souhaitez faire une sorte de demande IPC, ou des choses similaires, vous vous tournerez vers le système d'exploitation pour obtenir de l'aide. Mais le système d'exploitation ne suit pas toutes les instructions que votre application exécute. En d'autres termes, lorsque ce tampon est plein, le système d'exploitation ne surveille pas du tout comment la mémoire de cette pile est utilisée. Tout cet espace d'adressage vous appartient, en tant qu'initiateur du processus, et cela ne s'applique pas au système d'exploitation. Vous pouvez faire ce que vous voulez avec cela, et le système d'exploitation ne peut pas vous aider avec des problèmes.

Plus tard, nous discuterons de certaines des choses que le système d'exploitation peut faire concernant le matériel afin de se défendre contre ce type d'attaque. Permettez-moi de vous rappeler à nouveau - en fait, seul le matériel surveille ce que vous faites et y réagit. Ainsi, vous pouvez profiter de certains types de protection spéciaux, nous en discuterons plus loin.

Voici à quoi ressemble un débordement de tampon. Comment allons-nous régler toutes ces choses?

Une façon d'empêcher les dépassements de tampon est simplement d'éviter les erreurs dans le code C. C'est une approche constructive, car si votre programme n'a pas d'erreurs, l'attaquant ne peut pas les utiliser. Cependant, cela est plus facile à dire qu'à faire. Il y a des choses très simples que les programmeurs peuvent faire pour fournir une «hygiène» de sécurité. Par exemple, des fonctionnalités telles que Gets, que nous connaissons maintenant, peuvent être appelées «go-tos» ou «capturer le système d'exploitation», ce qui constitue une faille de sécurité.

Ainsi, lorsque vous compilez votre code à l'aide d'un compilateur moderne tel que GCC ou Visual Studio, ils indiqueront les inconvénients de ces fonctions. Ils diront: "Hé, vous utilisez une chose dangereuse, mieux vaut envisager d'utiliser la fonction fgets ou d'autres fonctions qui peuvent vraiment suivre la conformité des frontières." C'est l'une des choses simples que les programmeurs peuvent faire.

Mais notez que de nombreuses applications manipulent réellement les tampons sans recourir à toutes ces fonctions. Ceci est très courant dans les serveurs réseau qui définissent leurs propres procédures d'analyse et s'assurent ensuite que les données sont récupérées du tampon comme elles le souhaitent. Ainsi, en se limitant simplement à la sélection des fonctions de commande correctes, il ne sera pas possible de résoudre complètement le problème.

Une autre circonstance qui rend cette approche du problème plus difficile est qu'il n'est pas toujours évident que le problème est dû à une erreur dans un programme écrit en C. Si vous avez déjà travaillé sur un programme à grande échelle écrit en C, vous savez , comme cela se produit avec les identificateurs de fonction qui ont 18 étoiles au-dessus du pointeur void *. Je pense que seul Zeus sait ce que cela peut signifier, non? Avec des langages comme C, même un programmeur peut trouver très difficile de comprendre si une erreur s'est produite ou non.

En général, l'un des principaux sujets de nos conférences sera que le langage C est un produit du diable. Et nous l'utilisons uniquement parce que nous voulons toujours être plus rapides que tout le monde, non? Mais à mesure que le matériel devient de plus en plus rapide, nous utilisons des langages plus avancés pour écrire du code système volumineux. Cependant, cela n'a pas toujours de sens d'écrire votre code C, même si vous pensez que ce sera plus rapide. Nous discuterons de ce problème plus tard.



Ainsi, la première approche pour résoudre le problème consiste à éviter les erreurs dans le code du programme C, et la seconde consiste à créer des outils qui aident les programmeurs à trouver de telles erreurs. Un exemple d'un tel outil est l'analyse de code statique. Plus tard, nous en parlerons en détail, et maintenant je dirai que l'analyse statique est un moyen d'analyser le code source de votre programme avant même qu'il ne démarre, et aide à détecter les problèmes potentiels.

Imaginez que vous ayez une telle fonction, appelons-la void foo (int, * p) , elle contient des données entières et un pointeur. Disons qu'il déclare une valeur de décalage entier int off . Cette fonction déclare un autre pointeur et lui ajoute un décalage: int * z = p + off . Même maintenant, lors de l'écriture d'une fonction, l'analyse de code statique peut nous dire que cette variable de décalage n'est pas initialisée.



Ainsi, en analysant le programme, il est possible de répondre à la question de savoir si notre fonction fonctionnera correctement. Et dans cet exemple, il est très simple de voir la réponse «non, ce ne sera pas le cas» car il n'y a pas d'initialisation de décalage. L'analyse statique est un logiciel, et lorsque vous utilisez le compilateur populaire pour construire votre code, il vous dira: «Hé, mon pote, cette chose n'est pas initialisée. Êtes-vous sûr de vouloir faire exactement cela? " Il s'agit de l'un des exemples les plus simples d'utilisation de l'analyse statique.

Un autre exemple considère le cas où nous avons une branche d'une fonction, c'est-à-dire son exécution sous une certaine condition.



Donc, si le décalage est supérieur à 8, si (off> 8) , cela conduit à un appel à une barre de fonction (off) . Cette condition nous indique donc quelle est la valeur de décalage. Même en ignorant le fait que le décalage n'est pas initialisé, lors de l'analyse de cette branche de la fonction, nous constatons toujours qu'elle peut être supérieure à 8. Par conséquent, lorsque nous commençons à effectuer une analyse statique de la barre, nous constatons que le décalage ne peut prendre que certaines valeurs. Je note encore une fois qu'il s'agit d'une introduction très superficielle à l'analyse statique, nous examinerons plus tard cet outil plus en détail. Mais cet exemple montre comment vous pouvez détecter certains types d'erreurs même sans exécuter de code.

Donc, une autre chose à laquelle vous pourriez penser, c'est qu'elle fait la même chose que l'analyse statique. Il s'agit de fuzzing logiciel. Son idée est que vous preniez toutes les fonctions de votre code de programme, puis que vous y saisissiez des valeurs aléatoires. Ainsi, toutes les options pour les valeurs et les formats de votre code se chevauchent. C'est-à-dire que Fuzzing est un outil pour rechercher automatiquement les vulnérabilités en soumettant des données invalides ou des données au mauvais format à l'entrée du programme. Par exemple, vous entrez les valeurs 2, 4, 8 et 15 dans les tests unitaires et vous obtenez un message indiquant que le nombre 15 est probablement incorrect, car tous les nombres sont pairs, mais il est impair.

En fait, vous devez regarder combien de branches du programme dans leur ensemble affectent votre code de test, car ce sont généralement les endroits où les «bugs» sont cachés. Les programmeurs ne pensent pas à ces «coins et recoins» et, par conséquent, ils réussissent certains tests unitaires, vous pouvez dire la plupart de ces tests. Cependant, ils n'examinent pas tous les «coins et recoins» du programme, et c'est là que l'analyse statique peut aider. Encore une fois, en utilisant des choses comme le concept de restriction. Par exemple, dans notre section programme, il s'agit d'une condition de branchement d'une fonction qui définit un décalage supérieur à huit. Ainsi, nous pouvons découvrir que ce déplacement est statique. Et si nous utilisons la génération automatique de Fuzzing des données d'entrée sur la base de cette restriction, nous pouvons nous assurer que l'une des valeurs d'entrée pour le décalage serait inférieure à 8, une serait 8 et une serait supérieure à 8. Est-ce clair?



C'est donc l'idée principale derrière le concept de création d'outils pour aider les programmeurs à trouver des erreurs. Même une analyse partielle du code peut être très utile lorsque vous travaillez avec le langage C. De nombreux outils que nous examinerons qui servent à empêcher les dépassements de tampon ou à vérifier l'initialisation des variables ne sont pas en mesure de détecter tous les problèmes du code du programme. Mais ils peuvent être utiles pour améliorer la sécurité de ces programmes. L'inconvénient de ces outils est qu'ils ne sont pas complets. Les progrès potentiels ne sont pas des progrès complets. Par conséquent, vous devez explorer activement le problème de la protection contre les exploits à la fois dans les programmes écrits en C et dans d'autres programmes. Nous avons examiné 2 approches pour résoudre le problème de protection contre le débordement de la mémoire tampon, mais il existe d'autres approches.

Ainsi, la troisième approche consiste à utiliser un langage sans danger pour la mémoire, ou un langage qui assure la sécurité de la mémoire. Ces langages incluent Python, Java, c #. Je ne veux pas mettre Perl sur un pied d'égalité avec eux, car il est utilisé par des "méchants". De cette façon, vous pouvez utiliser un langage sans danger pour la mémoire, et il semble que ce soit la chose la plus évidente que vous puissiez faire. Je viens de vous expliquer que, fondamentalement, C est un encodeur d'assemblage de haut niveau, mais il fournit des pointeurs bruts et d'autres choses indésirables, alors pourquoi ne pas simplement utiliser l'un de ces langages de programmation de haut niveau?

Il y a plusieurs raisons à cela. Premièrement, dans ces langages, il existe de nombreux éléments du code hérité de C. Tout va bien si vous démarrez un nouveau projet et utilisez l'un des langages de haut niveau qui lui garantissent la sécurité de la mémoire. Mais que se passe-t-il si vous avez reçu un gros fichier binaire ou une grande distribution source écrite en C et maintenue pendant 10 à 15 ans, c'était un projet générationnel, je veux dire que même nos enfants continueront à travailler ? Dans ce cas, vous ne pourrez pas dire: "Je viens de tout réécrire en C # et de changer le monde!".

Et le problème n'est pas seulement dans le langage C, il existe des systèmes dont vous devriez avoir encore plus peur, car ils utilisent des codes Fortran et COBOL, des choses de la guerre civile. Pourquoi cela se produit-il? Parce qu'en tant qu'ingénieurs, nous voulons penser que nous pouvons simplement tout construire nous-mêmes, et ce sera génial, ce sera comme je veux, et j'appellerai mes variables comme je veux.

Mais dans le monde réel, cela ne se produit pas. Vous apparaissez au travail, et vous avez ce système qui existe déjà, et vous regardez la base du code et pensez pourquoi il ne fait pas ce qui est nécessaire? Et puis ils vous disent: "Écoutez, nous ferons tout ce que vous voulez, mais seulement dans la deuxième version du programme, et maintenant vous devez faire ce que nous devons travailler, car sinon les clients reprendront leur argent."

Alors, comment traitons-nous l'énorme problème de l'utilisation forcée du code hérité? Comme vous le savez, l'un des avantages des systèmes avec une définition erronée des limites est qu'ils fonctionnent parfaitement avec ce code obsolète. C'est l'une des raisons pour lesquelles vous ne pouvez pas vous débarrasser du problème de dépassement de la mémoire tampon en passant simplement aux langues qui fournissent une utilisation sécurisée de la mémoire.



Et si nous avons besoin d'un accès bas niveau au matériel? Par exemple, pour mettre à jour les pilotes et autres choses.

Ainsi, un autre problème survient si vous avez besoin d'un accès de bas niveau à l'équipement, ce qui se produit lors de l'écriture de pilotes pour certains périphériques. Dans ce cas, vous avez vraiment besoin des avantages que C offre, par exemple, la possibilité de regarder des registres et des éléments de fonction similaires.

De plus, la nécessité d'utiliser C survient lorsque vous êtes préoccupé par les performances du système. , , , . , , . , , memory-safe . , JIT. , Java, Java Script. , , «». , . , «» x86.

, , -. , , JVM, - Java. , - . , - JVM , . , , . . , , .

, , 86. JIT- , . JIT- , .

, JavaScript , , «» 32- , . JIT-, «» . , , JIT-, , , .



«» , asm.js – JavaScript, , , . , , , JavaScript , . JavaScript, JavaScript, C ++.

, -, IO. . , , , , . «» , .

. , - . , C C++, , . Python, , , . . .

, , , .

, . , . , . , «» , . , C C++, .

, ? , , ? ?

. , – . , - , . , . , -, , . IP , , . , - . , , . , , «» .

, , , , , , . , - , . , , , . , , , , , , .



, . stack canaries, « », , . « » , , , . , , .

Dessinons un diagramme de notre pile. Nous devons nous assurer que l'attaquant "entre dans le canari" avant d'arriver à l'adresse de retour. Et si nous pouvons détecter cela avant de revenir de la fonction, alors nous pouvons détecter le «mal».

28h30 min

Suite:

Cours MIT "Sécurité des systèmes informatiques". Conférence 2: «Contrôle des attaques de pirates», partie 2


La version complète du cours est disponible ici .

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/fr414505/


All Articles