P/Invoke CryptUnprotectData破坏了SqlConnection构造函数。

6

我正在尝试使用CryptUnprotectData来读取使用CryptProtectData密码保护的内容到一个SecureString中,并使用它连接到数据库。我可以正确地获取密码,但是之后尝试创建一个新的SqlConnection失败,错误信息如下:

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: 

只需简单调用CryptUnprotectDataSqlConnection就会失败,连接本身不需要使用返回的SecureString。我在我的最小复现中使用这里描述的扩展方法,如此文所述。
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();
        }
    }
}

如果我在加载密码之前创建一个新的 SqlConnection,那么在应用程序运行期间,我可以正常创建新的 SqlConnection,因为它似乎使用相同的 SqlConnectionFactory。但这意味着我必须在应用程序开始时执行以下操作:
new SqlConnection().Dispose();

我希望避免以下内容:

  • Debug和Release构建
  • 在Visual Studio中调试与通过命令行运行的调试
  • 更改传递给CryptUnprotectDataCryptProtectFlags
  • 从保护方法中删除RuntimeHelpers.PrepareConstrainedRegions()

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

更新:在另一个线程中运行数据保护代码时会产生类似的异常,但根本原因不同:

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:

如果P/Invoke代码存在任何问题(这似乎很可能),如果您打开所有托管调试助手(从VS的“异常设置”中),诊断将更容易。即使如此,这也不能诊断所有问题,但它会有所帮助。 - Jeroen Mostert
@JeroenMostert 在throw上启用所有托管调试助手似乎没有触发任何东西。我在try / catch中包装了代码,但它直接进入catch。 - Jussi Kosunen
作为另一种调试策略,如果您在单独的线程上执行保护/取消保护操作会发生什么?如果这样做可以解决问题,那么这表明存在某种形式的堆栈/TLS损坏;如果仍然无法解决问题,那么肯定不是这个问题,其他内部可能已经出现了问题。(顺便说一句,我并不建议使用单独的线程作为解决方案。) - Jeroen Mostert
是的,x86和x64的异常完全相同。 - Jussi Kosunen
只是想说我在使用winforms ReportViewer(最新的nuget包)时遇到了这个问题。在该控件的深处,System.Runtime.Remoting.Identity的类型初始化器失败了。通过传递null来修复szDataDescr; 我将其保留为字符串。 奇怪的是,SqlConnection运作良好,我似乎无法像这里描述的那样破坏它。 - andrensairr
显示剩余6条评论
2个回答

4
有趣的是,错误代码为:
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);
    ...

调用 ComputerName.ToLower(CultureInfo.InvariantCulture) 的这一行是导致异常的原因。

您可以通过调用以下代码来复现相同的行为

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

TextInfo 类的构造函数中,有一些操作。
this.m_dataHandle = CompareInfo.InternalInitSortHandle(m_textInfoName, out handleOrigin);

如果在调用 CryptUnprotectData 函数之前没有调用此函数,则会返回无效数据。

这似乎是框架中的一个错误。您可以将其提交给 Microsoft。与此同时,您可以提前调用此行代码以防止出现错误。

"".ToLower(CultureInfo.InvariantCulture); 

1
实际故障发生在无关代码中,强烈指向某种形式的内存损坏(但可能不是完全野指针,因为那样的话,故障就不会总是出现在相同(或类似)的位置了。 - Zastai
我无法访问 pastebin.com。我看不到异常。你确定你使用了 CultureInfo.InvariantCulture 调用了 ToLower 吗?没有参数的那个不能解决问题。在 ToLower(CultureInfo.InvariantCulture) 之后,我可以成功创建 SqlConnection - Yusuf Tarık Günaydın
是的,我逐字复制了那一行。我已经使用pastebin的内容更新了我的原始问题,ToLower之后的异常与在单独的线程中运行保护代码时完全相同。 - Jussi Kosunen
非常奇怪...如果你调用 "".ToLower(CultureInfo.InvariantCulture); 两次,那个异常也会消失。一定有什么疯狂的事情发生了。 - Yusuf Tarık Günaydın
让我们在聊天中继续这个讨论:点击此处进入聊天室 - Yusuf Tarık Günaydın
显示剩余4条评论

2
我最近遇到了类似的症状,使用与 http://www.griffinscs.com/?p=12 相同的代码:任何对 CryptUnprotectData 的调用都会导致一些无关的代码中出现异常。有趣的是,这个故障只发生在 Windows 10 机器上;相同的代码在 Windows 7 上运行良好。
我通过将 CryptProtectDataCryptUnprotectData 中的 szDataDescr 参数的声明从 string 更改为 IntPtr,并在两个调用中传递 IntPtr.Zero 而不是 string.Empty 来解决了这个问题。

就此而言,pinvoke.net的P/Invoke签名使用了“CharSet.Auto”,但Win32函数定义使用了始终为Unicode的“LPWSTR”。 - yaakov

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接