rdpear: handle basic NTLM commands and fix server-side

This patch adds the handling of basic NTLM commands. Because there's some mysterious
4 zero bytes after pickle header in Kerberos packets, not present in NTLM commands, the
patch also had to rework a bit the packet parsing / forging.

The patch also addresses a server-side bug when parsing supplemental creds, if the client
was sending an empty list, we were considering this as an error.

And finally we also implement the parsing of MSV1_0_REMOTE_SUPPLEMENTAL_CREDENTIAL.
This breaks the public API, anyway this was basically unused (as not parsed before) and
the previous API was wrong as what we receive is MSV1_0_REMOTE_SUPPLEMENTAL_CREDENTIAL
not MSV1_0_SUPPLEMENTAL_CREDENTIAL, so I guess the API breakage is ok.
This commit is contained in:
David Fort
2025-12-04 09:56:17 +01:00
parent c411b13370
commit 65bb6c59fc
8 changed files with 219 additions and 45 deletions

View File

@@ -193,13 +193,13 @@ out:
}
static BOOL rdpear_send_payload(RDPEAR_PLUGIN* rdpear, IWTSVirtualChannelCallback* pChannelCallback,
RdpEarPackageType packageType, wStream* payload)
BOOL isKerb, wStream* payload)
{
GENERIC_CHANNEL_CALLBACK* callback = (GENERIC_CHANNEL_CALLBACK*)pChannelCallback;
BOOL ret = FALSE;
wStream* finalStream = NULL;
SecBuffer cryptedBuffer = { 0 };
wStream* unencodedContent = rdpear_encodePayload(packageType, payload);
wStream* unencodedContent = rdpear_encodePayload(isKerb, payload);
if (!unencodedContent)
goto out;
@@ -248,7 +248,7 @@ out:
return ret;
}
static BOOL rdpear_prepare_response(NdrContext* rcontext, UINT16 callId, UINT32 status,
static BOOL rdpear_prepare_response(NdrContext* rcontext, BOOL isKerb, UINT16 callId, UINT32 status,
NdrContext** pwcontext, wStream* retStream)
{
WINPR_ASSERT(rcontext);
@@ -266,8 +266,17 @@ static BOOL rdpear_prepare_response(NdrContext* rcontext, UINT16 callId, UINT32
Stream_Write(retStream, payloadHeader, sizeof(payloadHeader));
if (!ndr_write_header(wcontext, retStream) || !ndr_start_constructed(wcontext, retStream) ||
!ndr_write_pickle(wcontext, retStream) || /* pickle header */
!ndr_write_uint16(wcontext, retStream, callId) || /* callId */
!ndr_write_pickle(wcontext, retStream)) /* pickle header */
goto out;
if (isKerb)
{
/* for some reason there's 4 zero undocumented bytes here after the pickle record
* in the kerberos package packets */
UINT32 v = 0;
if (!ndr_write_uint32(wcontext, retStream, v))
goto out;
}
if (!ndr_write_uint16(wcontext, retStream, callId) || /* callId */
!ndr_write_uint16(wcontext, retStream, 0x0000) || /* align padding */
!ndr_write_uint32(wcontext, retStream, status) || /* status */
!ndr_write_uint16(wcontext, retStream, callId) || /* callId */
@@ -748,8 +757,22 @@ out:
return TRUE;
}
static BOOL rdpear_ntlm_version(NdrContext* rcontext, wStream* s, UINT32* pstatus, UINT32* pversion)
{
*pstatus = ERROR_INVALID_DATA;
if (!ndr_read_uint32(rcontext, s, pversion))
return TRUE;
WLog_DBG(TAG, "-> NtlmNegotiateVersion(v=0x%x)", *pversion);
*pstatus = 0;
return TRUE;
}
static UINT rdpear_decode_payload(RDPEAR_PLUGIN* rdpear,
IWTSVirtualChannelCallback* pChannelCallback, wStream* s)
IWTSVirtualChannelCallback* pChannelCallback,
const WinPrAsn1_OctetString* packageName, wStream* s)
{
UINT ret = ERROR_INVALID_DATA;
NdrContext* context = NULL;
@@ -761,7 +784,6 @@ static UINT rdpear_decode_payload(RDPEAR_PLUGIN* rdpear,
CreateApReqAuthenticatorResp createApReqAuthenticatorResp = { 0 };
UnpackKdcReplyBodyResp unpackKdcReplyBodyResp = { 0 };
PackApReplyResp packApReplyResp = { 0 };
void* resp = NULL;
NdrMessageType respDescr = NULL;
@@ -769,16 +791,40 @@ static UINT rdpear_decode_payload(RDPEAR_PLUGIN* rdpear,
if (!respStream)
goto out;
Stream_Seek(s, 16); /* skip first 16 bytes */
BOOL isKerb = FALSE;
switch (rdpear_packageType_from_name(packageName))
{
case RDPEAR_PACKAGE_KERBEROS:
isKerb = TRUE;
break;
case RDPEAR_PACKAGE_NTLM:
isKerb = FALSE;
break;
default:
WLog_ERR(TAG, "unknown package type");
goto out;
}
Stream_Seek(s, 16); /* skip first 16 bytes */
wStream commandStream = { 0 };
UINT16 callId = 0;
UINT16 callId2 = 0;
context = ndr_read_header(s);
if (!context || !ndr_read_constructed(context, s, &commandStream) ||
!ndr_read_pickle(context, &commandStream) ||
!ndr_read_uint16(context, &commandStream, &callId) ||
!ndr_read_pickle(context, &commandStream))
goto out;
if (isKerb)
{
/* for some reason there's 4 zero undocumented bytes here after the pickle record
* in the kerberos package packets */
UINT32 v = 0;
if (!ndr_read_uint32(context, &commandStream, &v))
goto out;
}
if (!ndr_read_uint16(context, &commandStream, &callId) ||
!ndr_read_uint16(context, &commandStream, &callId2) || (callId != callId2))
goto out;
@@ -837,9 +883,12 @@ static UINT rdpear_decode_payload(RDPEAR_PLUGIN* rdpear,
if (rdpear_kerb_PackApReply(rdpear, context, &commandStream, &status, &packApReplyResp))
ret = CHANNEL_RC_OK;
break;
case RemoteCallNtlmNegotiateVersion:
WLog_ERR(TAG, "don't wanna support NTLM");
resp = &uint32Resp;
respDescr = ndr_uint32_descr();
if (rdpear_ntlm_version(context, &commandStream, &status, &uint32Resp))
ret = CHANNEL_RC_OK;
break;
default:
@@ -849,7 +898,7 @@ static UINT rdpear_decode_payload(RDPEAR_PLUGIN* rdpear,
break;
}
if (!rdpear_prepare_response(context, callId, status, &wcontext, respStream))
if (!rdpear_prepare_response(context, isKerb, callId, status, &wcontext, respStream))
goto out;
if (resp && respDescr)
@@ -870,7 +919,7 @@ static UINT rdpear_decode_payload(RDPEAR_PLUGIN* rdpear,
}
if (!ndr_end_constructed(wcontext, respStream) ||
!rdpear_send_payload(rdpear, pChannelCallback, RDPEAR_PACKAGE_KERBEROS, respStream))
!rdpear_send_payload(rdpear, pChannelCallback, isKerb, respStream))
{
WLog_DBG(TAG, "rdpear_send_payload !!!!!!!!");
goto out;
@@ -946,7 +995,7 @@ static UINT rdpear_on_data_received(IWTSVirtualChannelCallback* pChannelCallback
wStream payloadStream = { 0 };
Stream_StaticInit(&payloadStream, payload.data, payload.len);
ret = rdpear_decode_payload(rdpear, pChannelCallback, &payloadStream);
ret = rdpear_decode_payload(rdpear, pChannelCallback, &packageName, &payloadStream);
out:
sspi_SecBufferFree(&decrypted);
return ret;

View File

@@ -216,7 +216,7 @@ BOOL ndr_read_pickle(NdrContext* context, wStream* s)
if (!ndr_read_uint32(context, s, &v) || v != 0x20000)
return FALSE;
return ndr_read_uint32(context, s, &v); // padding
return TRUE;
}
BOOL ndr_write_pickle(NdrContext* context, wStream* s)
@@ -227,7 +227,6 @@ BOOL ndr_write_pickle(NdrContext* context, wStream* s)
if (!ndr_write_uint32(context, s, 0x20000))
return FALSE;
ndr_write_uint32(context, s, 0); /* padding */
return TRUE;
}

View File

@@ -79,8 +79,9 @@ typedef enum
// End NTLM remote calls
} RemoteGuardCallId;
FREERDP_LOCAL RdpEarPackageType rdpear_packageType_from_name(WinPrAsn1_OctetString* package);
FREERDP_LOCAL wStream* rdpear_encodePayload(RdpEarPackageType packageType, wStream* payload);
FREERDP_LOCAL RdpEarPackageType rdpear_packageType_from_name(const WinPrAsn1_OctetString* package);
WINPR_ATTR_MALLOC(Stream_Free, 1)
FREERDP_LOCAL wStream* rdpear_encodePayload(BOOL isKerb, wStream* payload);
#define RDPEAR_COMMON_MESSAGE_DECL(V) \
FREERDP_LOCAL BOOL ndr_read_##V(NdrContext* context, wStream* s, const void* hints, V* obj); \

View File

@@ -31,7 +31,7 @@ static char kerberosPackageName[] = {
};
static char ntlmPackageName[] = { 'N', 0, 'T', 0, 'L', 0, 'M', 0 };
RdpEarPackageType rdpear_packageType_from_name(WinPrAsn1_OctetString* package)
RdpEarPackageType rdpear_packageType_from_name(const WinPrAsn1_OctetString* package)
{
if (package->len == sizeof(kerberosPackageName) &&
memcmp(package->data, kerberosPackageName, package->len) == 0)
@@ -44,7 +44,7 @@ RdpEarPackageType rdpear_packageType_from_name(WinPrAsn1_OctetString* package)
return RDPEAR_PACKAGE_UNKNOWN;
}
wStream* rdpear_encodePayload(RdpEarPackageType packageType, wStream* payload)
wStream* rdpear_encodePayload(BOOL isKerb, wStream* payload)
{
wStream* ret = NULL;
WinPrAsn1Encoder* enc = WinPrAsn1Encoder_New(WINPR_ASN1_DER);
@@ -57,18 +57,15 @@ wStream* rdpear_encodePayload(RdpEarPackageType packageType, wStream* payload)
/* packageName [1] OCTET STRING */
WinPrAsn1_OctetString packageOctetString;
switch (packageType)
if (isKerb)
{
case RDPEAR_PACKAGE_KERBEROS:
packageOctetString.data = (BYTE*)kerberosPackageName;
packageOctetString.len = sizeof(kerberosPackageName);
break;
case RDPEAR_PACKAGE_NTLM:
packageOctetString.data = (BYTE*)ntlmPackageName;
packageOctetString.len = sizeof(ntlmPackageName);
break;
default:
goto out;
packageOctetString.data = (BYTE*)kerberosPackageName;
packageOctetString.len = sizeof(kerberosPackageName);
}
else
{
packageOctetString.data = (BYTE*)ntlmPackageName;
packageOctetString.len = sizeof(ntlmPackageName);
}
if (!WinPrAsn1EncContextualOctetString(enc, 1, &packageOctetString))

View File

@@ -322,7 +322,7 @@ static int TestNdrEarRead(int argc, char* argv[])
};
size_t sizeofPayload4 = sizeof(payload4);
#endif
#if 1
size_t sizeofPayload4 = 0;
BYTE* payload4 = parseHexBlock("03 01 03 01 \
04 00 02 00 38 9e ef 6b 0c 00 02 00 18 00 02 00 \
@@ -346,7 +346,6 @@ static int TestNdrEarRead(int argc, char* argv[])
if (!payload4)
goto out;
#endif
CreateApReqAuthenticatorReq createApReqAuthenticatorReq = { 0 };
s = Stream_StaticInit(&staticS, payload4, sizeofPayload4);

View File

@@ -56,8 +56,18 @@ extern "C"
typedef BOOL (*psPeerHasMoreToRead)(freerdp_peer* peer);
typedef BOOL (*psPeerClose)(freerdp_peer* peer);
typedef void (*psPeerDisconnect)(freerdp_peer* peer);
/** callback called when we receive remote credential guard credentials during NLA
* @param peer the associated freerdp_peer
* @param logonCreds the KERB_TICKET_LOGON containing the TGT and the host service ticket
* @param suppCreds some MSV1_0_REMOTE_SUPPLEMENTAL_CREDENTIAL containing NTLM hashes
* @return if the treatment was successful
* @bug before 3.19.0 suppCreds were a pointer to MSV1_0_SUPPLEMENTAL_CREDENTIAL, not
* MSV1_0_REMOTE_SUPPLEMENTAL_CREDENTIAL as now
*/
typedef BOOL (*psPeerRemoteCredentials)(freerdp_peer* peer, KERB_TICKET_LOGON* logonCreds,
MSV1_0_SUPPLEMENTAL_CREDENTIAL* suppCreds);
MSV1_0_REMOTE_SUPPLEMENTAL_CREDENTIAL* suppCreds);
typedef BOOL (*psPeerCapabilities)(freerdp_peer* peer);
typedef BOOL (*psPeerPostConnect)(freerdp_peer* peer);
typedef BOOL (*psPeerActivate)(freerdp_peer* peer);

View File

@@ -1196,6 +1196,40 @@ static BOOL nla_read_KERB_TICKET_LOGON(WINPR_ATTR_UNUSED rdpNla* nla, wStream* s
return TRUE;
}
WINPR_ATTR_MALLOC(free, 1)
static MSV1_0_REMOTE_SUPPLEMENTAL_CREDENTIAL* nla_read_NtlmCreds(WINPR_ATTR_UNUSED rdpNla* nla,
wStream* s)
{
WINPR_ASSERT(nla);
WINPR_ASSERT(s);
if (!Stream_CheckAndLogRequiredLength(TAG, s, 32 + 4))
return NULL;
size_t pos = Stream_GetPosition(s);
Stream_Seek(s, 32);
ULONG EncryptedCredsSize = Stream_Get_UINT32(s);
if (!Stream_CheckAndLogRequiredLength(TAG, s, EncryptedCredsSize))
return NULL;
Stream_SetPosition(s, pos);
MSV1_0_REMOTE_SUPPLEMENTAL_CREDENTIAL* ret = (MSV1_0_REMOTE_SUPPLEMENTAL_CREDENTIAL*)calloc(
1, sizeof(MSV1_0_REMOTE_SUPPLEMENTAL_CREDENTIAL) - 1 + EncryptedCredsSize);
if (!ret)
return NULL;
ret->Version = Stream_Get_UINT32(s);
ret->Flags = Stream_Get_UINT32(s);
Stream_Read(s, ret->CredentialKey.Data, MSV1_0_CREDENTIAL_KEY_LENGTH);
ret->CredentialKeyType = Stream_Get_UINT32(s);
ret->EncryptedCredsSize = EncryptedCredsSize;
Stream_Read(s, ret->EncryptedCreds, EncryptedCredsSize);
return ret;
}
/** @brief kind of RCG credentials */
typedef enum
{
@@ -1372,10 +1406,11 @@ static BOOL nla_read_ts_credentials(rdpNla* nla, SecBuffer* data)
}
/* supplementalCreds [1] SEQUENCE OF TSRemoteGuardPackageCred OPTIONAL, */
MSV1_0_SUPPLEMENTAL_CREDENTIAL* suppCreds = NULL;
MSV1_0_REMOTE_SUPPLEMENTAL_CREDENTIAL* suppCreds = NULL;
WinPrAsn1Decoder suppCredsSeq = { 0 };
if (WinPrAsn1DecReadContextualSequence(&dec2, 1, &error, &suppCredsSeq))
if (WinPrAsn1DecReadContextualSequence(&dec2, 1, &error, &suppCredsSeq) &&
Stream_GetRemainingLength(&suppCredsSeq.source))
{
WinPrAsn1Decoder ntlmCredsSeq = { 0 };
if (!WinPrAsn1DecReadSequence(&suppCredsSeq, &ntlmCredsSeq))
@@ -1393,7 +1428,12 @@ static BOOL nla_read_ts_credentials(rdpNla* nla, SecBuffer* data)
return FALSE;
}
/* TODO: suppCreds = &ntlmCreds; and parse NTLM creds */
suppCreds = nla_read_NtlmCreds(nla, &ntlmPayload);
if (!suppCreds)
{
WLog_ERR(TAG, "invalid supplementalCreds");
return FALSE;
}
}
else if (error)
{
@@ -1403,6 +1443,7 @@ static BOOL nla_read_ts_credentials(rdpNla* nla, SecBuffer* data)
freerdp_peer* peer = nla->rdpcontext->peer;
ret = IFCALLRESULT(TRUE, peer->RemoteCredentials, peer, &kerbLogon, suppCreds);
free(suppCreds);
break;
}
default:
@@ -1486,6 +1527,40 @@ out:
return ret;
}
static BOOL nla_write_TSRemoteGuardNtlmCred(rdpNla* nla, WinPrAsn1Encoder* enc,
const MSV1_0_REMOTE_SUPPLEMENTAL_CREDENTIAL* pntlm)
{
WINPR_UNUSED(nla);
BOOL ret = FALSE;
BYTE ntlm[] = { 'N', '\0', 'T', '\0', 'L', '\0', 'M', '\0' };
const WinPrAsn1_OctetString packageName = { sizeof(ntlm), ntlm };
/* packageName [0] OCTET STRING */
if (!WinPrAsn1EncContextualOctetString(enc, 0, &packageName))
return FALSE;
/* credBuffer [1] OCTET STRING */
wStream* s = Stream_New(NULL, 300);
if (!s)
goto out;
Stream_Write_UINT32(s, pntlm->Version); /* Version */
Stream_Write_UINT32(s, pntlm->Flags); /* Flags */
Stream_Write(s, pntlm->CredentialKey.Data, MSV1_0_CREDENTIAL_KEY_LENGTH);
Stream_Write_UINT32(s, pntlm->CredentialKeyType);
Stream_Write_UINT32(s, pntlm->EncryptedCredsSize);
Stream_Write(s, pntlm->EncryptedCreds, pntlm->EncryptedCredsSize);
Stream_Zero(s, 6 + 16 * 4 + 14);
WinPrAsn1_OctetString credBuffer = { Stream_GetPosition(s), Stream_Buffer(s) };
ret = WinPrAsn1EncContextualOctetString(enc, 1, &credBuffer) != 0;
out:
Stream_Free(s, TRUE);
return ret;
}
/**
* Encode TSCredentials structure.
* @param nla A pointer to the NLA to use
@@ -1665,13 +1740,26 @@ static BOOL nla_encode_ts_credentials(rdpNla* nla)
if (!nla_write_TSRemoteGuardKerbCred(nla, enc) || !WinPrAsn1EncEndContainer(enc))
goto out;
/* supplementalCreds [1] SEQUENCE OF TSRemoteGuardPackageCred OPTIONAL,
*
* no NTLM supplemental creds for now
*
*/
if (!WinPrAsn1EncContextualSeqContainer(enc, 1) || !WinPrAsn1EncEndContainer(enc))
goto out;
/* TODO: compute the NTLM supplemental creds */
MSV1_0_REMOTE_SUPPLEMENTAL_CREDENTIAL* ntlm = NULL;
if (ntlm)
{
/* supplementalCreds [1] SEQUENCE OF TSRemoteGuardPackageCred OPTIONAL */
if (!WinPrAsn1EncContextualSeqContainer(enc, 1))
goto out;
if (!WinPrAsn1EncSeqContainer(enc)) /* start NTLM */
goto out;
if (!nla_write_TSRemoteGuardNtlmCred(nla, enc, ntlm))
goto out;
if (!WinPrAsn1EncEndContainer(enc)) /* end NTLM */
goto out;
if (!WinPrAsn1EncEndContainer(enc)) /* supplementalCreds */
goto out;
}
/* End TSRemoteGuardCreds */
if (!WinPrAsn1EncEndContainer(enc))

View File

@@ -70,6 +70,37 @@ typedef struct
#define MSV1_0_CRED_VERSION_REMOTE 0xffff0002
typedef enum _MSV1_0_CREDENTIAL_KEY_TYPE
{
InvalidCredKey,
DeprecatedIUMCredKey,
DomainUserCredKey,
LocalUserCredKey,
ExternallySuppliedCredKey
} MSV1_0_CREDENTIAL_KEY_TYPE;
#define MSV1_0_CREDENTIAL_KEY_LENGTH 20
#define MSV1_0_CRED_LM_PRESENT 0x1
#define MSV1_0_CRED_NT_PRESENT 0x2
#define MSV1_0_CRED_REMOVED 0x4
#define MSV1_0_CRED_CREDKEY_PRESENT 0x8
#define MSV1_0_CRED_SHA_PRESENT 0x10
typedef struct
{
UCHAR Data[MSV1_0_CREDENTIAL_KEY_LENGTH];
} MSV1_0_CREDENTIAL_KEY, *PMSV1_0_CREDENTIAL_KEY;
typedef struct
{
ULONG Version;
ULONG Flags;
MSV1_0_CREDENTIAL_KEY CredentialKey;
MSV1_0_CREDENTIAL_KEY_TYPE CredentialKeyType;
ULONG EncryptedCredsSize;
UCHAR EncryptedCreds[1];
} MSV1_0_REMOTE_SUPPLEMENTAL_CREDENTIAL, *PMSV1_0_REMOTE_SUPPLEMENTAL_CREDENTIAL;
#endif /* _WIN32 */
#ifndef KERB_LOGON_FLAG_REDIRECTED