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 3Conférence 2: «Contrôle des attaques de pirates»
Partie 1 /
Partie 2 /
Partie 3 Donc, nous avons un tampon sur lequel nous mettons le «canari». Au-dessus se trouve la valeur
EBP enregistrée du pointeur de point d'arrêt et l'adresse de retour est placée au-dessus. Si vous vous souvenez, le débordement va de bas en haut, donc avant d'arriver à l'adresse de retour, il détruira d'abord le "canari".
Public: pourquoi affectera-t-il le "canari"?
Professeur: car on suppose que l'attaquant ne sait pas «sauter» arbitrairement en mémoire. Les attaques de dépassement de mémoire traditionnelles commencent par un pirate qui examine la limite de taille de la mémoire tampon, après quoi le débordement commence à partir de la ligne de fond. Mais vous avez raison - si un attaquant peut accéder directement à la barre d'adresse de retour, aucun «canari» ne nous aidera. Cependant, avec une attaque de débordement de tampon traditionnelle, tout devrait se passer exactement de cette façon - de bas en haut.
Ainsi, l'idée principale d'utiliser un «canari» est que nous permettons à un exploit malveillant de déborder la mémoire tampon. Nous avons un code d'exécution qui, lors du retour d'une fonction, vérifie le "canari" pour s'assurer qu'il a la bonne valeur.
Public: Un attaquant peut-il réécrire l'adresse de retour et changer le «canari»? Comment peut-il vérifier qu'elle a été modifiée, mais continue de remplir sa fonction?
Professeur: oui, peut-être. Ainsi, vous devriez avoir un morceau de code qui vérifiera cela avant le retour de la fonction. Autrement dit, dans ce cas, il est nécessaire d'avoir le support d'un compilateur, qui va réellement étendre la
convention d'appel convention d'appel . Donc, une partie de la séquence de retour se produit avant que nous considérions la validité de cette valeur pour nous assurer que le «canari» n'a pas été détruit. Ce n'est qu'après cela que nous pouvons penser à autre chose.
Public: Un attaquant ne peut-il pas savoir ou deviner ce que signifie «canari»?
Professeur: c'est exactement ce dont je vais parler! Quel est le problème avec ce circuit? Et si, par exemple, nous mettons la valeur A dans chaque programme? Ou une branche entière de 4 valeurs de A? Bien entendu, tout pirate peut connaître la taille du tampon, sa capacité, et ainsi déterminer la position du "canari" dans n'importe quel système. Par conséquent, nous pouvons utiliser différents types de quantités que nous mettons dans notre «canari» pour éviter cela.
Il y a une chose que vous pouvez faire avec notre «canari». Ce sera un type de "canari" très drôle, qui utilise les fonctions du programme C et traite des caractères spéciaux, le type dit "canari" déterministe.

Imaginez que vous avez utilisé le caractère 0 pour le «canari». La valeur binaire de zéro est l'octet zéro, le caractère zéro en ASCII. Une valeur de -1 signifie un retour à la position précédente et ainsi de suite. De nombreuses fonctions arrêtent ou changent de fonctionnement lorsqu'elles rencontrent des caractères ou des valeurs telles que 0, CR, LF, -1. Imaginez que vous, en tant que pirate, utilisiez une fonction de gestion de chaîne pour remonter le tampon, rencontrez le caractère 0 dans le "canari" et le processus s'arrête! Si vous utilisez la fonction «retour chariot» -1, qui est souvent utilisée comme terminaison de ligne, le processus s'arrête également. -1 est donc un autre signe magique.
Il y a encore une chose qui peut être utilisée dans le «canari» - ce sont des valeurs aléatoires difficiles à deviner pour l'attaquant. La puissance de la valeur aléatoire est basée sur la difficulté pour un attaquant de la deviner. Par exemple, si un attaquant se rend compte qu'il n'y a que 3 bits d'entropie dans votre système, il pourra alors utiliser une attaque par force brute. Par conséquent, les possibilités d'utiliser des nombres aléatoires pour se protéger contre les attaques sont assez limitées.
Public: Il arrive généralement que je lis dans un autre tampon et que j'écrive ce que j'ai lu dans ce tampon de cette pile. Dans cette situation, il semble que la valeur aléatoire du "canari" soit inutile, car je lis les données d'un autre tampon et je sais où se trouve le "canari". J'ai un autre tampon que je contrôle et que je ne vérifie jamais. Et dans ce tampon, je peux mettre beaucoup de ce que je veux mettre. Je n'ai pas besoin d'un «canari» aléatoire, car je peux le réécrire en toute sécurité. Je ne vois donc pas comment cela fonctionne vraiment - dans le scénario que vous avez proposé, lorsque la fonction s'arrête lors de la lecture des données du tampon.
Professeur: Je comprends votre question - vous voulez dire que nous utilisons un "canari" déterministe, mais n'utilisez pas l'une des fonctions de la bibliothèque standard qui peut être "trompée" par nos caractères 0, CR, LF, -1. Alors oui, dans la situation que vous avez décrite, un «canari» n'est pas nécessaire.
L'idée est que vous pouvez remplir ce tampon avec des octets de n'importe où, mais tout ce qui vous permet de deviner ces valeurs ou de les obtenir de manière aléatoire entraînera un échec.
Public: Est-il possible d'utiliser quelque chose comme le nombre de secondes ou de millisecondes comme nombres aléatoires et de les utiliser dans un «canari»?
Professeur: Les appels de données ne contiennent pas autant d'accidents que vous le pensez. Parce que le programme a des journaux ou une fonction que vous pouvez appeler pour savoir quand le programme a été téléchargé et d'autres choses similaires. Mais en général, vous avez raison - dans la pratique, si vous pouvez utiliser un périphérique matériel, généralement de bas niveau, avec de meilleurs temps système, ce type d'approche peut fonctionner.
Public: même si nous parvenons à afficher les journaux sur le début du débordement de la mémoire tampon, il est toujours important de savoir à quelle heure nous refusons la demande. Et si nous ne pouvons pas contrôler le temps que prend la demande d’un ordinateur au serveur, il est douteux que l’heure exacte puisse être déterminée de manière déterministe.
Professeur: très bien, j'ai déjà dit que le mal réside dans les détails, c'est juste un tel cas. En d'autres termes, si vous avez, par exemple, un moyen de déterminer le type de canal de synchronisation, vous constaterez peut-être que la quantité d'entropie ou le nombre d'aléatoire ne remplit pas un horodatage complet, mais beaucoup moins. Par conséquent, un attaquant peut déterminer l'heure et la minute où vous avez fait cela, mais pas une seconde.
Public: pour mémoire, essayer de limiter votre propre caractère aléatoire est une mauvaise idée?
Professeur: tout à fait raison!
Public: c'est-à-dire que nous devons généralement utiliser tout ce que nos systèmes prennent en charge, non?
Professeur: oui, c'est vrai. C'est comme l'invention de notre propre cryptosystème, qui est une autre chose populaire que nos diplômés veulent parfois faire. Mais nous ne sommes pas la NSA, nous ne sommes pas des mathématiciens, donc cela échoue généralement. Vous avez donc tout à fait raison.
Mais même si vous utilisez l'aléatoire du système, vous pouvez toujours obtenir moins d'entropie que vous ne le pensez. Permettez-moi de vous donner un exemple de randomisation de phase des adresses. C'est sur ce principe que l'approche des
canaris de pile fonctionne . Puisque nous sommes engagés dans la sécurité informatique, vous vous demandez probablement dans quels cas les "canaris" ne peuvent pas faire face à leur tâche et s'il existe des moyens d'échouer le "canari".
Une telle manière est une attaque en réécrivant des pointeurs de fonction. Parce que si un coup est porté sur le pointeur de fonction, le «canari» ne peut rien faire.
Supposons que vous ayez un code de la forme
int * ptr ... .. , le pointeur initiateur, peu importe comment, alors vous avez le
tampon char buf [128] , la fonction
gets (buf) , et tout en bas un pointeur auquel est affectée une valeur :
* ptr = 5 .
Je note que nous n'avons pas essayé d'attaquer l'adresse de retour de la fonction contenant ce code. Comme vous pouvez le voir, lorsque le tampon déborde, l'adresse du pointeur située au-dessus de celui-ci sera endommagée. Si un attaquant peut endommager ce pointeur, il peut alors attribuer 5 à l'une des adresses qu'il contrôle. Est-ce que tout le monde peut voir que le «canari» n'aidera pas ici? Parce que nous n'attaquons pas le chemin par lequel la fonction revient.
Public: le pointeur peut-il être situé sous le tampon?
Professeur: c'est possible, mais l'ordre des variables spécifiques dépend de beaucoup de choses différentes, de la façon dont le compilateur organise le contenu, de la taille de la colonne matérielle, etc. Mais vous avez raison, si le débordement de la mémoire tampon monte et que le pointeur est situé sous la mémoire tampon, le débordement ne peut pas l'endommager.
Public: pourquoi ne pouvez-vous pas associer le «canari» à la fonction «canari», comme vous l'avez fait avec l'adresse de retour?
Professeur: c'est un moment intéressant! Vous pouvez faire de telles choses. En fait, vous pouvez essayer d'imaginer un compilateur qui chaque fois qu'il a un pointeur, il essaie toujours d'ajouter un module complémentaire pour certaines choses. Cependant, vérifier toutes ces choses sera trop cher. Parce que chaque fois que vous voulez utiliser un pointeur ou appeler une fonction, vous devez avoir un code qui vérifiera si ce "canari" est correct. Fondamentalement, vous pourriez faire quelque chose de similaire, mais cela a-t-il un sens? Nous voyons que les «canaris» n'aident pas dans cette situation.
Et une autre chose dont nous avons discuté plus tôt est que si l'attaquant peut deviner le caractère aléatoire, alors, en principe, les «canaris» aléatoires ne fonctionneront pas. La création de ressources de sécurité basées sur l'aléatoire est un sujet distinct et très complexe, nous n'y allons donc pas.
Public: Donc, le canari contient moins de bits qu'une adresse de retour? Parce que sinon, vous ne pourriez pas simplement vous souvenir de cette adresse et vérifier si elle a changé?
Professeur: voyons. Vous parlez de ce schéma lorsque le «canari» est situé au-dessus du tampon, et vous voulez dire que le système ne peut pas être sûr s'il est impossible de regarder l'adresse de retour et de vérifier si elle a été modifiée.

Oui et non. Veuillez noter que si une attaque de dépassement de tampon se produit, tout ce qui se trouve au-dessus sera écrasé, donc cela peut toujours causer des problèmes. Mais en gros, si ces choses étaient immuables d'une certaine manière, alors vous pourriez faire quelque chose comme ça. Mais le problème est que dans de nombreux cas, la manipulation de l'adresse de retour est une chose plutôt compliquée. Parce que vous pouvez imaginer qu'une fonction spéciale peut être appelée à partir de différents endroits, etc. Dans ce cas, nous avons un peu d'avance, et s'il reste du temps à la fin de la conférence, nous y reviendrons.
Ce sont des situations dans lesquelles un «canari» peut échouer. Il existe d'autres endroits où l'échec est possible, par exemple, lors de l'attaque du
malloc et
des fonctions
libres . La fonction malloc alloue un bloc de mémoire d'une certaine taille en octets et renvoie un pointeur au début du bloc. Le contenu du bloc de mémoire alloué n'est pas initialisé, il reste avec des valeurs non définies. Et la fonction
libre libère de la mémoire précédemment allouée dynamiquement.
Il s'agit d'une attaque unique dans le style de C. Voyons ce qui se passe ici. Imaginez que vous ayez ici deux pointeurs, p et q, pour lesquels nous utilisons
malloc pour allouer 1.024 octets de mémoire à chacun de ces pointeurs. Supposons que nous
créons la fonction
strcpy pour p à partir d'une sorte d'erreur de tampon contrôlée par un attaquant. C'est là que le débordement se produit. Et puis nous exécutons la commande
free q et
free p . C'est du code assez simple, non?

Nous avons 2 pointeurs pour lesquels nous avons alloué de la mémoire, nous utilisons l'un d'eux pour une certaine fonction, un débordement de tampon se produit et nous libérons la mémoire des deux pointeurs.
Supposons que les lignes de mémoire de p et q soient situées l'une à côté de l'autre dans l'espace mémoire. Dans ce cas, de mauvaises choses peuvent arriver, non? Parce que la fonction
strcpy est utilisée pour copier le contenu de
str2 vers
str1 .
Str2 doit être un pointeur sur une chaîne se terminant par zéro et
strcpy renvoie un pointeur sur
str1 . Si les lignes
str1 et
str2 se chevauchent, le comportement de la fonction
strcpy n'est pas défini.
Par conséquent, la fonction
strycpy qui traite la mémoire
p peut en même temps affecter la mémoire allouée à
q . Et cela peut causer des problèmes.
Il est possible que vous ayez fait quelque chose comme ça dans votre propre code par inadvertance lorsque vous avez utilisé une sorte de type de pointeurs étranges. Et tout semble fonctionner, mais lorsque vous devez appeler la fonction
gratuite , une telle nuisance se produit. Et un attaquant peut en profiter, je vais vous expliquer pourquoi cela se produit.
Imaginez qu'à l'intérieur de l'implémentation des fonctions
free et
malloc , le bloc en surbrillance ressemble à ceci.
Supposons qu'en haut du bloc, il y a des données d'application visibles, et en dessous, nous avons la taille de la variable. Cette taille n'est pas celle que voit directement l'application, mais une sorte de «comptabilité» menée par
free ou
malloc , pour que vous connaissiez la taille du tampon mémoire alloué. Un bloc libre est situé à côté du bloc en surbrillance. Supposons qu'un bloc libre ait des métadonnées qui ressemblent à ceci: nous avons la taille du bloc au-dessus, il y a de l'espace libre en dessous, le pointeur arrière et le pointeur avant en dessous. Et tout en bas du bloc, la taille est à nouveau affichée.

Pourquoi avons-nous 2 pointeurs ici? Parce que le système d'allocation de mémoire dans ce cas utilise une liste doublement liée pour suivre la façon dont les blocs libres sont liés les uns aux autres. Par conséquent, lorsque vous sélectionnez un bloc libre, vous l'excluez de cette liste doublement liée. Et puis, lorsque vous le libérez, vous allez faire de l'arithmétique pour le pointeur et mettre ces choses en ordre. Après cela, vous l'ajoutez à cette liste chaînée, non?
Chaque fois que vous entendez parler de l'arithmétique des pointeurs, vous devez penser que c'est votre «canari». Parce qu'il y aura beaucoup de problèmes. Permettez-moi de vous rappeler que nous avons eu un débordement de tampon
p . Si nous supposons que
p et
q sont côte à côte, ou très proches dans l'espace mémoire, il peut finalement arriver que ce débordement de tampon puisse écraser certaines données de taille pour le pointeur alloué
q - c'est le bas de notre bloc alloué. Si vous continuez à suivre ma pensée depuis le tout début, votre imagination vous dira où tout commence à mal tourner. En effet, en substance, ce qui se passe finalement avec ces opérations est
free q et
free p - ils regardent ces métadonnées dans le bloc sélectionné pour faire toutes les manipulations nécessaires avec le pointeur.

Autrement dit, à un certain moment de l'exécution, les fonctions
libres vont obtenir un certain pointeur basé sur la valeur de taille:
p = get.free.block (size) , et la taille est ce que l'attaquant contrôle, car il a effectué un débordement de tampon, correctement ?
Il a fait un tas de calculs arithmétiques, a regardé la fonction
arrière et les pointeurs de ce bloc et va maintenant faire quelque chose comme mettre à jour les pointeurs «arrière» et «avant» - ce sont les deux dernières lignes.

Mais en réalité, cela ne devrait pas vous déranger. Ceci est juste un exemple du code qui a lieu dans ce cas. Mais le fait est qu'en raison de la taille réécrite par le pirate, il contrôle maintenant ce pointeur, qui passe par la fonction
libre . Et à cause de cela, les deux états ici dans les lignes de fond sont en fait des mises à jour de pointeur. Et puisque l'attaquant a pu contrôler ce
p , il contrôle en fait ces deux pointeurs. C'est à cet endroit qu'une attaque peut se produire.
Par conséquent, lorsque vous exécutez
gratuitement et essayez de faire quelque chose comme combiner ces deux blocs, vous avez une liste doublement liée. Parce que si vous avez deux blocs qui entrent en collision et que les deux sont gratuits, vous voulez les combiner en un seul gros bloc.
Mais si nous contrôlons la taille, cela signifie que nous contrôlons l'ensemble du processus à partir des quatre lignes ci-dessus. Cela signifie que si nous comprenons comment fonctionne le débordement, nous pouvons écrire des données dans la mémoire de la manière que nous choisissons. Comme je l'ai dit, de telles choses se produisent souvent avec votre propre code, si vous n'êtes pas intelligent avec un pointeur. Lorsque vous faites une double erreur libre comme
free q et
free p ou autre chose, votre fonction se bloque. Parce que vous avez gâché les métadonnées qui vivent dans chacun de ces blocs sélectionnés, et à un moment donné, ce calcul indiquera une sorte de valeur «poubelle», après quoi vous serez «mort». Mais si vous êtes un attaquant, vous pouvez choisir cette valeur et l'utiliser à votre avantage.
Passons à une autre approche pour empêcher les attaques par débordement de tampon. Cette approche consiste à vérifier les limites. Le but de la vérification des limites est de s'assurer que lorsque vous utilisez un pointeur spécifique, il ne fera référence qu'à ce qui est un objet mémoire. Et ce pointeur se trouve dans les limites autorisées de cet objet mémoire. C'est l'idée principale de la vérification. — . , C, . , : , , ?
, – . 1024 , :
char [1024] ,
char *y = & [108].
? ? C'est difficile à dire. , , . , , - .
- , , , . . , , , . , , . , , .
, ,
struct union . , . :
integer ,
struct ,
int .
,
union , . ,
integer ,
struct , .
, , - :
int p: & (u,s,k) , : u, s, k.

, , , , . , ,
union integer ,
struct . , , , . .
p' ,
p ,
p' , .

, , . , ,
union . , - -
union , , , . , , X. , , , , . , , . .
, . ,
p p' , . .
? Electric fencing – . , , , , .

, - , . , , . , . , , , .
- C C++, , , . - , , - . , . , «» — , , , . , , .
, guard page – ! , .
59:00
:
MIT « ». 2: « », 2La version complète du cours est disponible
ici .
, . ? Vous voulez voir des matériaux plus intéressants? ,
30% entry-level , : VPS (KVM) E5-2650 v4 (6 Cores) 10GB DDR4 240GB SSD 1Gbps $20 ? ( RAID1 RAID10, 24 40GB DDR4).
Dell R730xd 2 ? 2 Intel Dodeca-Core Xeon E5-2650v4 128GB DDR4 6x480GB SSD 1Gbps 100 $249 ! . c Dell R730xd 5-2650 v4 9000 ?