Est-ce une bonne pratique de programmation sécurisée de remplacer les données sensibles stockées dans une variable avant de les supprimer (ou de les exclure)? Je pense que cela empêcherait un pirate de lire des données latentes en RAM en raison de la rémanence des données. Y aurait-il une sécurité supplémentaire en les écrasant plusieurs fois? Voici une petite exemple de ce dont je parle, en C++ (avec commentaires inclus).
void doSecret()
{
// The secret you want to protect (probably best not to have it hardcoded like this)
int mySecret = 12345;
// Do whatever you do with the number
...
// **Clear out the memory of mySecret by writing over it**
mySecret = 111111;
mySecret = 0;
// Maybe repeat a few times in a loop
}
Une pensée est que si cela ajoute réellement de la sécurité, il serait bien que le compilateur ajoute automatiquement les instructions pour le faire (peut-être par défaut, ou peut-être en disant au compilateur de le faire lors de la suppression des variables).
Cette question a été présentée comme un Question de sécurité de la semaine de la sécurité de l'information.
Lisez le 12 décembre 2014 entrée de blog pour plus de détails ou soumettez votre propre question de la semaine.
Oui, c'est une bonne idée d'écraser puis de supprimer/libérer la valeur. Ne présumez pas que tout ce que vous avez à faire est de "remplacer les données" ou de les laisser hors de portée pour le GC, car chaque langue interagit avec le matériel différemment.
Lors de la sécurisation d'une variable, vous devrez peut-être penser à:
La mise en œuvre réelle de "l'effacement" dépend de la langue et de la plate-forme. Recherchez la langue que vous utilisez et voir si c'est possible pour coder en toute sécurité.
Pourquoi est-ce une bonne idée? Les crashs et tout ce qui contient le tas peuvent contenir vos données sensibles. Pensez à utiliser les éléments suivants lors de la sécurisation de vos données en mémoire
Veuillez vous référer à StackOverflow pour les guides d'implémentation par langue.
Vous devez savoir que même lorsque vous utilisez les instructions du fournisseur (MSFT dans ce cas), il est toujours possible de vider le contenu de SecureString , et peut avoir directives d'utilisation spécifiques pour les scénarios de haute sécurité.
Vous stockez une valeur qui n'est plus utilisée? On dirait quelque chose qui serait optimisé, indépendamment de tout avantage qu'il pourrait apporter.
En outre, vous ne pouvez pas réellement écraser les données en mémoire selon le fonctionnement de la langue elle-même. Par exemple, dans une langue utilisant un garbage collector, il ne serait pas supprimé immédiatement (et cela suppose que vous n'avez laissé aucune autre référence traîner).
Par exemple, en C #, je pense que ce qui suit ne fonctionne pas.
string secret = "my secret data";
...lots of work...
string secret = "blahblahblah";
"my secret data"
traîne jusqu'à ce que les ordures soient collectées car elles sont immuables. Cette dernière ligne crée en fait une nouvelle chaîne et contient un point secret. Il n'accélère pas la vitesse à laquelle les données secrètes réelles sont supprimées.
Y a-t-il un avantage? En supposant que nous l'écrivions dans Assembly ou dans un langage à faible niveau afin que nous puissions nous assurer que nous remplaçons les données, que nous mettons notre ordinateur en veille ou que nous le laissions avec l'application en cours d'exécution, et nous avons notre RAM = gratté par une femme de chambre maléfique, et la femme de chambre maléfique a obtenu nos données RAM après que le secret a été écrasé mais avant qu'il n'ait été supprimé (probablement un très petit espace), et rien d'autre dans = RAM ou sur le disque dur trahirait ce secret ... alors je vois une augmentation possible de la sécurité.
Mais le coût par rapport à l'avantage semble rendre cette optimisation de la sécurité très faible sur notre liste d'optimisations (et en dessous du point de "valeur" sur la plupart des applications en général).
Je pourrais peut-être voir une utilisation limitée de cela dans des puces spéciales destinées à détenir des secrets pendant une courte période afin de s'assurer qu'elles le détiennent le plus rapidement possible, mais même dans ce cas, je ne suis pas sûr de tout avantage pour les coûts.
Vous ne devriez même pas commencer à penser à remplacer les variables de sécurité avant d'avoir un modèle de menace décrivant les types de piratages que vous essayez d'empêcher. La sécurité a toujours un coût. Dans ce cas, le coût est le coût de développement d'enseigner aux développeurs de maintenir tout ce code supplémentaire pour sécuriser les données. Ce coût signifie qu'il est plus probable que vos développeurs commettent des erreurs, et ces erreurs sont plus susceptibles d'être à l'origine d'une fuite qu'un problème de mémoire.
Ces questions doivent être abordées avant d'envisager d'essayer d'écraser des données sensibles. Essayer d'écraser des données sans adresser le modèle de thread est un faux sentiment de sécurité.
Oui, il est recommandé, sur le plan de la sécurité, d'écraser des données particulièrement sensibles lorsque les données ne sont plus nécessaires, c'est-à-dire dans le cadre d'un destructeur d'objets (soit un destructeur explicite fourni par le langage, soit une action que le programme entreprend avant de désallouer le objet). Il est même recommandé d'écraser des données qui ne sont pas sensibles en soi, par exemple pour mettre à zéro les champs de pointeur dans une structure de données qui n'est plus utilisée, et aussi pour mettre à zéro les pointeurs lorsque l'objet vers lequel ils pointent est libéré, même si vous le savez vous n'allez plus utiliser ce champ.
Une des raisons pour cela est dans le cas où les données fuient par des facteurs externes tels qu'un vidage de mémoire exposé, une image d'hibernation volée, un serveur compromis permettant un vidage de la mémoire des processus en cours d'exécution, etc. Attaques physiques où un attaquant extrait le RAM colle et utilise la rémanence des données sont rarement un problème, sauf sur les ordinateurs portables et peut-être les appareils mobiles tels que les téléphones (où la barre est plus élevée parce que le RAM est soudée), et même alors surtout dans des scénarios ciblés uniquement. La rémanence des valeurs écrasées n'est pas un problème: il faudrait du matériel très coûteux pour sonder à l'intérieur d'une puce RAM pour détecter toute différence de tension microscopique persistante qui pourrait être influencée). par une valeur écrasée. Si vous craignez des attaques physiques sur la RAM, une plus grande préoccupation serait de vous assurer que les données sont écrasées dans RAM et pas seulement dans le cache du processeur. Mais, encore une fois, c'est généralement une préoccupation très mineure.
La raison la plus importante pour écraser les données périmées est comme défense contre les bogues du programme qui entraînent l'utilisation de mémoire non initialisée, comme l'infâme Heartbleed . Cela va au-delà des données sensibles car le risque ne se limite pas à une fuite des données: s'il y a un bug logiciel qui provoque un déréférencement d'un champ de pointeur sans avoir été initialisé, le bug est à la fois moins sujet à exploitation et plus facile à tracer si le champ contient tous les bits à zéro que s'il pointe potentiellement vers un emplacement mémoire valide mais vide de sens.
Attention, de bons compilateurs optimiseront la mise à zéro s'ils détectent que la valeur n'est plus utilisée. Vous devrez peut-être utiliser une astuce spécifique au compilateur pour faire croire au compilateur que la valeur reste utilisée et générer ainsi le code de mise à zéro.
Dans de nombreuses langues avec gestion automatique, les objets peuvent être déplacés en mémoire sans préavis. Cela signifie qu'il est difficile de contrôler les fuites de données périmées, à moins que le gestionnaire de mémoire lui-même n'efface la mémoire inutilisée (ce n'est souvent pas le cas, pour des performances). Du côté positif, les fuites externes sont généralement tout ce dont vous avez à vous soucier, car les langages de haut niveau ont tendance à empêcher l'utilisation de la mémoire non initialisée (méfiez-vous des fonctions de création de chaînes dans les langues avec des chaînes modifiables).
Soit dit en passant, j'ai écrit "zéro" ci-dessus. Vous pouvez utiliser un modèle de bits autre que tous les zéros; tous les zéros a l'avantage d'être un pointeur invalide dans la plupart des environnements. Un modèle de bits que vous connaissez est un pointeur non valide, mais qui est plus distinctif peut être utile pour le débogage.
De nombreuses normes de sécurité imposent l'effacement de données sensibles telles que les clés. Par exemple, la norme FIPS 140-2 pour les modules cryptographiques l'exige même au niveau d'assurance le plus bas, ce qui en dehors de cela ne nécessite qu'une conformité fonctionnelle et non une résistance contre les attaques.
Pour un ajout (espérons-le intéressant) au reste des réponses, beaucoup de gens sous-estiment la difficulté à écraser correctement la mémoire avec C. Je vais citer abondamment le billet de blog de Colin Percival intitulé " Comment mettre à zéro un tampon ".
Le principal problème rencontré par les tentatives naïves d'écraser la mémoire en C est l'optimisation du compilateur. La plupart des compilateurs modernes sont suffisamment "intelligents" pour reconnaître que les idoms communs utilisés pour écraser la mémoire ne changent pas en fait le comportement observable du programme et peuvent être optimisés. Malheureusement, cela rompt complètement ce que nous voulons réaliser. Certaines des astuces courantes sont décrites dans le billet de blog lié ci-dessus. Pire encore, une astuce qui fonctionne pour une version d'un compilateur peut ne pas nécessairement fonctionner avec un autre compilateur ou même une version différente du même compilateur. Sauf si vous distribuez des binaires uniquement , il s'agit d'une situation problématique.
La seule façon d'écraser de manière fiable la mémoire en C que je connaisse est la fonction memset_s . Malheureusement, cela n'est disponible qu'en C11, donc les programmes écrits pour les anciennes versions de C n'ont pas de chance.
La fonction memset_s copie la valeur de c (convertie en un caractère non signé) dans chacun des n premiers caractères de l'objet pointé par s. Contrairement à memset, tout appel à memset_s doit être évalué strictement selon les règles de la machine abstraite, comme décrit au 5.1.2.3. Autrement dit, tout appel à memset_s doit supposer que la mémoire indiquée par s et n peut être accessible à l'avenir et doit donc contenir les valeurs indiquées par c.
Malheureusement, Colin Percival est d'avis que l'écrasement de la mémoire ne suffit pas. À partir d'un article de blog de suivi intitulé " la mise à zéro des tampons est insuffisante ", il déclare
Avec un peu de soin et un compilateur coopératif, nous pouvons mettre à zéro un tampon - mais ce n'est pas ce dont nous avons besoin. Ce que nous devons faire, c'est mettre à zéro chaque emplacement où des données sensibles peuvent être stockées. N'oubliez pas que la raison pour laquelle nous avions en premier lieu des informations sensibles en mémoire était que nous pouvions les utiliser; et cette utilisation a presque certainement entraîné la copie de données sensibles sur la pile et dans des registres.
Il continue et donne l'exemple des implémentations AES utilisant le jeu d'instructions AESNI sur les plates-formes x86 qui fuient des données dans les registres.
Il affirme que,
Il est impossible de mettre en œuvre en toute sécurité un cryptosystème fournissant un secret de retransmission en C.
Une affirmation troublante en effet.
Il est important d'écraser les données sensibles immédiatement après leur utilisation, car sinon:
En effet, si vous regardez le code source des applications sensibles à la sécurité (par exemple openssh ), vous constaterez qu'il remet à zéro soigneusement les données sensibles après utilisation.
Il est également vrai que les compilateurs peuvent essayer d'optimiser l'écrasement et, même si ce n'est pas le cas, il est important de savoir comment les données sont stockées physiquement (par exemple, si le secret est stocké sur SSD, l'écrasement peut ne pas effacer l'ancien contenu en raison à nivellement d'usure ).
L'exemple d'origine montre une variable de pile car il s'agit d'un type int natif.
Le remplacer est une bonne idée, sinon il persiste sur la pile jusqu'à ce qu'il soit remplacé par autre chose.
Je soupçonne que si vous êtes en C++ avec des objets tas ou des types natifs C alloués via des pointeurs et malloc, ce serait une bonne idée de
Sous JVM ou C #, je pense que tous les paris sont désactivés.
Il est possible que le bloc de mémoire ait été paginé avant que vous alliez le supprimer, selon la façon dont la distribution d'utilisation dans le fichier d'échange joue, les données peuvent y vivre pour toujours .
Donc, pour réussir à supprimer le secret de l'ordinateur, vous devez d'abord vous assurer que les données n'atteignent jamais la mémoire persistante en épinglant les pages. Assurez-vous ensuite que le compilateur n'optimise pas les écritures dans la mémoire à supprimer.
En fait, la réponse est oui, vous devriez probablement l'écraser avant de le supprimer. Si vous êtes un développeur d'applications Web, vous devriez probablement écraser toutes les données avant d'utiliser les fonctions delete
ou free
.
Exemple de la façon dont ce bogue peut être exploité:
L'utilisateur peut insérer des données malveillantes dans le champ de saisie. Vous poursuivez les données, puis appelez la fonction free
de la mémoire allouée sans la remplacer, de sorte que les données restent dans la mémoire. Ensuite, l'utilisateur fait tomber votre application Web (par exemple en téléchargeant une très grande image ou quelque chose comme ça) et le système UNIX sécurisera le vidage de la mémoire de base dans core
ou core.<pid>
fichier. Ensuite, si l'utilisateur peut brutaliser le <pid>
, qui ne prendra pas trop de temps, et le fichier de vidage principal sera interprété comme un Web-Shell, car il contient des données malveillantes de l'utilisateur.