From a71532f2778812b1c5103b0de0dce12df04f9a08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Moreau?= Date: Tue, 30 Jun 2026 08:32:20 -0400 Subject: [PATCH] Add opt-in KERB_CERTIFICATE_LOGON SSPI rewrite for smart card logon Smart card logon via RDP fails when a certificate thumbprint and PIN are both pre-supplied: the CredSSP credential reaching LSASS is not a KERB_CERTIFICATE_LOGON, so tspkg calls CryptAcquireCertificatePrivateKey without CRYPT_ACQUIRE_ALLOW_NCRYPT_KEY_FLAG and fails for CNG-backed keys. Implement the client-side workaround by extending the existing SSPI API hooking instead of patching LSASS. The AcquireCredentialsHandleW detour now, strictly per session and only when KerbCertificateLogon:i:1 is set, rewrites a marshaled smart card certificate credential into the equivalent CredsspCertificateCreds / KERB_CERTIFICATE_LOGON packed buffer before it is handed to LSASS. CREDSSP_CRED_EX wrapping is preserved when present. Per-session isolation reuses the existing instance/session machinery: a new thread-local SSPI session scope is bracketed around CMsRdpClient::raw_Connect() (with a guarded, non-ambiguous global fallback) so the hook can resolve the owning CMsRdpExtendedSettings without affecting other concurrent sessions. Safety: rewrite is opt-in only, never triggered automatically by PasswordContainsSCardPin; unknown credential shapes pass through unchanged; no PINs, passwords, or certificate bytes are logged; PIN-bearing buffers are zeroed before free. Optional secret-free diagnostics are gated behind MSRDPEX_SSPI_SMARTCARD_DEBUG. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- README.md | 21 ++ dll/MsRdpClient.cpp | 3 + dll/RdpSettings.cpp | 48 +++ dll/Sspi.cpp | 660 +++++++++++++++++++++++++++++++++- include/MsRdpEx/RdpSettings.h | 4 + include/MsRdpEx/Sspi.h | 2 + 6 files changed, 735 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6ce7e41..418e3b6 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,27 @@ MsRdpEx processes additional .RDP file options that are not normally supported b | EnableHardwareMode:i:value | Disable DirectX client presenter (force GDI client presenter) | 0/1 | 1 | | ClearTextPassword:s:value | Target RDP server password - use for testing only | Insecure password | - | | GatewayPassword:s:value | RD Gateway server password - use for testing only | Insecure password | - | +| KerbCertificateLogon:i:value | Force smart card credentials into a KERB_CERTIFICATE_LOGON buffer (see below) | 0/1 | 0 | + +### Smart card certificate logon (KerbCertificateLogon) + +When a smart card certificate thumbprint and PIN are both pre-supplied (for example by a +connection manager that stores them and sets `PasswordContainsSCardPin:i:1`), the CredSSP/SSPI +credential that reaches LSAS can take a code path in `tspkg` that calls +`CryptAcquireCertificatePrivateKey` without `CRYPT_ACQUIRE_ALLOW_NCRYPT_KEY_FLAG`, which fails for +keys backed by a CNG/NCrypt key storage provider (the common case for smart cards). The interactive +Windows credential prompt avoids this because it builds a packed `KERB_CERTIFICATE_LOGON` structure. + +Setting `KerbCertificateLogon:i:1` opts the session in to a client-side workaround: MsRdpEx hooks +`AcquireCredentialsHandleW` and, only for the matching session, rewrites a smart card CredSSP +credential into the equivalent `KERB_CERTIFICATE_LOGON`-shaped buffer +(`CredsspCertificateCreds`) before it is handed to LSAS. The rewrite is strictly opt-in per session +and only activates when the existing credential is a marshaled certificate credential; in every +other case the original credential is passed through unchanged. No PINs, passwords, or certificate +bytes are ever logged, and PIN-bearing buffers are zeroed before they are freed. + +Set the `MSRDPEX_SSPI_SMARTCARD_DEBUG=1` environment variable to emit additional (secret-free) +CredSSP credential metadata to the log while diagnosing smart card logon issues. ## Extended RDP client logs diff --git a/dll/MsRdpClient.cpp b/dll/MsRdpClient.cpp index 1cd5f2a..3899ef1 100644 --- a/dll/MsRdpClient.cpp +++ b/dll/MsRdpClient.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include "TSObjects.h" @@ -502,7 +503,9 @@ class CMsRdpClient : public IMsRdpClient10 m_pMsRdpExtendedSettings->PrepareVideoRecorder(); m_pMsRdpExtendedSettings->PrepareExtraSystemMenu(); + MsRdpEx_Sspi_BeginSession(&m_sessionId); hr = m_pMsTscAx->raw_Connect(); + MsRdpEx_Sspi_EndSession(&m_sessionId); return hr; } diff --git a/dll/RdpSettings.cpp b/dll/RdpSettings.cpp index f0f3c1b..f7156b7 100644 --- a/dll/RdpSettings.cpp +++ b/dll/RdpSettings.cpp @@ -750,6 +750,38 @@ HRESULT __stdcall CMsRdpExtendedSettings::put_Property(BSTR bstrPropertyName, VA delete[] propValue; hr = S_OK; } + else if (MsRdpEx_StringEquals(propName, "KerbCertificateLogon") || + MsRdpEx_StringEquals(propName, "KerbCertificateLogonEnabled")) + { + if ((pValue->vt != VT_BOOL) && (pValue->vt != VT_I4) && (pValue->vt != VT_UI4)) + goto end; + + if (pValue->vt == VT_BOOL) + m_KerbCertificateLogonEnabled = pValue->boolVal ? true : false; + else if (pValue->vt == VT_I4) + m_KerbCertificateLogonEnabled = pValue->intVal ? true : false; + else + m_KerbCertificateLogonEnabled = pValue->uintVal ? true : false; + + hr = S_OK; + } + else if (MsRdpEx_StringEquals(propName, "PasswordContainsSCardPin")) + { + if ((pValue->vt != VT_BOOL) && (pValue->vt != VT_I4) && (pValue->vt != VT_UI4)) + goto end; + + if (pValue->vt == VT_BOOL) + m_PasswordContainsSCardPin = pValue->boolVal ? true : false; + else if (pValue->vt == VT_I4) + m_PasswordContainsSCardPin = pValue->intVal ? true : false; + else + m_PasswordContainsSCardPin = pValue->uintVal ? true : false; + + if (m_CoreProps) + hr = m_CoreProps->put_Property(bstrPropertyName, pValue); + else + hr = S_OK; + } else if (MsRdpEx_StringEquals(propName, "EnableMouseJiggler")) { if (pValue->vt != VT_BOOL) @@ -1349,8 +1381,14 @@ HRESULT CMsRdpExtendedSettings::ApplyRdpFile(void* rdpFilePtr) else if (MsRdpEx_RdpFileEntry_IsMatch(entry, 's', "ClearTextPassword")) { pMsRdpExtendedSettings->SetTargetPassword(entry->value); } + else if (MsRdpEx_RdpFileEntry_IsMatch(entry, 'i', "KerbCertificateLogon")) { + if (MsRdpEx_RdpFileEntry_GetVBoolValue(entry, &value)) { + m_KerbCertificateLogonEnabled = value.boolVal ? true : false; + } + } else if (MsRdpEx_RdpFileEntry_IsMatch(entry, 'i', "PasswordContainsSCardPin")) { if (MsRdpEx_RdpFileEntry_GetVBoolValue(entry, &value)) { + m_PasswordContainsSCardPin = value.boolVal ? true : false; bstr_t propName = _com_util::ConvertStringToBSTR(entry->name); pMsRdpExtendedSettings->put_CoreProperty(propName, &value); } @@ -1617,6 +1655,16 @@ char* CMsRdpExtendedSettings::GetKdcProxyName() return MsRdpEx_KdcProxyUrlToName(m_KdcProxyUrl); } +bool CMsRdpExtendedSettings::GetKerbCertificateLogonEnabled() +{ + return m_KerbCertificateLogonEnabled; +} + +bool CMsRdpExtendedSettings::GetPasswordContainsSCardPin() +{ + return m_PasswordContainsSCardPin; +} + bool CMsRdpExtendedSettings::GetMouseJigglerEnabled() { return m_MouseJigglerEnabled; diff --git a/dll/Sspi.cpp b/dll/Sspi.cpp index ace0383..ad3bb1d 100644 --- a/dll/Sspi.cpp +++ b/dll/Sspi.cpp @@ -4,10 +4,12 @@ #include #include +#include #include #include #include +#include #include @@ -18,6 +20,7 @@ static bool g_PcapEnabled = false; static char g_PcapFilePath[MSRDPEX_MAX_PATH] = { 0 }; static bool g_SspiDump = false; +static bool g_SspiSmartCardDebug = false; void MsRdpEx_SetPcapEnabled(bool pcapEnabled) { @@ -42,6 +45,9 @@ void MsRdpEx_PcapEnvInit() bool sspiDump = MsRdpEx_GetEnvBool("MSRDPEX_SSPI_DUMP", false); g_SspiDump = sspiDump; + bool sspiSmartCardDebug = MsRdpEx_GetEnvBool("MSRDPEX_SSPI_SMARTCARD_DEBUG", false); + g_SspiSmartCardDebug = sspiSmartCardDebug; + envvar = MsRdpEx_GetEnv("MSRDPEX_PCAP_FILE_PATH"); if (envvar) { @@ -111,6 +117,189 @@ static QUERY_CREDENTIALS_ATTRIBUTES_EX_FN_W Real_QueryCredentialsAttributesExW = static const char* MsRdpEx_GetSecurityStatusString(SECURITY_STATUS status); +typedef struct _MsRdpEx_SspiSessionContext +{ + bool active; + uint32_t depth; + GUID sessionId; +} MsRdpEx_SspiSessionContext; + +static INIT_ONCE g_SspiSessionTlsInitOnce = INIT_ONCE_STATIC_INIT; +static INIT_ONCE g_SspiSessionLockInitOnce = INIT_ONCE_STATIC_INIT; +static DWORD g_SspiSessionTlsIndex = TLS_OUT_OF_INDEXES; +static CRITICAL_SECTION g_SspiSessionLock; +static bool g_SspiGlobalSessionValid = false; +static bool g_SspiGlobalSessionAmbiguous = false; +static uint32_t g_SspiGlobalSessionDepth = 0; +static GUID g_SspiGlobalSessionId = { 0 }; + +static BOOL CALLBACK sspi_InitSessionTls(PINIT_ONCE, PVOID, PVOID*) +{ + g_SspiSessionTlsIndex = TlsAlloc(); + return (g_SspiSessionTlsIndex != TLS_OUT_OF_INDEXES) ? TRUE : FALSE; +} + +static BOOL CALLBACK sspi_InitSessionLock(PINIT_ONCE, PVOID, PVOID*) +{ + InitializeCriticalSection(&g_SspiSessionLock); + return TRUE; +} + +static bool sspi_EnsureSessionTls() +{ + return InitOnceExecuteOnce(&g_SspiSessionTlsInitOnce, sspi_InitSessionTls, NULL, NULL) ? true : false; +} + +static bool sspi_EnsureSessionLock() +{ + return InitOnceExecuteOnce(&g_SspiSessionLockInitOnce, sspi_InitSessionLock, NULL, NULL) ? true : false; +} + +static MsRdpEx_SspiSessionContext* sspi_GetThreadSessionContext(bool create) +{ + MsRdpEx_SspiSessionContext* ctx = NULL; + + if (!sspi_EnsureSessionTls()) + return NULL; + + ctx = (MsRdpEx_SspiSessionContext*) TlsGetValue(g_SspiSessionTlsIndex); + + if (!ctx && create) + { + ctx = (MsRdpEx_SspiSessionContext*) calloc(1, sizeof(MsRdpEx_SspiSessionContext)); + + if (ctx) + TlsSetValue(g_SspiSessionTlsIndex, ctx); + } + + return ctx; +} + +void MsRdpEx_Sspi_BeginSession(GUID* sessionId) +{ + MsRdpEx_SspiSessionContext* ctx = NULL; + + if (!sessionId) + return; + + ctx = sspi_GetThreadSessionContext(true); + + if (ctx) + { + if (!ctx->active || (ctx->depth == 0)) + { + MsRdpEx_GuidCopy(&ctx->sessionId, sessionId); + ctx->active = true; + ctx->depth = 1; + } + else + { + if (!MsRdpEx_GuidIsEqual(&ctx->sessionId, sessionId)) + MsRdpEx_LogPrint(WARN, "SSPI session scope changed while nested"); + + ctx->depth++; + } + } + + if (sspi_EnsureSessionLock()) + { + EnterCriticalSection(&g_SspiSessionLock); + + if (g_SspiGlobalSessionDepth == 0) + { + MsRdpEx_GuidCopy(&g_SspiGlobalSessionId, sessionId); + g_SspiGlobalSessionValid = true; + g_SspiGlobalSessionAmbiguous = false; + } + else if (!MsRdpEx_GuidIsEqual(&g_SspiGlobalSessionId, sessionId)) + { + g_SspiGlobalSessionAmbiguous = true; + } + + g_SspiGlobalSessionDepth++; + LeaveCriticalSection(&g_SspiSessionLock); + } +} + +void MsRdpEx_Sspi_EndSession(GUID* sessionId) +{ + MsRdpEx_SspiSessionContext* ctx = sspi_GetThreadSessionContext(false); + + if (ctx && ctx->active && (ctx->depth > 0)) + { + if (sessionId && !MsRdpEx_GuidIsEqual(&ctx->sessionId, sessionId)) + MsRdpEx_LogPrint(WARN, "SSPI session scope ended with a different session id"); + + ctx->depth--; + + if (ctx->depth == 0) + { + ctx->active = false; + MsRdpEx_GuidSetNil(&ctx->sessionId); + } + } + + if (sspi_EnsureSessionLock()) + { + EnterCriticalSection(&g_SspiSessionLock); + + if (g_SspiGlobalSessionDepth > 0) + g_SspiGlobalSessionDepth--; + + if (g_SspiGlobalSessionDepth == 0) + { + g_SspiGlobalSessionValid = false; + g_SspiGlobalSessionAmbiguous = false; + MsRdpEx_GuidSetNil(&g_SspiGlobalSessionId); + } + + LeaveCriticalSection(&g_SspiSessionLock); + } +} + +static bool sspi_GetCurrentSessionId(GUID* sessionId, bool* usedGlobalFallback) +{ + bool found = false; + MsRdpEx_SspiSessionContext* ctx = sspi_GetThreadSessionContext(false); + + if (usedGlobalFallback) + *usedGlobalFallback = false; + + if (ctx && ctx->active && (ctx->depth > 0)) + { + MsRdpEx_GuidCopy(sessionId, &ctx->sessionId); + return true; + } + + if (!sspi_EnsureSessionLock()) + return false; + + EnterCriticalSection(&g_SspiSessionLock); + + if (g_SspiGlobalSessionValid && !g_SspiGlobalSessionAmbiguous) + { + MsRdpEx_GuidCopy(sessionId, &g_SspiGlobalSessionId); + found = true; + + if (usedGlobalFallback) + *usedGlobalFallback = true; + } + + LeaveCriticalSection(&g_SspiSessionLock); + + return found; +} + +static CMsRdpExtendedSettings* sspi_GetCurrentExtendedSettings(bool* usedGlobalFallback) +{ + GUID sessionId = { 0 }; + + if (!sspi_GetCurrentSessionId(&sessionId, usedGlobalFallback)) + return NULL; + + return MsRdpEx_FindExtendedSettingsBySessionId(&sessionId); +} + static SECURITY_STATUS SEC_ENTRY sspi_EnumerateSecurityPackagesW(ULONG* pcPackages, PSecPkgInfoW* ppPackageInfo) { @@ -250,6 +439,446 @@ static bool sspi_DumpCredSspAuthData(void* pAuthData) return true; } +static const char* sspi_GetCredSspTypeName(CREDSPP_SUBMIT_TYPE type) +{ + switch (type) + { + case CredsspPasswordCreds: + return "CredsspPasswordCreds"; + case CredsspSchannelCreds: + return "CredsspSchannelCreds"; + case CredsspCertificateCreds: + return "CredsspCertificateCreds"; + case CredsspSubmitBufferBoth: + return "CredsspSubmitBufferBoth"; + case CredsspSubmitBufferBothOld: + return "CredsspSubmitBufferBothOld"; + case CredsspCredEx: + return "CredsspCredEx"; + default: + return "Unknown"; + } +} + +static const char* sspi_GetCallerModuleName(void* returnAddress, char* moduleName, size_t moduleNameSize) +{ + HMODULE hModule = NULL; + char modulePath[MSRDPEX_MAX_PATH] = { 0 }; + + if (!moduleName || (moduleNameSize < 1)) + return ""; + + moduleName[0] = '\0'; + + if (!returnAddress) + return moduleName; + + if (!GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | + GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, (LPCSTR)returnAddress, &hModule)) + { + return moduleName; + } + + if (!GetModuleFileNameA(hModule, modulePath, sizeof(modulePath))) + return moduleName; + + strncpy_s(moduleName, moduleNameSize, MsRdpEx_FileBase(modulePath), _TRUNCATE); + return moduleName; +} + +static bool sspi_TryGetCredSspCredential(void* pAuthData, CREDSSP_CRED** ppCred, + CREDSSP_CRED_EX** ppCredEx) +{ + CREDSSP_CRED* pCred = NULL; + CREDSSP_CRED_EX* pCredEx = NULL; + + if (ppCred) + *ppCred = NULL; + + if (ppCredEx) + *ppCredEx = NULL; + + if (!pAuthData || !MsRdpEx_CanReadUnsafePtr(pAuthData, sizeof(CREDSSP_CRED))) + return false; + + pCred = (CREDSSP_CRED*)pAuthData; + + if (pCred->Type == CredsspCredEx) + { + if (!MsRdpEx_CanReadUnsafePtr(pAuthData, sizeof(CREDSSP_CRED_EX))) + return false; + + pCredEx = (CREDSSP_CRED_EX*)pAuthData; + pCred = &pCredEx->Cred; + + if (ppCredEx) + *ppCredEx = pCredEx; + } + + if (ppCred) + *ppCred = pCred; + + return true; +} + +static DWORD sspi_ReadAuthDataMessageType(void* pAuthData, bool* pRead) +{ + DWORD messageType = 0; + + if (pRead) + *pRead = false; + + if (!pAuthData || !MsRdpEx_CanReadUnsafePtr(pAuthData, sizeof(DWORD))) + return 0; + + CopyMemory(&messageType, pAuthData, sizeof(DWORD)); + + if (pRead) + *pRead = true; + + return messageType; +} + +static DWORD sspi_GetLocalAllocSizeAsDword(void* ptr) +{ + SIZE_T size = 0; + + if (!ptr) + return 0; + + size = LocalSize((HLOCAL)ptr); + + if (size > MAXDWORD) + return 0; + + return (DWORD)size; +} + +static void sspi_LogCredSspAuthMetadata(const char* pszPackageA, void* pAuthData, void* returnAddress) +{ + CREDSSP_CRED* pCred = NULL; + CREDSSP_CRED_EX* pCredEx = NULL; + bool readMessageType = false; + char callerModule[MSRDPEX_MAX_PATH] = { 0 }; + + if (!g_SspiSmartCardDebug) + return; + + sspi_GetCallerModuleName(returnAddress, callerModule, sizeof(callerModule)); + + if (!sspi_TryGetCredSspCredential(pAuthData, &pCred, &pCredEx)) + { + MsRdpEx_LogPrint(DEBUG, "CredSSP auth metadata: package=%s authData=%p caller=%s unreadable", + pszPackageA ? pszPackageA : "", pAuthData, callerModule); + return; + } + + DWORD messageType = sspi_ReadAuthDataMessageType(pCred->pSpnegoCred, &readMessageType); + DWORD spnegoSize = sspi_GetLocalAllocSizeAsDword(pCred->pSpnegoCred); + + MsRdpEx_LogPrint(DEBUG, + "CredSSP auth metadata: package=%s credEx=%d type=%d(%s) spnego=%p localSize=%u firstDword=%s%u schannel=%p caller=%s", + pszPackageA ? pszPackageA : "", + pCredEx ? 1 : 0, + pCred->Type, + sspi_GetCredSspTypeName(pCred->Type), + pCred->pSpnegoCred, + spnegoSize, + readMessageType ? "" : "unreadable:", + readMessageType ? messageType : 0, + pCred->pSchannelCred, + callerModule); +} + +static void sspi_FreeUnpackedCredentials(WCHAR* userName, WCHAR* domainName, WCHAR* password) +{ + if (userName) + { + SecureZeroMemory(userName, wcslen(userName) * sizeof(WCHAR)); + free(userName); + } + + if (domainName) + { + SecureZeroMemory(domainName, wcslen(domainName) * sizeof(WCHAR)); + free(domainName); + } + + if (password) + { + SecureZeroMemory(password, wcslen(password) * sizeof(WCHAR)); + free(password); + } +} + +static bool sspi_UnpackAuthenticationBuffer(DWORD flags, void* pAuthBuffer, DWORD cbAuthBuffer, + WCHAR** ppUserName, WCHAR** ppDomainName, WCHAR** ppPassword) +{ + BOOL success; + DWORD error; + DWORD cchUserName = 0; + DWORD cchDomainName = 0; + DWORD cchPassword = 0; + WCHAR* userName = NULL; + WCHAR* domainName = NULL; + WCHAR* password = NULL; + + if (ppUserName) + *ppUserName = NULL; + + if (ppDomainName) + *ppDomainName = NULL; + + if (ppPassword) + *ppPassword = NULL; + + if (!pAuthBuffer || (cbAuthBuffer < sizeof(DWORD))) + return false; + + success = CredUnPackAuthenticationBufferW(flags, pAuthBuffer, cbAuthBuffer, + NULL, &cchUserName, NULL, &cchDomainName, NULL, &cchPassword); + + if (!success) + { + error = GetLastError(); + + if (error != ERROR_INSUFFICIENT_BUFFER) + return false; + } + + if ((cchUserName < 1) || (cchPassword < 1)) + return false; + + userName = (WCHAR*) calloc((size_t)cchUserName + 1, sizeof(WCHAR)); + domainName = (WCHAR*) calloc((size_t)((cchDomainName > 0) ? cchDomainName : 1) + 1, sizeof(WCHAR)); + password = (WCHAR*) calloc((size_t)cchPassword + 1, sizeof(WCHAR)); + + if (!userName || !domainName || !password) + goto fail; + + success = CredUnPackAuthenticationBufferW(flags, pAuthBuffer, cbAuthBuffer, + userName, &cchUserName, domainName, &cchDomainName, password, &cchPassword); + + if (!success) + goto fail; + + *ppUserName = userName; + *ppDomainName = domainName; + *ppPassword = password; + return true; + +fail: + sspi_FreeUnpackedCredentials(userName, domainName, password); + return false; +} + +static bool sspi_UnpackSmartCardPinCredential(void* pAuthBuffer, DWORD cbAuthBuffer, + WCHAR** ppUserName, WCHAR** ppDomainName, WCHAR** ppPassword) +{ + if (sspi_UnpackAuthenticationBuffer(0, pAuthBuffer, cbAuthBuffer, ppUserName, ppDomainName, ppPassword)) + return true; + + return sspi_UnpackAuthenticationBuffer(CRED_PACK_PROTECTED_CREDENTIALS, + pAuthBuffer, cbAuthBuffer, ppUserName, ppDomainName, ppPassword); +} + +typedef struct _SspiCertificateLogonRewrite +{ + bool active; + void* pAuthData; + CREDSSP_CRED cred; + CREDSSP_CRED_EX credEx; + HLOCAL packedCredentials; + DWORD cbPackedCredentials; +} SspiCertificateLogonRewrite; + +static void sspi_FreeCertificateLogonRewrite(SspiCertificateLogonRewrite* rewrite) +{ + if (!rewrite) + return; + + if (rewrite->packedCredentials) + { + SecureZeroMemory(rewrite->packedCredentials, rewrite->cbPackedCredentials); + LocalFree(rewrite->packedCredentials); + rewrite->packedCredentials = NULL; + } + + ZeroMemory(rewrite, sizeof(SspiCertificateLogonRewrite)); +} + +static bool sspi_IsCertificateMarshaledUserName(WCHAR* userName) +{ + bool result = false; + CRED_MARSHAL_TYPE credType; + PVOID pCredential = NULL; + + if (!userName || !CredIsMarshaledCredentialW(userName)) + return false; + + if (!CredUnmarshalCredentialW(userName, &credType, &pCredential)) + return false; + + result = (credType == CertCredential) ? true : false; + + if (pCredential) + CredFree(pCredential); + + return result; +} + +static bool sspi_CreatePackedCertificateLogon(WCHAR* marshaledCertificateUserName, + WCHAR* pin, HLOCAL* phPackedCredentials, DWORD* pcbPackedCredentials) +{ + BOOL success; + DWORD cbPackedCredentials = 0; + HLOCAL hPackedCredentials = NULL; + bool readMessageType = false; + DWORD messageType = 0; + + *phPackedCredentials = NULL; + *pcbPackedCredentials = 0; + + if (!marshaledCertificateUserName || !pin) + return false; + + success = CredPackAuthenticationBufferW(0, marshaledCertificateUserName, pin, + NULL, &cbPackedCredentials); + + if (!success && (GetLastError() != ERROR_INSUFFICIENT_BUFFER)) + { + MsRdpEx_LogPrint(WARN, "CredPackAuthenticationBufferW certificate probe failed: error=%u", GetLastError()); + return false; + } + + if (cbPackedCredentials < sizeof(DWORD)) + return false; + + hPackedCredentials = LocalAlloc(LMEM_FIXED | LMEM_ZEROINIT, cbPackedCredentials); + + if (!hPackedCredentials) + return false; + + success = CredPackAuthenticationBufferW(0, marshaledCertificateUserName, pin, + (PBYTE)hPackedCredentials, &cbPackedCredentials); + + if (!success) + { + MsRdpEx_LogPrint(WARN, "CredPackAuthenticationBufferW certificate pack failed: error=%u", GetLastError()); + LocalFree(hPackedCredentials); + return false; + } + + messageType = sspi_ReadAuthDataMessageType(hPackedCredentials, &readMessageType); + + if (!readMessageType || (messageType != KerbCertificateLogon)) + { + MsRdpEx_LogPrint(WARN, "CredPackAuthenticationBufferW did not produce KerbCertificateLogon: firstDword=%u", messageType); + SecureZeroMemory(hPackedCredentials, cbPackedCredentials); + LocalFree(hPackedCredentials); + return false; + } + + *phPackedCredentials = hPackedCredentials; + *pcbPackedCredentials = cbPackedCredentials; + return true; +} + +static bool sspi_TryBuildCertificateLogonRewrite(void* pAuthData, + CMsRdpExtendedSettings* extendedSettings, SspiCertificateLogonRewrite* rewrite) +{ + CREDSSP_CRED* pCred = NULL; + CREDSSP_CRED_EX* pCredEx = NULL; + DWORD cbSpnegoCred = 0; + bool readMessageType = false; + DWORD messageType = 0; + WCHAR* userName = NULL; + WCHAR* domainName = NULL; + WCHAR* password = NULL; + HLOCAL hPackedCredentials = NULL; + DWORD cbPackedCredentials = 0; + + ZeroMemory(rewrite, sizeof(SspiCertificateLogonRewrite)); + + if (!extendedSettings || !extendedSettings->GetKerbCertificateLogonEnabled()) + return false; + + if (!sspi_TryGetCredSspCredential(pAuthData, &pCred, &pCredEx)) + return false; + + if (!pCred->pSpnegoCred) + return false; + + messageType = sspi_ReadAuthDataMessageType(pCred->pSpnegoCred, &readMessageType); + + if ((pCred->Type == CredsspCertificateCreds) && readMessageType && + (messageType == KerbCertificateLogon)) + { + if (g_SspiSmartCardDebug) + MsRdpEx_LogPrint(DEBUG, "CredSSP smart-card credential is already KerbCertificateLogon"); + + return false; + } + + cbSpnegoCred = sspi_GetLocalAllocSizeAsDword(pCred->pSpnegoCred); + + if (cbSpnegoCred < sizeof(DWORD)) + { + if (g_SspiSmartCardDebug) + MsRdpEx_LogPrint(WARN, "CredSSP smart-card rewrite skipped: pSpnegoCred has no LocalAlloc size"); + + return false; + } + + if (!sspi_UnpackSmartCardPinCredential(pCred->pSpnegoCred, cbSpnegoCred, + &userName, &domainName, &password)) + { + if (g_SspiSmartCardDebug) + MsRdpEx_LogPrint(WARN, "CredSSP smart-card rewrite skipped: cannot unpack authentication buffer"); + + return false; + } + + if (!sspi_IsCertificateMarshaledUserName(userName)) + { + if (g_SspiSmartCardDebug) + MsRdpEx_LogPrint(WARN, "CredSSP smart-card rewrite skipped: username is not a marshaled certificate credential"); + + sspi_FreeUnpackedCredentials(userName, domainName, password); + return false; + } + + if (!sspi_CreatePackedCertificateLogon(userName, password, &hPackedCredentials, &cbPackedCredentials)) + { + sspi_FreeUnpackedCredentials(userName, domainName, password); + return false; + } + + rewrite->cred = *pCred; + rewrite->cred.Type = CredsspCertificateCreds; + rewrite->cred.pSpnegoCred = hPackedCredentials; + rewrite->packedCredentials = hPackedCredentials; + rewrite->cbPackedCredentials = cbPackedCredentials; + + if (pCredEx) + { + rewrite->credEx = *pCredEx; + rewrite->credEx.Cred = rewrite->cred; + rewrite->pAuthData = &rewrite->credEx; + } + else + { + rewrite->pAuthData = &rewrite->cred; + } + + rewrite->active = true; + + MsRdpEx_LogPrint(DEBUG, "CredSSP smart-card credential replaced with KerbCertificateLogon (size=%u)", cbPackedCredentials); + + sspi_FreeUnpackedCredentials(userName, domainName, password); + return true; +} + static bool sspi_SetKdcProxySettings(PCredHandle phCredential, const char* proxyServer) { SECURITY_STATUS status; @@ -294,6 +923,10 @@ static SECURITY_STATUS SEC_ENTRY sspi_AcquireCredentialsHandleW( SECURITY_STATUS status; char* pszPrincipalA = NULL; char* pszPackageA = NULL; + void* returnAddress = _ReturnAddress(); + bool isCredSsp = false; + SspiCertificateLogonRewrite rewrite = { 0 }; + void* pEffectiveAuthData = pAuthData; if (pszPrincipal) MsRdpEx_ConvertFromUnicode(CP_UTF8, 0, pszPrincipal, -1, &pszPrincipalA, 0, NULL, NULL); @@ -301,16 +934,37 @@ static SECURITY_STATUS SEC_ENTRY sspi_AcquireCredentialsHandleW( if (pszPackage) MsRdpEx_ConvertFromUnicode(CP_UTF8, 0, pszPackage, -1, &pszPackageA, 0, NULL, NULL); - if (g_SspiDump) { - if (pAuthData && MsRdpEx_StringIEquals(pszPackageA, "CREDSSP")) { + isCredSsp = (pAuthData && MsRdpEx_StringIEquals(pszPackageA, "CREDSSP")) ? true : false; + + if (isCredSsp) { + if (g_SspiDump) { //sspi_DumpCredSspAuthData(pAuthData); } + + sspi_LogCredSspAuthMetadata(pszPackageA, pAuthData, returnAddress); + + bool usedGlobalFallback = false; + CMsRdpExtendedSettings* extendedSettings = sspi_GetCurrentExtendedSettings(&usedGlobalFallback); + + if (extendedSettings && extendedSettings->GetKerbCertificateLogonEnabled()) { + if (sspi_TryBuildCertificateLogonRewrite(pAuthData, extendedSettings, &rewrite) && rewrite.active) { + if (usedGlobalFallback) + MsRdpEx_LogPrint(WARN, "CredSSP KerbCertificateLogon rewrite using global session fallback"); + + pEffectiveAuthData = rewrite.pAuthData; + } + } } status = Real_AcquireCredentialsHandleW(pszPrincipal, pszPackage, fCredentialUse, pvLogonID, - pAuthData, pGetKeyFn, pvGetKeyArgument, + pEffectiveAuthData, pGetKeyFn, pvGetKeyArgument, phCredential, ptsExpiry); + if (rewrite.active) { + MsRdpEx_LogPrint(DEBUG, "sspi_AcquireCredentialsHandleW CredSSP KerbCertificateLogon rewrite applied, status = 0x%08X", status); + sspi_FreeCertificateLogonRewrite(&rewrite); + } + MsRdpEx_LogPrint(DEBUG, "sspi_AcquireCredentialsHandleW(principal=\"%s\", package=\"%s\", phCredential=%p,%p), status = 0x%08X", pszPrincipalA ? pszPrincipalA : "", pszPackageA ? pszPackageA : "", diff --git a/include/MsRdpEx/RdpSettings.h b/include/MsRdpEx/RdpSettings.h index 3e99258..479601c 100644 --- a/include/MsRdpEx/RdpSettings.h +++ b/include/MsRdpEx/RdpSettings.h @@ -53,6 +53,8 @@ class CMsRdpExtendedSettings : public IMsRdpExtendedSettings HRESULT __stdcall PrepareExtraSystemMenu(); char* __stdcall GetKdcProxyUrl(); char* __stdcall GetKdcProxyName(); + bool GetKerbCertificateLogonEnabled(); + bool GetPasswordContainsSCardPin(); bool GetMouseJigglerEnabled(); uint32_t GetMouseJigglerInterval(); uint32_t GetMouseJigglerMethod(); @@ -83,6 +85,8 @@ class CMsRdpExtendedSettings : public IMsRdpExtendedSettings CMsRdpPropertySet* m_BaseProps = NULL; CMsRdpPropertySet* m_TransportProps = NULL; char* m_KdcProxyUrl = NULL; + bool m_KerbCertificateLogonEnabled = false; + bool m_PasswordContainsSCardPin = false; bool m_MouseJigglerEnabled = false; uint32_t m_MouseJigglerInterval = 60; uint32_t m_MouseJigglerMethod = 0; diff --git a/include/MsRdpEx/Sspi.h b/include/MsRdpEx/Sspi.h index fd9a5fe..e6b7704 100644 --- a/include/MsRdpEx/Sspi.h +++ b/include/MsRdpEx/Sspi.h @@ -15,6 +15,8 @@ extern "C" { LONG MsRdpEx_AttachSspiHooks(); LONG MsRdpEx_DetachSspiHooks(); +void MsRdpEx_Sspi_BeginSession(GUID* sessionId); +void MsRdpEx_Sspi_EndSession(GUID* sessionId); #ifdef __cplusplus }