4

There is a launcher - a program in C++, which you need to run as administrator. The launcher launches another process as well on behalf of the administrator and because of this, third-party programs (AutoHotkey) running with the rights of a normal user can not access it. The second process does not require administrator rights, so I would like to implement the launch with the rights of a normal user. How to do it?

At the moment I'm running the process using boost::process::system.

kotbrain
  • 87
  • 1
  • 6
  • you can use [this](https://stackoverflow.com/a/45921237/6401656) – RbMm Jan 05 '18 at 20:05
  • I know of two relatively simple ways that rely on the normal case in which the Windows shell is Explorer running non-elevated. The first way gives you more control and preserves your process as the parent: duplicate Explorer's access token (`GetShellWindow`, `GetWindowThreadProcessId`, `OpenProcess`, `OpenProcessToken`, `DuplicateTokenEx`) and call `CreateProcessWithTokenW`. The [2nd way](https://blogs.msdn.microsoft.com/oldnewthing/20131118-00/?p=2643) is simpler: get the `Shell.Application` object in Explorer and call `ShellExecute` via its `IShell­Dispatch2` interface. – Eryk Sun Jan 05 '18 at 20:18
  • @eryksun - instead explorer (which can not exist or exist multiple) we need any process token with `AuthenticationId` (logon session) the same as in our linked token. so can enumerate processes and search for such token luid, but not by name. however I prefer first got tcb privilege, which is possible from elevated process and duplicate self linked token as primary – RbMm Jan 05 '18 at 22:21
  • @RbMm, SeTcbPrivilege (act as part of the OS) is not granted to an elevated administrator, not by default at least. That's available when running as SYSTEM, typically from a service, so it's unrelated to this question. I'm aware we can search for the logon authentication ID of the elevated user's linked token, but if `GetShellWindow` succeeds, it's a more efficient, direct approach to use the shell's token. Also, if it's an OTS logon, the linked token may not actually be in use by any other process. – Eryk Sun Jan 06 '18 at 06:15
  • @eryksun - but we can open system process token which have `SeTcbPrivilege` and impersonate with it - this is perfect work – RbMm Jan 06 '18 at 08:19

2 Answers2

3

Raymond Chen has a blog article on this very topic:

How can I launch an unelevated process from my elevated process and vice versa?

Going from an unelevated process to an elevated process is easy. You can run a process with elevation by passing the runas verb to Shell­Execute or ShellExecute­Ex.

Going the other way is trickier. For one thing, it's really hard to munge your token to remove the elevation nature properly. And for another thing, even if you could do it, it's not the right thing to do, because the unelevated user may be different from the elevated user.

...

The solution here is to go back to Explorer and ask Explorer to launch the program for you. Since Explorer is running as the original unelevated user, the program ... will run as [the user]. 

His article provides the following code to launch an unelevated process, and a detailed explanation of what the code is actually doing (the FindDesktopFolderView function mentioned below is defined in his Manipulating the positions of desktop icons blog article):

#define STRICT
#include <windows.h>
#include <shldisp.h>
#include <shlobj.h>
#include <exdisp.h>
#include <atlbase.h>
#include <stdlib.h>

// FindDesktopFolderView incorporated by reference

void GetDesktopAutomationObject(REFIID riid, void **ppv)
{
    CComPtr<IShellView> spsv;
    FindDesktopFolderView(IID_PPV_ARGS(&spsv));
    CComPtr<IDispatch> spdispView;
    spsv->GetItemObject(SVGIO_BACKGROUND, IID_PPV_ARGS(&spdispView));
    spdispView->QueryInterface(riid, ppv);
}

void ShellExecuteFromExplorer(
    PCWSTR pszFile,
    PCWSTR pszParameters = nullptr,
    PCWSTR pszDirectory = nullptr,
    PCWSTR pszOperation = nullptr,
    int nShowCmd = SW_SHOWNORMAL)
{
    CComPtr<IShellFolderViewDual> spFolderView;
    GetDesktopAutomationObject(IID_PPV_ARGS(&spFolderView));
    CComPtr<IDispatch> spdispShell;
    spFolderView->get_Application(&spdispShell);
    CComQIPtr<IShellDispatch2>(spdispShell)
        ->ShellExecute(CComBSTR(pszFile),
                        CComVariant(pszParameters ? pszParameters : L""),
                        CComVariant(pszDirectory ? pszDirectory : L""),
                        CComVariant(pszOperation ? pszOperation : L""),
                        CComVariant(nShowCmd));
}

int __cdecl wmain(int argc, wchar_t **argv)
{
    if (argc < 2) return 0;

    CCoInitialize init;
    ShellExecuteFromExplorer(
        argv[1],
        argc >= 3 ? argv[2] : L"",
        argc >= 4 ? argv[3] : L"",
        argc >= 5 ? argv[4] : L"",
        argc >= 6 ? _wtoi(argv[5]) : SW_SHOWNORMAL);

    return 0;
}
Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
1

if we run as admin (more concrete have S-1-5-32-544 'Administrators' group enabled in token) we can open local system process token (it grant all needed to as access for 'Administrators'). so we can do next:

  • get self terminal session id
  • enable debug privileges in token, for be able open any process with PROCESS_QUERY_LIMITED_INFORMATION
  • enumerate running processes in system
  • open process with PROCESS_QUERY_LIMITED_INFORMATION
  • open process token with TOKEN_QUERY|TOKEN_DUPLICATE
  • query token privileges - are it have SE_ASSIGNPRIMARYTOKEN_PRIVILEGE and SE_INCREASE_QUOTA_PRIVILEGE - it need for call CreateProcessAsUser and SE_TCB_PRIVILEGE it require for WTSQueryUserToken
  • if token have all this privileges (LocalSystem token have) - duplicate this token to TokenImpersonation type.
  • if need enable some of this 3 privileges in token
  • impersonate with this token

and now we can

  • call WTSQueryUserToken with self session id - for get not elevated user token
  • and finally CreateProcessAsUser with this token
  • reset impersonation

code:

static volatile UCHAR guz;

ULONG RunNonElevated(_In_ ULONG SessionId,
                     _In_ HANDLE hToken, 
                     _In_opt_ LPCWSTR lpApplicationName,
                     _Inout_opt_ LPWSTR lpCommandLine,
                     _In_opt_ LPSECURITY_ATTRIBUTES lpProcessAttributes,
                     _In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes,
                     _In_ BOOL bInheritHandles,
                     _In_ DWORD dwCreationFlags,
                     _In_opt_ LPVOID lpEnvironment,
                     _In_opt_ LPCWSTR lpCurrentDirectory,
                     _In_ LPSTARTUPINFOW lpStartupInfo,
                     _Out_ LPPROCESS_INFORMATION lpProcessInformation)
{
    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))
                                    {
                                        HANDLE hUserToken;
                                        if (WTSQueryUserToken(SessionId, &hUserToken))
                                        {
                                            if (!CreateProcessAsUserW(hUserToken, 
                                                lpApplicationName,
                                                lpCommandLine,
                                                lpProcessAttributes,
                                                lpThreadAttributes,
                                                bInheritHandles,
                                                dwCreationFlags,
                                                lpEnvironment,
                                                lpCurrentDirectory,
                                                lpStartupInfo,
                                                lpProcessInformation))
                                            {
                                                err = GetLastError();
                                            }

                                            CloseHandle(hUserToken);
                                        }
                                        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(_In_opt_ LPCWSTR lpApplicationName,
                     _Inout_opt_ LPWSTR lpCommandLine,
                     _In_opt_ LPSECURITY_ATTRIBUTES lpProcessAttributes,
                     _In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes,
                     _In_ BOOL bInheritHandles,
                     _In_ DWORD dwCreationFlags,
                     _In_opt_ LPVOID lpEnvironment,
                     _In_opt_ LPCWSTR lpCurrentDirectory,
                     _In_ LPSTARTUPINFOW lpStartupInfo,
                     _Out_ LPPROCESS_INFORMATION lpProcessInformation
                     )
{

    ULONG SessionId;
    if (!ProcessIdToSessionId(GetCurrentProcessId(), &SessionId))
    {
        return GetLastError();
    }

    BOOLEAN b;
    RtlAdjustPrivilege(SE_DEBUG_PRIVILEGE, TRUE, FALSE, b);

    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(
                                SessionId,
                                hToken, 
                                lpApplicationName,
                                lpCommandLine,
                                lpProcessAttributes,
                                lpThreadAttributes,
                                bInheritHandles,
                                dwCreationFlags,
                                lpEnvironment,
                                lpCurrentDirectory,
                                lpStartupInfo,
                                lpProcessInformation);

                            CloseHandle(hToken);
                        }
                        else
                        {
                            err = GetLastError();
                        }
                        CloseHandle(hProcess);
                    }
                    else
                    {
                        err = GetLastError();
                    }
                }
            } while (err && Process32NextW(hSnapshot, &pe));
        }
        else
        {
            err = GetLastError();
        }
        CloseHandle(hSnapshot);
    }

    return err;
}

and test:

void test()
{
    STARTUPINFO si = { sizeof(si)};
    PROCESS_INFORMATION pi;
    WCHAR ApplicationName[MAX_PATH];
    if (GetEnvironmentVariableW(L"ComSpec", ApplicationName, RTL_NUMBER_OF(ApplicationName)))
    {
        if (!RunNonElevated(ApplicationName, L"cmd /k whoami.exe /priv /groups",0,0,0,0,0,0,&si, &pi))
        {
            CloseHandle(pi.hProcess);
            CloseHandle(pi.hThread);
        }
    }
}

with this we can not only run not elevated process, but ,if need, start it with duplicated handles

RbMm
  • 31,280
  • 3
  • 35
  • 56
  • @RemyLebeau - it impersonating not by random token (process as you said), but by token which have required for as privileges - `SE_TCB_PRIVILEGE`, `SE_ASSIGNPRIMARYTOKEN_PRIVILEGE`. and i use not token of *elevated user* - i use **linked** token, which is not elevated. my code ,despite more big, better and more universal and power, compare Raymond's – RbMm Jan 06 '18 at 19:19
  • @RemyLebeau - *Raymond's approach launches the new process using the user who is logged in to the current session, not the elevated user.* - exactly this and i do. - you can view that i use `tlt.LinkedToken` for create process. it exactly corresponded to not elevated user logon session – RbMm Jan 06 '18 at 19:21
  • however my code not depend are explorer run at all in system – RbMm Jan 06 '18 at 19:21
  • impersonate of "rundom" token we need for 1.) get primary form linked token (for this calling thread must have `SE_TCB_PRIVILEGE`, however this not documented) and 2.) for call `CreateProcessAsUserW` (`SE_TCB_PRIVILEGE` and `SE_ASSIGNPRIMARYTOKEN_PRIVILEGE` need - documented). token i use correct - linked from elevated process, if it exist, otherwise we need direct call `CreateProcess` – RbMm Jan 06 '18 at 19:25
  • @RemyLebeau - i need concrete privilege in token - `SE_TCB_PRIVILEGE` and `SE_ASSIGNPRIMARYTOKEN_PRIVILEGE` - **any** token with this privileges is ok. i use it for get linked token and `CreateProcessAsUser` - so any *random* is ok here. and i use linked token (so from logon session from which was elevation). *which may be a different user than the one logged into the current session* - no, you mistake here. my code is absolutely accurate and correct – RbMm Jan 06 '18 at 21:53
  • @RemyLebeau - again - the **linked** process token - this is token from logon session, which exec our process with elevation. this is most correct choise use exactly this token. my code is absolute correct and perfect worked on all systems – RbMm Jan 06 '18 at 21:55
  • You'll probably need to fall back on using the shell token (e.g. starting with `GetShellWindow`) if the elevated user is logged on with an over-the-shoulder UAC consent (i.e. the session user is a standard user, and the elevated administrator is not the session user). In this case the elevation type is `TokenElevationTypeFull`, but there is no linked token; the limited-token and its logon session is already deleted, and trying to get the linked token fails with `ERROR_NO_SUCH_LOGON_SESSION`. – Eryk Sun Jan 11 '18 at 16:19
  • @eryksun - in this case (non admin user) elevation type will be `TokenElevationTypeDefault` (not `TokenElevationTypeFull`), but really with `TokenElevationTypeDefault` no linked token (session), so will be not worked in this case – RbMm Jan 11 '18 at 16:28
  • No, I just checked this, the elevation type is full, not default. – Eryk Sun Jan 11 '18 at 16:29
  • @eryksun - strange, i also check this (run from guest elevated program) - the token is exactly `TokenElevationTypeDefault` – RbMm Jan 11 '18 at 16:30
  • @eryksun - yes, i recheck - you are right - i elevate to built-in admin. in this case i got `TokenElevationTypeDefault`. if elevate to another admin user - really got `TokenElevationTypeFull` and `STATUS_NO_SUCH_LOGON_SESSION` error – RbMm Jan 11 '18 at 16:35
  • Actually, since you have SeTcbPrivilege via impersonation, just get the session ID and call `WTSQueryUserToken`. – Eryk Sun Jan 11 '18 at 16:50
  • @eryksun - yes, just think about this :) – RbMm Jan 11 '18 at 16:50