Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,21 @@ The workaround is strictly opt-in and stays inert unless a PIN is available: wit
credential is left untouched so the normal Windows prompt path is used. It only activates for a
marshaled certificate credential; any other credential is passed through unchanged.

When the certificate records the legacy **Microsoft Base Smart Card Crypto Provider**, the credential is
repointed at the **Microsoft Smart Card Key Storage Provider** instead. A card minidriver is reachable
both through the Base CSP (CAPI) and through the KSP (CNG) under the same container name, but
[KB5066793](https://support.microsoft.com/help/5066793) stopped honouring the CAPI route for RSA smart
card keys. A certificate still associated with the legacy CSP therefore yields a credential LSASS cannot
service — the Kerberos certificate logon is declined locally, SPNEGO falls back to NTLM, and where NTLM
is disabled the failure surfaces as *"Authentication failed because NTLM authentication has been
disabled"* rather than as a smart card error. Naming the KSP reaches the same key over the path that
remains supported.

The provider name is rewritten in place, so the credential keeps the exact byte layout
`CredPackAuthenticationBufferW` produced; the two provider names are the same length, so nothing moves.
Certificates recording any other provider — including third-party CSPs, which do not imply a minidriver
— are left exactly as they were.

`KerbCertificateLogon` is independent of `PasswordContainsSCardPin`. The latter is the stock RDP
setting that tells the client the password field holds a smart card PIN, so that a smart card
credential is delegated to the remote and no prompt is shown; a connection manager doing an
Expand Down
23 changes: 21 additions & 2 deletions dll/MsRdpClient.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -575,7 +575,15 @@ class CMsRdpClient : public IMsRdpClient10
}

HRESULT __stdcall get_ExtendedDisconnectReason(ExtendedDisconnectReasonCode* pExtendedDisconnectReason) {
return m_pMsRdpClient->get_ExtendedDisconnectReason(pExtendedDisconnectReason);
HRESULT hr = m_pMsRdpClient->get_ExtendedDisconnectReason(pExtendedDisconnectReason);

// Why a session dropped is otherwise invisible in the log: the client goes quiet at the point of
// failure. The host asks for this itself, so reporting what it was told costs nothing. Logged even
// when the answer is exDiscReasonNoInfo, because "the host asked and got nothing" is also a datum.
if (SUCCEEDED(hr) && pExtendedDisconnectReason)
MsRdpEx_LogPrint(DEBUG, "CMsRdpClient::ExtendedDisconnectReason: %d", (int) *pExtendedDisconnectReason);

return hr;
}

HRESULT __stdcall put_FullScreen(VARIANT_BOOL pfFullScreen) {
Expand Down Expand Up @@ -639,7 +647,18 @@ class CMsRdpClient : public IMsRdpClient10
unsigned int ExtendedDisconnectReason,
BSTR* pBstrErrorMsg
) {
return m_pMsRdpClient5->raw_GetErrorDescription(disconnectReason, ExtendedDisconnectReason, pBstrErrorMsg);
HRESULT hr = m_pMsRdpClient5->raw_GetErrorDescription(disconnectReason, ExtendedDisconnectReason, pBstrErrorMsg);

// The host resolves the failure text here, which is the closest thing to a stated reason the client
// side ever produces. Both codes plus the resolved message, logged where they are already computed.
if (SUCCEEDED(hr) && pBstrErrorMsg && *pBstrErrorMsg) {
char* messageA = _com_util::ConvertBSTRToString(*pBstrErrorMsg);
MsRdpEx_LogPrint(WARN, "CMsRdpClient::GetErrorDescription(reason=%u extended=%u): %s",
disconnectReason, ExtendedDisconnectReason, messageA ? messageA : "");
delete[] messageA;
}

return hr;
}

HRESULT __stdcall get_RemoteProgram(struct ITSRemoteProgram** ppRemoteProgram) {
Expand Down
39 changes: 39 additions & 0 deletions dll/RdpSettings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include <MsRdpEx/Environment.h>
#include <MsRdpEx/NameResolver.h>
#include <MsRdpEx/Detours.h>
#include <MsRdpEx/Sspi.h>

#include <intrin.h>
#include <dpapi.h>
Expand Down Expand Up @@ -40,6 +41,16 @@ static HRESULT Hook_ITSPropertySet_SetBoolProperty(ITSPropertySet* This, const c

MsRdpEx_LogPrint(TRACE, "ITSPropertySet::SetBoolProperty(%s, %d)", propName, propValue);

// The single most diagnostic bit of a failed connection: the CredSSP handshake can complete with
// SEC_E_OK and still leave the server unauthenticated, which is what a rejected credential looks like
// from the client. Surface it above TRACE so it survives in a log captured at the default level.
if (MsRdpEx_StringIEquals(propName, "ServerAuthenticated")) {
if (propValue)
MsRdpEx_LogPrint(DEBUG, "Server authentication succeeded");
else
MsRdpEx_LogPrint(WARN, "Server authentication FAILED");
}

if (MsRdpEx_StringIEquals(propName, "UsingSavedCreds")) {
// Workaround for "Always prompt for password upon connection" GPO":
// The RDP ActiveX sets the "UsingSavedCreds" to true if the password is set.
Expand Down Expand Up @@ -152,10 +163,33 @@ static HRESULT Hook_ITSPropertySet_SetStringProperty(ITSPropertySet* This, const
return hr;
}

// The RDP core reads these from the connection's worker thread in the moments before it acquires a CredSSP
// credential. That read is the only in-band signal tying that thread to a specific connection, so use it to
// bind the two. Restricted to this short list to keep the property hooks cheap: they are very hot.
static void RdpSettings_BindCallingThreadToSession(ITSPropertySet* This, const char* propName)
{
CMsRdpExtendedSettings* settings = NULL;
GUID sessionId = { 0 };

if (!MsRdpEx_StringIEquals(propName, "UserName") &&
!MsRdpEx_StringIEquals(propName, "ServerNameUsedForAuthentication"))
return;

settings = MsRdpEx_FindExtendedSettingsByCoreProps(This);

if (!settings)
return;

settings->GetSessionIdGuid(&sessionId);
MsRdpEx_Sspi_BindCurrentThreadToSession(&sessionId);
}

static HRESULT Hook_ITSPropertySet_GetStringProperty(ITSPropertySet* This, const char* propName, WCHAR** propValue)
{
HRESULT hr;

RdpSettings_BindCallingThreadToSession(This, propName);

hr = Real_ITSPropertySet_GetStringProperty(This, propName, propValue);

if (SUCCEEDED(hr)) {
Expand Down Expand Up @@ -1878,6 +1912,11 @@ const char* CMsRdpExtendedSettings::GetSessionId()
return m_sessionIdStr;
}

void CMsRdpExtendedSettings::GetSessionIdGuid(GUID* pSessionId)
{
MsRdpEx_GuidCopy(pSessionId, &m_sessionId);
}

bool CMsRdpExtendedSettings::GetOutputMirrorEnabled()
{
return m_OutputMirrorEnabled;
Expand Down
Loading