Search code examples
c#.netimpersonationdcom

How to impersonate another user for a DCOM connection in C#?


I am trying to connect to a DCOM object on a remote machine which is logged in as a different user from a C# application. I have gotten my code working to connect to the same DCOM object while it is running on the same computer, like so:

MyDCOMType dcomObject;
Type remoteSessionContextType = Type.GetTypeFromProgID("ServerApp.MyDCOMType");
dcomObject = (MyDCOMType)Activator.CreateInstance(remoteSessionContextType);
string version = dcomObject.GetVersion();

MyDCOMType is a type implemented in the DCOM application (a VB6 app) that I added as a reference in my project. I have been able to create an instance of it in the C# code and call all of the methods with the expected results. Now I am trying to get it to connect to the same object on the remote machine. I am attempting to impersonate the user on the remote system like so (variable definitions and error handling omitted):

if (LogonUser(
       userName,
       domain,
       password,
       LOGON32_LOGON_NEW_CREDENTIALS,
       LOGON32_PROVIDER_WINNT50,
       out token) != false)
{
      if (DuplicateToken(token, (int)_SECURITY_IMPERSONATION_LEVEL.SecurityDelegation, out tokenDuplicate) != false)
      {
            impersonationContext = WindowsIdentity.Impersonate(tokenDuplicate.DangerousGetHandle());
      }
 }

Which is then followed by similar code to create the object:

MyDCOMType dcomObject;
Type remoteSessionContextType = Type.GetTypeFromProgID("ServerApp.MyDCOMType", ipAddress);
dcomObject = (MyDCOMType)Activator.CreateInstance(remoteSessionContextType);
string version = dcomObject.GetVersion();

Except that the CreateInstance call throws an UnauthorizedAccessException error with error code 80070005 for unauthorized access.

The other thing is that I have C++ code that does the same thing that works perfectly. That code goes like so:

COAUTHINFO      AuthInfo;
COAUTHIDENTITY  AuthIdentity;
COSERVERINFO    ServerInfo;
MULTI_QI        Results;
HRESULT     hr;
BSTR        version;
_MyDCOMType     *pSession = NULL;

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.dwReserved1  = 0;
ServerInfo.dwReserved2  = 0;
ServerInfo.pAuthInfo    = &AuthInfo;
ServerInfo.pwszName     = w_nodename;

hr = CoCreateInstanceEx(clsid, NULL, CLSCTX_ALL, &ServerInfo, (ULONG) 1, &Results);
hr = CoSetProxyBlanket(Results.pItf, RPC_C_AUTHN_WINNT, RPC_C_AUTHZ_NONE, NULL,
    RPC_C_AUTHN_LEVEL_CALL, RPC_C_IMP_LEVEL_IMPERSONATE, &AuthIdentity, EOAC_NONE);
pSession = (_MyDCOMType *)Results.pItf;

hr = pSession->raw_GetVersion(&version);

It connects to the remote server, creates the DCOM object, and gets data from it just fine. I'd like to figure out how to get my C# code to do the same. Any ideas? Did I mess up one of the enums going to the functions? Am I missing some critical call or doing something all wrong? As background, my computer and the computer I am trying to connect to are both logged in under domain accounts, but neither has permissions on the other. The C++ code impersonates the remote user for the purpose of the DCOM connection. I am trying to do a .NET impersonation in such a way that it will be valid for the network connection given a manually entered username and password.

Alternatively, if it just isn't possible, I suppose I could write a small C++/CLR DLL with the same C++ code to handle the DCOM connection part and reference it from the C# code, but I'm hoping to avoid the extra complexity.


Solution

  • I banged away on it for a while and made no progress, so I gave up and wrote a C++/CLR DLL, which works perfectly. Code is as follows:

    MyDCOMTalker::MyDCOMTalker(String ^ipAddress, String ^username, String ^password, String ^domain)
    {
        COAUTHINFO      AuthInfo;
        COSERVERINFO    ServerInfo;
        MULTI_QI        Results;
        CLSID           clsid;
        AuthIdentity = new COAUTHIDENTITY;
    
        long        DSStatus;
        BSTR        umVersion;
    
        AuthIdentity->Domain            = (unsigned short *)Marshal::StringToHGlobalAuto(domain).ToPointer();
        AuthIdentity->DomainLength      = domain->Length;
        AuthIdentity->Flags             = SEC_WINNT_AUTH_IDENTITY_UNICODE;
        AuthIdentity->Password          = (unsigned short *)Marshal::StringToHGlobalAuto(password).ToPointer();
        AuthIdentity->PasswordLength    = password->Length;
        AuthIdentity->User              = (unsigned short *)Marshal::StringToHGlobalAuto(username).ToPointer();
        AuthIdentity->UserLength        = username->Length;
    
        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.dwReserved1  = 0;
        ServerInfo.dwReserved2  = 0;
        ServerInfo.pAuthInfo    = &AuthInfo;
        ServerInfo.pwszName     = (LPWSTR)Marshal::StringToHGlobalAuto(ipAddress).ToPointer();
    
        Results.pIID = &_uuidof(_MyDCOMType);
        Results.pItf = NULL;
        Results.hr   = 0;
    
        CoInitialize(NULL);
        Marshal::ThrowExceptionForHR(CLSIDFromProgID(L"MyDcomDLL.MyDCOMType", &clsid));
        Marshal::ThrowExceptionForHR(CoCreateInstanceEx(clsid, NULL, CLSCTX_ALL, &ServerInfo, (ULONG) 1, &Results));
        Marshal::ThrowExceptionForHR(CoSetProxyBlanket(Results.pItf, RPC_C_AUTHN_WINNT, RPC_C_AUTHZ_NONE, NULL, RPC_C_AUTHN_LEVEL_CALL, 
            RPC_C_IMP_LEVEL_IMPERSONATE, AuthIdentity, EOAC_NONE));
        pSession = (_MyDCOMType*)Results.pItf;
        Marshal::ThrowExceptionForHR(pSession->raw_GetVersion(&DSStatus, &umVersion));
        if (DSStatus == 0 && umVersion != NULL)
            version = Marshal::PtrToStringBSTR((System::IntPtr)umVersion);
        else
            version = String::Empty;
    }
    

    With class definition:

    public ref class MyDCOMTalker
    {
    private:
        _MyDCOMType *pSession;
        String ^version;
        COAUTHIDENTITY  *AuthIdentity;
    public: 
        MyDCOMTalker(String ^ipAddress, String ^username, String ^password, String ^domain);
    };
    

    The usual caveats apply - I chopped out the actual functionality and error checking/disposal details for simpler code, and I don't really know C++/CLR or COM and DCOM that well, so there may be things that could be done easier. If you know of any improvements or a way to do it in C#, please comment/answer as appropriate.