如何使用提供的凭据调用SSPI的“AcquireCredentialsHandle”?

时间:2017-07-24 22:11:19

标签: c# windows authentication winapi sspi

背景

Windows SSPI API是Windows安全服务的接口,允许您相互验证客户端和服务器。该API的主要用途之一是提供Windows集成身份验证(即单点登录) - 应用程序能够通过使用用户登录时的凭据自动向用户验证服务器。

此过程的正常流程:

  • 用户通常使用用户名和密码登录计算机。
  • 用户运行使用SSPI对服务的用户进行身份验证的应用程序。
  • 该应用程序调用{​​{3}}来获取用户登录时创建的现有凭据的句柄。
  • 应用程序使用AcquireCredentialsHandle()返回的句柄使用AcquireCredentialsHandle() API参与服务器的SSPI身份验证周期。
  • 应用程序和服务器使用InitializeSecurityContext()(客户端)和InitializeSecurityContext()(服务器端)API交换不透明的字节数组('tokens')以验证彼此的令牌并继续进行身份验证周期。
  • 当有足够的令牌来回传递时,身份验证周期就会完成。
  • 应用程序已代表用户对服务器进行了身份验证。身份验证已完成。

当使用API​​作为单点登录的一部分时,这是此过程的正常流程。

AcquireCredentialsHandle()的签名是:

SECURITY_STATUS SEC_Entry AcquireCredentialsHandle(
  _In_  SEC_CHAR       *pszPrincipal,
  _In_  SEC_CHAR       *pszPackage,
  _In_  ULONG          fCredentialUse,
  _In_  PLUID          pvLogonID,
  _In_  PVOID          pAuthData,
  _In_  SEC_GET_KEY_FN pGetKeyFn,
  _In_  PVOID          pvGetKeyArgument,
  _Out_ PCredHandle    phCredential,
  _Out_ PTimeStamp     ptsExpiry
);

如上所述,在典型的Windows集成身份验证案例中使用时,通常不提供pAuthData参数 - 而是提供空引用。

问题

我希望能够以我直接提供用户名和密码的方式使用AcquireCredentialsHandle()。它似乎可以解决这个问题,因为pAuthData参数(上面的空值)可以是对AcceptSecurityContext()结构的引用,它允许您直接指定用户名和密码。

我尝试以这种方式调用AcquireCredentialsHandle(),并使用我的用户名和密码为其填充SEC_WINNT_AUTH_IDENTITY结构。但是,每次调用它时,即使我使用虚构的用户名或密码,我也会获得成功。作为一个完整性检查,我尝试使用相同的凭据调用LogonUser(),他们要么按预期工作要么失败,这取决于我是否给它一个有效的用户名/密码组合。

我做错了什么? 为什么即使绝对不正确的凭据,AcquireCredentialsHandle()总是会返回成功?

下面显示了我用来调用AcquireCredentialsHandle()的代码的基础知识:

public class SuppliedCredential : Credential
{
    public SuppliedCredential( string securityPackage, CredentialUse use, string username, string domain, string password ) :
        base( securityPackage )
    { 
        Init( use, username, domain, password );
    }

    private void Init( CredentialUse use, string username, string domain, string password )
    {
        // Copy off for the call, since this.SecurityPackage is a property.
        string packageName = this.SecurityPackage;

        TimeStamp rawExpiry = new TimeStamp();

        SecurityStatus status = SecurityStatus.InternalError;

        AuthIdentity auth = new AuthIdentity();

        auth.User = username;
        auth.UserLength = username.Length;
        auth.Domain = domain;
        auth.DomainLength = domain.Length;
        auth.Password = password;
        auth.PasswordLength = password.Length;
        auth.Flags = 0x2; // unicode

        this.Handle = new SafeCredentialHandle();

        RuntimeHelpers.PrepareConstrainedRegions();
        try { }
        finally
        {
            status = CredentialNativeMethods.AcquireCredentialsHandle_2(
               "",
               packageName,
               use,
               IntPtr.Zero,
               ref auth,
               IntPtr.Zero,
               IntPtr.Zero,
               ref this.Handle.rawHandle,
               ref rawExpiry
           );
        }

        if( status != SecurityStatus.OK )
        {
            throw new SSPIException( "Failed to call AcquireCredentialHandle", status );
        }

        this.Expiry = rawExpiry.ToDateTime();
    }
}

...

[StructLayout( LayoutKind.Sequential )]
public struct AuthIdentity
{
    [MarshalAs(UnmanagedType.LPWStr)]
    public string User;
    public int UserLength;
    [MarshalAs(UnmanagedType.LPWStr)]
    public string Domain;
    public int DomainLength;
    [MarshalAs(UnmanagedType.LPWStr)]
    public string Password;
    public int PasswordLength;
    public int Flags;
};

...

[ReliabilityContract( Consistency.WillNotCorruptState, Cer.MayFail )]
[DllImport( "Secur32.dll", EntryPoint = "AcquireCredentialsHandle", CharSet = CharSet.Unicode )]
internal static extern SecurityStatus AcquireCredentialsHandle_2(
    string principleName,
    string packageName,
    CredentialUse credentialUse,
    IntPtr loginId,
    ref AuthIdentity packageData,
    IntPtr getKeyFunc,
    IntPtr getKeyData,
    ref RawSspiHandle credentialHandle,
    ref TimeStamp expiry
);

1 个答案:

答案 0 :(得分:1)

简而言之

即使凭据是伪造的,

AcquireCredentialsHandle()也会返回true;仅当客户端尝试调用InitializeSecurityContext()时,API才会验证用户名和密码。 AcquireCredentialsHandle()只执行参数验证(指针值有效,结构填写正确,参数彼此有意义等)。由于我正确提供了错误的参数,AcquireCredentialsHandle()并不关心。

...

总而言之,客户端应用程序参与SSPI身份验证的正常周期如下:

  • 客户端调用某种形式的AcquireCredentialsHandle()
  • 客户端调用InitializeSecurityContext(),它将一些令牌返回给服务器。
  • 服务器获取收到的令牌并随之调用AcceptSecurityContext(),返回令牌以发送回客户端。
  • 客户端收到令牌并调用AcquireCredentialsHandle()
  • ...循环继续,直到两端的验证周期完成。

在上述情况下,通过使用提供的AcquireCredentialsHandle()结构调用SEC_WINNT_AUTH_IDENTITY获取的凭据(依次填写有效的用户名,域名和密码)未在客户端验证一直到客户端第一次调用InitializeSecurityContext(),然后才将其第一个令牌发送到服务器。

回答类似的问题,Dave Christiansen(微软员工?)posted the following在新闻组&microsoft.public.platformsdk.security' 2003年9月19日:

  

您如何确定凭据是本地凭据   用户? SSPI有时候很棘手。你正在使用什么包   (如果您正在使用,听起来像NTLM,Kerberos或Negotiate   SEC_WINNT_AUTH_IDENTITY)?

     

请注意,即使是AcquireCredentialsHandle也会成功   给出的凭据不正确(例如密码错误),因为它   在你调用InitializeSecurityContext之前,它们实际上并没有使用它们。