【问题标题】:Remote Desktop Connection with Custom Credential Provider使用自定义凭据提供程序进行远程桌面连接
【发布时间】:2019-10-12 12:03:52
【问题描述】:

我使用来自 VistaCredentialProviderSamples 的 SampleWrapExistingCredentialProvider 开发了一个自定义凭据提供程序。凭据提供程序实现了一个过滤器,用于过滤所有其他凭据提供程序,我在登录时只看到我的凭据提供程序。问题是,如果我们使用远程桌面连接连接到它,用户名/密码不会从 Windows RDP 客户端传递到凭据提供程序,当 RDP 会话打开时我必须再次输入它(与默认提供程序的行为不同)

我正在尝试探索代码的哪一部分处理凭据提供程序从远程桌面客户端接受用户名/密码并且不再询问的情况。附件是在 RDP 客户端上提供成功凭据后我的凭据提供程序的屏幕截图。单击凭据提供程序的此图标后,将显示凭据提供程序磁贴,该磁贴再次询问用户名和密码。任何有关如何从 RDP 客户端接收凭据的帮助将不胜感激。

我已为 CREDUI 返回 S_OK。我的 SetUsageScenario 如下:

HRESULT CSampleProvider::SetUsageScenario(
CREDENTIAL_PROVIDER_USAGE_SCENARIO cpus,
DWORD dwFlags
)
{
    HRESULT hr;

// Create the password credential provider and query its interface for an
// ICredentialProvider we can use. Once it's up and running, ask it about the 
// usage scenario being provided.
IUnknown *pUnknown = NULL;
hr = ::CoCreateInstance(CLSID_PasswordCredentialProvider, NULL, CLSCTX_ALL, IID_PPV_ARGS(&pUnknown));
if (SUCCEEDED(hr))
{
    hr = pUnknown->QueryInterface(IID_PPV_ARGS(&(_pWrappedProvider)));
    if (SUCCEEDED(hr))
    {
        hr = _pWrappedProvider->SetUsageScenario(cpus, dwFlags);
        switch (cpus)
        {
        case CPUS_LOGON:
        case CPUS_UNLOCK_WORKSTATION:
        case CPUS_CREDUI:
        {
            hr = S_OK;
            break;
        }
        case CPUS_CHANGE_PASSWORD:
        default:
            hr = E_INVALIDARG;
            break;
        }
    }
}
if (FAILED(hr))
{
    if (_pWrappedProvider != NULL)
    {
        _pWrappedProvider->Release();
        _pWrappedProvider = NULL;
    }
}

return hr;
}

【问题讨论】:

  • 在 RDP 客户端上,凭据提供程序反复询问用户名和密码,即使您已成功输入?
  • @StriveSun-MSFT 不,它不会在 RDP 客户端上重复询问。 RDP 客户端成功验证并打开会话,但在会话内部,我留在登录屏幕,让 Windows 再次输入凭据
  • 正如我在回答中所说,如果用户连接的是非 Microsoft 凭据提供程序,那么终端服务器上会提示您再次输入凭据(两次)。
  • @StriveSun-MSFT 是的,我读过它,但我有些困惑,我记得我曾在一个组织中看到过一个 OTP CP,其中 RDP 会话仅在验证来自 RDP 客户端的凭据后才要求 OTP 并且确实不再要求用户名和密码。如果用户输入正确的 OTP,则用户已登录。他们为什么会这样做?
  • 对于 RDP,您根本不需要实现 CREDUI。但您需要正确实施 UpdateRemoteCredentialSetSerialization 方法。很明显,您在其中一个(或两个)实现中犯了错误。 * 过滤器实现过滤所有其他凭据提供程序* - 您需要将 UpdateRemoteCredential 中的 pcpcsOut->clsidCredentialProvider 更改为 your clsid 提供程序 - 这是关键点,否则将不会调用 SetSerialization (因为你禁用了原来的 clsid)并且在 SetSerialization 需要恢复原来的 clsidCredentialProvider 之前将它传递给原来的

标签: c++ windows winapi remote-desktop credential-providers


【解决方案1】:

用户名/密码不会从 Windows RDP 客户端传递到 凭据提供程序,我必须在 RDP 会话时再次输入 打开(与默认提供程序的行为不同)

windows 无法通过某种魔术知道客户端用户名/密码,通过 rdp 连接。

在客户端开始时,某些凭据提供程序必须创建CREDENTIAL_PROVIDER_CREDENTIAL_SERIALIZATION 并将其传递给服务器。在里面clsidCredentialProvider 说哪个具体的提供者收集了这个序列化。

服务器必须用这个CREDENTIAL_PROVIDER_CREDENTIAL_SERIALIZATION 做什么?显然将其传递给某些凭据提供程序SetSerialization 方法。但为了哪个?对所有人 ?不。仅对于 clsid 与 CREDENTIAL_PROVIDER_CREDENTIAL_SERIALIZATION 中的 clsidCredentialProvider 完全匹配的提供者再次显而易见。此提供程序(如果存在且未过滤)必须记住此凭据,然后在调用 GetCredentialCount 时 - 说它具有默认凭据(不是 CREDENTIAL_PROVIDER_NO_DEFAULT),并且它通常准备好使用它进行自动登录尝试。

从客户端 (mstsc) 密码提供程序创建序列化。 clsidCredentialProvider 中的 __uuidof(PasswordCredentialProvider)__uuidof(V1PasswordCredentialProvider)(如果客户端在 win7 上运行)也是如此。

但您在自过滤器中禁用了此提供程序。结果你自己打破了进程。

过滤器必须实现UpdateRemoteCredential 方法。这里复制和更新通过了pcpcsIn。其中最重要的部分 - 我们必须 替换 clsidCredentialProvider 为自己的 CLSID。结果我们的SetSerialization 方法将被调用。在这里,我们需要恢复原始CLSID,然后再将其传递给包装的凭据。

同样重要的地方 - 在GetCredentialCount 内部 - 首先将其传递给包装的凭据,然后执行*pbAutoLogonWithDefault = FALSE; - 禁用自动登录 - 如果您需要来自客户端的其他(OTP?)信息,则不能执行此操作(自动登录)。

UpdateRemoteCredential 方法中我们不能修改 pcpcsIn - 如果声明为 const。所以我们需要将更新凭证写入pcpcsOut。因为系统无法知道rgbSerialization 需要哪个大小 - 我们需要自己分配它。然后系统释放它。明显需要使用CoTaskMemAlloc 分配rgbSerialization

so - 所有这些都可以理解,无需任何文档。但是,如果所有这些都记录在案 - 也不错。

UpdateRemoteCredential 的代码:

HRESULT STDMETHODCALLTYPE CSampleProvider::UpdateRemoteCredential( 
    /* [annotation][in] */ 
    _In_  const CREDENTIAL_PROVIDER_CREDENTIAL_SERIALIZATION *pcpcsIn,
    /* [annotation][out] */ 
    _Out_  CREDENTIAL_PROVIDER_CREDENTIAL_SERIALIZATION *pcpcsOut)
{

    if (pcpcsIn->clsidCredentialProvider != __uuidof(PasswordCredentialProvider) && 
        pcpcsIn->clsidCredentialProvider != __uuidof(V1PasswordCredentialProvider))
    {
        // we dont know format of serialization
        return E_UNEXPECTED;
    }

    ULONG cbSerialization = pcpcsIn->cbSerialization;

    if (pcpcsOut->rgbSerialization = (PBYTE)CoTaskMemAlloc(cbSerialization + sizeof(GUID)))
    {
        memcpy(pcpcsOut->rgbSerialization, pcpcsIn->rgbSerialization, cbSerialization);
        memcpy(pcpcsOut->rgbSerialization + cbSerialization, &pcpcsIn->clsidCredentialProvider, sizeof(GUID));

        pcpcsOut->cbSerialization = cbSerialization + sizeof(GUID);
        pcpcsOut->ulAuthenticationPackage = pcpcsIn->ulAuthenticationPackage;
        pcpcsOut->clsidCredentialProvider = __uuidof(CSampleProvider);

        return S_OK;
    }

    return E_OUTOFMEMORY;
}

如果我们不知道clsidCredentialProvider - 只需返回E_UNEXPECTED

否则分配更多(在 sizeof(CLSID) 上)内存并最终保存原始 clsidCredentialProvider

现在SetSerialization:

HRESULT STDMETHODCALLTYPE CSampleProvider::SetSerialization(
    __in const CREDENTIAL_PROVIDER_CREDENTIAL_SERIALIZATION* pcpcs
    )
{   
    if (pcpcs->clsidCredentialProvider == __uuidof(CSampleProvider))
    {
        // can not query WTSIsRemoteSession, small optimization
        _IsRemoteSession = true;

        // we got this via ICredentialProviderFilter::UpdateRemoteCredential
        ULONG cbSerialization = pcpcs->cbSerialization;

        if (cbSerialization >= sizeof(GUID))
        {
            // restore original clsidCredentialProvider
            cbSerialization -= sizeof(GUID);
            memcpy(const_cast<GUID*>(&pcpcs->clsidCredentialProvider), pcpcs->rgbSerialization + cbSerialization, sizeof(GUID));
            const_cast<CREDENTIAL_PROVIDER_CREDENTIAL_SERIALIZATION*>(pcpcs)->cbSerialization = cbSerialization;
        }
    }

    return _pWrappedProvider->SetSerialization(pcpcs);
}

恢复原始clsidCredentialProvider 并修复cbSerialization。还因为 pcpcs-&gt;clsidCredentialProvider == __uuidof(CSampleProvider) 在我的情况下只能在 UpdateRemoteCredential 内设置(我不会在客户端为 RDP 执行 CPUS_CREDUI,仅用于“以管理员身份运行”) - 我只知道这是远程连接并保存此信息(_IsRemoteSession = true;) 不打电话给WTSIsRemoteSession

终于GetCredentialCount:

HRESULT STDMETHODCALLTYPE CSampleProvider::GetCredentialCount(
    __out DWORD* pdwCount,
    __out_range(<,*pdwCount) DWORD* pdwDefault,
    __out BOOL* pbAutoLogonWithDefault
    )
{
    HRESULT hr = _pWrappedProvider->GetCredentialCount(pdwCount, pdwDefault, pbAutoLogonWithDefault);

    *pbAutoLogonWithDefault = FALSE;//!!!

    return hr;
}

注意很重要的*pbAutoLogonWithDefault = FALSE;//!!!线

【讨论】:

  • 嗨。 _pWrappedProvider 在哪里? GitHub 中有代码吗?
  • @VOLVO - 嗨。想想不难理解,在具体代码中 ICredentialProvider* _pWrappedProvider;CSampleProvider 类的成员
  • 如果我们不仅为密码而且为多个提供者实现包装器,代码可以是下一个 - pastebin.com/P8sNLCXc
【解决方案2】:

根据官方文档https:RDC and Custom Credential Providers

如果用户与非 Microsoft 凭据提供程序连接,则 终端服务器将提示您再次输入凭据 (两次)。如果未启用 NLA,则尽管使用 连接之前客户端上不受支持的凭据提供程序, 用户仍将连接。您将在登录时留下 屏幕,您可以在其中使用任何受支持的凭据提供程序 用于本地身份验证。没有办法避免两者 使用不受支持的凭据提供程序时的身份验证。

话虽如此,如果您有自己的凭据提供程序并且您 尝试做一个远程桌面连接到一个 Vista 盒子(有这个 凭据提供程序),那么您将需要登录两次。这是一 预期的行为,这是设计使然,没有合法的方法 避免它。

【讨论】:

  • 嗯,对此感到困惑,因为我看到一些凭据提供程序实现显示登录屏幕已填满凭据(从 RDP 客户端传输到 Windows 登录字段)
  • 这不是真的,我们需要 两次 输入凭据。如果我们在 mstsc 中正确输入凭据 - 我们不需要在 rdp 屏幕中重新输入它。如果 MFA/OTP 提供商 - 设计用户还需要输入有效的 OTP 代码等,但不必再次输入登录名/密码
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-04-17
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多