Au cours des deux dernières semaines, nous avons vu une série d'articles parler de ce qui a été décrit comme un "crack de mot de passe principal" dans le populaire gestionnaire de mots de passe open source KeePass.
Le bogue a été considéré comme suffisamment important pour obtenir un identifiant officiel du gouvernement américain (il est connu sous le nom de CVE-2023-32784, si vous voulez le traquer), et étant donné que le mot de passe principal de votre gestionnaire de mots de passe est à peu près la clé de tout votre château numérique, vous pouvez comprendre pourquoi l'histoire a suscité beaucoup d'enthousiasme.
La bonne nouvelle est qu'un attaquant qui voulait exploiter ce bogue aurait presque certainement déjà infecté votre ordinateur avec un logiciel malveillant, et serait donc en mesure d'espionner vos frappes au clavier et les programmes en cours d'exécution de toute façon.
En d'autres termes, le bug peut être considéré comme un risque facile à gérer jusqu'à ce que le créateur de KeePass sorte une mise à jour, qui devrait apparaître prochainement (début juin 2023, apparemment).
Comme le divulgateur du bogue prend soin de signaler:
Si vous utilisez le chiffrement intégral du disque avec un mot de passe fort et que votre système est [exempt de logiciels malveillants], tout devrait bien se passer. Personne ne peut voler vos mots de passe à distance sur Internet avec cette seule découverte.
Les risques expliqués
Grossièrement résumé, le bug se résume à la difficulté de s'assurer que toutes les traces de données confidentielles sont purgées de la mémoire une fois qu'on en a fini avec elles.
Nous allons ignorer ici les problèmes de comment éviter d'avoir des données secrètes en mémoire du tout, même brièvement.
Dans cet article, nous voulons simplement rappeler aux programmeurs du monde entier que le code approuvé par un réviseur soucieux de la sécurité avec un commentaire tel que "semble se nettoyer correctement après lui-même"…
… pourrait en fait ne pas être complètement nettoyé du tout, et la fuite potentielle de données pourrait ne pas être évidente à partir d'une étude directe du code lui-même.
En termes simples, la vulnérabilité CVE-2023-32784 signifie qu'un mot de passe principal KeePass peut être récupéré à partir des données système même après la fermeture du programme KeyPass, car des informations suffisantes sur votre mot de passe (bien que pas réellement le mot de passe brut lui-même, sur lequel nous nous concentrerons activé dans un instant) peut rester dans les fichiers d'échange ou de veille du système, où la mémoire système allouée peut être enregistrée pour plus tard.
Sur un ordinateur Windows où BitLocker n'est pas utilisé pour chiffrer le disque dur lorsque le système est éteint, cela donnerait à un escroc qui a volé votre ordinateur portable une chance de démarrer à partir d'un lecteur USB ou CD et de récupérer même votre mot de passe principal. bien que le programme KeyPass lui-même prenne soin de ne jamais l'enregistrer de manière permanente sur le disque.
Une fuite de mot de passe à long terme en mémoire signifie également que le mot de passe pourrait, en théorie, être récupéré à partir d'un vidage mémoire du programme KeyPass, même si ce vidage a été saisi longtemps après que vous ayez tapé le mot de passe et longtemps après le KeePass lui-même n'avait plus besoin de le garder.
De toute évidence, vous devez supposer que les logiciels malveillants déjà présents sur votre système pourraient récupérer presque tous les mots de passe saisis via une variété de techniques d'espionnage en temps réel, tant qu'ils étaient actifs au moment où vous avez tapé. Mais vous pouvez raisonnablement vous attendre à ce que votre temps d'exposition au danger soit limité à la brève période de frappe, et non prolongé à plusieurs minutes, heures ou jours après, ou peut-être plus longtemps, y compris après avoir éteint votre ordinateur.
Que reste-t-il?
Nous avons donc pensé jeter un regard de haut niveau sur la façon dont les données secrètes peuvent être laissées en mémoire d'une manière qui n'est pas directement évidente à partir du code.
Ne vous inquiétez pas si vous n'êtes pas programmeur - nous allons faire simple et vous expliquer au fur et à mesure.
Nous commencerons par examiner l'utilisation et le nettoyage de la mémoire dans un programme C simple qui simule la saisie et le stockage temporaire d'un mot de passe en procédant comme suit :
- Allocation d'un morceau de mémoire dédié spécialement pour stocker le mot de passe.
- Insertion d'une chaîne de texte connue afin que nous puissions facilement le retrouver en mémoire si besoin.
- Ajout de 16 caractères ASCII 8 bits pseudo-aléatoires de la gamme AP.
- En cours d'impression le tampon de mot de passe simulé.
- Libérer la mémoire dans l'espoir d'effacer le tampon de mot de passe.
- Quitter le programme.
Très simplifié, le code C pourrait ressembler à ceci, sans vérification d'erreur, en utilisant des nombres pseudo-aléatoires de mauvaise qualité à partir de la fonction d'exécution C rand()
, et en ignorant les vérifications de dépassement de tampon (ne faites jamais rien de tout cela en code réel !) :
// Ask for memory char* buff = malloc(128); // Copy in fixed string we can recognise in RAM strcpy(buff,"unlikelytext"); // Append 16 pseudo-random ASCII characters for (int i = 1; i <= 16; i++) { // Choose a letter from A (65+0) to P (65+15) char ch = 65 + (rand() & 15); // Modify the buff string directly in memory strncat(buff,&ch,1); } // Print it out, so we're done with buff printf("Full string was: %sn",buff); // Return the unwanted buffer and hope that expunges it free(buff);
En fait, le code que nous avons finalement utilisé dans nos tests comprend quelques éléments supplémentaires présentés ci-dessous, afin que nous puissions vider le contenu complet de notre tampon de mot de passe temporaire au fur et à mesure que nous l'utilisions, pour rechercher du contenu indésirable ou restant.
Notez que nous vidons délibérément le tampon après avoir appelé free()
, qui est techniquement un bug d'utilisation après libération, mais nous le faisons ici comme un moyen sournois de voir si quelque chose de critique reste après avoir rendu notre tampon, ce qui pourrait conduire à un dangereux trou de fuite de données dans la vie réelle.
Nous avons également inséré deux Waiting for [Enter]
invites dans le code pour nous donner une chance de créer des vidages de mémoire à des points clés du programme, nous donnant des données brutes à rechercher plus tard, afin de voir ce qui a été laissé pendant que le programme s'exécutait.
Pour faire des vidages de mémoire, nous utiliserons Microsoft Outil Sysinternals procdump
des -ma
Option (vider toute la mémoire), ce qui évite d'avoir à écrire notre propre code pour utiliser Windows DbgHelp
système et son assez complexe MiniDumpXxxx()
fonctions.
Pour compiler le code C, nous avons utilisé notre propre construction petite et simple du code libre et open-source de Fabrice Bellard Petit compilateur C, disponible pour Windows 64 bits dans source et forme binaire directement depuis notre page GitHub.
Le texte à copier-coller de tout le code source illustré dans l'article apparaît au bas de la page.
Voici ce qui s'est passé lorsque nous avons compilé et exécuté le programme de test :
C:UsersduckKEYPASS> petcc64 -stdinc -stdlib unl1.c Tiny C Compiler - Copyright (C) 2001-2023 Fabrice Bellard Démonté par Paul Ducklin pour être utilisé comme outil d'apprentissage Version petcc64-0.9.27 [0006] - Génère 64 bits PE uniquement -> unl1.c -> c:/users/duck/tcc/petccinc/stdio.h [. . . .] -> c:/users/duck/tcc/petcclib/libpetcc1_64.a -> C:/Windows/system32/msvcrt.dll -> C:/Windows/system32/kernel32.dll -------- ----------------------- section de taille de fichier virt 1000 200 438 .text 2000 800 2ac .data 3000 c00 24 .pdata -------- ----------------------- <- unl1.exe (3584 octets) C:UsersduckKEYPASS> unl1.exe Vidage du 'nouveau' tampon au démarrage 00F51390 : 90 57 F5 00 00 00 00 00 50 01 F5 00 00 00 00 00 .W......P....... 00F513A0 : 73 74 65 6D 33 32 5C 63 6D 64 2E 65 78 65 00 44 tige32cmd. exe.D 00F513B0 : 72 69 76 65 72 44 61 74 61 3D 43 3A 5C 57 69 6E riverData=C:Win 00F513C0 : 64 6F 77 73 5C 53 79 73 74 65 6D 33 32 5C 44 72 dowsSystem32Dr 00F513D0 : 69 76 65 72 73 5C 44 72 69 76 65 72 44 61 74 61 iversDriverData 00F513E0 : 00 45 46 43 5F 34 33 37 32 3D 31 00 46 50 53 5F .EFC_4372=1.FPS_ 00F 513F0 : 42 52 4F 57 53 45 52 5F 41 50 50 5F 50 52 4F 46 BROWSER_APP_PROF 00F51400 : 49 4C 45 5F 53 54 52 49 4E 47 3D 49 6E 74 65 72 ILE_STRING=Inter 00F51410 : 6E 65 74 20 45 78 70 6 7C 56A 4 F3 4C CA 00B 00 00 net ExplzV.< .K.. Chaîne complète : texte improbableJHKNEJJCPOMDJHAN 51390F75 : 6 6E 69C 6 65B 6 79C 74 65 78 74 4 48A 4 4B 00E texte improbableJHKN 513F0A45 : 4 4A 43A 50 4 4F 44D 4 48A 41 4 00 65E 00 44 00 513 EJJCPOMDJHAN.eD 0F72B69 : 76 65 72 44 61 74 61 3 43 3D 5 57A 69C 6 00 513E riverData=C:Win 0F64C6 : 77 73F 5 53 79C 73 74 65 6 33 32D 5 44 72C 32 00 dowsSystem513Dr 0F69D76 : 65 72 73 5 44 72C 69 76 65 72 44 61 74 61 00 513 iversDriverData 0F00E45 : 46 43 5 34 33F 37 32 3 31 00D 46 50 53 5 4372 1F .EFC_00=513.FPS_ 0F42F52 : 4 57 53F 45 52 5 41 50F 50 5 50 52F 4 46 00F 51400 BROWSER_APP_PROF 49F4 : 45 5C 53 54F 52 49 4 47 3E 49 6D 74 65E 72 00 51410 ILE_STRING=Inter 6F65 : 74E 20 45 78 70 6 7 56C 4A 3 F4 00C AC 00B 00 51390 net ExplzV.<.K.. Attente de [ENTREE] pour libérer le tampon... Vidage du tampon après libération() 0F67 : A5 00 F00 00 00 00 50 01 5 00 F00 00 00 00 00 513 .g......P...... 0F45A4 : 4 43A 50A 4 4 44F 4D 48 41A 4 00 65E 00 44 00 513 0 72E riverData=C:Win 69F76C65 : 72 44F 61 74 61C 3 43 3 5 57 69D 6 00 513C 0 64 6 77 73 5 53F 79 73 74 65 6D 33 32 5 44 72 32F .EFC_00=513.FPS_ 0F69F76 : 65 72 73F 5 44 72 69 76F 65 72 44 61F 74 61 00F 513 NAVIGATEUR_APP_PROF 0F00 : 45 46C 43 5F 34 33 37 32 3E 31 00D 46 50E 53 5 4372 ILE_STRING=Inter 1F00 : 513E 0 42 52 4 57 53 45C 52D 5 41 50D AC 50B 5 50 net ExplM..MK. En attente de [ENTER] pour quitter main()... C:UsersduckKEYPASS>
Dans cette exécution, nous n'avons pas pris la peine de récupérer des vidages de mémoire de processus, car nous pouvions voir tout de suite à partir de la sortie que ce code perdait des données.
Juste après avoir appelé la fonction de bibliothèque d'exécution Windows C malloc()
, nous pouvons voir que le tampon que nous récupérons inclut ce qui ressemble à des données de variable d'environnement laissées par le code de démarrage du programme, avec les 16 premiers octets apparemment modifiés pour ressembler à une sorte d'en-tête d'allocation de mémoire restante.
(Notez comment ces 16 octets ressemblent à deux adresses mémoire de 8 octets, 0xF55790
ainsi que le 0xF50150
, qui sont respectivement juste après et juste avant notre propre mémoire tampon.)
Lorsque le mot de passe est censé être en mémoire, nous pouvons voir clairement la chaîne entière dans le tampon, comme on pouvait s'y attendre.
Mais après avoir appelé free()
, notez comment les 16 premiers octets de notre tampon ont été réécrits avec ce qui ressemble à nouveau à des adresses mémoire proches, probablement pour que l'allocateur de mémoire puisse garder une trace des blocs en mémoire qu'il peut réutiliser…
… mais le reste du texte de notre mot de passe « expurgé » (les 12 derniers caractères aléatoires EJJCPOMDJHAN
) a été abandonné.
Non seulement devons-nous gérer nos propres allocations et désallocations de mémoire en C, mais nous devons également nous assurer que nous choisissons les bonnes fonctions système pour les tampons de données si nous voulons les contrôler avec précision.
Par exemple, en passant à ce code à la place, nous obtenons un peu plus de contrôle sur ce qui est en mémoire :
En passant de malloc()
ainsi que le free()
pour utiliser les fonctions d'allocation Windows de niveau inférieur VirtualAlloc()
ainsi que le VirtualFree()
directement, nous obtenons un meilleur contrôle.
Cependant, nous payons un prix en termes de rapidité, car chaque appel à VirtualAlloc()
fait plus de travail qu'un appel à malloc()
, qui fonctionne en divisant et subdivisant continuellement un bloc de mémoire de bas niveau préallouée.
En utilisant VirtualAlloc()
à plusieurs reprises pour les petits blocs utilise également plus de mémoire globale, car chaque bloc distribué par VirtualAlloc()
consomme généralement un multiple de 4 Ko de mémoire (ou 2 Mo, si vous utilisez ce que l'on appelle grandes pages de mémoire), de sorte que notre tampon de 128 octets ci-dessus est arrondi à 4096 octets, gaspillant les 3968 octets à la fin du bloc de mémoire de 4 Ko.
Mais, comme vous pouvez le voir, la mémoire que nous récupérons est automatiquement effacée (mise à zéro), donc nous ne pouvons pas voir ce qu'il y avait avant, et cette fois le programme plante lorsque nous essayons de faire notre utilisation après libération astuce, car Windows détecte que nous essayons de jeter un œil à la mémoire que nous ne possédons plus :
C:UsersduckKEYPASS> unl2 Vidage du 'nouveau' tampon au démarrage 0000000000EA0000 : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0000000000EA0010 : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0000000000EA0020 : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 .. .............. 0000000000EA0030 : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0000000000EA0040 : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0000000000EA0050 : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0000000000EA0060 : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0000000000EA0070 : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0000000000EA0080 : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0000000000 ................ La chaîne complète était : improbabletextIBIPJPPHEOPOIDLL 0000EA75 : 6 6E 69C 6 65B 6 79C 74 65 78 74 49 42 49 50 0000000000 improbabletextIBIP 0010EA4 : 50A 50 48 45 4 50 4F 49 44F 4 4 00C 00C 00 00 0000000000 0020 JPPHEOPOIDLL ... : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0000000000EA0030 : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0000000000EA0040 : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ............... 0000000000EA0050 : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0000000000EA0060 : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0000000000EA0070 : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ............. ... En attente de [ENTER] pour libérer le tampon... Vidage du tampon après free() 0000000000EA0080 : [Le programme s'est terminé ici car Windows a intercepté notre utilisation après libération]
Parce que la mémoire que nous avons libérée devra être réallouée avec VirtualAlloc()
avant de pouvoir être réutilisé, nous pouvons supposer qu'il sera éliminé avant d'être recyclé.
Cependant, si nous voulions nous assurer qu'il était masqué, nous pourrions appeler la fonction spéciale Windows RtlSecureZeroMemory()
juste avant de le libérer, pour garantir que Windows écrira d'abord des zéros dans notre tampon.
La fonction associée RtlZeroMemory()
, si vous vous posiez la question, fait une chose similaire, mais sans la garantie de fonctionner réellement, car les compilateurs sont autorisés à le supprimer comme théoriquement redondant s'ils remarquent que le tampon n'est plus utilisé par la suite.
Comme vous pouvez le constater, nous devons faire très attention à utiliser les bonnes fonctions Windows si nous voulons minimiser le temps pendant lequel les secrets stockés en mémoire peuvent rester pour plus tard.
Dans cet article, nous n'allons pas voir comment vous empêchez les secrets d'être sauvegardés accidentellement dans votre fichier d'échange en les verrouillant dans la RAM physique. (Indice: VirtualLock()
n'est pas suffisant en soi.) Si vous souhaitez en savoir plus sur la sécurité de la mémoire Windows de bas niveau, faites-le nous savoir dans les commentaires et nous l'examinerons dans un prochain article.
Utilisation de la gestion automatique de la mémoire
Une bonne façon d'éviter d'avoir à allouer, gérer et désallouer de la mémoire par nous-mêmes est d'utiliser un langage de programmation qui prend soin de malloc()
ainsi que le free()
ou VirtualAlloc()
ainsi que le VirtualFree()
automatiquement
Langage de script tel que Perl, Python, Lua, JavaScript et d'autres se débarrassent des bogues de sécurité de la mémoire les plus courants qui affligent le code C et C++, en suivant l'utilisation de la mémoire pour vous en arrière-plan.
Comme nous l'avons mentionné précédemment, notre exemple de code C mal écrit ci-dessus fonctionne bien maintenant, mais uniquement parce que c'est toujours un programme super simple, avec des structures de données de taille fixe, où nous pouvons vérifier par inspection que nous n'écraserons pas notre 128- tampon d'octets, et qu'il n'y a qu'un seul chemin d'exécution qui commence par malloc()
et se termine par un correspondant free()
.
Mais si nous le mettions à jour pour permettre la génération de mots de passe de longueur variable ou si nous ajoutions des fonctionnalités supplémentaires au processus de génération, alors nous (ou quiconque maintenait le code ensuite) pourrions facilement nous retrouver avec des débordements de tampon, des bogues d'utilisation après libération ou de la mémoire qui n'est jamais libérée et laisse donc traîner des données secrètes longtemps après qu'elles ne sont plus nécessaires.
Dans un langage comme Lua, nous pouvons laisser l'environnement d'exécution Lua, qui fait ce que l'on appelle dans le jargon collecte automatique des ordures, gérer l'acquisition de la mémoire du système et la restituer lorsqu'il détecte que nous avons cessé de l'utiliser.
Le programme C que nous avons répertorié ci-dessus devient beaucoup plus simple lorsque l'allocation et la désallocation de mémoire sont prises en charge pour nous :
Nous allouons de la mémoire pour contenir la chaîne s
simplement en affectant la chaîne 'unlikelytext'
à elle.
Nous pouvons plus tard soit laisser entendre explicitement à Lua que nous ne sommes plus intéressés par s
en lui attribuant la valeur nil
(tout nils
sont essentiellement le même objet Lua), ou arrêtez d'utiliser s
et attendez que Lua détecte qu'il n'est plus nécessaire.
Dans tous les cas, la mémoire utilisée par s
seront finalement récupérés automatiquement.
Et pour éviter les débordements de tampon ou la mauvaise gestion de la taille lors de l'ajout de chaînes de texte (l'opérateur Lua ..
, prononcé concaténer, ajoute essentiellement deux chaînes ensemble, comme +
en Python), chaque fois que nous étendons ou raccourcissons une chaîne, Lua alloue comme par magie de l'espace pour une toute nouvelle chaîne, plutôt que de modifier ou de remplacer l'original dans son emplacement de mémoire existant.
Cette approche est plus lente et conduit à des pics d'utilisation de la mémoire plus élevés que ceux que vous obtiendriez en C en raison des chaînes intermédiaires allouées lors de la manipulation de texte, mais elle est beaucoup plus sûre en ce qui concerne les débordements de tampon.
Mais ce type de gestion automatique des chaînes (appelé dans le jargon immutabilité, car les chaînes n'obtiennent jamais muté, ou modifiés sur place, une fois qu'ils ont été créés), apporte ses propres problèmes de cybersécurité.
Nous avons exécuté le programme Lua ci-dessus sur Windows, jusqu'à la deuxième pause, juste avant la fin du programme :
C:UsersduckKEYPASS> lua s1.lua La chaîne complète est : improbabletextHLKONBOJILAGLNLN En attente de [ENTRÉE] avant de libérer la chaîne... En attente de [ENTRÉE] avant de quitter...
Cette fois, nous avons effectué un dump de la mémoire du processus, comme ceci :
C:UsersduckKEYPASS> procdump -ma lua lua-s1.dmp ProcDump v11.0 - Utilitaire de vidage de processus Sysinternals Copyright (C) 2009-2022 Mark Russinovich et Andrew Richards Sysinternals - www.sysinternals.com [00:00:00] Dump 1 lancé : C:UsersduckKEYPASSlua-s1.dmp [00:00:00] Écriture du vidage 1 : la taille estimée du fichier de vidage est de 10 Mo. [00:00:00] Vidage 1 terminé : 10 Mo écrits en 0.1 seconde [00:00:01] Nombre de vidages atteint.
Ensuite, nous avons exécuté ce script simple, qui relit le fichier de vidage, trouve partout en mémoire que la chaîne connue unlikelytext
est apparu et l'imprime, ainsi que son emplacement dans le fichier de vidage et les caractères ASCII qui suivent immédiatement :
Même si vous avez déjà utilisé des langages de script ou travaillé dans un écosystème de programmation doté de ce que l'on appelle chaînes gérées, où le système garde une trace des allocations et désallocations de mémoire pour vous, et les gère comme bon lui semble…
… vous pourriez être surpris de voir le résultat produit par cette analyse de mémoire :
C:UsersduckKEYPASS> lua findit.lua lua-s1.dmp 006D8AFC : texte improbableALJBNGOAPLLBDEB 006D8B3C : texte improbableALJBNGOA 006D8B7C : texte improbableALJBNGO 006D8BFC : texte improbableALJBNGOAPLLBDEBJ 006D8CBC : texte improbableALJBN 006D8D7C : texte improbable ALJBNGOAP 006D903C : texte peu probableALJBNGOAPL 006D90BC : texte peu probableALJBNGOAPLL 006D90FC : texte peu probableALJBNG 006D913C : texte peu probableALJBNGOAPLLB 006D91BC : texte peu probableALJB 006D91FC : texte peu probableALJBNGOAPLLBD 006D923C : texteimprobableALJBNGOAPLLBDE 006DB70C : texteimprobableALJ 006DBB8C : texteimprobableAL 006DBD0C :texteimprobableA
Et voilà, à l'époque on a récupéré notre dump mémoire, même si on en avait fini avec la ficelle s
(et a dit à Lua que nous n'en avions plus besoin en disant s = nil
), toutes les chaînes que le code avait créées en cours de route étaient toujours présentes dans la RAM, pas encore récupérées ou supprimées.
En effet, si nous trions la sortie ci-dessus par les chaînes elles-mêmes, plutôt que de suivre l'ordre dans lequel elles sont apparues dans la RAM, vous pourrez imaginer ce qui s'est passé pendant la boucle où nous avons concaténé un caractère à la fois à notre chaîne de mot de passe :
C:UsersduckKEYPASS> lua findit.lua lua-s1.dmp | sort /+10 006DBD0C : texteimprobableA 006DBB8C : texteimprobableAL 006DB70C : texteimprobableALJ 006D91BC : texteimprobableALJB 006D8CBC : texteimprobableALJBN 006D90FC : texteimprobableALJBNG 006D8B7C :texte improbableALJBNGO 006D8B3C : texteimprobableALJBNGOA 006 D8D7C : texte peu probableALJBNGOAP 006D903C : texte peu probableALJBNGOAPL 006D90BC : texte peu probableALJBNGOAPLL 006D913C : texte peu probableALJBNGOAPLLB 006D91FC : texte peu probableALJBNGOAPLLBD 006D923C : texte peu probableALJBNGOAPLLBDE 006D8AFC : texte peu probableALJBNGOAPLLBD EB 006D8BFC : texte improbableALJBNGOAPLLBDEBJ
Toutes ces chaînes intermédiaires temporaires sont toujours là, donc même si nous avions réussi à effacer la valeur finale de s
, nous divulguerions toujours tout sauf son dernier caractère.
En fait, dans ce cas, même lorsque nous avons délibérément forcé notre programme à se débarrasser de toutes les données inutiles en appelant la fonction spéciale Lua collectgarbage()
(la plupart des langages de script ont quelque chose de similaire), la plupart des données de ces chaînes temporaires embêtantes sont de toute façon bloquées dans la RAM, car nous avions compilé Lua pour faire sa gestion automatique de la mémoire en utilisant le bon vieux malloc()
ainsi que le free()
.
En d'autres termes, même après que Lua lui-même ait récupéré ses blocs de mémoire temporaires pour les réutiliser, nous ne pouvions pas contrôler comment ou quand ces blocs de mémoire seraient réutilisés, et donc combien de temps ils resteraient à l'intérieur du processus avec leur gauche- sur les données en attente d'être reniflées, jetées ou autrement divulguées.
Entrez .NET
Mais qu'en est-il de KeePass, où cet article a commencé ?
KeePass est écrit en C#, et utilise le runtime .NET, il évite donc les problèmes de mauvaise gestion de la mémoire que les programmes C amènent avec eux…
…mais C# gère ses propres chaînes de texte, un peu comme le fait Lua, ce qui soulève la question :
Même si le programmeur évitait de stocker l'intégralité du mot de passe principal à un seul endroit après en avoir terminé, les attaquants ayant accès à un vidage de mémoire pourraient-ils néanmoins trouver suffisamment de données temporaires restantes pour deviner ou récupérer le mot de passe principal de toute façon, même si ces des attaquants ont eu accès à votre ordinateur quelques minutes, heures ou jours après que vous ayez tapé le mot de passe ?
En termes simples, y a-t-il des restes détectables et fantomatiques de votre mot de passe principal qui survivent dans la RAM, même après que vous vous attendiez à ce qu'ils aient été effacés ?
Ennuyeux, en tant qu'utilisateur de Github Vdohney a découvert, la réponse (pour les versions KeePass antérieures à 2.54, au moins) est "Oui".
Pour être clair, nous ne pensons pas que votre mot de passe principal réel puisse être récupéré sous la forme d'une seule chaîne de texte à partir d'un vidage de mémoire KeePass, car l'auteur a créé une fonction spéciale pour la saisie du mot de passe principal qui fait tout son possible pour éviter de stocker le plein mot de passe où il pourrait facilement être repéré et flairé.
Nous nous en sommes satisfaits en définissant notre mot de passe principal sur SIXTEENPASSCHARS
, en le tapant, puis en effectuant des vidages de mémoire immédiatement, peu de temps et longtemps après.
Nous avons cherché dans les vidages avec un simple script Lua qui cherchait partout ce texte de mot de passe, à la fois au format ASCII 8 bits et au format UTF-16 16 bits (Widechar Windows), comme ceci :
Les résultats étaient encourageants :
C:UsersduckKEYPASS> lua searchknown.lua kp2-post.dmp Lecture dans le fichier de vidage... TERMINÉ. Recherche de SIXTEENPASSCHARS en ASCII 8 bits... introuvable. Recherche de SIXTEENPASSCHARS en UTF-16... introuvable.
Mais Vdohney, le découvreur de CVE-2023-32784, a remarqué que lorsque vous tapez votre mot de passe principal, KeePass vous donne un retour visuel en construisant et en affichant une chaîne d'espace réservé composée de caractères "blob" Unicode, jusqu'à et y compris la longueur de votre mot de passe. mot de passe:
Dans les chaînes de texte Widechar sous Windows (qui se composent de deux octets par caractère, et pas seulement d'un octet chacun comme en ASCII), le caractère "blob" est codé dans la RAM en tant qu'octet hexadécimal. 0xCF
suivie par 0x25
(qui se trouve être un signe de pourcentage en ASCII).
Ainsi, même si KeePass fait très attention aux caractères bruts que vous saisissez lorsque vous entrez le mot de passe lui-même, vous pouvez vous retrouver avec des chaînes restantes de caractères "blob", facilement détectables en mémoire lors d'exécutions répétées telles que CF25CF25
or CF25CF25CF25
...
… et, si c'est le cas, la plus longue série de caractères blob que vous avez trouvé révélerait probablement la longueur de votre mot de passe, ce qui serait une forme modeste de fuite d'informations de mot de passe, si rien d'autre.
Nous avons utilisé le script Lua suivant pour rechercher des signes de chaînes d'espace réservé de mot de passe restantes :
La sortie était surprenante (nous avons supprimé des lignes successives avec le même nombre de blobs, ou avec moins de blobs que la ligne précédente, pour économiser de l'espace) :
C:UsersduckKEYPASS> lua findblobs.lua kp2-post.dmp 000EFF3C : * [. . .] 00BE621B : ** 00BE64C7 : *** [. . .] 00BE6E8F : **** [. . .] 00BE795F : ***** [. . .] 00BE84F7 : ****** [. . .] 00BE8F37 : ******* [ continue de la même manière pour 8 blobs, 9 blobs, etc. ] [ jusqu'à deux dernières lignes d'exactement 16 blobs chacune ] 00C0503B : ************* *** 00C05077 : **************** 00C09337 : * 00C09738 : * [toutes les correspondances restantes durent une goutte] 0123B058 : *
À des adresses mémoire rapprochées mais toujours croissantes, nous avons trouvé une liste systématique de 3 blobs, puis 4 blobs, et ainsi de suite jusqu'à 16 blobs (la longueur de notre mot de passe), suivis de nombreuses instances dispersées au hasard de chaînes à un seul blobs .
Ainsi, ces chaînes "blob" d'espace réservé semblent effectivement fuir dans la mémoire et rester derrière pour divulguer la longueur du mot de passe, longtemps après que le logiciel KeePass a fini avec votre mot de passe principal.
L'étape suivante
Nous avons décidé de creuser plus loin, tout comme Vdohney l'a fait.
Nous avons changé notre code de correspondance de modèle pour détecter les chaînes de caractères blob suivis de n'importe quel caractère ASCII au format 16 bits (les caractères ASCII sont représentés en UTF-16 comme leur code ASCII 8 bits habituel, suivi d'un octet zéro).
Cette fois, pour économiser de l'espace, nous avons supprimé la sortie pour toute correspondance qui correspond exactement à la précédente :
Surprise Surprise:
C:UsersduckKEYPASS> lua searchkp.lua kp2-post.dmp 00BE581B: *I 00BE621B: **X 00BE6BD3: ***T 00BE769B: ****E 00BE822B: *****E 00BE8C6B: ******N 00BE974B: *******P 00BEA25B: ********A 00BEAD33: *********S 00BEB81B: **********S 00BEC383: ***********C 00BECEEB: ************H 00BEDA5B: *************A 00BEE623: **************R 00BEF1A3: ***************S 03E97CF2: *N 0AA6F0AF: *W 0D8AF7C8: *X 0F27BAF8: *S
Regardez ce que nous obtenons de la région de mémoire de chaîne gérée de .NET !
Un ensemble étroitement groupé de "chaînes de blob" temporaires qui révèlent les caractères successifs de notre mot de passe, en commençant par le deuxième caractère.
Ces chaînes qui fuient sont suivies de correspondances à un seul caractère largement distribuées qui, selon nous, sont apparues par hasard. (Un fichier de vidage KeePass a une taille d'environ 250 Mo, il y a donc beaucoup de place pour que les caractères "blob" apparaissent comme par hasard.)
Même si nous tenons compte de ces quatre correspondances supplémentaires, plutôt que de les rejeter comme des incompatibilités probables, nous pouvons deviner que le mot de passe principal est l'un des suivants :
?IXTEENPASSCHARS ?NXTEENPASSCHARS ?WXTEENPASSCHARS ?SXTEENPASSCHARS
De toute évidence, cette technique simple ne trouve pas le premier caractère du mot de passe, car la première "chaîne blob" n'est construite qu'après la saisie de ce premier caractère.
Notez que cette liste est belle et courte car nous avons filtré les correspondances qui ne se terminaient pas par des caractères ASCII.
Si vous recherchiez des caractères dans une gamme différente, tels que des caractères chinois ou coréens, vous pourriez vous retrouver avec plus de résultats accidentels, car il y a beaucoup plus de caractères possibles à faire correspondre…
... mais nous pensons que vous vous rapprocherez de toute façon de votre mot de passe principal, et les "chaînes blob" qui se rapportent au mot de passe semblent être regroupées dans la RAM, probablement parce qu'elles ont été allouées à peu près au même moment par la même partie de l'environnement d'exécution .NET.
Et là, en un mot certes long et discursif, est l'histoire fascinante de CVE-2023-32784.
Que faire?
- Si vous êtes un utilisateur de KeePass, ne paniquez pas. Bien qu'il s'agisse d'un bogue et d'une vulnérabilité techniquement exploitable, les attaquants distants qui souhaitaient déchiffrer votre mot de passe à l'aide de ce bogue devraient d'abord implanter un logiciel malveillant sur votre ordinateur. Cela leur donnerait de nombreuses autres façons de voler directement vos mots de passe, même si ce bogue n'existait pas, par exemple en enregistrant vos frappes au fur et à mesure que vous tapez. À ce stade, vous pouvez simplement surveiller la prochaine mise à jour et la récupérer lorsqu'elle sera prête.
- Si vous n'utilisez pas le chiffrement intégral du disque, envisagez de l'activer. Pour extraire les mots de passe restants de votre fichier d'échange ou de votre fichier d'hibernation (fichiers de disque du système d'exploitation utilisés pour enregistrer temporairement le contenu de la mémoire lors d'une charge importante ou lorsque votre ordinateur est "en veille"), les attaquants auraient besoin d'un accès direct à votre disque dur. Si vous avez activé BitLocker ou son équivalent pour d'autres systèmes d'exploitation, ils ne pourront pas accéder à votre fichier d'échange, à votre fichier d'hibernation ou à toute autre donnée personnelle telle que des documents, des feuilles de calcul, des e-mails enregistrés, etc.
- Si vous êtes programmeur, tenez-vous informé des problèmes de gestion de la mémoire. Ne présumez pas que simplement parce que chaque
free()
correspond à son correspondantmalloc()
que vos données sont en sécurité et bien gérées. Parfois, vous devrez peut-être prendre des précautions supplémentaires pour éviter de laisser traîner des données secrètes, et ces précautions varient d'un système d'exploitation à l'autre. - Si vous êtes un testeur QA ou un réviseur de code, pensez toujours « dans les coulisses ». Même si le code de gestion de la mémoire semble bien rangé et bien équilibré, soyez conscient de ce qui se passe dans les coulisses (car le programmeur d'origine n'a peut-être pas su le faire) et préparez-vous à effectuer des travaux de style pentesting tels que la surveillance de l'exécution et la mémoire dumping pour vérifier que le code sécurisé se comporte vraiment comme il est censé le faire.
CODE DE L'ARTICLE : UNL1.C
#inclure #inclure #inclure void hexdump(unsigned char* buff, int len) { // Imprimer le tampon en morceaux de 16 octets pour (int i = 0; i < len+16; i = i+16) { printf("%016X: ",buff +i); // Affiche 16 octets en tant que valeurs hexadécimales pour (int j = 0 ; j < 16 ; j = j+1) { printf("%02X ",buff[i+j]); } // Répétez ces 16 octets comme caractères pour (int j = 0; j < 16; j = j+1) { ch non signé = buff[i+j]; printf("%c",(ch>=32 && ch<=127)?ch:'.'); } printf("n"); } printf("n"); } int main(void) { // Acquiert de la mémoire pour stocker le mot de passe, et montre // ce qu'il y a dans le tampon lorsqu'il est officiellement "nouveau"... char* buff = malloc(128); printf("Dumping 'new' buffer at startn"); vidage hexadécimal (buff, 128); // Utilise une adresse de tampon pseudo-aléatoire comme graine aléatoire srand((unsigned)buff); // Commencez le mot de passe avec un texte fixe et consultable strcpy(buff,"unlikelytext"); // Ajoute 16 lettres pseudo-aléatoires, une à la fois for (int i = 1; i <= 16; i++) { // Choisissez une lettre de A (65+0) à P (65+15) char ch = 65 + (rand() & 15); // Modifiez ensuite la chaîne buff à la place strncat(buff,&ch,1); } // Le mot de passe complet est maintenant en mémoire, donc imprimez-le // sous forme de chaîne et affichez tout le tampon... printf("La chaîne complète était : %sn",buff); vidage hexadécimal (buff, 128); // Pause pour vider la RAM du processus maintenant (essayez : 'procdump -ma') puts("En attente de [ENTRÉE] pour libérer le tampon..."); getchar(); // Libérez formellement() la mémoire et affichez à nouveau le tampon // pour voir s'il reste quelque chose... free(buff); printf("Dumping buffer after free()n"); vidage hexadécimal (buff, 128); // Pause pour vider à nouveau la RAM pour inspecter les différences puts("En attente de [ENTRÉE] pour quitter main()..."); getchar(); renvoie 0 ; }
CODE DE L'ARTICLE : UNL2.C
#inclure #inclure #inclure #inclure void hexdump(unsigned char* buff, int len) { // Imprimer le tampon en morceaux de 16 octets pour (int i = 0; i < len+16; i = i+16) { printf("%016X: ",buff +i); // Affiche 16 octets en tant que valeurs hexadécimales pour (int j = 0 ; j < 16 ; j = j+1) { printf("%02X ",buff[i+j]); } // Répétez ces 16 octets comme caractères pour (int j = 0; j < 16; j = j+1) { ch non signé = buff[i+j]; printf("%c",(ch>=32 && ch<=127)?ch:'.'); } printf("n"); } printf("n"); } int main(void) { // Acquiert de la mémoire pour stocker le mot de passe, et montre // ce qu'il y a dans le tampon lorsqu'il est officiellement "nouveau"... char* buff = VirtualAlloc(0,128,MEM_COMMIT,PAGE_READWRITE); printf("Dumping 'new' buffer at startn"); vidage hexadécimal (buff, 128); // Utilise une adresse de tampon pseudo-aléatoire comme graine aléatoire srand((unsigned)buff); // Commencez le mot de passe avec un texte fixe et consultable strcpy(buff,"unlikelytext"); // Ajoute 16 lettres pseudo-aléatoires, une à la fois for (int i = 1; i <= 16; i++) { // Choisissez une lettre de A (65+0) à P (65+15) char ch = 65 + (rand() & 15); // Modifiez ensuite la chaîne buff à la place strncat(buff,&ch,1); } // Le mot de passe complet est maintenant en mémoire, donc imprimez-le // sous forme de chaîne et affichez tout le tampon... printf("La chaîne complète était : %sn",buff); vidage hexadécimal (buff, 128); // Pause pour vider la RAM du processus maintenant (essayez : 'procdump -ma') puts("En attente de [ENTRÉE] pour libérer le tampon..."); getchar(); // Libérez formellement() la mémoire et affichez à nouveau le tampon // pour voir s'il reste quelque chose... VirtualFree(buff,0,MEM_RELEASE); printf("Dumping buffer after free()n"); vidage hexadécimal (buff, 128); // Pause pour vider à nouveau la RAM pour inspecter les différences puts("En attente de [ENTRÉE] pour quitter main()..."); getchar(); renvoie 0 ; }
CODE DE L'ARTICLE : S1.LUA
-- Commencez par un texte fixe et consultable s = 'unlikelytext' -- Ajoutez 16 caractères aléatoires de 'A' à 'P' for i = 1,16 do s = s .. string.char(65+math.random( 0,15)) end print('Full string is:',s,'n') -- Pause pour vider la RAM du processus print('Attente de [ENTER] avant de libérer la chaîne...') io.read() - - Effacer la chaîne et marquer la variable inutilisée s = nil -- Vider à nouveau la RAM pour rechercher les différences print('Waiting for [ENTER] before exiting...') io.read()
CODE DE L'ARTICLE : TROUVER.LUA
-- lire dans le fichier de vidage local f = io.open(arg[1],'rb'):read('*a') -- rechercher le texte du marqueur suivi d'un -- ou plusieurs caractères ASCII aléatoires local b,e ,m = 0,0,nil while true do -- recherche la correspondance suivante et se souvient de l'offset b,e,m = f:find('(unlikelytext[AZ]+)',e+1) -- quitte quand il n'y en a plus correspond si ce n'est pas b alors casser end -- position du rapport et chaîne trouvée print(string.format('%08X: %s',b,m)) end
CODE DE L'ARTICLE : SEARCHKNOWN.LUA
io.write('Reading in dump file... ') local f = io.open(arg[1],'rb'):read('*a') io.write('DONE.n') io. write('Recherche de SIXTEENPASSCHARS en ASCII 8 bits... ') local p08 = f:find('SIXTEENPASSCHARS') io.write(p08 et 'FOUND' ou 'not found','.n') io.write ('Recherche de SIXTEENPASSCHARS en UTF-16... ') local p16 = f:find('Sx00Ix00Xx00Tx00Ex00Ex00Nx00Px00'.. 'Ax00Sx00Sx00Cx00Hx00Ax00Rx00Sx00') io.write(p16 et 'FOUND' ou 'not found' ,'.n')
CODE DE L'ARTICLE : FINDBLOBS.LUA
-- lire dans le fichier de vidage spécifié sur la ligne de commande local f = io.open(arg[1],'rb'):read('*a') -- Rechercher un ou plusieurs mots de passe blobs, suivis de tout non-blob -- Notez que les caractères blob (●) sont encodés dans les caractères larges Windows -- en tant que codes UTF-16 litte-endian, sortant sous la forme CF 25 en hexadécimal. local b,e,m = 0,0,nil while true do -- Nous voulons un ou plusieurs blobs, suivis de n'importe quel non-blob. -- Nous simplifions le code en recherchant un CF25 explicite -- suivi de toute chaîne qui ne contient que CF ou 25, -- nous trouverons donc CF25CFCF ou CF2525CF ainsi que CF25CF25. -- Nous filtrerons les "faux positifs" plus tard s'il y en a. -- Nous devons écrire '%%' au lieu de x25 car le caractère x25 -- (signe pourcentage) est un caractère de recherche spécial en Lua ! b,e,m = f:find('(xCF%%[xCF%%]*)',e+1) -- quitter lorsqu'il n'y a plus de correspondance sinon b then break end -- CMD.EXE ne peut pas imprimer blobs, nous les convertissons donc en étoiles. print(string.format('%08X: %s',b,m:gsub('xCF%%','*'))) end
CODE DE L'ARTICLE : SEARCHKP.LUA
-- lire dans le fichier de vidage spécifié sur la ligne de commande local f = io.open(arg[1],'rb'):read('*a') local b,e,m,p = 0,0,nil,nil while true do -- Maintenant, nous voulons un ou plusieurs blobs (CF25) suivis du code -- for A..Z suivi d'un octet 0 pour convertir ACSCII en UTF-16 b,e,m = f:find(' (xCF%%[xCF%%]*[AZ])x00',e+1) -- quitter lorsqu'il n'y a plus de correspondance sinon b puis pause fin -- CMD.EXE ne peut pas imprimer les blobs, nous les convertissons donc en étoiles. -- Pour gagner de la place nous supprimons les correspondances successives if m ~= p then print(string.format('%08X: %s',b,m:gsub('xCF%%','*'))) p = m fin fin
- Contenu propulsé par le référencement et distribution de relations publiques. Soyez amplifié aujourd'hui.
- PlatoAiStream. Intelligence des données Web3. Connaissance Amplifiée. Accéder ici.
- Frapper l'avenir avec Adryenn Ashley. Accéder ici.
- Achetez et vendez des actions de sociétés PRE-IPO avec PREIPO®. Accéder ici.
- La source: https://nakedsecurity.sophos.com/2023/05/31/serious-security-that-keepass-master-password-crack-and-what-we-can-learn-from-it/
- :possède
- :est
- :ne pas
- :où
- ][p
- $UP
- 1
- 10
- 12
- 15%
- 20
- 200
- 2023
- 24
- 250
- 27
- 31
- 3d
- 49
- 50
- 67
- 70
- 72
- 77
- 8
- 9
- a
- Capable
- Qui sommes-nous
- au dessus de
- Absolute
- AC
- accès
- Compte
- acquérir
- acquisition
- infection
- présenter
- actually
- ajoutée
- Supplémentaire
- propos
- adresses
- Ajoute
- Après
- après
- encore
- Tous
- consacrée
- alloue
- allocation
- allocations
- permettre
- seul
- le long de
- déjà
- aussi
- modifié
- Bien que
- toujours
- an
- ainsi que le
- Andrew
- répondre
- tous
- quoi que ce soit d'artificiel
- tout ce qui est critique
- apparaître
- paru
- une approche
- ,
- SONT
- autour
- article
- sur notre blog
- AS
- At
- auteur
- auto
- Automatique
- automatiquement
- disponibles
- éviter
- évité
- conscients
- et
- RETOUR
- fond
- image de fond
- BE
- car
- devient
- était
- before
- Début
- derrière
- Dans les coulisses
- ci-dessous
- Améliorée
- Bit
- Block
- Blocs
- frontière
- tous les deux
- Bas et Leggings
- brand
- NOUVEAU
- Pause
- brièvement
- apporter
- tampon
- débordement de tampon
- Punaise
- bogues
- construire
- mais
- by
- C + +
- Appelez-nous
- appel
- CAN
- Peut obtenir
- les soins
- maisons
- pris
- CD
- Canaux centraux
- Assurément
- Chaînes
- Chance
- modifié
- caractère
- caractères
- vérification
- Contrôles
- chinois
- Selectionnez
- clair
- clairement
- Fermer
- code
- Couleur
- COM
- vient
- Venir
- commentaire
- commentaires
- Commun
- complet
- complexe
- ordinateur
- Considérer
- considérable
- considéré
- Qui consiste
- la construction
- contenu
- contenu
- continuellement
- continue
- des bactéries
- convertir
- droit d'auteur
- Correspondant
- pourriez
- couverture
- fissure
- engendrent
- créée
- créateur
- critique
- Cybersécurité
- DANGER
- dangereux
- données
- fuite de données
- jours
- affaire
- décidé
- dévoué
- décrit
- DID
- différences
- différent
- Difficulté
- DIG
- numérique
- Accès direct
- directement
- Commande
- afficher
- disposer
- do
- INSTITUTIONNELS
- Ne fait pas
- faire
- fait
- Ne pas
- down
- motivation
- deux
- déverser
- pendant
- e
- chacun
- Plus tôt
- même
- risque numérique
- non plus
- d'autre
- emails
- permettant
- encourageant
- chiffrement
- fin
- se termine
- assez
- assurer
- assurer
- Entrer
- entrant
- Tout
- entrée
- Environment
- Équivalent
- erreur
- essentiellement
- estimé
- etc
- Ether (ETH)
- Pourtant, la
- faire une éventuelle
- de plus en plus
- Chaque
- peut
- exactement
- exemple
- Sauf
- Excitation
- exécution
- exister
- existant
- Sortie
- Quitter
- attendre
- Expliquer
- Exploiter
- exposé
- étendre
- supplémentaire
- extrait
- fait
- non
- fascinant
- Fonctionnalités:
- Réactions
- moins
- lutte
- Déposez votre dernière attestation
- Fichiers
- une fonction filtre
- finale
- finalement
- Trouvez
- trouver
- trouve
- fin
- Prénom
- fixé
- Focus
- suivi
- Abonnement
- Pour
- formulaire
- Officiellement
- le format
- à venir
- trouvé
- quatre
- Gratuit
- de
- plein
- d’étiquettes électroniques entièrement
- fonction
- fonctions
- plus
- avenir
- génère
- génération
- obtenez
- obtention
- GitHub
- Donner
- donné
- donne
- Don
- Go
- Goes
- aller
- Bien
- Gouvernement
- saisir
- l'
- guarantir
- ait eu
- Poignées
- arrivé
- EN COURS
- arrive
- Dur
- Vous avez
- ayant
- maux de tête
- lourd
- la taille
- ici
- HEX
- de haut niveau
- augmentation
- Hits
- appuyez en continu
- Trou
- d'espérance
- HEURES
- flotter
- Comment
- How To
- HTTPS
- chasse
- i
- identifiant
- if
- immédiatement
- important
- in
- inclut
- Y compris
- d'information
- Actualités
- plutôt ;
- intéressé
- Intermédiaire
- Internet
- développement
- vous aider à faire face aux problèmes qui vous perturbent
- IT
- SES
- lui-même
- jargon
- juin
- juste
- juste un
- XNUMX éléments à
- ACTIVITES
- Savoir
- connu
- Coréen
- langue
- Langues
- portatif
- Nom de famille
- plus tard
- conduire
- Conduit
- fuite
- Fuites
- APPRENTISSAGE
- apprentissage
- au
- départ
- à gauche
- Longueur
- lettre
- Bibliothèque
- VIE
- comme
- Probable
- limité
- Gamme
- lignes
- Liste
- Listé
- ll
- charge
- locales
- emplacement
- enregistrement
- Location
- long-term
- plus long
- Style
- ressembler
- regardé
- recherchez-
- LOOKS
- Lot
- chance
- maintient
- a prendre une
- malware
- gérer
- gérés
- gestion
- manager
- gère
- Manipulation
- de nombreuses
- Marge
- marque
- marqueur
- maître
- Match
- assorti
- largeur maximale
- Mai..
- veux dire
- Mémoire
- mentionné
- Microsoft
- pourrait
- minutes
- modeste
- modifié
- modifier
- moment
- Stack monitoring
- PLUS
- (en fait, presque toutes)
- beaucoup
- plusieurs
- Soigné
- Besoin
- nécessaire
- net
- n'allons jamais
- Néanmoins
- Nouveauté
- nouvelles
- next
- agréable
- aucune
- Ordinaire
- rien
- Remarquer..
- maintenant
- nombre
- numéros
- objet
- évident
- of
- de rabais
- officiel
- Officiellement
- compenser
- Vieux
- on
- une fois
- ONE
- uniquement
- open source
- d'exploitation
- le système d'exploitation
- systèmes d'exploitation
- opérateur
- Option
- or
- de commander
- original
- Autre
- Autres
- autrement
- nos
- nous-mêmes
- ande
- sortie
- plus de
- global
- propre
- page
- Panique
- partie
- Mot de Passe
- Password Manager
- mots de passe
- chemin
- Patron de Couture
- paul
- pause
- Payer
- pour cent
- être
- période
- définitivement
- personnel
- données à caractère personnel
- Physique
- image
- pièces
- Place
- espace réservé
- Peste
- Platon
- Intelligence des données Platon
- PlatonDonnées
- Beaucoup
- Point
- des notes bonus
- Populaire
- position
- possible
- Poteaux
- défaillances
- précisément
- représentent
- assez
- empêcher
- précédent
- prix
- Imprimé
- impressions
- Probablement
- d'ouvrabilité
- processus
- Programme
- Programmeur
- Programmeurs
- Programmation
- Programmes
- prononcé
- mettre
- Python
- Questions et réponses
- question
- soulève
- RAM
- aléatoire
- gamme
- plutôt
- raw
- les données brutes
- RE
- atteint
- Lire
- en cours
- solutions
- réal
- la vie réelle
- en temps réel
- vraiment
- reconnaître
- Récupérer
- récupération
- en relation
- restant
- rappeler
- éloigné
- supprimez
- répéter
- répété
- À PLUSIEURS REPRISES
- rapport
- représenté
- respect
- respectivement
- REST
- Résultats
- retourner
- retour
- révéler
- Débarrasser
- bon
- Analyse
- risques
- Salle
- Courir
- pour le running
- surveillance de l'exécution
- s
- des
- plus sûre
- même
- satisfait
- Épargnez
- dire
- balayage
- épars
- Scènes
- Rechercher
- recherche
- Deuxièmement
- secondes
- secret
- Section
- sécurisé
- sécurité
- sur le lien
- seed
- voir
- sembler
- vu
- voit
- Série
- grave
- set
- mise
- Shorts
- Peu de temps
- devrait
- montrer
- montré
- signer
- Signes
- similaires
- De même
- étapes
- simplifié
- simplifier
- simplement
- unique
- Taille
- sleep
- petit
- Sournois
- snooping
- So
- Logiciels
- solide
- quelques
- quelque chose
- disponible
- Identifier
- code source
- Space
- spécial
- spécialement
- spécifié
- vitesse
- Étoiles
- Commencer
- j'ai commencé
- Commencez
- départs
- Commencez
- Encore
- a volé
- Arrêter
- arrêté
- Boutique
- stockée
- Histoire
- Chaîne
- STRONG
- Étude
- Avec succès
- tel
- suffisant
- supposé
- surprise
- surpris
- surprenant
- survivre
- SVG
- échange
- combustion propre
- Système
- Prenez
- tâches
- prend
- prise
- parlant
- techniquement
- techniques
- temporaire
- tester
- tests
- que
- qui
- Les
- La Source
- leur
- Les
- se
- puis
- théorie
- Là.
- donc
- l'ont
- chose
- penser
- this
- ceux
- bien que?
- pensée
- fiable
- Titre
- à
- ensemble
- a
- outil
- top
- suivre
- Tracking
- transition
- communication
- oui
- Essai
- Tourné
- deux
- type
- typiquement
- comprendre
- unicode
- jusqu'à
- inutilisé
- indésirable
- Mises à jour
- a actualisé
- URL
- us
- gouvernement des États-Unis
- Utilisation
- usb
- utilisé
- utilisation-après-libre
- d'utiliser
- Utilisateur
- Usages
- en utilisant
- utilitaire
- Plus-value
- Valeurs
- variété
- vérifier
- version
- très
- via
- vulnérabilité
- W
- attendez
- Attendre
- souhaitez
- voulu
- était
- Montres
- Façon..
- façons
- we
- Semaines
- WELL
- ont été
- Quoi
- quand
- que
- qui
- tout en
- WHO
- quiconque
- la totalité
- why
- sera
- gagner
- fenêtres
- Essuyer
- comprenant
- sans
- demande
- des mots
- Activités principales
- travaillé
- de travail
- vos contrats
- s'inquiéter
- pourra
- donnerait
- écrire
- écriture
- code écrit
- encore
- you
- Votre
- vous-même
- zéphyrnet
- zéro