是否可以从 VB .NET 安全地获取 SecureString 值?

Posted

技术标签:

【中文标题】是否可以从 VB .NET 安全地获取 SecureString 值?【英文标题】:Is it possible to safely get a SecureString value from VB .NET? 【发布时间】:2009-10-13 14:51:07 【问题描述】:

我一直觉得 SecureString 有点奇怪,但假设我的大部分问题是由于我不理解的安全问题造成的。今天我决定坐下来自学,但我遇到了一个看似致命的障碍。

我设想的场景是“用户在文本框中输入密码,该密码被散列并与存储的散列进行比较”。起初我担心文本框包含字符串,但后来我意识到您可以滚动使用 SecureString 作为其存储的自定义文本框。凉爽的。给我带来麻烦的是“密码经过哈希处理和比较......”部分。

我对 VB .NET 中的问题的第一次破解是天真和错误的:

Dim passwordHandle As IntPtr
Dim insecurePassword As String = Nothing
Try
    passwordHandle = Marshal.SecureStringToBSTR(_password)
    insecurePassword = Marshal.PtrToStringBSTR(passwordHandle)
Catch ex As Exception

Finally
    If passwordHandle <> IntPtr.Zero Then
        Marshal.ZeroFreeBSTR(passwordHandle)
    End If
End Try

If insecurePassword <> Nothing Then
    ' Do hash and comparison
End If

这只是将密码塞入常规字符串中,并首先破坏了使用 SecureString 的目的。所以我一直在搜索,发现a blog post 很好地解决了 C# 中的问题:将字符串制作成 BSTR,复制到固定字符串,然后 BSTR 和固定字符串在使用后都归零。这似乎是一个更好的主意,因为它最大限度地减少了不安全字符串在内存中的时间。但是,似乎没有办法在 VB .NET 中实现这一目标。 C# 正在使用其不安全的代码特性来进行指针操作,但 VB .NET 无法做到这一点。我看了一下 Marhsall.Copy(),但它看起来像是面向数组的。我曾想过尝试将对象的 IntPtr 变量和 BSTR 转换为字符串,但这仍然让我使用了像 String.Replace() 这样的方法,它将创建一个新字符串。

从 VB .NET 根本不可能做到这一点,还是我缺少什么?

编辑 我接受了 AMissico 的回答,只是略有保留。 Marshal.ReadByte() 方法将从非托管内存中复制一个字节并在非托管内存中创建一个字节。这使得攻击者找到密码的各个字符的机会很小。我认为这远低于找到整个字符串的几率,但是我引用的(显然已失效)文章中的 C# 能够使用不安全的代码巧妙地避免这种情况。思考的过程是它使用 GCHandle 将字符串固定在内存中,然后使用不安全的代码来解决 .NET 字符串的不变性。在 VB .NET 中似乎不可能的巧妙技巧。我将尝试返回 C# 代码本身。

【问题讨论】:

有什么理由必须用VB.NET编写吗?如果 C# 对所需内容有更好的支持,那么我会用 C# 编写解决方案。 原因是因为我想向最熟悉 VB .NET 的人展示它。尽管如此,这并不是一个答案。假设我有一个用 VB .NET 编写的 100,000 行庞大的企业应用程序;为 1 个 C# 文件滚动一个类库来支持这一点真的有意义吗?这是一个属于 .NET Framework 的类,对于相对常见的用例很重要;如果其中一种主要的 .NET 语言不能使用它,那就太重要了。 shrugs - 从你链接的文章看,问题似乎是 SecureString 比较新,一旦框架的其余部分更新了,就可以使用了无需将其转换为字符串,一切都会好起来的(在 VB 和 C# 中)。就个人而言,如果必须做某事,我会完成,即使这意味着使用不同语言的小型类库。 您是否继续这样做?我对你最终的 VB.NET 代码很感兴趣。阅读文章后,您将不得不使用 addressof/methods 而不是 Lamba 表达式,但这应该没什么大不了的。 【参考方案1】:

“将 SecureString 密码编组为字符串”的链接 - Mark Nicholson "http://dotnet.org.za/markn/archive/2008/10/04/handling-passwords.aspx 处终于显示出来了。

因此,您的声明“C# 正在使用其不安全的代码功能进行指针操作”似乎可以通过http://support.microsoft.com/kb/321695 的“如何:在 Visual Basic .NET 中将 UCOMIStream 包装到流类中”来解决。 (我搜索了“AddrOfPinnedObject”。)

我没有阅读您的全部问题(帖子链接总是超时),但是这些类和测试代码有用吗?密码永远不会以 System.String 的形式存在;因此,您需要一个 SecureStringTextBox 实现,如您的问题中所述。

我不喜欢添加所有这些代码。让我知道哪些代码有用,我将编辑答案以仅保留有用的内容。

Imports System.Security
Imports System.Security.Principal
Imports System.Security.Permissions
Imports System.Runtime.InteropServices

''' <summary>
''' Helper class to programmatically impersonate a user, load and unload a user's profile, and perform other maintenance-related tasks for impersonating a user.
''' </summary>
Public Class ImpersonationHelper
    Implements IDisposable

#Region " IDisposable Implementaton "

    Private _disposed As Boolean

    Protected Overrides Sub Finalize()
        Dispose(False)
        MyBase.Finalize()
    End Sub

    ''' <summary>
    ''' Implementation of the <b>IDisposable</b> interface.
    ''' </summary>
    ''' <remarks>This method calls <see>Undo</see> if impersonation is still being performed. This method calls the common language runtime version of the Dispose method.</remarks>
    Public Overloads Sub Dispose() Implements IDisposable.Dispose
        Dispose(True)
        System.GC.SuppressFinalize(Me)
    End Sub

    ''' <summary>
    ''' Implementation of the <b>IDisposable</b> interface.
    ''' </summary>
    ''' <param name="disposing">If <b>true</b>, the object to be disposed is finalized and collected by the garbage collector; otherwise, <b>false</b>.</param>
    ''' <remarks>This method calls Undo if impersonation is still being performed. This method calls the common language runtime version of the Dispose method.</remarks>
    Protected Overloads Sub Dispose(ByVal disposing As Boolean)
        If Not _disposed Then
            If disposing Then
                If Not IsNothing(_impersonationContext) Then
                    _impersonationContext.Undo()
                    _impersonationContext.Dispose()
                End If
            End If
            _impersonationContext = Nothing
        End If
        _disposed = True
    End Sub

#End Region

    '2009.02.12 AMJ
    '   Modified From:
    '       How to implement impersonation in an ASP.NET application (KB306158)
    '       http://support.microsoft.com/kb/306158
    '   Implemented IDisposable based on ImpersonationHelper class of
    '       Namespace: Microsoft.Office.Excel.Server.Addins.ComputeCluster.Security
    '       Assembly: Microsoft.Office.Excel.Server.Addins.ComputeCluster (in microsoft.office.excel.server.addins.computecluster.dll)

    Const LOGON32_LOGON_INTERACTIVE As Integer = 2
    Const LOGON32_LOGON_BATCH As Integer = 4
    Const LOGON32_LOGON_SERVICE As Integer = 5

    Const LOGON32_PROVIDER_DEFAULT As Integer = 0
    Const LOGON32_PROVIDER_WINNT35 As Integer = 1

    Private Enum SECURITY_IMPERSONATION_LEVEL
        SecurityAnonymous = 0
        SecurityIdentification = 1
        SecurityImpersonation = 2
        SecurityDelegation = 3
    End Enum

    Private Declare Auto Function LogonUser Lib "advapi32.dll" ( _
        ByVal username As String, _
        ByVal domain As String, _
        ByVal password As IntPtr, _
        ByVal logonType As Integer, _
        ByVal logonProvider As Integer, _
        ByRef token As IntPtr) As Boolean

    Private Declare Auto Function DuplicateToken Lib "advapi32.dll" ( _
        ByVal ExistingTokenHandle As IntPtr, _
        ByVal ImpersonationLevel As SECURITY_IMPERSONATION_LEVEL, _
        ByRef DuplicateTokenHandle As IntPtr) As Integer

    Private Declare Auto Function RevertToSelf Lib "advapi32.dll" () As Long
    Private Declare Auto Function CloseHandle Lib "kernel32.dll" (ByVal handle As IntPtr) As Long

    Dim _impersonationContext As WindowsImpersonationContext
    Dim _domain As String
    Dim _login As String
    Dim _password As SecureString

#Region " Standard Constructor & Properties "

    ''' <summary>
    ''' Initializes a new instance of the ImpersonationHelper class.
    ''' </summary>
    ''' <param name="domain">The domain or computer name of the user to impersonate.</param>
    ''' <param name="userName">The user name of the user to impersonate.</param>
    ''' <param name="password">The secure string password of UserName. For more information about secure strings, see the <see cref="System.Security.SecureString">SecureString</see> class.</param>
    <DebuggerNonUserCode()> _
    Public Sub New(ByVal domain As String, ByVal userName As String, ByVal password As SecureString)
        Me.Domain = domain
        Me.Login = userName
        Me.Password = password
    End Sub

    ''' <summary>
    ''' Do not allow a new instance of the ImpersonationHelper class without credentials.
    ''' </summary>
    Private Sub New()

    End Sub

    ''' <summary>
    ''' Gets or sets the domain of the user to impersonate.
    ''' </summary>
    ''' <value>The domain of the user.</value>
    <DebuggerNonUserCode()> _
    Public Property Domain() As String
        Get
            Return _domain
        End Get
        Set(ByVal value As String)
            _domain = value
        End Set
    End Property

    ''' <summary>
    ''' Gets or sets the user name of the user to impersonate.
    ''' </summary>
    ''' <value>The user name.</value>
    <DebuggerNonUserCode()> _
    Public Property Login() As String
        Get
            Return _login
        End Get
        Set(ByVal value As String)
            _login = value
        End Set
    End Property

    ''' <summary>
    ''' Sets the encrypted password of the user to impersonate. 
    ''' </summary>
    ''' <value>The encrypted password.</value>
    <DebuggerNonUserCode()> _
    Public WriteOnly Property Password() As SecureString
        Set(ByVal value As SecureString)
            _password = value
        End Set
    End Property

#End Region

    ''' <summary>
    ''' Performs the impersonation of the user based on the parameters provided in the constructor. 
    ''' </summary>
    ''' <remarks>
    ''' <para>If logon fails using the supplied credentials, an exception is thrown. The exception is thrown because this method is unable to duplicate the logged-on user's token for purposes of impersonation or is unable to create a Windows identity from the user's impersonated token.</para>
    ''' <para>For details about the direct cause of the impersonation failure, you can inspect the inner exception.</para>
    ''' </remarks>
    <PermissionSetAttribute(SecurityAction.Demand, Name:="FullTrust")> _
    Public Sub ImpersonateUser()

        Dim fResult As Boolean = False  'assume impersonation failed

        Dim hPassword As IntPtr = IntPtr.Zero
        Dim hToken As IntPtr = IntPtr.Zero
        Dim hTokenDuplicate As IntPtr = IntPtr.Zero
        Dim oException As ImpersonationException = Nothing

        If RevertToSelf <> 0 Then

            hPassword = Marshal.SecureStringToGlobalAllocUnicode(_password)

            If LogonUser(Me.Login, Me.Domain, hPassword, LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT, hToken) Then
                If DuplicateToken(hToken, SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation, hTokenDuplicate) <> 0 Then
                    _impersonationContext = New WindowsIdentity(hTokenDuplicate).Impersonate()
                    If Not _impersonationContext Is Nothing Then
                        fResult = True
                    End If
                End If
            Else
                oException = New ImpersonationException(Me.Login, Me.Domain)
            End If

            If hPassword.Equals(IntPtr.Zero) = False Then
                Marshal.ZeroFreeGlobalAllocUnicode(hPassword)
            End If

        End If

        If Not hTokenDuplicate.Equals(IntPtr.Zero) Then
            CloseHandle(hTokenDuplicate)
        End If

        If Not hToken.Equals(IntPtr.Zero) Then
            CloseHandle(hToken)
        End If

        If Not (oException Is Nothing) Then
            Throw oException
        End If

    End Sub

    ''' <summary>
    ''' Undoes the impersonation of the user, if it is impersonated.
    ''' </summary>
    ''' <remarks>Use this method to free the objects associated with impersonation.</remarks>
    <PermissionSetAttribute(SecurityAction.Demand, Name:="FullTrust")> _
    <DebuggerNonUserCode()> _
    Public Sub Undo()
        _impersonationContext.Undo()
        _impersonationContext = Nothing
    End Sub

    Public Shared Function InvokeAsUser(ByVal userName As String, ByVal domain As String, ByVal password As SecureString, ByVal methodToCall As [Delegate], ByVal ParamArray parameters() As Object) As Object
        Dim oResult As Object = Nothing

        Using oImpersonation As New ImpersonationHelper(domain, userName, password)

            oImpersonation.ImpersonateUser()

            oResult = methodToCall.DynamicInvoke(parameters)

        End Using

        Return oResult
    End Function

End Class

Public Class ImpersonationException
    Inherits System.Exception

    Public ReadOnly Login As String
    Public ReadOnly Domain As String

    Public Sub New(ByVal userName As String, ByVal domain As String)
        MyBase.New(String.Format("Impersonation failure: 1\0", userName, domain), New System.ComponentModel.Win32Exception)
    End Sub

End Class

Imports Missico.Personal

Imports System.Security

Imports Microsoft.VisualStudio.TestTools.UnitTesting

<TestClass()> _
Public Class ImpersonationHelperTest

    Private testContextInstance As TestContext

    Public Property TestContext() As TestContext
        Get
            Return testContextInstance
        End Get
        Set(ByVal value As TestContext)
            testContextInstance = value
        End Set
    End Property

    <TestMethod()> _
    Public Sub ImpersonationHelperTest()

        'testing only, never initialize the characters of the password in this fashion
        'replace with valid password

        Dim oPassword As New System.Security.SecureString

        oPassword.AppendChar("o"c)
        oPassword.AppendChar("o"c)
        oPassword.AppendChar("p"c)
        oPassword.AppendChar("s"c)
        oPassword.AppendChar("!"c)
        oPassword.AppendChar(" "c)
        oPassword.AppendChar("n"c)
        oPassword.AppendChar("o"c)
        oPassword.AppendChar(" "c)
        oPassword.AppendChar("p"c)
        oPassword.AppendChar("a"c)
        oPassword.AppendChar("s"c)
        oPassword.AppendChar("s"c)
        oPassword.AppendChar("w"c)
        oPassword.AppendChar("o"c)
        oPassword.AppendChar("r"c)
        oPassword.AppendChar("d"c)

        Using oImpersonation As New ImpersonationHelper("ANTHONY", "amissico", oPassword)

            oImpersonation.ImpersonateUser()

            '...

        End Using

        Try

            Using oImpersonation As New ImpersonationHelper("INVALID", "amissico", oPassword)

                oImpersonation.ImpersonateUser()

                '...

            End Using

        Catch ex As ImpersonationException
            'expected
            '   due to invalid domain
        End Try


        Try

            Using oImpersonation As New ImpersonationHelper("ANTHONY", "INVALID", oPassword)

                oImpersonation.ImpersonateUser()

                '...

            End Using

        Catch ex As ImpersonationException
            'expected
            '   due to invalid user

        End Try

        Try

            oPassword.AppendChar(" "c) 'invalidate password

            Using oImpersonation As New ImpersonationHelper("ANTHONY", "amissico", oPassword)

                oImpersonation.ImpersonateUser()

                '...

            End Using

        Catch ex As ImpersonationException
            'expected
            '   due to invalid password

        End Try


    End Sub

End Class

Imports System.Security
Imports System.Runtime.InteropServices
Imports System.Runtime.CompilerServices

Public Module SecureStringExtensions

    ''' <summary>
    ''' Determines whether the specified <see cref="System.Security.SecureString">System.Security.SecureString</see> instances are considered equal.
    ''' </summary>
    ''' <param name="valueA">The first <see cref="System.Security.SecureString">System.Security.SecureString</see> to compare.</param>
    ''' <param name="valueB">The second <see cref="System.Security.SecureString">System.Security.SecureString</see> to compare.</param>
    ''' <returns>True if valueA is equal to valueB; otherwise, False.</returns>
    <Extension()> _
    Public Function Equals(ByVal valueA As SecureString, ByVal valueB As SecureString) As Boolean
        Return IsEqual(valueA, valueB)
    End Function

    ''' <summary>
    ''' Determines whether the specified <see cref="System.Security.SecureString">System.Security.SecureString</see> instances are considered equal.
    ''' </summary>
    ''' <param name="valueA">The first <see cref="System.Security.SecureString">System.Security.SecureString</see> to compare.</param>
    ''' <param name="valueB">The second <see cref="System.Security.SecureString">System.Security.SecureString</see> to compare.</param>
    ''' <returns>True if valueA is equal to valueB; otherwise, False.</returns>
    ''' <remarks>Comparison loop based on Microsoft souce code for String.EqualsHelper method.</remarks>
    <Extension()> _
    Public Function IsEqual(ByVal valueA As SecureString, ByVal valueB As SecureString) As Boolean
        Dim fResult As Boolean = False  'assume failure

        'short-circuit if lengths are not the same

        If valueA.Length <> valueB.Length Then
            'cannot be the same value
            Return False
        End If

        Using oCopyA As SecureString = valueA.Copy, oCopyB As SecureString = valueB.Copy

            Dim iLength As Integer = oCopyA.Length

            Dim oPtrA As IntPtr = Marshal.SecureStringToBSTR(oCopyA)
            Dim oPtrB As IntPtr = Marshal.SecureStringToBSTR(oCopyB)

            Try

                Do While (iLength > 0)

                    If Marshal.ReadByte(oPtrA, iLength) <> Marshal.ReadByte(oPtrB, iLength) Then
                        Exit Do
                    End If

                    iLength -= 1

                Loop

                fResult = (iLength <= 0)

            Finally
                Marshal.ZeroFreeBSTR(oPtrA)
                Marshal.ZeroFreeBSTR(oPtrA)

            End Try

        End Using

        Return fResult
    End Function

End Module

Imports System.Security
Imports System.Diagnostics

Imports Microsoft.VisualStudio.TestTools.UnitTesting

Imports Missico.Security.SecureStringExtensions

<TestClass()> _
Public Class SecureStringExtensionsFixture

#Region " TestContext "

    Private testContextInstance As TestContext

    Public Property TestContext() As TestContext
        Get
            Return testContextInstance
        End Get
        Set(ByVal value As TestContext)
            testContextInstance = value
        End Set
    End Property

#End Region

    <TestMethod()> _
    Public Sub EqualsTest()

        Dim oValueA As New SecureString
        Dim oValueB As New SecureString

        oValueA.AppendChar("p"c)
        oValueA.AppendChar("a"c)
        oValueA.AppendChar("s"c)
        oValueA.AppendChar("s"c)
        oValueA.AppendChar("w"c)
        oValueA.AppendChar("o"c)
        oValueA.AppendChar("r"c)
        oValueA.AppendChar("d"c)

        oValueB.AppendChar("p"c)
        oValueB.AppendChar("a"c)
        oValueB.AppendChar("s"c)
        oValueB.AppendChar("s"c)
        oValueB.AppendChar("w"c)
        oValueB.AppendChar("o"c)
        oValueB.AppendChar("r"c)
        oValueB.AppendChar("d"c)


        'The Object.Equal method does not work because you cannot compare to secure strings.

        If oValueA.Equals(oValueB) Then
            'expected, but does not work
            'you cannot compare two secure strings
        Else
            'always fails
        End If


        'Using the fully-qualified path to the Equal extension method.

        If Missico.Security.SecureStringExtensions.Equals(oValueA, oValueB) Then
            'expected
        Else
            Assert.Fail("SecureString values are not equal, which is not expected.")
        End If


        'Using the IsEqual extension method that does not conflict with the Object.Equal method.

        If oValueA.IsEqual(oValueB) Then
            'expected
        Else
            Assert.Fail("SecureString values are not equal, which is not expected.")
        End If


        'change the second value

        oValueB.AppendChar(" "c)

        If oValueA.IsEqual(oValueB) Then
            Assert.Fail("SecureString values are equal, which is not expected.")
        Else
            'expected
        End If

    End Sub

End Class

【讨论】:

我不这么认为,但是您没有列出很多方法的代码。尽管如此,密码仍被复制到 _password 字符串中,该字符串受 GC 影响,因此会比需要的时间更长。我链接到的代码将托管字符串固定在内存中,因此它不会被移动,将密码复制到其中,完成后会将字符串的内存归零。在 GC 收集它之前,字符串仍然存在,但现在它被归零了。您的代码不会这样做,它只会将非托管字符串归零。 “将 SecureString 密码编组为字符串 - Mark Nicholson”的链接终于显示出来了。 很抱歉我没有早点回来;这是我忘记的事情。我无法再加载原始页面;很遗憾,因为我认为它还有一些额外的问题。就目前而言,您的代码看起来最接近我能找到的所有 C# 示例在 VB .NET 中的样子。我有一个担忧,我会在我的问题中进行编辑,但我认为在有人能提出更好的解决方案之前,你值得称赞。 @OwenP:由于显示问题,我保存了文章。你要我送吗? 那肯定很好;如果您访问我的网站owenpellegrin.com,我的电子邮件非常容易猜测甚至强烈暗示【参考方案2】:

我看到的所有示例都没有考虑到登录类型不是一刀切的解决方案这一事实。

例如,这仅在您模拟的用户有权登录目标系统时才有效。访问远程 SQL Server 框时并非总是如此。 LOGON32_LOGON_INTERACTIVE

NetworkClearText 是唯一一种始终适用于 SQL Server 连接的文本。 - 没有明文并不意味着它以不安全的方式传递凭据。

当您在工作组中并且您需要模拟域用户时,NewCredentials 是可行的。 (未使用 SQL Server 连接测试)

【讨论】:

欢迎来到 Stack Overflow!在发布多个问题的复制和粘贴样板/逐字答案时要小心,这些往往被社区标记为“垃圾邮件”。如果您这样做,则通常意味着问题是重复的,因此请标记它们。【参考方案3】:

只是对上面代码的更正:

一行

   Dim iLength As Integer = oCopyA.Length

不正确。正如所写的那样,函数 IsEqual 将仅测试字符串中的一半 + 1 个字符。

正确的行应该是:

   Dim iLength As Integer = oCopyA.Length*2-1

因为 BSTR 每个字符使用 2 个字节,并且索引像往常一样从零开始。长度给出了字符数,而不是字节数。

否则,它似乎工作。

cignaciob

问候

【讨论】:

以上是关于是否可以从 VB .NET 安全地获取 SecureString 值?的主要内容,如果未能解决你的问题,请参考以下文章

从 vb.net 中的表单外部获取突出显示的文本

从 VB.NET 中的注册表获取实际 REG_DWORD 十进制数的另一种方法?

VB.Net从任务管理器中隐藏进程[重复]

如何在 VB.net 上从数据库中获取数据到文本框

如何从 VB.NET 中的 USB 端口获取数据

我们可以隐藏 VB.NET 的第一个选择列吗?