I am trying to read a credential from a smart card on one workstation (local), then send it to a different workstation(remote) and use it to logon to that workstation from within a credential provider.
I've been searching and, and researching for weeks now but have not found anyone showing or telling how to do this.
I have found several references to an excellent work at https://www.idrix.fr/Root/Samples/LsaSmartCardLogon2.cpp, which is working code for a smart card credential provider, but unfortunately requires that the credential provider have direct access to the smart card with the credential on it.
The actual code I am working with seems a little to long to include in the post so I thought I try asking the question using sudo code.
On the local workstation:
MySendCredentialsFromSmartCard(){
CryptAcquireContext(hProv, containerName, cardName, PROV_RSA_FULL, CRYPT_SILENT)
CryptGetUserKey(*hProv, AT_KEYEXCHANGE, &hCryptKey);
CryptGetKeyParam(hCryptKey, KP_CERTIFICATE, encodedCert, &size, 0);
MySendCertToRemote(encodedCert,size);
}
On Remote workstation running in custom credential provider:
HRESULT CSampleCredential::GetSerialization(
_Out_ CREDENTIAL_PROVIDER_GET_SERIALIZATION_RESPONSE *pcpgsr,
_Out_ CREDENTIAL_PROVIDER_CREDENTIAL_SERIALIZATION *pcpcs,
_Outptr_result_maybenull_ PWSTR *ppwszOptionalStatusText,
_Out_ CREDENTIAL_PROVIDER_STATUS_ICON *pcpsiOptionalStatusIcon){
MyReadCertFromRemote(&cert,&size);
CryptAcquireContext(&hCryptProv, NULL, NULL, PROV_RSA_FULL, 0);
CryptCreateHash(hCryptProv, CALG_SHA1, NULL, 0, &hHash);
CryptHashData(hHash, (BYTE *)cert, (DWORD)size, 0);
CERT_CREDENTIAL_INFO certInfo;
CryptGetHashParam(hHash, HP_HASHVAL, certInfo.rgbHashOfCert, &dwHashLen, 0);
CredMarshalCredential(CertCredential, &certInfo, marshalledCred);
/****************************for debug only*****************************/
LogonUser((*marshalledCred, NULL, NULL, LOGON32_LOGON_NEW_CREDENTIALS, LOGON32_PROVIDER_WINNT50, &userHandle);
//this reuturns 1 (success)
/**************************************************************************/
KERB_INTERACTIVE_UNLOCK_LOGON kiul;
CREDENTIAL_PROVIDER_USAGE_SCENARIO cpus
KerbInteractiveUnlockLogonInit(L"", (PWSTR)marshaledCred, L"", cpus, &kiul);//(note:I've tried this with the third param set to the card's pin)
KerbInteractiveUnlockLogonPack(kiul, &pcpcs->rgbSerialization, &pcpcs->cbSerialization);
RetrieveNegotiateAuthPackage(&ulAuthPackage);
pcpcs->ulAuthenticationPackage = ulAuthPackage;
pcpcs->clsidCredentialProvider = CLSID_OfProvider;
*pcpgsr = CPGSR_RETURN_CREDENTIAL_FINISHED;
}
LogonUI.exe displays "A specified logon session does not exist. It may already have been terminated." This seams to correspond to Windows error code 1312. It seems that most developers struggling with this error code are having trouble configuring IIS with an SSL cert. I have found no reference to this error in relation to a credential provider.
Various threads seem to indicate that LogonUser is mearly a wrapper around LSALogonUser and the returned values from ICredentialProviderCredential::GetSerialization will be passed to LSALogonUser. The fact that my call to LogonUser with the marshalled credential succeeds would seem to suggest that on I'm on the right track, but I haven't been able to verify that nor figure out what I'm missing.
I would like to try to avoid solutions that require storing the credential on the remote workstation as it is typically a domain admin credential.
Any ideas anybody? Any advise on how to proceed?
Thanks
I guess you can do it using a communication mechanism where you send your data from one workstation to another one and in the remote one you serialize your data in SetSerialization() instead of GetSerialization(), preparing all the fields and processing the logon.
From my understanding point of you, the Provider class (the one inherited by ICredentialProvider) has to wait for those credentials somehow and pass them to SetSerialization
Related
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;
}
Issue:
I successfully call CoSetProxyBlanket on a proxy (if that's the right term for it) and then I call QueryInterface on that same proxy, but I receive a result of 0x80070005 ("Access Denied"). However, if I first call CoInitializeSecurity (which I am trying to avoid) with that same credentials then the call succeeds.
Question:
How can I successfully get the interface I need without having to call CoInitializeSecurity? From what I understand, a process can only call this method once so its not compatible with making a dll and can usually be substituted with calls to CoSetProxyBlanket.
Details:
I am experimenting with building my own OPC client that can communicate to computers running on different domains without matching user accounts.
First, I create an identity structure with a domain, username, and password that are valid on the server:
COAUTHINFO authInfo;
COAUTHIDENTITY authIdentity;
authIdentity.Domain = (unsigned short *) w_domain;
authIdentity.DomainLength = wcslen( w_domain);
authIdentity.Flags = SEC_WINNT_AUTH_IDENTITY_UNICODE;
authIdentity.Password = (unsigned short *) w_password;
authIdentity.PasswordLength = wcslen(w_password);
authIdentity.User = (unsigned short *) w_username;
authIdentity.UserLength = wcslen(w_username);
authInfo.dwAuthnLevel = RPC_C_AUTHN_LEVEL_CALL;
authInfo.dwAuthnSvc = RPC_C_AUTHN_WINNT;
authInfo.dwAuthzSvc = RPC_C_AUTHZ_NONE;
authInfo.dwCapabilities = EOAC_NONE;
authInfo.dwImpersonationLevel = RPC_C_IMP_LEVEL_IMPERSONATE;
authInfo.pAuthIdentityData = &authIdentity;
authInfo.pwszServerPrincName = NULL;
ServerInfo.pAuthInfo = &authInfo;
Then I am able to call CoCreateInstanceEx with this server info an obtain a handle (m_IOPCServer) to my OPC server (IID_IOPCServer).
After I obtain the handle, I've found that it is necessary to once again set more permissions (see How does impersonation in DCOM work?) with this call:
hr = CoSetProxyBlanket(m_IOPCServer, RPC_C_AUTHN_WINNT, RPC_C_AUTHZ_NONE,
NULL, RPC_C_AUTHN_LEVEL_CALL, RPC_C_IMP_LEVEL_IMPERSONATE,
&authIdentity, EOAC_NONE);
After this I am able to successfully obtain a handle to a OPC Item Group:
hr = m_IOPCServer->AddGroup(L"", FALSE, reqUptRate, clientHandle,
NULL, NULL, lcid, &m_hServerGroup, &revisedUptRate,
IID_IOPCItemMgt,(LPUNKNOWN*)&m_IOPCItemMgt);
However, when I try to use this code:
hr = m_IOPCItemMgt->QueryInterface(IID_IOPCSyncIO, (void**)&m_IOPCSyncIO);
The result is 0x80070005 ("Access Denied"). This is the case even if I successfully call CoSetProxyBlanket on m_IOPCItemMgt. However if I first call CoInitializeSecurity, then the call succeeds.
I believe the issue related to How does impersonation in DCOM work? in that the QueryInterface function is a form of object creation so it doesn't use the same security as the other method calls like AddGroup. However in the Microsoft reference QueryInterface, under notes to implementer, it makes it sound like QueryInterface shouldn't be checking ACLs and under return values, Access Denied is not mentioned as a possibility. I don't think that this issue is implementation specific though because I have tried my code on some well known commercial OPC servers (e.g. Matrikon Simulation Server) as well as the opensource LightOPC which doesn't implement any extra security.
I am guessing that what I need to do is to find a way to replicate this command
hr = m_IOPCItemMgt->QueryInterface(IID_IOPCSyncIO, (void**)&m_IOPCSyncIO);
but do so while also supplying authIdentity. Is this possible? Can it be done with CoCreateInstanceEx or CoGetClassObject or some other COM call?
Without going into too much detail: CoInitializeSecurity is always invoked at least once per process. This can be done implicitly or explicitly. If your code doesn't make an explicit call, the DCOM runtime does it for you with parameters populated from the registry. You can try to tweak the appropriate registry values to force DCOm using values similar to those used in your explicit call. The registry key that holds those values is "HKEY_LOCAL_MACHINE\SOFTWARE\Classes\AppID{AppID_GUID}" This key is described here:https://msdn.microsoft.com/en-us/library/windows/desktop/ms693736(v=vs.85).aspx
You have to call CoSetProxyBlanket on every new COM object instance, so in your case you have to call it even for m_IOPCItemMgt.
You need to call CoSetProxyBlanket in the IUnknown interface before using QueryInterface
CComPtr<IUnknown> pUnknown;
hr = m_IOPCItemMgt->QueryInterface(IID_IUnknown, (void**)&pUnknown);
hr = CoSetProxyBlanket(pUnknown, RPC_C_AUTHN_WINNT, RPC_C_AUTHZ_NONE,
NULL, RPC_C_AUTHN_LEVEL_CALL, RPC_C_IMP_LEVEL_IMPERSONATE,
&authIdentity, EOAC_NONE);
hr = pUnknown->QueryInterface(IID_IOPCSyncIO, (void**)&m_IOPCSyncIO);
You can check this for more info.
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.)
This question already has answers here:
How to check if a process has the administrative rights
(2 answers)
Closed 8 years ago.
I have the window handle of the application. How do I find out whether the application is running in administrator mode?
Just a simple answer, which will basically point you to some API calls to read up on.
First up you need to get the ID of the owning process, then get a handle to that process:
DWORD proc_id = 0U;
DWORD thread_id = GetWindowThreadProcessId(hWnd, &proc_id);
// assuming it all works, open the process
HANDLE hProc = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, proc_id);
The PROCESS_QUERY_INFORMATION means you're trying to open the process with rights to query its information... such as its token, etc. You then have a couple of options - which you use depends partly on why you need the information.
Option 1 is to get Security information about the process using the GetSecurityInfo API. This will allow you to request information such as the owner (SID) of the process, Security Access information (SACL/DACL), and so on. Off the top of my head, something like this:
GetSecurityInfo(hProc, SE_KERNEL_OBJECT, ...);
The parameters you pass in will depend on what information you want. Bear in mind that you may not have rights to request this information.
Option 2 is to get the Process Token, which allows you to get similar information in a different way. You do this by calling OpenProcessToken and then GetTokenInformation. Again, the parameters you pass will depend on for what purpose you want the information. Something like this should get you started:
HANDLE hToken;
OpenProcessToken(hProc, TOKEN_READ, &hToken);
GetTokenInformation(hToken, TOKEN_USER, ...); // get the user associated with the token
Note that I've tried none of this, I simply looked on MSDN and made educated guesses. I've also put in no error checking... bear in mind that if you're querying processes, then it's likely that some of the calls will fail as you won't have permission to request that information.
You can use following piece of code which returns TRUE if User is admin or False if not.
BOOL IsUserAnAdmin()
For more refrence: http://msdn.microsoft.com/en-us/library/windows/desktop/bb776463(v=vs.85).aspx
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.