P/Invoke CryptUnprotectData breaks SqlConnection constructor
Asked Answered
G

2

6

I'm attempting to use CryptUnprotectData to read a password protected using CryptProtectData into a SecureString and use that to connect to a database. I can get the correct password out, but trying to create a new SqlConnection after that fails with the following:

System.TypeInitializationException was unhandled
  HResult=-2146233036
  Message=The type initializer for 'System.Data.SqlClient.SqlConnection' threw an exception.
  Source=System.Data
  TypeName=System.Data.SqlClient.SqlConnection
  StackTrace:
       at System.Data.SqlClient.SqlConnection..ctor()
       at System.Data.SqlClient.SqlConnection..ctor(String connectionString, SqlCredential credential)
       at System.Data.SqlClient.SqlConnection..ctor(String connectionString)
       at ProtectedSqlTest.Program.Main() in C:\Git\ProtectedSqlTest\ProtectedSqlTest\Program.cs:line 16
       at System.AppDomain._nExecuteAssembly(RuntimeAssembly assembly, String[] args)
       at System.AppDomain.ExecuteAssembly(String assemblyFile, Evidence assemblySecurity, String[] args)
       at Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly()
       at System.Threading.ThreadHelper.ThreadStart_Context(Object state)
       at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
       at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
       at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
       at System.Threading.ThreadHelper.ThreadStart()
  InnerException: 
       HResult=-2146233036
       Message=The type initializer for 'System.Data.SqlClient.SqlConnectionFactory' threw an exception.
       Source=System.Data
       TypeName=System.Data.SqlClient.SqlConnectionFactory
       StackTrace:
            at System.Data.SqlClient.SqlConnection..cctor()
       InnerException: 
            HResult=-2146233036
            Message=The type initializer for 'System.Data.SqlClient.SqlPerformanceCounters' threw an exception.
            Source=System.Data
            TypeName=System.Data.SqlClient.SqlPerformanceCounters
            StackTrace:
                 at System.Data.SqlClient.SqlConnectionFactory..cctor()
            InnerException: 
                 HResult=-2147024809
                 Message=The parameter is incorrect. (Exception from HRESULT: 0x80070057 (E_INVALIDARG))
                 Source=mscorlib
                 StackTrace:
                      at System.Globalization.TextInfo.InternalChangeCaseString(IntPtr handle, IntPtr handleOrigin, String localeName, String str, Boolean isToUpper)
                      at System.Globalization.TextInfo.ToLower(String str)
                      at System.String.ToLower(CultureInfo culture)
                      at System.Diagnostics.PerformanceCounterLib.GetPerformanceCounterLib(String machineName, CultureInfo culture)
                      at System.Diagnostics.PerformanceCounterLib.IsCustomCategory(String machine, String category)
                      at System.Diagnostics.PerformanceCounter.InitializeImpl()
                      at System.Diagnostics.PerformanceCounter.set_RawValue(Int64 value)
                      at System.Data.ProviderBase.DbConnectionPoolCounters.Counter..ctor(String categoryName, String instanceName, String counterName, PerformanceCounterType counterType)
                      at System.Data.ProviderBase.DbConnectionPoolCounters..ctor(String categoryName, String categoryHelp)
                      at System.Data.SqlClient.SqlPerformanceCounters..ctor()
                      at System.Data.SqlClient.SqlPerformanceCounters..cctor()
                 InnerException: 

It's enough to simply call CryptUnprotectData for the SqlConnection to fail, the connection itself doesn't need to use the returned SecureString.

I'm using the extension methods from here as described in this post for my minimal repro:

class Program
{
    const string ProtectedSecret = /* SNIP - base 64 encoded protected data here */;
    static void Main()
    {
        // calling AppendProtectedData breaks the following SqlConnection
        // without the following line the application works fine
        new SecureString().AppendProtectedData(Convert.FromBase64String(ProtectedSecret));

        using (var conn = new SqlConnection("Server=(localdb)\\MSSqlLocalDb;Trusted_Connection=true"))
        using (var cmd = new SqlCommand("select 1", conn))
        {
            conn.Open();
            cmd.ExecuteNonQuery();
        }
    }
}

If i create a new SqlConnection before I load the password, I can create new SqlConnections fine for the duration of the application as it seems to use the same SqlConnectionFactory, but that means as a workaround I have to do something like this at the start of the application:

new SqlConnection().Dispose();

... which I'd like to avoid.

The following do not help:

  • Debug vs Release build
  • Debugging in Visual Studio vs running through the command line
  • Changing the CryptProtectFlags that is passed to CryptUnprotectData.
  • Removing RuntimeHelpers.PrepareConstrainedRegions() from the protection method.

Windows 10, VS Enterprise 2015, Console Application (.NET 4.6.1)

UPDATE: Running the data protection code in another threads gives a similar exception with a different root cause:

System.TypeInitializationException was unhandled
  HResult=-2146233036
  Message=The type initializer for 'System.Data.SqlClient.SqlConnection' threw an exception.
  Source=System.Data
  TypeName=System.Data.SqlClient.SqlConnection
  StackTrace:
       at System.Data.SqlClient.SqlConnection..ctor()
       at System.Data.SqlClient.SqlConnection..ctor(String connectionString, SqlCredential credential)
       at System.Data.SqlClient.SqlConnection..ctor(String connectionString)
       at ProtectedSqlTest.Program.Main() in C:\Git\ProtectedSqlTest\ProtectedSqlTest\Program.cs:line 17
       at System.AppDomain._nExecuteAssembly(RuntimeAssembly assembly, String[] args)
       at System.AppDomain.ExecuteAssembly(String assemblyFile, Evidence assemblySecurity, String[] args)
       at Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly()
       at System.Threading.ThreadHelper.ThreadStart_Context(Object state)
       at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
       at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
       at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
       at System.Threading.ThreadHelper.ThreadStart()
  InnerException: 
       HResult=-2146233036
       Message=The type initializer for 'System.Data.SqlClient.SqlConnectionFactory' threw an exception.
       Source=System.Data
       TypeName=System.Data.SqlClient.SqlConnectionFactory
       StackTrace:
            at System.Data.SqlClient.SqlConnection..cctor()
       InnerException: 
            HResult=-2146233036
            Message=The type initializer for 'System.Data.SqlClient.SqlPerformanceCounters' threw an exception.
            Source=System.Data
            TypeName=System.Data.SqlClient.SqlPerformanceCounters
            StackTrace:
                 at System.Data.SqlClient.SqlConnectionFactory..cctor()
            InnerException: 
                 BareMessage=Configuration system failed to initialize
                 HResult=-2146232062
                 Line=0
                 Message=Configuration system failed to initialize
                 Source=System.Configuration
                 StackTrace:
                      at System.Configuration.ClientConfigurationSystem.EnsureInit(String configKey)
                      at System.Configuration.ClientConfigurationSystem.PrepareClientConfigSystem(String sectionName)
                      at System.Configuration.ClientConfigurationSystem.System.Configuration.Internal.IInternalConfigSystem.GetSection(String sectionName)
                      at System.Configuration.ConfigurationManager.GetSection(String sectionName)
                      at System.Configuration.PrivilegedConfigurationManager.GetSection(String sectionName)
                      at System.Diagnostics.DiagnosticsConfiguration.Initialize()
                      at System.Diagnostics.DiagnosticsConfiguration.get_SwitchSettings()
                      at System.Diagnostics.Switch.InitializeConfigSettings()
                      at System.Diagnostics.Switch.InitializeWithStatus()
                      at System.Diagnostics.Switch.get_SwitchSetting()
                      at System.Data.ProviderBase.DbConnectionPoolCounters..ctor(String categoryName, String categoryHelp)
                      at System.Data.SqlClient.SqlPerformanceCounters..ctor()
                      at System.Data.SqlClient.SqlPerformanceCounters..cctor()
                 InnerException: 
                      HResult=-2147024809
                      Message=Item has already been added. Key in dictionary: 'MACHINE'  Key being added: 'MACHINE'
                      Source=mscorlib
                      StackTrace:
                           at System.Collections.Hashtable.Insert(Object key, Object nvalue, Boolean add)
                           at System.Collections.Hashtable.Add(Object key, Object value)
                           at System.Configuration.Internal.InternalConfigRoot.GetConfigRecord(String configPath)
                           at System.Configuration.ClientConfigurationSystem.EnsureInit(String configKey)
                      InnerException:
Gala answered 20/12, 2016 at 11:53 Comment(11)
Is there any particular reason you must P/Invoke to CryptUnprotectData instead of just using the managed ProtectedData wrapper?Screening
If there is any problem with the P/Invoke code (which seems probable) diagnosing this is easier if you turn on all Managed Debugging Assistants ("Exception Settings" from VS). Even that won't diagnose all problems, but it helps.Screening
@JeroenMostert I'd like to avoid handling unprotected data in managed memory. @stuartd Disposing the SecureString doesn't help.Gala
@JeroenMostert Activating all Managed Debugging Assistants on throw doesn't seem to trigger anything. I wrapped the code in a try/catch and it falls straight into the catch.Gala
As another debugging tactic, what happens if you do the protect/unprotect on a separate thread? If it works then, this suggests some form of stack/TLS corruption; if it still doesn't work, that's definitely not the issue and other internals may have gotten messed up. (I don't suggest using the separate thread is a solution, incidentally.)Screening
@JeroenMostert Interestingly running the data protection stuff in another thread doesn't fix it, but the root cause is slightly different: pastebin.com/LCGriSnnGala
Does this happen in both 32 and 64-bit builds? A common issue with P/Invoke is field alignment and struct packing which can differ in those cases.Piper
Yup, identical exception for x86 and x64.Gala
This is not the point of the question, but is it not pointless to do all of this if you are copying the password onto the managed heap (as you are)?; Also, a simple fix seems to be to just use the built-in append methods which seems a good idea to me.Gallows
I'm not seeing where the secret is in managed memory. ProtectedSecret is encrypted, unmanagedString and out_blob.pbData are zeroed after use and I'm using SqlCredential to send the password to Sql Server (in the actual application).Gala
Just chiming in to say I encountered this issue using the winforms ReportViewer (latest nuget package). The type initialiser failed for System.Runtime.Remoting.Identity, somewhere in the depths of that control. Was fixed by passing null for szDataDescr; I left it as a string. Oddly, SqlConnection was working fine, and I couldn't seem to break it as described here.Desolate
P
2

I was recently experiencing similar symptoms, using the same code from http://www.griffinscs.com/?p=12: any call to CryptUnprotectData would lead to an exception in some unrelated code. Interestingly, the failure only occurred on a Windows 10 machine; the same code worked fine on a Windows 7 machine.

I fixed the problem by changing the declarations of the szDataDescr parameters in both CryptProtectData and CryptUnprotectData from string to IntPtr, and passing IntPtr.Zero instead of string.Empty in the two calls.

Pascoe answered 30/12, 2016 at 18:40 Comment(1)
FWIW, the P/Invoke signature from pinvoke.net uses CharSet.Auto, but the Win32 function definition uses LPWSTR, which is always Unicode.Dogtired
T
4

Interestingly the faulting code is:

internal static PerformanceCounterLib GetPerformanceCounterLib(string machineName, CultureInfo culture) {
    SharedUtils.CheckEnvironment();

    string lcidString = culture.LCID.ToString("X3", CultureInfo.InvariantCulture);
    if (machineName.CompareTo(".") == 0)
            machineName = ComputerName.ToLower(CultureInfo.InvariantCulture);
    else
        machineName = machineName.ToLower(CultureInfo.InvariantCulture);
    ...

the line that calls ComputerName.ToLower(CultureInfo.InvariantCulture) causes the exception.

You can reproduce the same behavior just calling code

new SecureString().AppendProtectedData(Convert.FromBase64String(ProtectedSecret));
string lower = "Something".ToLower(CultureInfo.InvariantCulture);

Somehow in the constructor of the TextInfo class

this.m_dataHandle = CompareInfo.InternalInitSortHandle(m_textInfoName, out handleOrigin);

returns invalid data if this is not called before CryptUnprotectData function.

This seems like a bug in the framework. You can submit it to Microsoft. In the meantime you can call this line beforehand to prevent the error.

"".ToLower(CultureInfo.InvariantCulture); 
Tymbal answered 21/12, 2016 at 9:8 Comment(9)
Making that ToLower call before the data protection code seems to have the same effect as performing the protection code in another thread. The SqlConnection constructor still fails, only with a different root cause: pastebin.com/LCGriSnnGala
That the actual fault happens in unrelated code strongly points to some form of memory corruption (but likely nota completely wild pointer, because then the fault wouldn't always be in the same(ish) place.Piper
I don't have access to pastebin.com. I can't see the exception. Are you sure you called ToLower with CultureInfo.InvariantCulture? Parameterless one does not solve the problem. I can successfully create SqlConnection after ToLower(CultureInfo.InvariantCulture).Bridegroom
Yup, copied the line verbatim. I updated my original question with the contents of the pastebin, the exception after ToLower is identical to when the protection code is run in a separate thread.Gala
Very weird... If you call "".ToLower(CultureInfo.InvariantCulture); twice that exception disappears as well. Something crazy must be going on there.Bridegroom
Doesn't seem to for me, same issue after multiple ToLowers.Gala
This code works for me.Bridegroom
Strange, same Item has already been added exception as in the question update for me.Gala
Let us continue this discussion in chat.Bridegroom
P
2

I was recently experiencing similar symptoms, using the same code from http://www.griffinscs.com/?p=12: any call to CryptUnprotectData would lead to an exception in some unrelated code. Interestingly, the failure only occurred on a Windows 10 machine; the same code worked fine on a Windows 7 machine.

I fixed the problem by changing the declarations of the szDataDescr parameters in both CryptProtectData and CryptUnprotectData from string to IntPtr, and passing IntPtr.Zero instead of string.Empty in the two calls.

Pascoe answered 30/12, 2016 at 18:40 Comment(1)
FWIW, the P/Invoke signature from pinvoke.net uses CharSet.Auto, but the Win32 function definition uses LPWSTR, which is always Unicode.Dogtired

© 2022 - 2024 — McMap. All rights reserved.