Classes of Impersonation

In general, the Win32 API has two sets of functions that allow impersonation of clients . The first set of functions is based on some token or connection representing the client; the second set is based on some concrete knowledge of the client and a way to authenticate as if the server process were the client. The first set of functions is easier to use and has the added bonus of being the easiest to administer. Of course, this set of functions is not necessarily the best solution for all systems just because it is easier to use.

Impersonation Based on a Connection

The easiest way to impersonate a client is to leverage what the system knows about a client and use that knowledge as the basis for impersonating the client. Three Win32 functions allow this to occur:

 BOOLImpersonateNamedPipeClient(HANDLEhNamedPipe//Handleofanamedpipe); BOOLDdeImpersonateClient(HCONVhConv//HandletoDDEconversation); RPC_STATUSRPC_ENTRYRpcImpersonateClient(RPC_BINDING_HANDLEBindingHandle); 

Dynamic Data Exchange (DDE) and RPC are beyond the scope of this book, but impersonation for DDE and RPC follows the same model as named pipe impersonation. Named pipes will be covered in more detail in Chapter 12. For this discussion, the named pipe handle can simply be seen as a black box. There are also impersonation methods associated with some COM interfaces, notably IServerSecurity . At their core , these methods employ RPC impersonation when used by Distributed COM. RPC impersonation is also beyond the scope of this book, but you can find out more about it in some of the books listed in the bibliography.

In named pipe impersonation, the server first obtains a named pipe handle, often after a call to ConnectNamedPipe . Once ConnectNamedPipe returns successfully, the named pipe handle can be used in a call to ImpersonateNamedPipeClient . Once that call is made, the server application can make calls on behalf of the client. What happens if the client does not want to allow the server to use its identity?

In the case of a named pipe, the client makes a connection using the CreateFile function. The flags that you send to that function control the level of impersonation allowed. These flags are covered in the SDK documentation for CreateFile , but in general, the client can choose to allow or disallow any impersonation on the part of the server or even allow or prevent the server's discovery of the identity of the client. The client can even allow impersonation only on remote systems. Clients usually do not manipulate these settings, and the client and server are often created together to allow them to work cooperatively. For the examples in this chapter, let's assume that the client will have allowed impersonation, though checking the return code from ImpersonateNamedPipeClient will, of course, allow detection of a client who doesn't want to allow impersonation.

Once the named pipe client has been impersonated, what is the next step? Most often, the program can simply do what it had planned to do and remain secure in the knowledge that if it performs an operation not allowed for the client being impersonated, the Win32 function will fail ”commonly with an error 5 (access denied ). When the impersonation is complete, the server can simply call the RevertToSelf Win32 API function, with the following simple prototype:

 BOOLRevertToSelf(VOID); 

If RPC was being used rather than a named pipe, either RpcRevertToSelf or RpcRevertToSelfEx is called instead. Chapter 12 will cover the use of ImpersonateNamedPipeClient in more detail in conjunction with communication APIs.

Impersonation Based on Information About the Client

A second way to impersonate a client is to know the client's user name and password. This obviously presents a problem: the server must somehow track the user names and corresponding passwords of the users involved. There is no easy way to directly determine the password of a user, since that information is not stored in an accessible way within a Windows 2000 system. If a system is administered so that passwords must be changed periodically, tracking user names and passwords can be even more difficult.

Keep in mind the preceding warnings about the difficulty of this type of impersonation. The way to impersonate some user whose user name and password you have is with the function LogonUser , using the prototype below:

 BOOLLogonUser(LPTSTRlpszUsername,//Stringthatspecifiestheusername LPTSTRlpszDomain,//Stringthatspecifiesthedomainor //server LPTSTRlpszPassword,//Stringthatspecifiesthepassword DWORDdwLogonType,//Specifiesthetypeoflogonoperation DWORDdwLogonProvider,//Specifiesthelogonprovider PHANDLEphToken//Pointertovariabletoreceivetoken //handle); 

If you search the Usenet Win32 programming newsgroups for "LogonUser," you will see more than a hundred matches, indicating that this function causes more than its share of problems for users. I will discuss some of the potential problems with using LogonUser after a brief description of the parameters.

The first three parameters are pointers to strings containing the user name, domain, and password respectively. The dwLogonType parameter contains the type of logon operation to be performed. Table 4-1 explains the possible values of this parameter as well as the implications of each type of logon.

The implications of the various logon types go beyond the simple selection of an appropriate logon type. Each type requires specific rights on the part of the user being passed to LogonUser . Unfortunately, the documentation is a bit sparse on specifics. In general, if LogonUser fails, the reason for the failure is the absence of the required rights.

The dwLogonProvider parameter tells LogonUser which logon provider to use. There are specific providers for Windows NT versions 3.5 and 4, as well as a Windows 2000 provider, identified as LOGON32_PROVIDER_WINNT50. You'll have virtually no reason to use any of these. Instead, use the default provider LOGON32_PROVIDER_DEFAULT. This provides maximum compatibility with future Windows releases.

Listing 4-1 is a simple console mode application named logonuser.exe that uses LogonUser to obtain a token for another user and then creates a process ”whoami.exe ”on behalf of the new user that operates in the context of the new user.

Table 4-1 Values for the dwLogonType Parameter

Value Description
LOGON32_LOGON_BATCH Intended for batch servers whose processing will not involve direct interaction with a user. Requires that user has "Log on as a batch job" rights.
LOGON32_LOGON_INTERACTIVE Intended for users who will be interactively using the machine ”for instance, being logged on to a terminal server. This method will cache logon information.
LOGON32_LOGON_SERVICE Intended for service type logon. The user account must have the "Log on as service" privilege enabled.
LOGON32_LOGON_NETWORK Intended for high-performance servers that authenticate clear text passwords. This type of logon has several limitations. First, the function will return an impersonation token, which, for instance, cannot be used directly with the CreateProcessAsUser Win32 API function. Second, even if the conversion from an impersonation token to a primary token is done, any process created with that converted token cannot access network resources.

The whoami.exe application is another simple console mode application that only prints the name of the user, in this case proving that the process is being created as the new user. Listing 4-2 is the source code for whoami.exe. Figure 4-3 illustrates the results when I ran whoami.exe, showing that I am currently logged on as the administrator. Next I ran logonuser.exe, which first logs on the user tester and then creates a process named tester that prints "Hello tester!", indicating that this process is running in the context of the user passed to LogonUser .

Listing 4-1

LogonUser.cpp

 #include"stdafx.h" intmain(intargc,char*argv[]) { HANDLEh; BOOLret; DWORDdwCreationFlags=0; STARTUPINFOstartupInfo; PROCESS_INFORMATIONprocessInformation; //Obviously,thefirstthreeparameterstoLogonUsershouldbe //modifiedfortheparticularuser. ret=LogonUser("tester","ACCESS","test", LOGON32_LOGON_BATCH, LOGON32_PROVIDER_DEFAULT,&h); printf("HelloWorld!Ret=%d\n",ret); printf("LastError%d\n",GetLastError()); memset((void*)&startupInfo,' 
 #include  "stdafx.h" int  main(int  argc,  char*  argv[]) {         HANDLE  h;         BOOL  ret;         DWORD  dwCreationFlags  =  0;         STARTUPINFO  startupInfo;         PROCESS_INFORMATION  processInformation;         //  Obviously,  the  first  three  parameters  to  LogonUser  should  be         //    modified  for  the  particular  user.         ret  =  LogonUser("tester",  "ACCESS",  "test",                 LOGON32_LOGON_BATCH,                 LOGON32_PROVIDER_DEFAULT,  &h);         printf("Hello  World!    Ret  =  %d\n",  ret);         printf("Last  Error  %d\n",  GetLastError());         memset((void  *)&startupInfo,  '\0',  sizeof(STARTUPINFO)); memset ((void  *)&processInformation,  '\0',                 sizeof(PROCESS_INFORMATION));         ret  =  CreateProcessAsUser(h,  "whoami.exe",                  NULL,  NULL,  NULL,  FALSE,                 dwCreationFlags,  NULL,  NULL,                 &startupInfo,  &processInformation);         //  Wait  until  the  created  process  exits  to  make  sure         //    whoami.exe  exits  first.         WaitForSingleObject(&processInformation.dwProcessId,  1000);         CloseHandle(h);         CloseHandle((HANDLE)processInformation.dwProcessId);         CloseHandle((HANDLE)processInformation.dwThreadId);         return  0; } 
',sizeof(STARTUPINFO)); memset((void*)&processInformation,'
 #include  "stdafx.h" int  main(int  argc,  char*  argv[]) {         HANDLE  h;         BOOL  ret;         DWORD  dwCreationFlags  =  0;         STARTUPINFO  startupInfo;         PROCESS_INFORMATION  processInformation;         //  Obviously,  the  first  three  parameters  to  LogonUser  should  be         //    modified  for  the  particular  user.         ret  =  LogonUser("tester",  "ACCESS",  "test",                 LOGON32_LOGON_BATCH,                 LOGON32_PROVIDER_DEFAULT,  &h);         printf("Hello  World!    Ret  =  %d\n",  ret);         printf("Last  Error  %d\n",  GetLastError());         memset((void  *)&startupInfo,  '\0',  sizeof(STARTUPINFO)); memset ((void  *)&processInformation,  '\0',                 sizeof(PROCESS_INFORMATION));         ret  =  CreateProcessAsUser(h,  "whoami.exe",                  NULL,  NULL,  NULL,  FALSE,                 dwCreationFlags,  NULL,  NULL,                 &startupInfo,  &processInformation);         //  Wait  until  the  created  process  exits  to  make  sure         //    whoami.exe  exits  first.         WaitForSingleObject(&processInformation.dwProcessId,  1000);         CloseHandle(h);         CloseHandle((HANDLE)processInformation.dwProcessId);         CloseHandle((HANDLE)processInformation.dwThreadId);         return  0; } 
', sizeof(PROCESS_INFORMATION)); ret=CreateProcessAsUser(h,"whoami.exe", NULL,NULL,NULL,FALSE, dwCreationFlags,NULL,NULL, &startupInfo,&processInformation); //Waituntilthecreatedprocessexitstomakesure //whoami.exeexitsfirst. WaitForSingleObject(&processInformation.dwProcessId,1000); CloseHandle(h); CloseHandle((HANDLE)processInformation.dwProcessId); CloseHandle((HANDLE)processInformation.dwThreadId); return0; }

Listing 4-2

WhoAmI.cpp

 #include"stdafx.h" intmain(intargc,char*argv[]) { charlpBuffer[255]; DWORDnSize=255; GetUserName(lpBuffer,&nSize); printf("Hello%s!\n",lpBuffer); return0; } 

The CreateProcessAsUser Win32 API function is virtually identical to the CreateProcess function, with the addition of the handle to the token that is retrieved by calling LogonUser or one of the impersonation functions.

click to view at full size.

Figure 4-3 Testing the LogonUser Win32 API function.

Protecting Server-Specific Resources

What if the service needed to protect a resource that was not natively protected by the operating system? NTFS disk volumes and registry keys have SDs associated with them, but other server-specific resources might not. Is there a way to use the information stored in the Windows 2000 user database to grant or deny access to server-specific resources? Thankfully, the answer is yes.

Internally, the Win32 functions use a function called AccessCheck to determine whether a given user has the proper authority to perform a given operation. AccessCheck is a complicated function that has the following prototype:

 BOOLAccessCheck(PSECURITY_DESCRIPTORpSecurityDescriptor,//Securitydescriptor HANDLEClientToken,//Handletoclientaccess //token DWORDDesiredAccess,//Requestedaccessrights PGENERIC_MAPPINGGenericMapping,//Mapgenerictospecific //rights PPRIVILEGE_SETPrivilegeSet,//Receivesprivilegesused LPDWORDPrivilegeSetLength,//Sizeofprivilege-setbuffer LPDWORDGrantedAccess,//Retrievesmaskofgranted //rights LPBOOLAccessStatus//Retrievesresultsofaccess //check); 

The first parameter, pSecurityDescriptor , is a pointer to a security descriptor ”a structure I described earlier in the chapter. Generally , SDs are filled in by the operating system and obtained from the operating system, but they can also be created by an application to describe who has rights to an application-provided resource. (I'll discuss this later in the chapter.) The second parameter, ClientToken , is an impersonation token. This token can be obtained from ImpersonateNamedPipeClient or any of the other impersonation functions. There are specific access levels to the underlying token that the handle must have. These access levels are described in some detail in the MSDN documentation.

The DesiredAccess parameter contains the rights that should be tested for by AccessCheck . GenericMapping is a structure that maps object-specific rights to a set of generic rights. Generic rights include read, write, execute, and all. All Windows 2000 objects have a set of rights associated with them. Many objects will require only the generic rights for reading, writing, and executing. For some objects, additional rights are needed. For example, the registry needs to have rights defined for setting a key, creating a subkey , enumerating subkeys, and so on. A directory or a file has no need for these rights. By the same token, the execute right is meaningful for a file but has no meaning in regard to registry keys. Windows 2000 uses a unified security model; all objects are therefore protected by the same security system. This model created the need to map a specific set of rights to some generic rights. Note that all objects have all the generic rights, so even a registry key has the execute right, although the right is generally not used.

Interestingly, Windows 2000 continues the practice of making registry security settings somewhat obscure to the average user. Run REGEDIT on a Windows 2000 machine and you can search high and low and not find a trace of information on permissions for the keys displayed. You must run REGEDT32 to do that, just as on Windows NT 4. Once on the registry key in question, select "Permissions " from the Security menu. If you assign a user or group "Special Access," the dialog in Figure 4-4 will appear, enumerating the various rights that can be assigned to registry entries.

Why is this hidden in the less commonly used REGEDT32? In general, the registry seems to have been created as an effort to avoid the random tinkering that INI files were subject to. Placing security settings in an "advanced" registry tool probably seemed like a reasonable way to prevent users from hurting themselves .

Figure 4-4 Dialog to set registry permissions.

PrivilegeSet points to a PRIVILEGE_SET structure that contains a count of the privileges, a bitflag specifying if all specified privileges are needed, and a pointer to an array of LUID_AND_ATTRIBUTES structures. The LUID_AND_ATTRIBUTES structures contain a locally unique identifier (LUID) and its attributes, represented by up to 32 one-bit flags. PrivilegeSetLength specifies the size, in bytes, of the buffer pointed to by PrivilegeSet .

GrantedAccess and AccessStatus together provide the results of the function call. If the requested access is allowed, the DWORD pointed to by AccessStatus is set to TRUE; otherwise it is set to FALSE. If the function fails (indicated by a return value of zero), GrantedAccess is not set, and you can presume that whatever value was there before the call will remain. In case of a zero return value, GetLastError will return the error that caused the failure.

Putting Impersonation and Custom Security Together

Given the availability of the AccessStatus function, how do you build custom security for objects within an application? There are three basic steps, but as the code to follow shows, there are also many complications.

  1. The application must create an SD and a DACL for the object to be secured. The DACL will need to have an access-allowed ACE for each user or group who needs to have access to the secured portion of the server. The SD, by default, is not something that can be persisted to disk, but can be made storable through a call to MakeSelfRelativeSD , a Win32 API function covered in the SDK documentation.
  2. The application must get the client's token. One method to do this is to use LogonUser , as shown in Listing 4-1. Another way to obtain the token is to use one of the impersonation functions.
  3. AccessCheck must be called with the client token and the SD constructed in step 1.

Listing 4-3 shows the code for a simple application. This application creates its own SD for a resource it will check for rights. It then logs on as another user by using LogonUser and checks that user's SD.

Listing 4-3

CheckSecurity.cpp

 //CheckSecurity.cpp:ModeledaftercodeinMSDNarticleQ171273 // #include"stdafx.h" #defineREAD1 #defineWRITE2 #defineEXECUTE4 #defineBUF_SIZE256 voidmain(intargc,char*argv[]) { PSECURITY_DESCRIPTORpSD; //User/SIDVariables: DWORDcbBuffer=BUF_SIZE; PSIDpUserSid; //ACEvariables: DWORDdwAccessMask=READWRITE; PACLpACL; DWORDdwACLSize; //AccessCheckvariables: DWORDdwAccessDesired; PRIVILEGE_SETPrivilegeSet; DWORDdwPrivSetSize; DWORDdwAccessGranted; BOOLbAccessGranted=FALSE; GENERIC_MAPPINGGenericMapping; //Usedtolookupaccountname,LogonUser charszSystem[BUF_SIZE]; charszAccountName[BUF_SIZE]; charszDomain[BUF_SIZE]; charszPassword[BUF_SIZE]; DWORDcBytesSID=BUF_SIZE; DWORDcBytesDomain=BUF_SIZE; HANDLEhUser; BOOLret; SID_NAME_USESidUser; //Obviously,theseshouldbemodifiedfortheparticularuser. strcpy(szSystem,"DUAL"); strcpy(szDomain,"DUAL"); strcpy(szAccountName,"tester"); strcpy(szPassword,"test"); pUserSid=(PSID)LocalAlloc(LPTR,BUF_SIZE); if(LookupAccountName(szSystem,szAccountName,pUserSid, &cBytesSID,szDomain,&cBytesDomain,&SidUser)==0) { printf("Error%dLookupAccountName\n",GetLastError()); } pSD=LocalAlloc(LPTR,SECURITY_DESCRIPTOR_MIN_LENGTH); if(!InitializeSecurityDescriptor(pSD,SECURITY_DESCRIPTOR_REVISION)) { printf("Error%d:InitializeSecurityDescriptor\n", GetLastError()); } // //ComputesizeneededfortheACL. // dwACLSize=sizeof(ACCESS_ALLOWED_ACE)+8+ GetLengthSid(pUserSid)-sizeof(DWORD); // //AllocatememoryforACL. // pACL=(PACL)LocalAlloc(LPTR,dwACLSize); // //InitializethenewACL. // if(!InitializeAcl(pACL,dwACLSize,ACL_REVISION2)) { printf("Error%d:InitializeAcl\n",GetLastError()); } // //Addtheaccess-allowedACEtotheDACL. // if(!AddAccessAllowedAce(pACL,ACL_REVISION2, dwAccessMask,pUserSid)) { printf("Error%d:AddAccessAllowedAce", GetLastError()); } // //SetourDACLtotheSD. // if(!SetSecurityDescriptorDacl(pSD, TRUE, pACL, FALSE)) { printf("Error%d:SetSecurityDescriptorDacl", GetLastError()); } // //AccessCheckispickyaboutwhatisintheSD.Set //thegroupandownerusingourconvenientSID. // SetSecurityDescriptorGroup(pSD,pUserSid,FALSE); SetSecurityDescriptorOwner(pSD,pUserSid,FALSE); //Herewelogonastheuserspecifiedabove... ret=LogonUser(szAccountName,szDomain,szPassword, LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT,&hUser); //...andthenimpersonatethatuser. ImpersonateLoggedOnUser(hUser); dwAccessDesired=READ; memset(&GenericMapping,0xff,sizeof(GENERIC_MAPPING)); GenericMapping.GenericRead=READ; GenericMapping.GenericWrite=WRITE; GenericMapping.GenericExecute=EXECUTE; GenericMapping.GenericAll=READWRITEEXECUTE; // //Thisonlydoessomethingifwewanttousegenericaccess //rights,suchasGENERIC_ALL,inourcalltoAccessCheck. //(Wearenotusinggenericaccessrights.) // MapGenericMask(&dwAccessDesired,&GenericMapping); dwPrivSetSize=sizeof(PRIVILEGE_SET); // //Makethecall. // if(!AccessCheck(pSD, hUser, dwAccessDesired, &GenericMapping, &PrivilegeSet, &dwPrivSetSize, &dwAccessGranted, &bAccessGranted)) { printf("ErrorinAccessCheck:%d\n",GetLastError()); } else { if(bAccessGranted) { printf("Accesswasgrantedusingmask%lx\n", dwAccessGranted); } else { printf("AccesswasNOTgranted!\n"); } } RevertToSelf(); LocalFree(pACL); LocalFree(pSD); LocalFree(pUserSid); } 

First some strings are initialized to allow the program to log on as and impersonate another user. Next the SID for that account is looked up, using LookupAccountName . This function accepts a series of parameters, most of which are self-explanatory.

 BOOLLookupAccountName(LPCTSTRlpSystemName,//Addressofstringforsystemname LPCTSTRlpAccountName,//Addressofstringforaccountname PSIDSid,//Addressofsecurityidentifier LPDWORDcbSid,//Addressofsizeofsecurityidentifier LPTSTRReferencedDomainName, //Addressofstringforreferenced //domain LPDWORDcbReferencedDomainName, //Addressofsizeofdomainstring PSID_NAME_USEpeUse//AddressofSID-typeindicator); 

The lpSystemName parameter can be used to specify a system where the account name will be looked up. This parameter can be NULL to have the lookup take place on the local machine. The Sid parameter points to a buffer that will hold the SID for the account if the function succeeds. A PSID is typedefed as a PVOID in the standard Windows header files, meaning that there is nothing to be derived from the value pointed to ”it should be accepted as an opaque handle. The peUse parameter is a pointer to a SID-type indicator. The value pointed to by this parameter will be set to equal one of the values in an enumerated type that includes such values as SidTypeUser , SidTypeGroup , SidTypeDomain , and so on. In Listing 4-3, the account name being looked up is a user, so SidTypeUser is returned.

We allocate some buffers using LocalAlloc and then initialize them using Win32 API functions. The SD is initialized through a call to InitializeSecurityDescriptor , and an ACL is initialized using InitializeAcl . We now call AddAccessAllowed , adding an ACL record for the user we are trying to impersonate by using the access mask previously set to READ WRITE. Next we set the SD ACL through a call to SetSecurityDescriptorAcl . We then set some additional fields in the SD as required by AccessCheck .

We log on as the user for whom we have the SID and then we set generic mappings to the mapping flags we have previously set. The generic mappings are then mapped using MapGenericMask . Finally, we can make the call to AccessCheck that determines whether the user has the access required ”READ access in our example. Of course, since we have just manually set the SD we already know that the access is allowed, but we check for either function failure or an indication that the user does not have access to the right we are requesting. We could have also requested what the maximum access allowed was instead of checking for a particular right by passing MAXIMUM_ALLOWED as the access desired in the dwAccessDesired parameter to AccessCheck.



Inside Server-Based Applications
Inside Server-Based Applications (DV-MPS General)
ISBN: 1572318171
EAN: 2147483647
Year: 1999
Pages: 91

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net