2

Scenario

I have a remote computer that I want to run installers (arbitrary executables) on programatically. These installers require two things:

  • They must run in Administrator mode.
  • They must run under a specific user context (Specifically, a local user who is a member of the Administrators group).

This has proven to be very challenging.

It appears as though there are a few external tools that exist that do this, but I am looking for a solution that comes with Windows.

What a valid solution to this problem would look like

From an elevated context (e.g. an elevated batch file or executable program) a valid solution should be able to programatically launch a process in Administrator mode under another user context. Assume that the other user's id and password are available, and that the other user is a member of the Administrators group. Additional restrictions:

  • A valid solution cannot rely on an external tool. Since newer versions of Windows come with .NET and PowerShell by default, these are valid tools to use.
  • A valid solution cannot require user interactions. This means that if a UAC window pops up, or if any user confirmation is required, the solution is invalid.

Please test your solution before posting it to make sure it works! If you are going to provide a link to another solution, please verify that the linked solution works before posting. Many people who claim to have working solutions to this problem in fact do not.

What I have tried

I have tried using Batch Scripts, PowerShell, and C#. As far as I can tell, none of these technologies will accomplish the task. They all suffer from the same fundamental problem - running a task as another user and in Administrator mode are mutually exclusive processes. Let me be more specific:

Why Not Batch

The command that one would use to run under a different user context is Runas, which does not launch the process elevated. There are several external tools that claim to get around this, but as stated earlier these are not permitted.

Why Not PowerShell

The command to start a new process, Start-Process, can elevate a new process and run it as a different user, but not at the same time. I have an open question here referring to this issue. Unfortunately no one has provided a solution, which leads me to believe that it is impossible.

Why Not C#

This also appears to be impossible, as the Process class does not appear to support launching a process in Administrator mode and under a different user's credentials.

Why not an external tool?

This forces me to rely on someone else's code to do the right thing, and I would rather code it up myself than do that. In fact I have a solution that is one step better than relying on someone else, but is rather hackish:

  • Create a task using the Task Scheduler to launch the executable in administrator mode on the specified account at some time in the very distant future.
  • Force the Task to run immediately.
  • Wait to see if the task has finished. Answered here.

Thanks in advance to anyone who tries to help! It is greatly appreciated and I hope that if nothing else, other people are able to find this for the Task Scheduler work around.

Community
  • 1
  • 1
Colin B
  • 367
  • 1
  • 4
  • 12
  • Is the process being launched something you wrote yourself? Do you know about Manifests? – paddy Feb 12 '14 at 01:14
  • @paddy The process is being launched by something I wrote myself. I am unfamiliar with Manifests. – Colin B Feb 12 '14 at 19:01
  • Sounds like you should be using [domain services to install SW](http://technet.microsoft.com/en-us/library/cc181465.aspx) on PCs joined to the domain, maybe through a group policy. – Fozi Feb 12 '14 at 19:14
  • Does the installer need to interact with or be visible to the user? – Harry Johnston Feb 13 '14 at 00:17
  • @Fozi If you can provide an elegant solution to this problem using services, I would love to see it :). – Colin B Feb 13 '14 at 07:59
  • @HarryJohnston The installer will not require user interaction. It would be kind of silly to require absolutely no interaction on launching the installer just to have it pop open a user dialog a second later :P. – Colin B Feb 13 '14 at 08:00
  • @ColinB MS is trying hard to prevent people who have no business in installing SW on PCs on a network to do so. On the other hand, it's really easy to do so if you are the domain administrator. Are you? – Fozi Feb 13 '14 at 16:06
  • @Fozi No I am not the domain administrator. I am running this on a worker role in Azure, which for all intents and purposes is just a VM with Server 2012 on it. The catch is that to install software on it when it is created, it needs to be done without user interaction and under a different user context (just a local user who is a member of Administrators) than the Worker Role itself runs in. – Colin B Feb 13 '14 at 19:00
  • @ColinB I don't see why you could not [connect to it](http://www.windowsazure.com/en-us/documentation/articles/virtual-machines-log-on-windows-server/) and install the SW over Remote Desktop. – Fozi Feb 13 '14 at 19:23
  • @Fozi That is possible but is extremely undesirable for several reasons. For one, it means that every time I spin up a machine I manually have to remote into it and finish setting it up. This means I cannot do things like have machines dynamically spin up and down based on demand, which is what I would like to be doing. – Colin B Feb 14 '14 at 02:49
  • @ColinB I assume you mean that you need clones of that machine. So create a machine, set it up to your heart's content, [create an image from it](http://www.windowsazure.com/en-us/documentation/articles/virtual-machines-capture-image-windows-server/) and create all the other machines from that image. – Fozi Feb 14 '14 at 04:57
  • @Fozi I am using PaaS, not IaaS. This means that I do not create a golden image of an OS, I run an application on an OS that is handed to me. A good breakdown of the relative advantages/disadvantages of PaaS/IaaS can be found [here](http://blogs.msdn.com/b/hanuk/archive/2013/12/03/which-windows-azure-cloud-architecture-paas-or-iaas.aspx). – Colin B Feb 18 '14 at 23:16

1 Answers1

11

OK, so it turns out that CreateProcessWithLogonW function filters the user token, and so does LogonUser. This would seem to leave us stuck, since we don't have the right privileges to correct the problem (see footnote) but it turns out that LogonUser does not filter the token if you use LOGON32_LOGON_BATCH rather than LOGON32_LOGON_INTERACTIVE.

Here's some code that actually works. We use the CreateProcessAsTokenW function to launch the process, because this particular variant requires only SE_IMPERSONATE_NAME privilege, which is granted to administrator accounts by default.

This sample program launches a subprocess which creates a directory in c:\windows\system32, which would not be possible if the subprocess was not elevated.

#define _WIN32_WINNT 0x0501

#include <Windows.h>
#include <Sddl.h>
#include <conio.h>

#include <stdio.h>

wchar_t command[] = L"c:\\windows\\system32\\cmd.exe /c md c:\\windows\\system32\\proof-that-i-am-an-admin";

int main(int argc, char **argv)
{
    HANDLE usertoken;
    STARTUPINFO sinfo;
    PROCESS_INFORMATION pinfo;

    ZeroMemory(&sinfo, sizeof(sinfo));
    sinfo.cb = sizeof(sinfo);

    if (!LogonUser(L"username", L"domain", L"password", LOGON32_LOGON_BATCH, LOGON32_PROVIDER_DEFAULT, &usertoken))
    {
        printf("LogonUser: %u\n", GetLastError());
        return 1;
    }

    if (!CreateProcessWithTokenW(usertoken, LOGON_WITH_PROFILE, L"c:\\windows\\system32\\cmd.exe", command, 0, NULL, NULL, &sinfo, &pinfo)) 
    {
        printf("CreateProcess: %u\n", GetLastError());
        return 1;
    }

    return 0;
}

However, if the target process is a GUI process (including a process with a visible console) it won't display properly. Apparently CreateProcessWithTokenW only assigns the minimum desktop and window station permissions necessary for a process to run, which is not enough to actually display a GUI.

Even if you don't actually need to see the output, there's a risk that the broken GUI will cause functional problems with the program.

So, unless the target process runs in the background, we should probably assign permissions appropriately. In general, it is best to create a new window station and a new desktop, to isolate the target process; in this case, though, the target process is going to be running as admin anyway, so there's no point - we can make life easier by just changing the permissions on the existing window station and desktop.

Edit 24 November 2014: corrected access rights in window station ACE so they will work for non-administrative users. Note that doing this may allow the non-admin user in question to compromise processes in the target session.

#define _WIN32_WINNT 0x0501

#include <Windows.h>
#include <AccCtrl.h>
#include <Aclapi.h>
#include <stdio.h>

wchar_t command[] = L"c:\\windows\\system32\\notepad.exe";

int main(int argc, char **argv)
{
    HANDLE usertoken;
    STARTUPINFO sinfo;
    PROCESS_INFORMATION pinfo;
    HDESK desktop;
    EXPLICIT_ACCESS explicit_access;

    BYTE buffer_token_user[SECURITY_MAX_SID_SIZE];
    PTOKEN_USER token_user = (PTOKEN_USER)buffer_token_user;
    PSECURITY_DESCRIPTOR existing_sd;
    SECURITY_DESCRIPTOR new_sd;
    PACL existing_dacl, new_dacl;
    BOOL dacl_present, dacl_defaulted;
    SECURITY_INFORMATION sec_info_dacl = DACL_SECURITY_INFORMATION;
    DWORD dw, size;
    HWINSTA window_station;

    if (!LogonUser(L"username", L"domain", L"password", LOGON32_LOGON_BATCH, LOGON32_PROVIDER_DEFAULT, &usertoken))
    {
        printf("LogonUser: %u\n", GetLastError());
        return 1;
    }

    if (!GetTokenInformation(usertoken, TokenUser, buffer_token_user, sizeof(buffer_token_user), &dw)) 
    {
        printf("GetTokenInformation(TokenUser): %u\n", GetLastError());
        return 1;
    }

    window_station = GetProcessWindowStation();
    if (window_station == NULL)
    {
        printf("GetProcessWindowStation: %u\n", GetLastError());
        return 1;
    }

    if (!GetUserObjectSecurity(window_station, &sec_info_dacl, &dw, sizeof(dw), &size) && GetLastError() != ERROR_INSUFFICIENT_BUFFER)
    {
        printf("GetUserObjectSecurity(window_station) call 1: %u\n", GetLastError());
        return 1;
    }

    existing_sd = malloc(size);
    if (existing_sd == NULL)
    {
        printf("malloc failed\n");
        return 1;
    }

    if (!GetUserObjectSecurity(window_station, &sec_info_dacl, existing_sd, size, &dw))
    {
        printf("GetUserObjectSecurity(window_station) call 2: %u\n", GetLastError());
        return 1;
    }

    if (!GetSecurityDescriptorDacl(existing_sd, &dacl_present, &existing_dacl, &dacl_defaulted))
    {
        printf("GetSecurityDescriptorDacl(window_station): %u\n", GetLastError());
        return 1;
    }

    if (!dacl_present)
    {
        printf("no DACL present on window station\n");
        return 1;
    }

    explicit_access.grfAccessMode = SET_ACCESS;
    explicit_access.grfAccessPermissions = WINSTA_ALL_ACCESS | READ_CONTROL;
    explicit_access.grfInheritance = NO_INHERITANCE;
    explicit_access.Trustee.pMultipleTrustee = NULL;
    explicit_access.Trustee.MultipleTrusteeOperation = NO_MULTIPLE_TRUSTEE;
    explicit_access.Trustee.TrusteeForm = TRUSTEE_IS_SID;
    explicit_access.Trustee.TrusteeType = TRUSTEE_IS_USER;
    explicit_access.Trustee.ptstrName = (LPTSTR)token_user->User.Sid;

    dw = SetEntriesInAcl(1, &explicit_access, existing_dacl, &new_dacl);
    if (dw != ERROR_SUCCESS) {
        printf("SetEntriesInAcl(window_station): %u\n", dw);
        return 1;
    }

    if (!InitializeSecurityDescriptor(&new_sd, SECURITY_DESCRIPTOR_REVISION))
    {
        printf("InitializeSecurityDescriptor(window_station): %u\n", GetLastError());
        return 1;
    }

    if (!SetSecurityDescriptorDacl(&new_sd, TRUE, new_dacl, FALSE))
    {
        printf("SetSecurityDescriptorDacl(window_station): %u\n", GetLastError());
        return 1;
    }

    if (!SetUserObjectSecurity(window_station, &sec_info_dacl, &new_sd))
    {
        printf("SetUserObjectSecurity(window_station): %u\n", GetLastError());
        return 1;
    }

    free(existing_sd);
    LocalFree(new_dacl);

    desktop = GetThreadDesktop(GetCurrentThreadId());
    if (desktop == NULL)
    {
        printf("GetThreadDesktop: %u\n", GetLastError());
        return 1;
    }

    if (!GetUserObjectSecurity(desktop, &sec_info_dacl, &dw, sizeof(dw), &size) && GetLastError() != ERROR_INSUFFICIENT_BUFFER)
    {
        printf("GetUserObjectSecurity(desktop) call 1: %u\n", GetLastError());
        return 1;
    }

    existing_sd = malloc(size);
    if (existing_sd == NULL)
    {
        printf("malloc failed\n");
        return 1;
    }

    if (!GetUserObjectSecurity(desktop, &sec_info_dacl, existing_sd, size, &dw))
    {
        printf("GetUserObjectSecurity(desktop) call 2: %u\n", GetLastError());
        return 1;
    }

    if (!GetUserObjectSecurity(desktop, &sec_info_dacl, existing_sd, 4096, &dw))
    {
        printf("GetUserObjectSecurity: %u\n", GetLastError());
        return 1;
    }

    if (!GetSecurityDescriptorDacl(existing_sd, &dacl_present, &existing_dacl, &dacl_defaulted))
    {
        printf("GetSecurityDescriptorDacl: %u\n", GetLastError());
        return 1;
    }

    if (!dacl_present)
    {
        printf("no DACL present\n");
        return 1;
    }

    explicit_access.grfAccessMode = SET_ACCESS;
    explicit_access.grfAccessPermissions = GENERIC_ALL;
    explicit_access.grfInheritance = NO_INHERITANCE;
    explicit_access.Trustee.pMultipleTrustee = NULL;
    explicit_access.Trustee.MultipleTrusteeOperation = NO_MULTIPLE_TRUSTEE;
    explicit_access.Trustee.TrusteeForm = TRUSTEE_IS_SID;
    explicit_access.Trustee.TrusteeType = TRUSTEE_IS_USER;
    explicit_access.Trustee.ptstrName = (LPTSTR)token_user->User.Sid;

    dw = SetEntriesInAcl(1, &explicit_access, existing_dacl, &new_dacl);
    if (dw != ERROR_SUCCESS) {
        printf("SetEntriesInAcl: %u\n", dw);
        return 1;
    }

    if (!InitializeSecurityDescriptor(&new_sd, SECURITY_DESCRIPTOR_REVISION))
    {
        printf("InitializeSecurityDescriptor: %u\n", GetLastError());
        return 1;
    }

    if (!SetSecurityDescriptorDacl(&new_sd, TRUE, new_dacl, FALSE))
    {
        printf("SetSecurityDescriptorDacl: %u\n", GetLastError());
        return 1;
    }

    if (!SetUserObjectSecurity(desktop, &sec_info_dacl, &new_sd))
    {
        printf("SetUserObjectSecurity(window_station): %u\n", GetLastError());
        return 1;
    }

    free(existing_sd);
    LocalFree(new_dacl);

    ZeroMemory(&sinfo, sizeof(sinfo));
    sinfo.cb = sizeof(sinfo);

    if (!CreateProcessWithTokenW(usertoken, LOGON_WITH_PROFILE, L"c:\\windows\\system32\\notepad.exe", command, 0, NULL, NULL, &sinfo, &pinfo)) 
    {
        printf("CreateProcess: %u\n", GetLastError());
        return 1;
    }

    return 0;
}

Note the use of LOGON_WITH_PROFILE. This is not necessary to display a GUI, and it slows down launching the process considerably, so remove it if you don't need it - but if you are an administrator, the most likely reason that you are launching a process as a different administrator is that you need something in that administrator's user profile. (Another scenario might be that you need to use a specific domain account in order to access resources on another machine.)


Footnote:

Specifically, you need SeTcbPrivilege in order to use GetTokenInformation and TokenLinkedToken to obtain a usable handle to the elevated token that LogonUser generates. Unfortunately, this privilege is usually only available if you are running as local system.

If you do not have SeTcbPrivilege you can still obtain a copy of the linked token, but in this case it is an impersonation token at SecurityIdentification level so is of no use when creating a new process. Thanks to RbMm for helping me clarify this.

Harry Johnston
  • 35,639
  • 6
  • 68
  • 158
  • I tried compiling and running this sample program, but it appears that cmd.exe launches in non-administrator mode. Is there a flag or switch that needs to be passed to CreateProcessWithLogonW to make it launch cmd in Administrator mode? – Colin B Feb 12 '14 at 18:59
  • I must apologize; you're quite right, it doesn't work here either. I find that *extremely* surprising. The security log shows that the account was logged in with admin privilege, I can only guess that the token is being filtered at the same time that the logon ID is altered. – Harry Johnston Feb 13 '14 at 00:16
  • OK, I've posted some new code that actually works (at least on my machine). – Harry Johnston Feb 13 '14 at 22:27
  • Did you have an issue where everything opens with the GUI messed up and all black? – Colin B Feb 14 '14 at 02:57
  • My tests involved console processes only, so it wouldn't have come up. Try adding the `LOGON_WITH_PROFILE` flag. – Harry Johnston Feb 14 '14 at 03:10
  • Which argument does the `LOGON_WITH_PROFILE` flag go into? EDIT: It looks like its the second argument, and it still makes a badly formatted interface. For the record this solution does launch in administrator mode! – Colin B Feb 14 '14 at 03:19
  • When I get back to work on Monday (time permitting) I'll try launching some GUI processes, see if I can reproduce this new issue. Does it only happen with certain exectuables, or can you reproduce it with, for example, notepad.exe? – Harry Johnston Feb 15 '14 at 01:13
  • cmd.exe, notepad.exe, and iexplore.exe all have the issue. – Colin B Feb 15 '14 at 19:50
  • 1
    You have to change the window station and desktop permissions in order to allow the new process to display a GUI. I've added new sample code to launch a GUI process. – Harry Johnston Feb 16 '14 at 22:23
  • 1
    Incredible. This is really impressive. Everything I was looking for and more! – Colin B Feb 17 '14 at 22:52