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
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,42 @@ 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 | Hand LSASS a KERB_CERTIFICATE_LOGON for smart card logon (see below) | 0/1 | 0 |

### Smart card certificate logon (KerbCertificateLogon)

When a smart card certificate and its PIN are both pre-supplied (an unattended logon, with no
interactive prompt), the CredSSP/SSPI credential that reaches LSASS 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 is not
affected, because it packs the credential as a `KERB_CERTIFICATE_LOGON` structure that `tspkg`
handles on a different path.

Setting `KerbCertificateLogon:i:1` opts the session in to a client-side workaround: MsRdpEx hooks
`AcquireCredentialsHandleW` and, for the matching session, hands LSASS a `KERB_CERTIFICATE_LOGON`
credential (`CredsspCertificateCreds`). It obtains it either by:

- **synthesizing** it from the session's marshaled certificate user name and PIN — used when the
host does not pass the credential in-band, such as the in-process RDP ActiveX control; or
- **rewriting** a marshaled smart card credential that is already present in the
`AcquireCredentialsHandleW` auth data — such as the out-of-process `mstsc.exe` case.

The workaround is strictly opt-in and stays inert unless a PIN is available: with no PIN the
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.

`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
unattended certificate + PIN logon typically sets both.

The captured PIN is kept only for the lifetime of the connection, encrypted in memory with DPAPI
(`CryptProtectMemory`), decrypted only transiently while the credential is built, and zeroed
before it is released. No PINs, passwords, or certificate bytes are logged.

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

Expand Down
1 change: 1 addition & 0 deletions dll/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ set(MSRDPEX_LIBS
secur32.lib
credui.lib
advapi32.lib
crypt32.lib
comctl32.lib)

target_compile_options(MsRdpEx_Dll PRIVATE /W4)
Expand Down
29 changes: 28 additions & 1 deletion dll/MsRdpClient.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include <MsRdpEx/RdpProcess.h>
#include <MsRdpEx/RdpInstance.h>
#include <MsRdpEx/RdpSettings.h>
#include <MsRdpEx/Sspi.h>
#include <MsRdpEx/NameResolver.h>

#include "TSObjects.h"
Expand Down Expand Up @@ -182,6 +183,8 @@ class CMsRdpClient : public IMsRdpClient10

~CMsRdpClient()
{
EndSspiSessionScope("destroy");

m_pUnknown->Release();
if (m_pDispatch) m_pDispatch->Release();
if (m_pMsTscAx) m_pMsTscAx->Release();
Expand Down Expand Up @@ -494,16 +497,20 @@ class CMsRdpClient : public IMsRdpClient10
HRESULT hr;
MsRdpEx_LogPrint(DEBUG, "CMsRdpClient::Connect");

CMsRdpExtendedSettings* pMsRdpExtendedSettings = m_pMsRdpExtendedSettings;
m_pMsRdpExtendedSettings->LoadRdpFile(NULL);
m_pMsRdpExtendedSettings->LoadRdpFileFromNamedPipe(NULL);
m_pMsRdpExtendedSettings->PrepareSspiSessionIdHack();
m_pMsRdpExtendedSettings->DiscardCapturedPinIfNotCertLogon();
m_pMsRdpExtendedSettings->PrepareMouseJiggler();
m_pMsRdpExtendedSettings->PrepareVideoRecorder();
m_pMsRdpExtendedSettings->PrepareExtraSystemMenu();

BeginSspiSessionScope("connect");
hr = m_pMsTscAx->raw_Connect();

if (FAILED(hr))
EndSspiSessionScope("connect-failed");

return hr;
}

Expand All @@ -512,10 +519,29 @@ class CMsRdpClient : public IMsRdpClient10
MsRdpEx_LogPrint(DEBUG, "CMsRdpClient::Disconnect");

hr = m_pMsTscAx->raw_Disconnect();
EndSspiSessionScope("disconnect");

return hr;
}

void BeginSspiSessionScope(const char* reason) {
if (m_sspiSessionActive)
return;

MsRdpEx_LogPrint(DEBUG, "CMsRdpClient::BeginSspiSessionScope(%s)", reason ? reason : "");
MsRdpEx_Sspi_BeginSession(&m_sessionId);
m_sspiSessionActive = true;
}

void EndSspiSessionScope(const char* reason) {
if (!m_sspiSessionActive)
return;

MsRdpEx_LogPrint(DEBUG, "CMsRdpClient::EndSspiSessionScope(%s)", reason ? reason : "");
MsRdpEx_Sspi_EndSession(&m_sessionId);
m_sspiSessionActive = false;
}

HRESULT __stdcall raw_CreateVirtualChannels(BSTR newVal) {
return m_pMsTscAx->raw_CreateVirtualChannels(newVal);
}
Expand Down Expand Up @@ -728,6 +754,7 @@ class CMsRdpClient : public IMsRdpClient10
IMsRdpClient10* m_pMsRdpClient10 = NULL;
CMsRdpExInstance* m_pMsRdpExInstance = NULL;
CMsRdpExtendedSettings* m_pMsRdpExtendedSettings = NULL;
bool m_sspiSessionActive = false;
};

class CClassFactory : IClassFactory
Expand Down
34 changes: 34 additions & 0 deletions dll/RdpInstance.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -627,6 +627,40 @@ CMsRdpExtendedSettings* MsRdpEx_FindExtendedSettingsBySessionId(GUID* sessionId)
return settings;
}

// Resolve the extended settings that owns a given core property set, so the SetSecureStringProperty hook can route a
// captured PIN to the right session instance (the hook only has the raw ITSPropertySet, not a session id).
CMsRdpExtendedSettings* MsRdpEx_FindExtendedSettingsByCoreProps(void* pCoreProps)
{
MsRdpEx_InstanceManager* ctx = g_InstanceManager;

if (!ctx || !pCoreProps)
return NULL;

CMsRdpExtendedSettings* found = NULL;
MsRdpEx_ArrayListIt* it = NULL;

it = MsRdpEx_ArrayList_It(ctx->instances, MSRDPEX_ITERATOR_FLAG_EXCLUSIVE);

while (!MsRdpEx_ArrayListIt_Done(it))
{
CMsRdpExInstance* obj = (CMsRdpExInstance*) MsRdpEx_ArrayListIt_Next(it);
void* corePropsRaw = NULL;

if (obj)
obj->GetCorePropsRawPtr(&corePropsRaw);

if (corePropsRaw == pCoreProps)
{
found = obj->m_pMsRdpExtendedSettings;
break;
}
}

MsRdpEx_ArrayListIt_Finish(it);

return found;
}

MsRdpEx_InstanceManager* MsRdpEx_InstanceManager_New()
{
MsRdpEx_InstanceManager* ctx;
Expand Down
Loading