3

I have an elevated process that has been started after the user has answered "Yes" in the UAC dialog.

The process is started fine, everything works as expected.

Now I need to "unelevate" that process at some point, in other words the process should become not elevated exactly as if it had been launched normally by the user.

Sample scenario

  • User A is logged on
  • User A launches process P1 which will be elevated via UAC
  • Elevated process P1 lauchches process P2 and P2 should'nt be elevated and should run again under user A.

Is there a way to do this?

Jabberwocky
  • 48,281
  • 17
  • 65
  • 115
  • 4
    You can't change the token of a process after the process has started. What you can do is to launch a new unelevated process from your elevated process. See https://blogs.msdn.microsoft.com/oldnewthing/20131118-00/?p=2643 and http://blogs.microsoft.co.il/sasha/2009/07/09/launch-a-process-as-standard-user-from-an-elevated-process/ – David Heffernan Aug 28 '17 at 09:34
  • 1
    [This link](https://blogs.msdn.microsoft.com/aaron_margosis/2009/06/06/faq-how-do-i-start-a-program-as-the-desktop-user-from-an-elevated-app/) is also very helpful. – Jabberwocky Aug 28 '17 at 11:26
  • 1
    See [this question](https://stackoverflow.com/q/37948064/7571258) how to create an unelevated process from an elevated one. The answers use `IShellDispatch2` to accomplish that. Another way that works similar is to use `CreateProcessWithTokenW` to [create a new process in the context of the shell](https://www.codeproject.com/Tips/23090/Creating-a-Process-with-Medium). This allows more control over the way the process is created. – zett42 Aug 28 '17 at 11:37
  • 1
    Note that you should be prepared that `CreateProcessWithTokenW` fails if users have the "Secondary Logon Service" disabled, which many "optimization" web sites suggest to do. `GetLastError()` returns a special value in that case (which I don't remember). – zett42 Aug 28 '17 at 11:47
  • 1
    Just checked, if the Secondary Logon Service is disabled, `CreateProcessWithTokenW` fails and `GetLastError()` returns 1058 (`ERROR_SERVICE_DISABLED`). The `IShellDispatch2` technique doesn't suffer from this issue, presumably because it talks directly with explorer.exe via the COM interface. – zett42 Aug 28 '17 at 12:54
  • 1
    @zett42 - for what need `CreateProcessWithTokenW` or `IShellDispatch2` ? all can be done with `CreateProcessAsUser`. if ok exec not elevated process in the same logon session (luid) as elevated - code can be very simply and small. simply create restricted token with `LUA_TOKEN`. if need exec in target session - code become large. need impersonate system token, get own linked token (as primary token), and use it. – RbMm Aug 28 '17 at 13:00
  • @RbMm if you can turn your last comment into a more complete answer, I'd upvote it. – Jabberwocky Aug 28 '17 at 13:03
  • @MichaelWalz - yes, i have better solution for exec not elevated process from elevated. you care of logon session where not elevated will be start ? or pass both code variants ? – RbMm Aug 28 '17 at 13:06
  • @RbMm I added the sample scenario in the question. The main issue with your solution is how to get the `hToken` parameter for `CreateProcessAsUser`. – Jabberwocky Aug 28 '17 at 13:11
  • @MichaelWalz - ok, i give complete answer to this. 30min :) – RbMm Aug 28 '17 at 13:13
  • 1
    The cleanest solution IMO is to re-architecture: have the user launch P3, which runs without elevation, launches P1, then waits. P1 can instruct P3 to launch P2 at the appropriate point, or it can use P3's token, similar to RbMm's answer but without having to go searching for a suitable process. – Harry Johnston Aug 29 '17 at 04:25
  • 1
    @HarryJohnston This won't work if user launches P3 already elevated (right click > launch as administrator). Also, user won't see "shield" overlay icon for P3, because it must not have "requireAdministrator" in manifest. Not so user friendly. – zett42 Aug 29 '17 at 12:53
  • @zett42 correct, but some installers (can't remember which ones though) work like this: you start them as unelevated and somewhat later you get the UAC prompt and if you reply "yes" the program performs the actual installation. – Jabberwocky Aug 29 '17 at 14:32
  • @zett42, if the user explicitly requests elevation when launching the application, that request should typically be honoured, which means P2 should be launched elevated even if it would not normally be. (Another option is for P3 to refuse to run.) I don't see the absence of the shield in the icon as an issue, but you could always put it there yourself. – Harry Johnston Aug 29 '17 at 21:16
  • @zett42, if the system administrator has intentionally disabled an essential system service, that's their own darn fault. :-) – Harry Johnston Aug 29 '17 at 23:32
  • @HarryJohnston Say a user launches my installer elevated and later clicks on a weblink in the GUI of my installer. They certainly don't want the security risk of running their browser elevated then. Most likely they aren't even aware of that risk. Regarding the system service, that's a valid point. If I can avoid that dependency, even better. Support team will thank me. – zett42 Aug 30 '17 at 08:47

1 Answers1

6

the elevated process have linked token - it refers to non elevated user session. we can use this linked token in 2 ways:

first way:

  1. get it as TokenPrimary (for this we need have SE_TCB_PRIVILEGE when we query this token)
  2. call CreateProcessAsUser with this token. for this we need also SE_ASSIGNPRIMARYTOKEN_PRIVILEGE and SE_INCREASE_QUOTA_PRIVILEGE
  3. for get all this privileges - enumerate processes, query it tokens, and if process token have all this 3 privileges - impersonate with it, before call CreateProcessAsUser. because elevated token have SE_DEBUG_PRIVILEGE the task is possible

second way:

  1. query the logon session id from linked token (AuthenticationId from TOKEN_STATISTICS)

  2. found process with the same AuthenticationId in process token.

  3. use this process as parent process by help PROC_THREAD_ATTRIBUTE_PARENT_PROCESS

implementation for way 1:

static volatile UCHAR guz;

ULONG RunNonElevated(HANDLE hToken, HANDLE hMyToken, PCWSTR lpApplicationName, PWSTR lpCommandLine)
{
    ULONG err;

    PVOID stack = alloca(guz);

    ULONG cb = 0, rcb = FIELD_OFFSET(TOKEN_PRIVILEGES, Privileges[SE_MAX_WELL_KNOWN_PRIVILEGE]);

    union {
        PVOID buf;
        PTOKEN_PRIVILEGES ptp;
    };

    do 
    {
        if (cb < rcb)
        {
            cb = RtlPointerToOffset(buf = alloca(rcb - cb), stack);
        }

        if (GetTokenInformation(hToken, TokenPrivileges, buf, cb, &rcb))
        {
            if (ULONG PrivilegeCount = ptp->PrivilegeCount)
            {
                int n = 3;
                BOOL fAdjust = FALSE;

                PLUID_AND_ATTRIBUTES Privileges = ptp->Privileges;
                do 
                {
                    switch (Privileges->Luid.LowPart)
                    {
                    case SE_ASSIGNPRIMARYTOKEN_PRIVILEGE:
                    case SE_INCREASE_QUOTA_PRIVILEGE:
                    case SE_TCB_PRIVILEGE:
                        if (!(Privileges->Attributes & SE_PRIVILEGE_ENABLED))
                        {
                            Privileges->Attributes |= SE_PRIVILEGE_ENABLED;
                            fAdjust = TRUE;
                        }

                        if (!--n)
                        {
                            err = NOERROR;

                            if (DuplicateTokenEx(hToken, 
                                TOKEN_ADJUST_PRIVILEGES|TOKEN_IMPERSONATE, 
                                0, SecurityImpersonation, TokenImpersonation, 
                                &hToken))
                            {
                                if (fAdjust)
                                {
                                    AdjustTokenPrivileges(hToken, FALSE, ptp, rcb, NULL, NULL);
                                    err = GetLastError();
                                }

                                if (err == NOERROR)
                                {
                                    if (SetThreadToken(0, hToken))
                                    {
                                        TOKEN_LINKED_TOKEN tlt;
                                        if (GetTokenInformation(hMyToken, TokenLinkedToken, &tlt, sizeof(tlt), &rcb))
                                        {
                                            STARTUPINFO si = { sizeof (si) };
                                            PROCESS_INFORMATION pi;

                                            if (!CreateProcessAsUserW(tlt.LinkedToken, lpApplicationName, lpCommandLine, 
                                                NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi))
                                            {
                                                err = GetLastError();
                                            }

                                            CloseHandle(tlt.LinkedToken);

                                            if (err == NOERROR)
                                            {
                                                CloseHandle(pi.hThread);
                                                CloseHandle(pi.hProcess);
                                            }
                                        }
                                        else
                                        {
                                            err = GetLastError();
                                        }
                                        SetThreadToken(0, 0);
                                    }
                                    else
                                    {
                                        err = GetLastError();
                                    }
                                }

                                CloseHandle(hToken);
                            }
                            else
                            {
                                err = GetLastError();
                            }

                            return err;
                        }
                    }
                } while (Privileges++, --PrivilegeCount);
            }

            return ERROR_NOT_FOUND;
        }

    } while ((err = GetLastError()) == ERROR_INSUFFICIENT_BUFFER);

    return err;
}

ULONG RunNonElevated(HANDLE hMyToken, PCWSTR lpApplicationName, PWSTR lpCommandLine)
{
    static TOKEN_PRIVILEGES tp = {
        1, { { { SE_DEBUG_PRIVILEGE } , SE_PRIVILEGE_ENABLED } }
    };

    AdjustTokenPrivileges(hMyToken, FALSE, &tp, sizeof(tp), NULL, NULL);

    ULONG err = NOERROR;

    // much more effective of course use NtQuerySystemInformation(SystemProcessesAndThreadsInformation) here
    HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0), hToken;

    if (hSnapshot != INVALID_HANDLE_VALUE)
    {
        PROCESSENTRY32W pe = { sizeof(pe) };

        if (Process32FirstW(hSnapshot, &pe))
        {
            err = ERROR_NOT_FOUND;

            do 
            {
                if (pe.th32ProcessID && pe.th32ParentProcessID)
                {
                    if (HANDLE hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pe.th32ProcessID))
                    {
                        if (OpenProcessToken(hProcess, TOKEN_QUERY|TOKEN_DUPLICATE, &hToken))
                        {
                            err = RunNonElevated(hToken, hMyToken, lpApplicationName, lpCommandLine);
                            CloseHandle(hToken);
                        }
                        else
                        {
                            err = GetLastError();
                        }
                        CloseHandle(hProcess);
                    }
                    else
                    {
                        err = GetLastError();
                    }
                }
            } while (err && Process32NextW(hSnapshot, &pe));
        }
        else
        {
            err = GetLastError();
        }
        CloseHandle(hSnapshot);
    }

    return err;
}

ULONG RunNonElevated(PCWSTR lpApplicationName, PWSTR lpCommandLine)
{
    HANDLE hToken;

    ULONG err = NOERROR;

    if (OpenProcessToken(NtCurrentProcess(), TOKEN_QUERY|TOKEN_ADJUST_PRIVILEGES, &hToken))
    {
        TOKEN_ELEVATION_TYPE tet;

        ULONG rcb;

        if (GetTokenInformation(hToken, ::TokenElevationType, &tet, sizeof(tet), &rcb))
        {
            if (tet == TokenElevationTypeFull)
            {
                RunNonElevated(hToken, lpApplicationName, lpCommandLine);
            }
            else
            {
                err = ERROR_ALREADY_ASSIGNED;
            }
        }
        else
        {
            err = GetLastError();
        }

        CloseHandle(hToken);
    }
    else
    {
        err = GetLastError();
    }

    return err;
}

implementation for way 2:

ULONG CreateProcessEx(HANDLE hProcess,
                      PCWSTR lpApplicationName,
                      PWSTR lpCommandLine)
{

    SIZE_T Size = 0;

    STARTUPINFOEX si = { sizeof(si) };
    PROCESS_INFORMATION pi;

    InitializeProcThreadAttributeList(0, 1, 0, &Size);

    ULONG err = GetLastError();

    if (err = ERROR_INSUFFICIENT_BUFFER)
    {
        si.lpAttributeList = (PPROC_THREAD_ATTRIBUTE_LIST)alloca(Size);

        if (InitializeProcThreadAttributeList(si.lpAttributeList, 1, 0, &Size))
        {
            if (UpdateProcThreadAttribute(si.lpAttributeList, 0, 
                PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, &hProcess, sizeof(hProcess), 0, 0) &&
                CreateProcessW(lpApplicationName, lpCommandLine, 0, 0, 0, 
                EXTENDED_STARTUPINFO_PRESENT, 0, 0, &si.StartupInfo, &pi))
            {
                CloseHandle(pi.hThread);
                CloseHandle(pi.hProcess);
            }
            else
            {
                err = GetLastError();
            }

            DeleteProcThreadAttributeList(si.lpAttributeList);
        }
        else
        {
            err = GetLastError();
        }
    }
    else
    {
        err = GetLastError();
    }

    return err;
}

ULONG CreateProcessEx(LUID AuthenticationId,
                      PCWSTR lpApplicationName,
                      PWSTR lpCommandLine)
{
    ULONG err = ERROR_NOT_FOUND;

    HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);

    if (hSnapshot != INVALID_HANDLE_VALUE)
    {
        PROCESSENTRY32W pe = { sizeof(pe) };

        ULONG rcb;

        if (Process32First(hSnapshot, &pe))
        {
            err = ERROR_NOT_FOUND;
            BOOL found = FALSE;

            do 
            {
                if (pe.th32ProcessID && pe.th32ParentProcessID)
                {
                    if (HANDLE hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION|PROCESS_CREATE_PROCESS, FALSE, pe.th32ProcessID))
                    {
                        HANDLE hToken;                  

                        if (OpenProcessToken(hProcess, TOKEN_QUERY, &hToken))
                        {
                            TOKEN_STATISTICS ts;

                            if (GetTokenInformation(hToken, TokenStatistics, &ts, sizeof(ts), &rcb))
                            {
                                if (ts.AuthenticationId.LowPart == AuthenticationId.LowPart && 
                                    ts.AuthenticationId.HighPart == AuthenticationId.HighPart)
                                {
                                    found = TRUE;

                                    err = CreateProcessEx(hProcess,
                                        lpApplicationName,
                                        lpCommandLine);
                                }
                            }
                            CloseHandle(hToken);
                        }

                        CloseHandle(hProcess);
                    }
                }

            } while (!found && Process32Next(hSnapshot, &pe));
        }
        else
        {
            err = GetLastError();
        }

        CloseHandle(hSnapshot);
    }
    else
    {
        err = GetLastError();
    }

    return err;
}

ULONG CreateProcessEx(PCWSTR lpApplicationName,
                      PWSTR lpCommandLine)
{
    HANDLE hToken;

    ULONG err = NOERROR;

    if (OpenProcessToken(NtCurrentProcess(), TOKEN_QUERY, &hToken))
    {
        union {
            TOKEN_ELEVATION_TYPE tet;
            TOKEN_LINKED_TOKEN tlt;
        };

        ULONG rcb;

        if (GetTokenInformation(hToken, TokenElevationType, &tet, sizeof(tet), &rcb))
        {
            if (tet == TokenElevationTypeFull)
            {
                if (GetTokenInformation(hToken, TokenLinkedToken, &tlt, sizeof(tlt), &rcb))
                {
                    TOKEN_STATISTICS ts;

                    BOOL fOk = GetTokenInformation(tlt.LinkedToken, TokenStatistics, &ts, sizeof(ts), &rcb);

                    CloseHandle(tlt.LinkedToken);

                    if (fOk)
                    {
                        err = CreateProcessEx(ts.AuthenticationId,
                            lpApplicationName,
                            lpCommandLine);
                    }
                    else
                    {
                        err = GetLastError();
                    }
                }
                else
                {
                    err = GetLastError();
                }
            }
            else
            {
                err = ERROR_ALREADY_ASSIGNED;
            }
        }
        else
        {
            err = GetLastError();
        }

        CloseHandle(hToken);
    }
    else
    {
        err = GetLastError();
    }

    return err;
}

and test:

WCHAR ApplicationName[MAX_PATH];

if (GetEnvironmentVariableW(L"ComSpec", ApplicationName, RTL_NUMBER_OF(ApplicationName)))
{
    WCHAR cmdline[] = L"cmd.exe /k whoami /priv /groups\r\n";
    CreateProcessEx(ApplicationName, cmdline);
    RunNonElevated(ApplicationName, cmdline);
}

for way #2 theoretical we can not found process with same logon id (AuthenticationId) as in our linked token. but way #1 always must work. always exist system process which have SeTcbPrivilege (for get primary form of linked token) + SeAssignPrimaryTokenPrivilege (for CreateProcessAsUser) (SeIncreaseQuotaPrivilege listen in msdn as typical require for CreateProcessAsUser but in my test this worked even if this privilege not enabled ). however all system processes (running as LocalSystem) have this 3 privilege in token (begin from smss.exe) and some system processes always run in system.

so way #1 must never fail and preferred. also we can here use for example inherited handles from our process, for interact with child process. this is impossible in way #2. it shown rather for completeness of the picture


at begin we check TOKEN_ELEVATION_TYPE and do job, only if it is TokenElevationTypeFull. in case TokenElevationTypeLimited we not elevated process - so nothing todo. case TokenElevationTypeDefault mean or UAC if off (LUA disabled) or we run as built-in Administrator, and lua not filter tokens for this account (so all processes is "elevated" or more exactly it tokens not filtered via CreateRestrictedToken(..LUA_TOKEN..) ) - in this case also no sense try run "not elevated" process under this user

RbMm
  • 31,280
  • 3
  • 35
  • 56
  • Pretty complete answer. I need to check this out in detail. Upvoted anyway. Thanks – Jabberwocky Aug 28 '17 at 14:53
  • @MichaelWalz exist else one way (shorter code) - by `CreateRestrictedToken(hToken, LUA_TOKEN, 0, 0, 0, 0, 0, 0, &hLowToken)` (on own process token), than set `TokenIntegrityLevel` for `WinMediumLabelSid` and finally `CreateProcessAsUser` with this token (*If hToken is a restricted version of the caller's primary token, the `SE_ASSIGNPRIMARYTOKEN_NAME` privilege is not required*). but process will be run in another logon session. as result all console application fail to run (allocate console), but say notepad and gui apps run well – RbMm Aug 28 '17 at 15:04
  • 1
    Clever. There may be edge cases, though, where there is no process currently running with the token you want. – Harry Johnston Aug 29 '17 at 04:24
  • @HarryJohnston - for way 2 theoretical yes. but way 1 always must work. always exist system process which have `SeTcbPrivilege` (for get primary linked token) + `SeAssignPrimaryTokenPrivilege` (for `CreateProcessAsUser`) – RbMm Aug 29 '17 at 07:21
  • I wonder if `CreateProcessAsUserW()` still needs the secondary logon service? – zett42 Aug 29 '17 at 08:43
  • @zett42 - no, because no any logon is used here. we use already existing token. however you can easy check my code. `seclogon` service (if you mean it) not running but way#1 work as well – RbMm Aug 29 '17 at 08:47
  • Sorry, my previous comment was referring to method 2, I should have made that clear. My only practical concern with method 1 is that it might look suspicious to anti-virus software. (Plus a vague worry that you might wind up with some undesirable association between the system token that you've "borrowed" and the new process. But I don't think that will be a problem in practice.) Personally I'd prefer to explicitly create a system service to do the job on my behalf, but that does require more code. – Harry Johnston Aug 29 '17 at 21:26
  • @HarryJohnston - `some undesirable association between the system token that you've "borrowed" and the new process.` - i impersonate **my thread** during query `TokenLinkedToken` and call `CreateProcessAsUser` and end impersonation after this. i not use system token for create child process - so here no any effect on it. i also impersonate only current thread in my process - so this not affect another threads in process. here all absolute correct and no any side effect in in principle. are you not exactly understand this point of code ? about `might look suspicious` .. this already .. – RbMm Aug 29 '17 at 21:58
  • `CreateProcessAsUser` uses the impersonation token when loading the executable (when performing the access check for the executable file) so it isn't entirely implausible to suppose that the choice of token *might* affect the new process in some subtle way. I don't believe it will, but it's hard to rule out entirely. As for anti-virus software, I just mean that code that enumerates all processes and then impersonates one of them seems more likely to trigger a false alarm than code that installs a system service. That's just a guess. I have no hard statistics. – Harry Johnston Aug 29 '17 at 22:03
  • @HarryJohnston - yes, the system token will be used when we open executable, but i not think that this is serious issue. we need only open file as read only. usual even restricted, low-level process can do this (how it loads system dlls for example ?). also this is usual practice when *LocalSystem* code create "user" (elevated or not) process. how about start user process from service ? or how all elevated(run as admin) process is started ? it exec not from *explorer.exe* but from `svchost.exe -k netsvcs` really. `CreateProcessWithTokenW` also do this. – RbMm Aug 29 '17 at 22:16
  • @HarryJohnston - so i try say - exec process, while thread under system token - is common practice in windows. it used permanently, by system code itself. this is definitelly not a problem by design. – RbMm Aug 29 '17 at 22:16
  • about av - this is clear of course. but this already not windows system programming question. i try be correct as possible from windows system view. if some third party program (av) try modify native system behavior - this is already separate question – RbMm Aug 29 '17 at 22:21
  • There's certainly no problem in a system process launching an executable. My concern is that you're choosing a system process *at random*, and it might be in an unexpected state - some kind of sandbox, perhaps, introduced by Microsoft in a future version of the the OS, or a process that has special meaning to a third-party hypervisor. Again, this is just a vague worry, I have no concrete objections. – Harry Johnston Aug 29 '17 at 22:32
  • @HarryJohnston - `My concern is that you're choosing a system process at random, and it might be in an unexpected state` - really i search not for *process* but for *token*. and select *token* not on *random* base, but i query it privilege - and choose first which have all 3 needed for me. the *process*, from which i get *token* here absolute unrelated - it can be in any state (even suspended), he even can terminate, just after i get *token* - this nothing change. the *token* is independ from any process, impossible even back get process from token. – RbMm Aug 29 '17 at 22:40
  • @RbMm, apart from checking that the token has the three privileges you need, it is selected at random. There *could* be something special about the associated logon session, or the token itself might contain a SID that the operating system (or third-party software) considers special, or a future version of Windows might introduce new token flags. None of these are particularly likely, but they're not impossible. – Harry Johnston Aug 29 '17 at 22:43
  • ... in other words, *because* you've chosen the process at random, you don't know very much about the state of the token, just what you've explicitly checked. Personally, that would worry me, not much but probably enough that I'd choose to do it the other way. Obviously, your risk tolerance is somewhat higher than mine, and that's OK too. – Harry Johnston Aug 29 '17 at 22:45
  • @HarryJohnston - so `unexpected state` of process - no matter here, it state not play role at all. this is important point. about token - formally system check only it privilege set, but of course in theory can be all. can be bugs in windows itself :) `certainly no problem in a system process launching an executable` - just now, when research this [question](https://stackoverflow.com/questions/45940033/clientname-is-not-set-after-a-process-is-created-with-createprocessasuser-and-cr) found interesting bug in `CreateEnvironmentBlock` function, via which not all Environment variables set – RbMm Aug 29 '17 at 22:49
  • Completely (for 100%) errors still cannot be avoided. Always something that can break. And the system contains errors in itself too (rarely , but exist) – RbMm Aug 29 '17 at 22:53
  • Now I know how to impersonate process like `winlogon` properly. Previously my attempt failed so bad I had to spawn my process with process token of `winlogon` (to run as SYSTEM and get privileges) and call `CreateProcessAsUserW` through that process, which was a mess. – raymai97 Dec 21 '17 at 15:47