String that is secure

I’ve previously used the SecureString class to handle passwords in memory, but I’ve realized I wasn’t using it correctly. Here’s what I’ve learned to avoid future misuse:

Clearing Sensitive Data from Memory

It’s crucial to minimize the time sensitive data resides in memory. While it’s tempting to just release the memory, the .NET garbage collection process isn’t immediate. Overwriting the memory is a better approach. However, strings in .NET are immutable, making overwriting difficult. Using a mutable structure like a char array is recommended.

It’s important to note that while strings shouldn’t be used for sensitive data, they aren’t as risky as I initially thought. String interning doesn’t mean a string stays in memory forever; the interning table is located in the Large Object Heap (LOH), which gets garbage collected eventually.

Understanding SecureString

SecureString is often touted as the solution for storing sensitive data. It encrypts the stored string using a symmetric algorithm and a session-specific key managed by DAPI (see here for more details). However, the vulnerability lies in the initial clean string before encryption. Some framework components, like WPF’s PasswordBox, work directly with SecureStrings, avoiding the exposure of plain text passwords. However, support for SecureString is not universal.

A Common Use Case: SecureString and Impersonation

Consider an application that needs to start a process or thread as a different user (UserB). Assume UserB’s password is stored encrypted with a symmetric key. It’s crucial to decrypt this into a SecureString. Some older symmetric encryption implementations might return a string, which is incorrect. For a good example of an implementation returning a SecureString, see find here. Note how they decrypt into a char array, build the SecureString, and then clear the array containing the plain text password.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
  public void Decrypt(byte\[\] input, out SecureString output, byte\[\] key,
            byte\[\] iv)
        {
            byte\[\] decryptedBuffer = null;

            try
            {
                // do our normal decryption of a byte array
                decryptedBuffer = Decrypt(input, key, iv);

                char\[\] outputBuffer = null;
                
                try
                {
                    // convert the decrypted array to an explicit
                    // character array that we can "flush" later
                    outputBuffer = \_utf8.GetChars(decryptedBuffer);

                    // Create the result and copy the characters
                    output = new SecureString();
                    try
                    {
                        for (int i = 0; i < outputBuffer.Length; i++)
                            output.AppendChar(outputBuffer\[i\]);
                        return;
                    }
                    finally
                    {
                        output.MakeReadOnly();
                    }
                }
                finally
                {
                    if (outputBuffer != null)
                        Array.Clear(outputBuffer, 0, outputBuffer.Length);
                }
            }
            finally
            {
                if (decryptedBuffer != null)
                    Array.Clear(decryptedBuffer, 0, decryptedBuffer.Length);
            }
        }

Now, we have the password in a SecureString. This is sufficient for starting a process as ProcessInfo.Password accepts a SecureString. But what about impersonation?

For impersonation, we need to use the LogonUser Win32 function to get an access token (see As you know). This function expects a plain text password, not a SecureString. Does this mean we are stuck with the risks of having the password as plain text in memory until the GC cleans it up?

Fortunately, there is a solution explained in detail here: here. The key is to define the LogonUser PInvoke signature to accept an IntPtr for the password instead of a string. Then, you can use Marshal.SecureStringToGlobalAllocUnicode (Marshal.SecureStringToGlobalAllocUnicode) to decrypt the SecureString and place it in unmanaged memory. Once you are done with LogonUser, ensure you clean up the unmanaged memory containing the decrypted password using Marshal.ZeroFreeGlobalAllocUnicode (Marshal.ZeroFreeGlobalAllocUnicode).

ProcessStartInfo.Password

Licensed under CC BY-NC-SA 4.0
Last updated on Sep 06, 2023 13:06 +0100