Friday, February 6, 2015

ProtectMemory vs ProtectData

Main Reference

Code Observations

There are two major ways of internally protecting data in your applications. 'ProtectData' and 'ProtectedMemory'. The purpose of ProtectData is to protect the data within the scope of:
  • The current user who is logged in and using the application that is doing the protecting
  • The local machine on which the application is running on
If the data is protected within the CurrentUser scope, only that user account is able to decrypt that data and can usually only decrypt the data on that machine unless the user account is a 'roaming profile'. If the data is protected within the CurrentMachine scope, all the users who are authenticated on that machine can decrypt the data.

The purpose of ProtectedMemory is to protect data within the scope of:
  • The current user account (the application must be run by the same user account)
  • The same process (only the exact same process can decrypt)
  • Cross Process (different processes can decrypt but only within 
    One of the big differences between ProtectedData and ProtectedMemory is that ProtectedData can be stored somewhere such as a file and be decrypted even after your computer restarts. If you encrypt data with ProtectedMemory and store it somewhere it will not be decryptable after your computer restarts so this is a consideration. ProtectedMemory is typically useful when you want to protect something temporarily and limit the scope within a particular process.

    Another difference is that ProtectedData also adds a MAC (Message Authentication Code) to verify that the data has not been tampered with. This is useful especially when the data is not just temporary data but stored somewhere. Ultimately this increases the size of the output as the MAC is appended to the end of the cipher text. ProtectedMemory, on the other hand, does not use a MAC but instead has the same size output as the input. One caveat is that ProtectMemory also requires an input that is a multiple of 16 bytes (128 bits). This is interesting as Triple DES is the encryption algorithm on older versions of windows and it encrypts in 64bit (8 byte) blocks. There is an interesting code comment:
The Rtl functions accept data in 8 byte increments, but we don't want applications to be able to make use of this, or else they're liable to break when the user upgrades.
I can't find much about this but I can only assume that this means that the developers were thinking ahead to a future change to the underlying algorithm and moving towards a more secure encryption algorithm that requires 128bit (16 byte) block sizes such as AES which is used in Windows7-8.

This article isn't meant to go into great detail of cryptography or the internals of the DPAPI but something to take note of is the underlying algorithms used in the different versions of windows (thanks to the folks at passcape):

OS Encryption Algorithm Hash Algorithm Number of iterations in PKCS#5
Windows2000 RC4 SHA1 1
WindowsXP 3DES SHA1 4000
WindowsVista 3DES SHA1 24000
Windows7 AES256 SHA512 5600

Protected Memory

ProtectedMemory is essentially a wrapper for the RtlEncryptMemory function also known as 'SystemFunction040' imported from Advapi32.dll. You can check out the source code online but I'll put it up here just for quick reference purposes:
[SecuritySafeCritical]
        public static void Protect (byte[] userData, 
                                    MemoryProtectionScope scope) {
            if (userData == null)
                throw new ArgumentNullException("userData");
            if (Environment.OSVersion.Platform == PlatformID.Win32Windows)
                throw new NotSupportedException("NotSupported_PlatformRequiresNT");
 
            VerifyScope(scope);
 
            // The RtlEncryptMemory and RtlDecryptMemory functions are available on WinXP and publicly published 
            // in the ntsecapi.h header file as of Windows Server 2003.
            // The Rtl functions accept data in 8 byte increments, but we don't want applications to be able to make use of this, 
            // or else they're liable to break when the user upgrades.
            if ((userData.Length == 0) || (userData.Length % CAPI.CRYPTPROTECTMEMORY_BLOCK_SIZE != 0))
                throw new CryptographicException("Cryptography_DpApi_InvalidMemoryLength");
 
            uint dwFlags = (uint) scope;
            try {
                // RtlEncryptMemory return an NTSTATUS
                int status = CAPI.SystemFunction040(userData,
                                                    (uint) userData.Length,
                                                    dwFlags);
                if (status < 0) // non-negative numbers indicate success
                    throw new CryptographicException(CAPI.CAPISafe.LsaNtStatusToWinError(status));
            }
            catch (EntryPointNotFoundException) {
                throw new NotSupportedException("NotSupported_PlatformRequiresNT");
            }
        }
The first couple things to notice is the parameters. The userData param is the input that you want to protect. 'scope' allows you to assert what kind of scope you want to protect your data in (User/Same Process/Cross Process). The first couple lines of code validate the data (make sure it's not null) and the environment. Advapi32.dll is only available as of WindowsXP (I say 'only' lol) and if you are running this on an older environment (good lord I hope not) then this will throw an exception. The next step is to verify the scope that your code asserted. It must be at least one of the three possible scopes or this will throw an exception. The next step is to verify the length. Must be greater than zero and also must be divisible by 16 (as noted earlier). The next step is to call into a very large class called 'CAPI' which defines constants and imports functions for much of System.Security.Cryptography. It's way too much code to display here so you'll just have to look it up. Essentially it takes the data, data length and converted scope (into a uint) and passes it to the external 'black box' of RtlEncryptMemory. Simple enough? The Unprotect function of course simply does the reverse so I won't waste time on it. Suffice it to say this is a very lightweight 'close to the vest' function to protect memory temporarily and have a minimal scope.

Protected Data

Protected data (on the other hand) is a bit more complicated. ProtectedMemory is very lightweight but ProtectedData is a bit on the heavier side, but with good reason.
[SecuritySafeCritical]
        public static byte[] Protect(byte[] userData,
            byte[] optionalEntropy,
            DataProtectionScope scope)
        {
            if (userData == null)
                throw new ArgumentNullException("userData");
            if (Environment.OSVersion.Platform == PlatformID.Win32Windows)
                throw new NotSupportedException("NotSupported_PlatformRequiresNT");

            GCHandle pbDataIn = new GCHandle();
            GCHandle pOptionalEntropy = new GCHandle();
            CAPI.CRYPTOAPI_BLOB blob = new CAPI.CRYPTOAPI_BLOB();

            RuntimeHelpers.PrepareConstrainedRegions();
            try
            {
                pbDataIn = GCHandle.Alloc(userData, GCHandleType.Pinned);
                CAPI.CRYPTOAPI_BLOB dataIn = new CAPI.CRYPTOAPI_BLOB();
                dataIn.cbData = (uint) userData.Length;
                dataIn.pbData = pbDataIn.AddrOfPinnedObject();
                CAPI.CRYPTOAPI_BLOB entropy = new CAPI.CRYPTOAPI_BLOB();
                if (optionalEntropy != null)
                {
                    pOptionalEntropy = GCHandle.Alloc(optionalEntropy, GCHandleType.Pinned);
                    entropy.cbData = (uint) optionalEntropy.Length;
                    entropy.pbData = pOptionalEntropy.AddrOfPinnedObject();
                }
                uint dwFlags = CAPI.CRYPTPROTECT_UI_FORBIDDEN;
                if (scope == DataProtectionScope.LocalMachine)
                    dwFlags |= CAPI.CRYPTPROTECT_LOCAL_MACHINE;
                unsafe
                {
                    if (!CAPI.CryptProtectData(new IntPtr(&dataIn),
                        String.Empty,
                        new IntPtr(&entropy),
                        IntPtr.Zero,
                        IntPtr.Zero,
                        dwFlags,
                        new IntPtr(&blob)))
                    {
                        int lastWin32Error = Marshal.GetLastWin32Error();

                        // One of the most common reasons that DPAPI operations fail is that the user
                        // profile is not loaded (for instance in the case of impersonation or running in a
                        // service.  In those cases, throw an exception that provides more specific details
                        // about what happened.
                        if (CAPI.ErrorMayBeCausedByUnloadedProfile(lastWin32Error))
                        {
                            throw new CryptographicException(
                                "Cryptography_DpApi_ProfileMayNotBeLoaded");
                        }
                        else
                        {
                            throw new CryptographicException(lastWin32Error);
                        }
                    }
                }

                // In some cases, the API would fail due to OOM but simply return a null pointer.
                if (blob.pbData == IntPtr.Zero)
                    throw new OutOfMemoryException();

                byte[] encryptedData = new byte[(int) blob.cbData];
                Marshal.Copy(blob.pbData, encryptedData, 0, encryptedData.Length);

                return encryptedData;
            }
            catch (EntryPointNotFoundException)
            {
                throw new NotSupportedException("NotSupported_PlatformRequiresNT");
            }
            finally
            {
                if (pbDataIn.IsAllocated)
                    pbDataIn.Free();
                if (pOptionalEntropy.IsAllocated)
                    pOptionalEntropy.Free();
                if (blob.pbData != IntPtr.Zero)
                {
                    CAPI.CAPISafe.ZeroMemory(blob.pbData, blob.cbData);
                    CAPI.CAPISafe.LocalFree(blob.pbData);
                }
            }
        }

First Steps:
First couple things to get out of the way is is the platform and input validation (of course). Then we have a method called PrepareConstrainedRegions. When this method is called before a try/catch it creates a Constrained Execution Region which is designed to avoid 'out of band' exceptions such as a stack overflow. It does this by calling ProbeForSufficientStack which probes 48KB of the stack to make sure there is sufficient space.

Pin The Data:
The next step is to call GCHandle.Alloc with the parameter 'Pinned'. What this means is that the code tells the CLR Garbage Collector not to move the memory address around. This is good in the sense that the Garbage Collector can relocate objects and leave several copies of them in memory. This is good for efficiency but bad for security. Another possibility is that the object may end up in the Page File which is typically unencrypted (but it can be encrypted) so it is possible that small amounts of data could be retrieved. So you can see why, if you are trying to protect data, it's important to pin that data in memory as soon as possible while it's plaintext.

Prepare The Data:
The next step is to gather the pieces together that the Win32 CryptProtectData function requires. The first thing we have to put together is the CRYPT_INTEGER_BLOB which is basically just a variable that has the plaintext to be secured and the length of that plaintext. On the C# side there is a struct created in CAPI.cs called CRYPTOAPI_BLOB that matches the CRYPT_INTEGER_BLOB requirements with a uint to set specify the length of the plaintext and an IntPtr as a pointer to the plaintext. So we set the cbData to the userData length and the Alloc function we called earlier gives us the pointer so, yay, we now have our blob that we can pass to the CryptProtectData api function. We then have some similar code for the 'entropy' such as a password. Basically this is just extra data but the thing to remember is that whatever is used to protect this data, must also be used to un-protect it. The last thing we have to do before calling our CryptProtectData function is to set our scope flags.

Protect The Data:
Yay! Now we can finally protect the data. The params we pass to CAPI.CryptProtectData are

  • The pointer to our plaintext blob (still pinned in memory)
  • An empty string to the underlying 'data description' (kinda weird)
  • The pointer to the entropy (if no entropy then it passes in a null pointer)
  • A null pointer to a reserved parameter. This is kinda weird too, it's pretty much just reserved for possible future upgrades to the win32 function.
  • A null pointer to the optional prompt (not necessary for most applications)
  • The scope flags
  • A pointer to the empty return blob which will have the ciphertext
Errors:
They happen! There is an interesting code comment that notes:
One of the most common reasons that DPAPI operations fail is that the user profile is not loaded (for instance in the case of impersonation or running in a service.  In those cases, throw an exception that provides more specific details about what happened. 
 A good example of this is running ASP.net code in IIS when the IIS setting 'Load User Profile' is set to false. If this is the case and you try to use the ProtectedData API then it will throw this exception.

Get The Ciphertext:
Assuming no exceptions have been thrown, the data now needs to be marshaled from the un-managed environment to the managed CLR environment. The developers do that by calling Marshal.Copy to copy the ciphertext to the 'encryptedData' variable and return it.

Cleanup:
Last but not least is the final cleanup. Free up the garbage collection 'pin' on pbDataIn (the plaintext) and then call a win32 function called ZeroMemory which is designed to very quickly replace the plaintext memory with zeros. This can work better/faster than setting it in code as some compilers, optimizers or managed code may not do what you would expect. Last step is to call LocalFree external function (Kernel32.dll) which zeroizes any memory locks on that memory location. If a process tries to access that memory an access violation will be thrown.

So that's about it. There is no such thing as perfect security and there have been some interesting demonstrations of weaknesses in DPAPI but it's certainly better than nothing and it's quite interesting to understand how it works under the surface. This, of course doesn't go into the deep internals of the win32 

1 comment:

  1. Fantastic article Beau. I'm currently studying for the Programming in C# exam and this helped a lot. What does puzzle me though is that you mentioned ProtectedData adds a MAC to the output whereas in a sample exam question it eludes to the fact that ProtectedData doesnt change the data whereas ProtectedMemory does. The MSDN articles relating to Data Protection doesnt mention anything about adding anything to the data. I could really do with some clarity on this please?

    ReplyDelete