Sécuriser les chaines de caractères dans un processus .NET

On peut imaginer que stocker un mot de passe dans une chaîne de caractères pendant l’exécution d’un processus, offre une solution sécurisée et qu’il est difficile de lire le contenu de cette chaîne. En réalité, ce type de stockage n’a rien de sécurisé.

On peut trouver dans des applications .NET des mots de passe stockés en clair dans un objet de type System.String. Toutefois avec très peu de moyen, on peut rapidement lire le contenu de n’importe quelle chaîne de caractères stockée dans un processus .NET. Ainsi, si ces mots de passe apparaissent en clair, il sera facile d’en lire le contenu.

Le but de cet article est d’abord de montrer comment on peut facilement lire une chaîne de caractères dans la mémoire d’un processus .NET. Ensuite, on proposera quelques solutions simples pour rendre une chaîne de caractères plus difficilement lisible par un outil externe.

Détecter le contenu d’une chaîne de caractères dans un processus

Dans un premier temps, on va montrer comment on peut facilement lire une chaîne de caractères stockée en clair dans un objet System.String dans un processus .NET.

La première étape consiste à simplement coder un exécutable contenant une chaîne de caractères non statique. Pour illustrer, on considère une application qui vérifie le mot de passe de l’utilisateur à 3 reprises. Si le couple user/password est correct, il répond OK sinon il réponds KO.

Le code correspondant à ce processus se trouve dans le repository suivant: github.com/msoft/secureString.

Les mots de passe sont stockés par un objet satisfaisant IPasswordFinder:

public interface IPasswordFinder 
{
    string GetPassword(string login); 
} 

L’objet satisfaisant IPasswordChecker est de type Mock<IPasswordFinder>. Il ne fait que renvoyer un mot de passe en fonction du nom d’un utilisateur. On l’instancie en utilisant PasswordFinderFactory dont voici l’implémentation:

public class PasswordFinderFactory 
{ 
    private readonly Dictionary<string, string> passwordPerUser = new Dictionary<string, string>(); 

    public void SetPassword(string login, string password) 
    { 
        this.passwordPerUser[login] = password; 
    } 
    
    public IPasswordFinder GetNewPasswordFinder() 
    { 
        var passwordFinderMock = new Mock<IPasswordFinder>(); 
    
        foreach (var user in this.passwordPerUser) 
            passwordFinderMock.Setup(f => f.GetPassword(user.Key)).Returns(user.Value); 
    
        return passwordFinderMock.Object; 
    } 
    
    public static PasswordFinderFactory GetFactory() 
    { 
        var factory = new PasswordFinderFactory(); 
    
        factory.SetPassword(
            "User1", 
            $"{(char)109}{(char)121}{(char)115}{(char)101}{(char)99}{(char)114}{(char)101}
                {(char)116}{(char)112}{(char)97}{(char)115}{(char)115}{(char)119}{(char)111}
                {(char)114}{(char)100}"
        ); 
    
        factory.SetPassword(
            "User2", 
            $"{(char)111}{(char)116}{(char)104}{(char)101}{(char)114}{(char)112}{(char)97}
            {(char)115}{(char)115}{(char)119}{(char)111}{(char)114}{(char)100}"
        ); 
    
        return factory; 
    } 
} 

Comme on peut le voir le mot de passe avec lequel on effectue la comparaison n’est pas écrit sous forme d’une chaîne de caractères unique pour éviter que le compilateur ne l’interprète comme un objet statique:

factory.SetPassword(
    "User1", 
    $"{(char)109}{(char)121}{(char)115}{(char)101}{(char)99}{(char)114}{(char)101}
        {(char)116}{(char)112}{(char)97}{(char)115}{(char)115}{(char)119}{(char)111}
        {(char)114}{(char)100}"
); 

factory.SetPassword(
    "User2", 
    $"{(char)111}{(char)116}{(char)104}{(char)101}{(char)114}{(char)112}{(char)97}
        {(char)115}{(char)115}{(char)119}{(char)111}{(char)114}{(char)100}"
); 

Si les chaînes étaient stockées de façon statique, il n’y aurait pas de nécessité d’exécuter l’application. En décompilant les assemblies, on verrait directement les chaînes de caractères.

La classe qui effectue la vérification du mot de passe est PasswordChecker:

public class PasswordChecker 
{ 
    private IPasswordFinder passwordFinder; 

    public PasswordChecker(PasswordFinderFactory passwordFinderFactory) 
    { 
        this.passwordFinder = passwordFinderFactory.GetNewPasswordFinder(); 
    } 
    
    public bool CheckPassword(string login, string password) 
    { 
        var foundPassword = this.passwordFinder.GetPassword(login); 
    
        return string.IsNullOrEmpty(foundPassword) ? false : foundPassword.Equals(password); 
    } 
} 

L’exécution est sans surprise:

Essai 1
Quel est le login ?
> User1
Quel est le mot de passe ?
> bad password
KO
Essai 2
Quel est le login ?
> User1
Quel est le mot de passe ?
> mysecretpassword
OK

Pour l’exemple avec des chaînes de caractères simples, il faut regarder le contenu du répertoire SecureString/WithSimpleString. Pour utiliser les classes de ce répertoire, la fonction main doit instancier passwordChecker de cette façon:

var passwordChecker = new WithSimpleString.PasswordChecker( 
    WithSimpleString.PasswordFinderFactory.GetFactory()); 

Récupérer le mot de passe avec un outil externe

On propose 2 méthodes pour lire les chaînes de caractères contenant les mots de passe en utilisant des outils externes au processus:

  • Avec ProcessHacker et
  • Avec WinDbg.

Avec ProcessHacker

Pour lire toutes les chaînes de l’assembly, on peut exécuter ProcessHacker (disponible gratuitement):

  1. Il faut chercher l’application dans la liste des processus
  2. Double-cliquer sur l’application “SecureString.exe”:
  3. Aller dans l’onglet Memory:
  4. Cliquer sur “Strings”
  5. Laisser les paramètres par défaut:
  6. Sauvegarder les chaînes dans un fichier texte en cliquant sur “Save”.

En cherchant dans le fichier texte, on peut voir que les chaînes de caractères correspondant à ce qu’a écrit l’utilisateur à chaque tentative, sont visibles même si la variable password est écrasée à chaque itération de la boucle. D’autre part, on peut voir les chaînes correspondant aux mots de passe à trouver en clair.

Par exemple:

0x162266c (84): ... 
0x16228e0 (108): NLS_CodePage_850_3_2_0_0ns.IMocked`1_get_MocktPassword 
0x1622b58 (32): mysecretpassword
0x1622d88 (26): otherpassword 
0x1623e20 (76): Invariant Language (Invariant Country) 
0x1623e90 (36): ...

Avec WinDbg

Une autre méthode pour parcourir la mémoire d’un processus est d’utiliser WinDbg. WinDbg est un outil puissant qui permet de déboguer un processus et de lire un dump de mémoire (cf. Les “dumps” mémoire en 5 min). Même si WinDbg est difficile à utiliser, il permet d’extraire plus d’informations d’un dump mémoire que Visual Studio.

On peut l’installer à partir SDK Windows 10:

Dans notre cas, le gros intérêt de WinDbg est de pouvoir travailler sur un dump mémoire du processus. Ce dump correspond à une photo de la mémoire occupée par un processus. Même si l’exécution du processus s’interrompt, on peut continuer d’exploiter le dump pour en extraire des informations.

Pour générer un dump, il faut ouvrir le gestionnaire de tâches en faisant [Ctrl] + [Maj] + [Echap]:

  1. Sélectionner le processus
  2. Faire un clique droit sur le processus
  3. Cliquer sur “Generate a dump”

Le dump est généralement écrit dans un fichier dans le répertoire:

C:\Users\<Nom utilisateur>\AppData\Local\Temp\SecureString.DMP 

Après génération du dump, il faut le charger avec WinDbg:

  1. Cliquer sur File
  2. Puis sur “Open Crash Dump”.
  3. Sélectionner le fichier de dump
  4. Cliquer sur Window puis “Dock All” pour agrandir la fenêtre.

Une autre méthode sans générer de dump de mémoire, peut consister à s’attacher directement au processus:

  1. Cliquer sur File
  2. Puis sur “Attach to a process…”
  3. Sélectionner le processus puis cliquer sur OK
  4. Cliquer sur Window puis “Dock All” pour agrandir la fenêtre.

Quand le fichier dump est chargé ou quand le processus est attaché, il suffit de chercher une chaîne de caractères correspondant aux mots de passe en tapant la commande suivante:

s –u 0 0FFFFFFF "<chaîne à chercher>" 

Cette instruction permet de chercher une chaîne Unicode (les chaînes de caractères en .NET sont stockées en UTF-16) de l’adresse mémoire 0 à l’adresse 0FFFFFFF (car on ne connait pas l’adresse de fin).

D’autres commandes permettent d’extraire d’autres informations:

  • Pour lister toutes les chaînes de caractères Unicode (la liste peut être très longue): s –su 0 0FFFFFFF
  • Pour lister toutes les chaînes de caractères ASCII: s –sa 0 0FFFFFFF
  • Pour voir le contenu d’une adresse en mémoire: dc <adresse mémoire>

Par exemple:

0:000> s -u 0 0FFFFFFF "mysecretpassword"  
01622b58  006d 0079 0073 0065 0063 0072 0065 0074  m.y.s.e.c.r.e.t. 

0:000> dc 01622b58   
01622b58  0079006d 00650073 00720063 00740065  m.y.s.e.c.r.e.t. 
01622b68  00610070 00730073 006f0077 00640072  p.a.s.s.w.o.r.d. 
01622b78  00000000 00000000 68363bc8 00000003  .........;6h.... 
01622b88  00000001 ffffffff 00000000 00000000  ................ 
01622b98  6835ca98 00000003 016225b8 01622b50  ..5h.....%b.P+b. 
01622ba8  2c1c7fa3 ffffffff 0162264c 01622d80  ...,....L&b..-b. 
01622bb8  2c1c7fa4 ffffffff 00000000 00000000  ...,............ 
01622bc8  00000000 00000000 00000000 68362158  ............X!6h 

On peut chercher toutes les chaînes de caractères contenus dans le processus en exécutant les commandes suivantes:

  • Pour les chaînes ASCII: s –sa 0 0FFFFFFF
  • Pour les chaînes Unicode: s –su 0 0FFFFFFF

Attention, l’exécution de ces commandes peut prendre du temps.

Comme pour ProcessHacker, on s’aperçoit qu’on peut voir le contenu des chaînes du processus en clair.

Quelques précisions sur “System.String” en .NET

Les chaînes de caractères sont le plus souvent stockées en .NET dans un objet de type System.String. Les objets de ce type sont de type référence c’est-à-dire qu’ils sont alloués et stockés dans le tas managé et qu’ils sont libérés lors des passages du garbage collector.

Les objets System.String ont aussi la particularité d’être immutables c’est-à-dire qu’ils sont passés par référence lors d’appels de fonction ou lors d’affectation de variables toutefois toute manipulation sur une chaîne entraîne la création d’une nouvelle instance de chaîne.

Ainsi si on écrit:

string finalString = "chaine1" + "chaine2" + "chaine3";

4 objets de type System.String sont créés dans le tas (finalString et les objets contenant "chaine1", "chaine2" et "chaine3"). Il faudra attendre le passage du garbage collector pour collecter les objets contenant "chaine1", "chaine2" et "chaine3". Ainsi avant le passage du garbage collector, on peut non seulement voir le contenu de finalString mais aussi "chaine1", "chaine2" et "chaine3".

Ainsi, si on stocke des mots de passe ou des sections de mot de passe dans un objet de type System.String, on ne pourra jamais vraiment savoir quand cet objet sera supprimé par le garbage collector, il peut rester dans la mémoire même longtemps après l’avoir utilisé. Avant sa suppression, on peut largement avoir le temps de générer un dump et de l’étudier pour éventuellement en extraire des chaînes avec des mots de passe. C’est la raison pour laquelle il faut éviter d’utiliser ce type d’objet pour stocker des chaînes dont le contenu est sensible.

Quelques solutions pour stocker des mots de passe

Ne pas utiliser de “System.String”

Cette solution peut paraître évidente mais c’est la plus simple pour ne pas dupliquer une chaîne de caractères au contenu sensible dans la mémoire: éviter d’utiliser trop d’instances d’objet de type System.String.

Eviter de conserver des références vers une chaîne de caractères peut, dans certain cas, permettre au garbage collector de “collecter” l’objet correspondant à la chaîne de caractères de façon à libérer l’espace occupé par l’objet et surtout écraser son contenu pour y affecter d’autres données. En effet, même si le garbage collector libère l’espace correspond à l’objet, le contenu en mémoire n’est pas forcément écrasé et la chaîne de caractères peut rester présente en mémoire pour une durée indéfinie.

Etant donné le manque de maitrise quant à la libération d’un objet de type System.String, il est conseillé d’utiliser des objets System.Security.SecureString pour stocker des chaînes de caractères sensibles.

Forcer l’exécution du garbage collector

Si on ne possède plus de références vers un objet de type System.String, le garbage collector est susceptible de s’exécuter et de collecter cet objet pour libérer l’espace mēmoire occupé. Au lieu d’attendre l’exécution du garbage collector, on peut la forcer quand on sait qu’on n’utilisera plus la chaîne de caractères. Pour forcer l’exécution du garbage collector, on peut exécuter l’instruction:

GC.Collect(); 

Utiliser une “SecureString”

L’objet de type System.Security.SecureString est spécialement conçu pour stocker des chaînes de caractères sensibles. Le contenu stocké par cet objet est crypté en utilisant la fonction RtlEncryptMemory du système d’exploitation (code source de System.Security.SecureString sur referencesource.microsoft.com). Ainsi il est beaucoup difficile de lire le contenu de ce type d’objet en mémoire.

La contrepartie des objets SecureString est que leur utilisation n’est pas forcément direct, en particulier si un mot de passe est stocké de façon provisoire dans un objet System.String.

Dans certains cas, on peut directement avoir une instance de SecureString: par exemple en utilisant l’objet System.Windows.Control.PasswordBox en WPF pour permettre à l’utilisateur d’entrer un mot de passe. Dans ce cas, pour récupérer la valeur du mot de passe, on peut utiliser directement la propriété PasswordBox.SecurePassword qui est de type SecureString. La récupération du mot de passe est donc sécurisée.

A l’opposé, dans d’autres cas, par exemple en WinForm, pour récupérer un mot de passe tapé par l’utilisateur, il faut utiliser une TextBox classique et utiliser la propriété TextBox.Text qui est de type System.String. Dans le cas de la TextBox, il existe des solutions pour ce type de control(1) (2) pour éviter de trop exposer ce qui est tapé par l’utilisateur.

Utiliser le contenu d’une SecureString

Utiliser les données contenus dans un objet SecureString n’est pas tout-à-fait direct. Ainsi, pour utiliser le contenu de ce type d’objet en limitant les risques de copie dans des parties de la mémoire dont on ne maîtrise pas la libération, on peut s’inspirer du code suivant:

public static TReturn UseSecureStringContent<TReturn>(
    System.Security.SecureString secureString, 
    Func<char[], TReturn> stringUse) 
{ 
    int stringLength = secureString.Length; 
    char[] bytes = new char[stringLength]; 
    IntPtr ptr = IntPtr.Zero; 

    try 
    { 
        ptr = Marshal.SecureStringToBSTR(secureString); 
        bytes = new char[stringLength]; 
        Marshal.Copy(ptr, bytes, 0, stringLength); 
    } 
    finally 
    { 
        if (ptr != IntPtr.Zero) 
            Marshal.ZeroFreeBSTR(ptr); 
    } 

    // Utiliser le chaîne de caractères sous forme de tableau de caractères  
    TReturn returnValue = stringUse(bytes); 
    for (int i = 0; i < stringLength; i++) 
        bytes[i] = '*'; 

    return returnValue; 
}

Dans cet exemple, au lieu de générer une chaîne de caractères, on extrait un tableau de caractères. L’intérêt d’utiliser un tableau de caractères est qu’on peut facilement écraser le contenu du tableau après utilisation. Pendant l’exécution de la fonction, la chaîne de caractères sera écrite en clair dans le tableau toutefois si l’exécution est rapide, il est plus difficile d’en lire le contenu en utilisant un dump ou en essayant de lire la mémoire.

Convertir une chaîne de caractères en “SecureString”

On peut convertir une chaîne de caractères en tableau de char de cette façon:

public static System.Security.SecureString GetNewSecureString(char[] clearString) 
{ 
    var secureString = new System.Security.SecureString(); 
    foreach (char caracter in clearString) 
    { 
        secureString.AppendChar(caracter); 
    } 

    return secureString; 
} 

Même si un tableau est alloué sur le tas managé, l’intérêt de son utilisation par rapport à la chaîne stockée dans un objet de type System.String est qu’on peut facilement en écraser le contenu.

On peut facilement utiliser une méthode similaire pour convertir un objet System.String en SecureString:

public static System.Security.SecureString GetNewSecureString(string clearString) 
{ 
    return GetNewSecureString(clearString.ToArray()); 
} 

Convertir une “SecureString” en chaîne de caractères

On peut convertir une SecureString en tableau de caractères de cette façon:

public static char[] ConvertToCharArray(SecureString secureString) 
{ 
    int stringLength = secureString.Length; 
    var charArray = new char[stringLength]; 
    IntPtr valuePtr = IntPtr.Zero; 

    try 
    { 
        valuePtr = Marshal.SecureStringToGlobalAllocUnicode(secureString); 

        for (int i = 0; i < stringLength; i++) 
        { 
            charArray[i] = (char)Marshal.ReadInt16(valuePtr, i * 2);
        } 
    } 
    finally 
    { 
        Marshal.ZeroFreeGlobalAllocUnicode(valuePtr); 
    } 

    return charArray; 
} 

Une conversion de SecureString vers System.String se fait plus directement de cette façon:

public static string ConvertToString(SecureString secureString) 
{ 
    IntPtr valuePtr = IntPtr.Zero; 
    try 
    { 
        valuePtr = Marshal.SecureStringToGlobalAllocUnicode(secureString); 
        return Marshal.PtrToStringUni(valuePtr); 
    } 
    finally 
    { 
        Marshal.ZeroFreeGlobalAllocUnicode(valuePtr); 
    } 
} 

Dans le cadre de l’exemple utilisé plus haut dans le repository GitHub SecureString, il faut adapter le code pour utiliser des objets de type SecureString plutôt que System.String. L’adaptation du code se trouve dans le répertoire SecureString/WithSecureString. La fonction main doit utiliser les classes de ce répertoire et instancier passwordChecker de cette façon:

var passwordChecker = new WithSecureString.PasswordChecker( 
    WithSecureString.PasswordFinderFactory.GetFactory()); 

Utiliser “System.String” et écraser son contenu après utilisation

Comme indiqué plus haut, les objets de type System.String sont des objets de type référence alloués dans le tas managé. Leur durée de vie et surtout leur temps de présence en mémoire dépend de l’exécution du garbage collector. Comme on ne maitrise pas complètement l’exécution du garbage collector, stocker des chaînes de caractères sensibles dans un objet System.String présente un risque. Toutefois dans certains cas d’utilisation, on peut être contraint d’utiliser un objet de ce type, par exemple pour transmettre un mot de passe à une bibliothèque dont on ne maitrise pas l’implémentation et qui impose l’utilisation d’objets de type System.String.

Dans ce cas, une solution est d’utiliser un objet de type System.String et d’en effacer le contenu après utilisation. Après écrasement, un mot de passe stocké dans un objet de ce type est effacé de la mémoire et ne peut plus être lu.

Comme on l’a expliqué plus haut, si on écrit:

string password = "mysecretpassword"; 

Et si on écrit ensuite la ligne suivante:

password = "*********"; 

On n’écrase pas le contenu de la mémoire car un objet de type System.String est immutable. Ce qui signifie que:

  • A la déclaration de la variable password: on alloue à la variable password une référence vers la chaîne de caractères "mysecretpassword" stockée dans le tas managé.
  • A l’affectation de "*********" à la variable password: on alloue une référence vers une nouvelle chaîne de caractères stockée dans le tas managé toutefois l’ancienne chaîne de caractères n’est pas modifiée. C’est juste la référence contenue dans la variable password qui est modifiée.

Il faut donc, un code qui permet réellement d’écraser le contenu de la chaîne de caractères comme par exemple, le code suivant:

public static void EraseStringContent(string stringToErase) 
{ 
    unsafe 
    { 
        fixed (char* stringContent = stringToErase) 
        { 
            for (int i = 0; i < stringToErase.Length; i++) 
                stringContent[i] = '*'; 
        } 
    } 
} 

Ce code permet d’écraser le contenu de la chaîne de caractères passée en paramètre:

  • Le mot-clé unsafe: permet d’indiquer que le code suivant contient des instructions manipulant directement des pointeurs.
  • Le mot-clé fixed: permet d’indiquer au garbage collector de ne pas modifier les emplacements en mémoire des objets manipulés. Sans l’utilisation du mot-clé fixed, le CLR pourrait changer l’emplacement de certains objets en fonction de l’exécution du garbage collector. Ces déplacements pourraient intervenir pendant la manipulation de pointeurs rendant, de fait, invalides les adresses pointées.

Le gros inconvénient de cette méthode est qu’elle nécessite que le code exécuté soit “unsafe” ce qui signifie qu’il faut rajouter une option de compilation permettant d’autoriser l’exécution de code “unsafe”.

Pour autoriser l’exécution de code “unsafe” dans une assembly, il faut:

  1. Effectuer un clique droit sur le projet concerné dans Visual Studio
  2. Cliquer sur Propriétés
  3. Dans l’onglet “Build”, il faut cocher “Allow unsafe code”.

En combinant les SecureString et l’écrasement des chaînes de caractères System.String, on peut proposer l’implémentation suivante pour utiliser le contenu d’une SecureString:

public static TReturn UseSecureStringContent<TReturn>(
    SecureString secureString, 
    Func<string, TReturn> useString) 
{ 
    string simpleString = ConvertToString(secureString); 
    var returnValue = useString(simpleString); 
    EraseStringContent(simpleString); 
    return returnValue; 
} 

De même que précédemment, l’utilisation d’un objet de type System.String rend plus facile la lecture des données sensibles en mémoire. Toutefois étant donné qu’on en efface le contenu après utilisation, la chaîne n’apparait pas longtemps en clair dans la mémoire ce qui limite le risque.

Le code correspondant aux manipulations de SecureString se trouve dans SecureStringHelper.

En conclusion

On a vu quelques méthodes pour facilement lire le contenu de chaînes de caractères dans la mémoire d’un processus quand cette chaîne est stockée en clair. On a pu remarquer que les objets System.String ne sont pas adaptés pour stocker des données sensibles comme des mots de passe. Il est préférable d’utiliser des objets comme les SecureString.

On peut aller plus loin pour stocker ces données sensibles en passant par la Data Protection API (i.e. DPAPI) qui est spécialement conçue pour ce type d’usage. On peut facilement trouver de la documentation pour utiliser la DPAPI(3).

(1) – My SecurePasswordTextBox control is famous: https://weblogs.asp.net/pglavich/440052
(2) – SecurePasswordTextBox update: https://weblogs.asp.net/pglavich/440191
(3) – Documentation pour utiliser la Data Protection API:

Références
2 responses... add one

Bonjour, Je suis tombé un peu par hasard sur ton blog ..et je le trouve tres interessant.

Je passe juste te remercier pour toutes les infos que tu mets à disposition de la communautés.
Ce que j’apprecie le plus tout est tres bien expliqué et souvent avec des exemples concret que l’on peut facilement reproduire afin de bien comprendre le tout …

Et ca …j’achete 🙂

bonne continuation
christophe

Leave a Reply