Remote Desktop Connection with Custom Credential Provider - c++

I have developed a custom credential provider using the SampleWrapExistingCredentialProvider from VistaCredentialProviderSamples. The credential provider has a filter implemented that filters all other credential providers and I see just my credential provider at the time of logon. The issue is that if we connect to it using remote desktop connection, the username/password are not passed from windows RDP client to the Credential provider and I have to enter it again when RDP session opens (unlike the behavior with default provider)
I am trying to explore which part of the code handles this scenario that the credential provider accepts the username/password from remote desktop client and does not ask again. Attached is the screenshot of my credential provider after providing successful credentials on the RDP client. After I click this icon of my credential provider, I am shown the credential provider tile that asking again for the username and password. Any help would be highly appreciated on how to receive credentials from RDP client.
I have returned S_OK for CREDUI. My SetUsageScenario is as follows:
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;
}

the username/password are not passed from windows RDP client to the
Credential provider and I have to enter it again when RDP session
opens (unlike the behavior with default provider)
windows can not by some magic know client username/password, which connecting by rdp.
at begin on client side some credential provider must create CREDENTIAL_PROVIDER_CREDENTIAL_SERIALIZATION and pass it to server. inside it clsidCredentialProvider say which concrete provider collect this serialization.
what server must do with this CREDENTIAL_PROVIDER_CREDENTIAL_SERIALIZATION ? obvious pass it to some credential provider SetSerialization method. but for which ? for all ? no. again obvivous only for provider which clsid exactly matches clsidCredentialProvider from CREDENTIAL_PROVIDER_CREDENTIAL_SERIALIZATION. this provider (if exist and not filtered) must remember this credentional and then when called GetCredentialCount - say that it have dafault credentional (not CREDENTIAL_PROVIDER_NO_DEFAULT) and it usually ready for auto logon attempt with this.
from client side (mstsc) the password provider create serialization. so will be __uuidof(PasswordCredentialProvider) or __uuidof(V1PasswordCredentialProvider) (if client run on win7) in clsidCredentialProvider.
but you disable this providers in self filter. as result you yourself break process.
filter must implement UpdateRemoteCredential method. and here copy and update passed pcpcsIn. most importand part of this - we must replace clsidCredentialProvider to self CLSID. as result our SetSerialization method will be called. here we need restore original CLSID before pass it to wrapped credentional.
also important place - inside GetCredentialCount - first pass it to wrapped credentional and then do *pbAutoLogonWithDefault = FALSE; - disable autologon - you can not do this (autologon) if you require additional (OTP ?) information from client.
inside UpdateRemoteCredential method we can not modify pcpcsIn - if declared as const. so we need write our update credentional to pcpcsOut. because system can not know which size is required for rgbSerialization - we need allocate it yourself. and system then free it. obvivous need use CoTaskMemAlloc for allocate rgbSerialization.
so - all this can be understanded and without any documentation. however if all this was documented - will be not bad too.
so code for 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;
}
if we dont know the clsidCredentialProvider - simply return E_UNEXPECTED
oterwise allocate more (on sizeof(CLSID)) memory and save original clsidCredentialProvider in the end
now 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);
}
restore original clsidCredentialProvider and fix cbSerialization. also because pcpcs->clsidCredentialProvider == __uuidof(CSampleProvider) can be set only inside UpdateRemoteCredential in my case (i not do CPUS_CREDUI on client side for RDP, only for "run as admin") - i just know that this is remote connection and save this information (_IsRemoteSession = true;) for not call WTSIsRemoteSession
finally 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;
}
note very important *pbAutoLogonWithDefault = FALSE;//!!! line

According to official documentationhttps: RDC and Custom Credential Providers
If the user connected with a non-Microsoft credential provider, then
you will be prompted on the terminal server to enter credentials again
(twice). If NLA is not enabled, then despite entering using an
unsupported credential provider on the client prior to the connection,
the user will still be connected. You will be left at the logon
screen, where you can use any credential provider that is supported
for local authentication. There’s no way to avoid the two
authentications when using unsupported credential providers.
Having said that, if you have your own credential providers and you
try to do a remote desktop connection to a Vista box (having this
Credential provider) then you would need to log-in twice. This is an
expected behavior and it is by design and there is no legitimate way
to avoid it.

Related

Cannot connect to Windows server active directory lightweight service

Am working with windows server I want to create active directory lightweight service user by c++ ie, By c++ using ldap connection. My server runs in virtual box and i set the network adapter to be bridged network and it connects to the network just fine.
Now when i used the code from microsoft documentation the ldap bind is not connecting .The error am getting is an operation error occured.
#include "atlbase.h"
#include "activeds.h"
int main(int argc, char* argv[])
{
HRESULT hr;
IADsContainer* pCont;
IDispatch* pDisp = NULL;
IADs* pUser;
// Initialize COM before calling any ADSI functions or interfaces.
CoInitialize(NULL);
hr = ADsGetObject(L"LDAP://ABC.local",
IID_IADsContainer,
(void**)&pCont);
if (!SUCCEEDED(hr))
{
return 0;
}
//-----------------
// Create a user
//-----------------
hr = pCont->Create(CComBSTR("user"), CComBSTR("cn=jeffsmith"), &pDisp);
// Release the container object.
pCont->Release();
if (!SUCCEEDED(hr))
{
return 0;
}
hr = pDisp->QueryInterface(IID_IADs, (void**)&pUser);
// Release the dispatch interface.
pDisp->Release();
if (!SUCCEEDED(hr))
{
return 0;
}
// Commit the object data to the directory.
pUser->SetInfo();
// Release the object.
pUser->Release();
CoUninitialize();
}
This is the code am using to check for successful connection. Any help will be really useful thank you.
You're asking for a container (IID_IADsContainer), but the path you give it is to the root of the domain (LDAP://ABC.local). Try specifying the distinguished name of the OU you want to put the user in. For example, if you want to put the user in the Users OU, which is at the root of the domain, it would look like this:
hr = ADsGetObject(L"LDAP://ABC.local/OU=Users,DC=ABC,DC=local",
IID_IADsContainer,
(void**)&pCont);
You can read more about the format of the LDAP path here: LDAP ADsPath

Forward credentials from app to credential provider

I've started to investigate how credential providers work, but due to poor documentation I got stuck in couple of places.
I have console application(sort of like ssh server) when I connect remotly to this application and authenticate(none is logged on), I want it to be able to forward those credentials to credential provider and I want CP to login user(create session).
I've also noticed that In documentation it says that SetSerialization method always gets called after SetusageScenario. But I've added logs and SetSerialization never gets called after SetusageScenario.
As I know SetSerialization is called when credential is provided by application. I can't understand what does it mean to provide credential through application? It seems like this is what I need, to forward credentials from application to credential provider, but How can I do that? Are there some functions that I could call from credential provider dll to provoke login?
Is there any way I can achieve this? I've read that when remote login is done, UpdateRemoteCredential is called, But As I understand(correct me if I'm wrong) in remote login it means RDP but my application is simple, it just listens to some port and gets username and password and then uses Logonuser function.
A program named MxLogon2 implements this scheme. A remote server with MxLogon2 installed can authenticate a USB key(But I want username/password) connected to the client side in a remote desktop session.
Any suggestion that would direct me to right way would be greatly appriciated.
P.S
I'm using pGina as my credential provider.
I wouldn't mind too much of SetSerialization in your case. This is used for pre-initializing tile.
One example would be, with RDP connection, you can store certificates of credentials on your system. This function is used to pre-inquire tiles with those credentials (not the only solution though).
Make it return E_NOTIMPL in your case.
The main purpose of the provider is to initialize Credentials and tiles :
Credentials initialization
is done through SetUsageScenario function (automatically called after Provider construction).
In there, you can customize the behavior depending on scenario you are in. Most basic one being CPUS_LOGON.
It shall look like this (this is only a preview)
HRESULT Provider::SetUsageScenario(
__in CREDENTIAL_PROVIDER_USAGE_SCENARIO cpus,
__in DWORD dwFlags
)
{
HRESULT hr;
switch(cpus)
{
case CPUS_UNLOCK_WORKSTATION:
case CPUS_LOGON:
hr = this->_InitCredential();
break;
default:
hr = E_INVALIDARG;
break;
}
return hr;
}
Tiles initialization
begins in GetCredentialCount function. It allows you to define how many tiles you gonna show (pdwCount out parameter), as well as the one to be used as the default tile (pdwDefault).
Auto-Logon is also handle in here (through pbAutoLogonWithDefault).
Tiles Enumeration is then beginning in GetCredentialAt function.
Passing credentials for authentication is done in the credential you're using through the GetSerialization function.
Examples of implementation can be found here.
I hardly insist on example as it easily divert depending on what you are trying to achieve. This is very flexible and you'd better understand the purpose of any function / parameter to fit your goals.
EDIT - cf comments :
#define IDCredential 0x1
Credential::Credential(Provider* parent)
{
//Do ssh connection or whatever
if(success)
{
parent->login = retrievedLogin;
parent->pwd = retrievedPwd;
parent->CredentialUsed = IDCredential;
parent->autoLogin = true;
parent->ProviderEvents->CredentialsChanged(AdviseContext);
}
}
HRESULT Provider::GetCredentialCount(
__out DWORD* pdwCount,
__out_range(<,*pdwCount) DWORD* pdwDefault,
__out BOOL* pbAutoLogonWithDefault
)
{
if(this->autoLogin && this->CredentialUsed)
{
*pbAutoLogonWithDefault = true; // --> Instant call to GetSerialization
*pdwDefault = this->CredentialUsed; //index of the tile
}
else
{
*pbAutoLogonWithDefault = false;
*pdwDefault = IDCredential;
}
return S_OK;
}

Can't query, run or stop a Windows Service

I have created my first Windows service following this instruction. I can start or stop my service from the Service panel in Manage-> My computer without problem.
The service account is "LocalSystem" so, I think, I'm ok with privileges.
Now I want to communicate with my service from another app, but first of all I want to stop or start my service.
Following this example I Write this code:
SC_HANDLE schSCManager;
SC_HANDLE schService;
LPCWSTR szSvcName = _T("MyNewService");
// Get a handle to the SCM database.
schSCManager = OpenSCManager(
NULL, // local computer
NULL, // ServicesActive database
SC_MANAGER_ALL_ACCESS); // full access rights
if (NULL == schSCManager)
{
printf("OpenSCManager failed (%d)\n", GetLastError());
return;
}
// Get a handle to the service.
schService = OpenService(
schSCManager, // SCM database
szSvcName, // name of service
SERVICE_CHANGE_CONFIG); // need change config access
if (schService == NULL)
{
printf("OpenService failed (%d)\n", GetLastError());
CloseServiceHandle(schSCManager);
return;
}
//OK until now
// Check the status in case the service is not stopped.
SERVICE_STATUS_PROCESS ssStatus;
DWORD dwBytesNeeded;
if (!QueryServiceStatusEx(
schService, // handle to service
SC_STATUS_PROCESS_INFO, // information level
(LPBYTE) &ssStatus, // address of structure
sizeof(SERVICE_STATUS_PROCESS), // size of structure
&dwBytesNeeded ) ) // size needed if buffer is too small
{
//printf("QueryServiceStatusEx failed (%d)\n", GetLastError());
CString str;
str.Format(_T("QueryServiceStatusEx failed (%d)\n"), GetLastError()); //ERROR 5: ERROR_ACCESS_DENIED
CloseServiceHandle(schService);
CloseServiceHandle(schSCManager);
return;
}
// Check if the service is already running. It would be possible
// to stop the service here, but for simplicity this example just returns.
if(ssStatus.dwCurrentState != SERVICE_STOPPED && ssStatus.dwCurrentState != SERVICE_STOP_PENDING)
{
printf("Cannot start the service because it is already running\n");
CloseServiceHandle(schService);
CloseServiceHandle(schSCManager);
return;
}
In any case I get the error number 5, that is "ERROR_ACCESS_DENIED: The handle does not have the SERVICE_QUERY_STATUS access right", but I'm not sure where is the problem. The service has LocalSystem account option that means all privileges and I'm the admin of my computer.. what's wrong?
I also tried with ControlService trying to start or stop the service but I get the same error about rights
BOOL success = ::ControlService(schService, SERVICE_CONTROL_STOP, &ss);
I'm on Visual Studio 2013 Update 2.
Thank you
You seem to be a little bit confused about terms "privileges" and "access rights". You are quite right that the LocalSystem account is really a privileged one, at least for actions on the local computer. However, even a privileged user must know how to take advantage if her privileges (how things work).
When an application wants to work with an object (e.g. a service), it has to declare its intention to the Windows kernel (done via OpenSCManager and OpenService in your code). The application identifies the object (e.g. by service name) and also informs the kernel what sorts of things it plans to do with it. These "sorts of things" are called "access rights" and the kenrel decides whether the application is privileged enough to obtain the access rights it is requesting. In your code, you are requesting all access rights (SC_MANAGER_ALL_ACCESS) for the SC manager and an access right to change configuration of your service (SERVICE_CHANGE_CONFIG). Then, you attempt to query status of your service, but you did not declare this intention to the kernel when requesting access to the service (the OpenService call). That's why you get the "access denied" error.
Here is a list of access rights defined for services and SC manager(s): https://msdn.microsoft.com/en-us/library/windows/desktop/ms685981(v=vs.85).aspx. The document also contains information about which access rights you need to perform which actions.
To open a service, you need only the SC_MANAGER_CONNECT access right to its SC manager. To query service status, the SERVICE-QUERY_STATUS access right is required. To stop the service, request the SERVICE_STOP right.
So, the short version is: specify SERVICE_STOP | SERVICE_QUERY_STATUS desired access mask when calling OpenService, and optionally SC_MANAGER_CONNECT in the OpenSCManager call.
Problem
Your example program does not have the correct privileges to stop/start your service. It does not matter what privileges your service has.
Solution
Try running your example program as administrator by right-clicking on the exe file and selecting "Run as administrator".
If you need to elevate your process programmatically then have a look at this article.
UINT GetServiceStatus(LPCTSTR ServiceName)
{
SC_HANDLE schService;
SC_HANDLE schSCManager;
DWORD ErrorCode;
SERVICE_STATUS ssStatus;
UINT return_value;
schSCManager = OpenSCManager(
NULL, // machine (NULL == local)
NULL, // database (NULL == default)
SC_MANAGER_ALL_ACCESS // access required
);
if (!schSCManager)
return -1;
schService = OpenService(schSCManager, ServiceName, SERVICE_ALL_ACCESS);
if (!schService)
{
ErrorCode = GetLastError();
CloseServiceHandle(schSCManager);
if (ErrorCode == ERROR_SERVICE_DOES_NOT_EXIST)
return -2;
else
return -1;
}
QueryServiceStatus(schService, &ssStatus);
if (ssStatus.dwCurrentState == SERVICE_RUNNING)
return_value = 1;
else
return_value = 0;
CloseServiceHandle(schService);
CloseServiceHandle(schSCManager);
return return_value;
}

Using setInfo() in C++ results in E_ADS_INVALID_USER_OBJECT

I am trying to use the setInfo() function in C++ to commit a few changes to a user's e-mail field in Windows AD. I am making a DLL that gets exported to a Windows service. But setInfo() keeps returning:
E_ADS_INVALID_USER_OBJECT
Everything works fine if I compile the EXACT same code into an EXE and run it in a command line. But when the DLL is exported, and setInfo() is called, it returns the above error. This is a portion of my code:
IADsUser *pUser = NULL;
RESULT hr = CoInitialize(0);
VARIANT var;
hr = ADsGetObject(L"LDAP://CN=Foo Bar,CN=Users,DC=mydomain,DC=com", IID_IADsUser, (void**) &pUser);
//hr = ADsOpenObject(L"LDAP://CN=Foo Bar,CN=Users,DC=mydomain,DC=com", NULL, NULL, ADS_SECURE_AUTHENTICATION, IID_IADsUser, (void**) &pUser);
VariantInit(&var);
V_BSTR(&var) = SysAllocString(L"foobar#email.com");
V_VT(&var) = VT_BSTR;
hr = pUser->Put(CComBSTR("mail"), var);
hr = pUser->SetInfo();
I am able to bind with LDAP successfully, the binding works also with this:
ADsOpenObject()
When I add the domain Administrator credentials in ADsOpenObject, setInfo() works.. but I do not want to use username/password for this. Is there a way to use setInfo() in a DLL without providing the credentials in code?
Also, the IADsUser Get function works without providing domain admin's credentials. Is it a read/write issue? Any help is appreciated!
If you don't explicitly provide a username and password when you bind to the AD, Windows uses the security context of the process to log in. For a service running as local system or as network service, this means that it logs in using the computer's AD account.
If you change the permissions within the AD to grant the necessary access to the computer account, it will work. (The username for the computer account is the computer name with a dollar sign appended.)

Calling remote COM component from MFC client?

I have a COM component that was originally written in Visual Studio 6. It is in a windows service which I have running on one of my development machines. I have written a quick MFC test app to call it and this works fine when run from this machine, the code looks like
COSERVERINFO si;
MULTI_QI qi;
COAUTHINFO cai = { RPC_C_AUTHN_NONE, RPC_C_AUTHZ_NONE, 0,RPC_C_AUTHN_LEVEL_NONE, RPC_C_IMP_LEVEL_IMPERSONATE,0, EOAC_NONE };
si.dwReserved1 = 0;
si.pwszName =L"{machine name}";
si.pAuthInfo = &cai;
si.dwReserved2 = 0;
qi.pIID = &IID_IMyComponent;
qi.pItf = NULL;
qi.hr = 1;
HRESULT hr = CoCreateInstanceEx(CLSID_MyComponent,NULL,CLSCTX_REMOTE_SERVER ,&si,1,&qi);
However, when I move the MFC test app to my other development machine and try and call the component on the other machine it fails. The hresult returned from CoCreateInstanceEx is -2147024891
I have already created the proxy stub DLL and registered it on both machines. Am I missing something else?
UPDATE:
I now updated the COUTHINFO structure to the below and it works.
COAUTHINFO cai = { RPC_C_AUTHN_WINNT, RPC_C_AUTHZ_NONE, 0, RPC_C_AUTHN_LEVEL_DEFAULT, RPC_C_IMP_LEVEL_IMPERSONATE,0, EOAC_NONE };
The ERROR_ACCESS_DENIED is most likely due to the wrong parameters supplied with COAUTHINFO. You set Authentication, Authorization levels to NONE, which is not enough to get permissions to connect with remote machine. Try to set these values: RPC_C_AUTHN_DEFAULT (have COM negotiate the best authentication service), RPC_C_AUTHZ_NONE, RPC_C_AUTHN_LEVEL_DEFAULT, RPC_C_IMP_LEVEL_IMPERSONATE. Also see the MSDN source - COAUTHINFO structure
An HRESULT of -2147024891 (or 0x80070005 when converted to hex) is ERROR_ACCESS_DENIED. So it's a permission error when trying to connect to the remote machine.
The error means E_ACCESS_DENIED. Make sure the current user has the rights to access the component. Run dcomcnfg (or "Component Services") on the server box, under "DCOM Config" find the right component, under "Security" change the permissions to allow both activation and access for the calling user.
Or make sure the calling user is an admin on the server box.