Monday 6 June 2011

Removing Administrator Privilages from Process

Ok,

So one of the things I see a lot of is people asking how to gain administrator privileges for their process or thread. Well for once, I had a need to do the opposite, I needed to ensure that my process wasn't executed as and elevated administrator (i.e. the process would only be run as the user, un-elevated).

Long story short, my aim was to show a save dialog where users could select a location to save to and a later process (definitely not running as elevated administrator) would save the file to the location. Clearly if the first process was picking administrator accessible locations, such as program files, the second process would fail to write (or even worse, write to the VirtualStore!) causing headaches.

So pre-amble done, how did I achieve this?

With the help of this great MSDN artical, My first idea was to simply check if the process is using an elevated Administrator group, and using AdjustTokenGroups() I would set the Administrator group to SE_GROUP_USE_FOR_DENY_ONLY. Unfortunately though, we can't modify the administrator group on the currently running process as it also has the SE_GROUP_MANDATORY attribute, which makes it inelligable for changing.

The MSDN document has this to say about it:
The AdjustTokenGroups function cannot disable groups with the SE_GROUP_MANDATORY attribute in the TOKEN_GROUPSstructure. Use CreateRestrictedToken instead.
So luckily, MSDN suggests and alternative, which is to use CreateRestrictedToken() instead.

So the new approach is to:
  1. Create the well known Administrator group SID
  2. Use this to check if the process contains the Administrator SID
  3. Providing the Administrator group SID is present, check if its enabled (A non-elevated administrator user will have the Administrator group present but it won't be enabled.
  4. Create a restricted token, based on the process token, specifying the Administrator Group SID to be disabled.
  5. Create a new duplicated process with restricted token and end the current process.
So you can see, that the only way to shed these administrator privilages is to completed terminate the current process and start a duplicate process, only under the new restricted token.

So for the code:

First we open the processes access token. This tells us what user groups the current process is running under.

if (!OpenProcessToken( GetCurrentProcess(), TOKEN_ASSIGN_PRIMARY | TOKEN_QUERY | TOKEN_DUPLICATE | TOKEN_ADJUST_GROUPS, &hToken )) 
  {
    return hasRestarted;
  }

Next we create the SID for the BUILTIN/Administrators group. This will allow us to see if the current process access token contains the Administrators group

if(! AllocateAndInitializeSid( &SIDAuth, 2,
    SECURITY_BUILTIN_DOMAIN_RID,
    DOMAIN_ALIAS_RID_ADMINS,
    0, 0, 0, 0, 0, 0,
    &pSID) ) 
  {
    CloseHandle(hToken);
    hToken = NULL;
    return hasRestarted;
  }

Next we check if the BUILTIN/Administrators group is present and enabled in the current access token. We need to check if its enabled because the access token can have the administrators group present but it can be denied. This would be the equivilant of running an executable with a user who is an administrator, but not right clicking and selecting 'Run as Administrator'.

BOOL isAdmin = FALSE;
  BOOL ok = CheckTokenMembership(NULL, pSID, &isAdmin);

If the administrators group is present and enabled, we know the user is running as an elevated administrator, so we can start creating our restricted administrator token. We do this by using CreateRestrictedToken(). This method will change our administrator group in the token from being enabled to being deny only. So it's like running as an administrator user, just not elevated.

// Create the SID structure for the administrator SID
    SID_AND_ATTRIBUTES adminSID = {0};
    adminSID.Sid = pSID;

    // Create a restricted token which denies the administrator group
    HANDLE restrictedToken;
    CreateRestrictedToken(hToken,0,1,&adminSID,NULL,NULL,NULL,NULL,&restrictedToken))

And now with our token that restricts the administrator group, we can start our process again.

// Create startup info
      STARTUPINFO si = {0};
      PROCESS_INFORMATION pi = {0};
      si.cb = sizeof( si );
      si.lpDesktop = "winsta0\\default";

      // Get the current executables name
      TCHAR exePath[MAX_PATH];
      GetModuleFileName(NULL,exePath,MAX_PATH);

      // Start the new (non-administrator elevated) restricted process
      hasRestarted = CreateProcessAsUser(restrictedToken,exePath,NULL,NULL,NULL,TRUE,NORMAL_PRIORITY_CLASS,NULL,NULL,&si,&pi);

Its probably a good idea to clean up your resources (handles etc) afterwards (I haven't done this for clarity) and check if the current machine your code is executing on is Vista or later first. If it isn't then we don't have to deal with any UAC stuff anyway, and you may get unexpected results from executing this too!

Hope this helps someone!