0

Suppose your Windows user account is in the Admin group, UAC is enabled, and you're running some program A with normal user privileges. A never asks for elevation and never receives it. Now suppose A wants to launch program B, which has highestAvailable in its manifest.

  • If A calls CreateProcess(B), this will fail with error 740 ("elevation required")

  • If A calls ShellExecuteEx(B), Windows will display a UAC box asking to run B elevated. The user can say Yes, in which case B will run elevated, or No, in which case the launch will fail.

My question is: is there any way to achieve a third option, where we simply launch B without elevation?

It seems like this should be possible in principle, since "highestAvailable" means that B prefers to run with elevation but is perfectly capable of running in normal user mode. But I can't figure out any way to accomplish it. I've tried all sorts of things with tokens and CreateProcessAsUser(), but it all seems to come down to this: "highestAvailable" seems to unalterably refer to the latent privileges inherent in the user account, not the actual privileges expressed in any explicitly constructed token.

I'm hoping that there actually is some way to use CreateProcessAsUser() to do this, and that I'm just missing the trick for properly constructing the token.

Update - solved: the __COMPAT_LAYER=RunAsInvoker solution below works nicely. One caveat, though. This coerces the subprocess to run "as invoker" unconditionally: it applies even if the exe being invoked specifies "requireAdministrator" in its manifest. I think the original "elevation required" error is generally preferable when the exe specifies "requireAdministrator". The whole reason I wanted the RunAsInvoker behavior for programs marked with "highestAvailable" is that such programs are expressly saying "I can function properly in either mode" - so let's go ahead and run in normal user mode when it's inconvenient to use Admin mode. But "requireAdministrator" is a different matter: such programs are saying "I can't function properly without elevated privileges". It seems better to fail up front for such programs than to force them to run un-elevated, which might make them encounter privilege/access errors that they're not properly programmed to handle. So I think a complete, general-purpose solution here would require checking the application manifest, and only applying the RunAsInvoker coercion if the manifest says "highestAvailable". An even completer solution would be to use one of the techniques discussed elsewhere to give the caller an option to invoke UAC when presented with a "requireAdministrator" program and offer the user a chance to launch it elevated. I can imagine a CreateProcessEx() cover with a couple of new flags for "treat process privileges as highest available privileges" and "invoke UAC if elevation is required". (The other approach described below, hooking NTDLL!RtlQueryElevationFlags() to tell CreateProcess() that UAC is unavailable, has exactly this same caveat with respect to requireAdministrator programs.)

(It's probably telling that the Windows shell doesn't even offer a way to do this... launching B directly from the shell would give you the UAC box that lets you either launch with Admin privs or not launch at all. If there were any way to accomplish it, the UAC box might offer a third button to launch without privileges. But then again that could just be a UX decision that the third option is too confusing for civilians.)

(Note that there are quite a lot of posts on StackOverflow and the Microsoft dev support sites asking about a very similar-seeming scenario that unfortunately doesn't apply here. That scenario is where you have a parent program that's running elevated, and it wants to launch a non-elevated child process. The canonical example is an installer, running elevated as installers tend to do, that wants to launch the program it just installed, at normal user level, just before it exits. There's lots of posted code about how to do that, and I've based my attempts on some of those techniques, but this is really a different scenario and the solutions don't work in my situation. The big difference is that the child program they're attempting to launch in this case isn't marked with highestAvailable - the child is just a normal program that would launch without any UAC involvement under normal circumstances. There's another difference as well, which is that in those scenarios, the parent is already running elevated, whereas in my scenario the parent is running as normal user level; that changes things slightly because the parent process in this other scenario has access to a few privileged operations on tokens that I can't use because A isn't itself elevated. But as far as I can tell those privileged token operations wouldn't help anyway; it's the fact that the child has the highestAvailable flag that's the key element of my scenario.)

mjr
  • 3
  • 4
  • legal way do this not exist. however you can hook [`RtlQueryElevationFlags`](http://undoc.airesoft.co.uk/ntdll.dll/RtlQueryElevationFlags.php) api in self process, before call `CreateProcess` (say by *DR* registers and vex handler) and set returned flags to 0. in this case child process will be started. – RbMm May 11 '18 at 19:50
  • @RbMm - thanks, I'll look into that. Although if it's not a "legal way" it might not be what I'm looking for. I do get the impression from all of the related questions about launching un-elevated from elevated that you're right that it's simply not possible "legally", though. – mjr May 11 '18 at 19:54
  • really this (hook api call with DR and vex, very easy to implement) but of course this can be break in future – RbMm May 11 '18 at 19:56
  • @RbMm - I read up on that. That definitely does the right thing, but you're sure right that it's a hack. – mjr May 11 '18 at 20:04
  • if want i can post ready code how do this. of course hack. but as is – RbMm May 11 '18 at 20:09
  • @RbMm - if you have something demonstrating it, I'd be interested. It would be great to capture here as a possible option for others running into the same problem. – mjr May 11 '18 at 20:16
  • i have src code which implement this - post it as answer ? or you want binary exe for test :) ? – RbMm May 11 '18 at 20:18
  • Silent elevation is not possible to mere mortals. Only executable files that have a certificate that indicates it is owned by Microsoft can get it. – Hans Passant May 11 '18 at 21:51
  • @HansPassant UAC can be bypassed but this question is about **not elevating**, not silent elevation. – Anders May 11 '18 at 21:57
  • I saw "where we simply launch B without elevation?" in bold type. **Sorry**. – Hans Passant May 11 '18 at 22:06
  • @HansPassant - just to clarify in case it was confusing, "launch B without elevation" literally means *without elevation* - i.e., without privileges. It's not about secretly getting privileges, it's about not having privileges at all. – mjr May 11 '18 at 23:31

2 Answers2

1

Set the __COMPAT_LAYER environment variable to RunAsInvoker in your process. I don't think this is formally documented anywhere but it works all the way back to Vista.

You can also make it permanent by setting the it under the AppCompatFlags\Layers key in the registry.

Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
Anders
  • 97,548
  • 12
  • 110
  • 164
  • Thanks - that seems simple enough! Strange that they don't document it anywhere. – mjr May 11 '18 at 23:39
  • Just tested the environment variable approach. Seems to work great. (For my purposes, the environment variable approach is the one I need, since I don't want it to be system-wide or permanent - it's just for the one application.) – mjr May 11 '18 at 23:49
0

the possible hack solution call CreateProcess from not elevated admin user (restricted admin) for exe with highestAvailable in manifest (or from any not elevated user for requireAdministrator exe) - this is hook RtlQueryElevationFlags call and set returned flags to 0. this is currently work, but of course no any grantee that will be work in next versions of windows, if something changed. however as is.

for hook single time api call - we can set hardware breakpoint to function address and VEX handler . demo working code:

NTSTATUS NTAPI hookRtlQueryElevationFlags (DWORD* pFlags) 
{
    *pFlags = 0;
    return 0;
}

PVOID pvRtlQueryElevationFlags;

LONG NTAPI OnVex(::PEXCEPTION_POINTERS ExceptionInfo)
{
    if (ExceptionInfo->ExceptionRecord->ExceptionCode == STATUS_SINGLE_STEP &&
        ExceptionInfo->ExceptionRecord->ExceptionAddress == pvRtlQueryElevationFlags)
    {
        ExceptionInfo->ContextRecord->
#if defined(_X86_)
        Eip
#elif defined (_AMD64_)
        Rip 
#else
#error not implemented
#endif
             = (ULONG_PTR)hookRtlQueryElevationFlags;

        return EXCEPTION_CONTINUE_EXECUTION;
    }

    return EXCEPTION_CONTINUE_SEARCH;
}

ULONG exec(PCWSTR lpApplicationName)
{
    ULONG dwError = NOERROR;

    if (pvRtlQueryElevationFlags = GetProcAddress(GetModuleHandle(L"ntdll"), "RtlQueryElevationFlags"))
    {
        if (PVOID pv = AddVectoredExceptionHandler(TRUE, OnVex))
        {
            ::CONTEXT ctx = {};
            ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;
            ctx.Dr7 = 0x404;
            ctx.Dr1 = (ULONG_PTR)pvRtlQueryElevationFlags;

            if (SetThreadContext(GetCurrentThread(), &ctx))
            {
                STARTUPINFO si = {sizeof(si)};
                PROCESS_INFORMATION pi;
                if (CreateProcessW(lpApplicationName, 0, 0, 0, 0, 0, 0, 0, &si,&pi))
                {
                    CloseHandle(pi.hThread);
                    CloseHandle(pi.hProcess);
                }
                else
                {
                    dwError = GetLastError();
                }

                ctx.Dr7 = 0x400;
                ctx.Dr1 = 0;
                SetThreadContext(GetCurrentThread(), &ctx);
            }
            else
            {
                dwError = GetLastError();
            }
            RemoveVectoredExceptionHandler(pv);
        }
        else
        {
            dwError = GetLastError();
        }
    }
    else
    {
        dwError = GetLastError();
    }

    return dwError;
}
RbMm
  • 31,280
  • 3
  • 35
  • 56
  • Thanks! That works very nicely. And your approach is definitely a bit nicer than other similar hacks I found based on your suggestion to look for RtlQueryElevationFlags() hooks. The other approaches I found all wanted to hijack the DLL entrypoint with memory patching, which seems really brittle to me. Your breakpoint hook seems a lot safer and more reliable. I think Microsoft would probably still frown on this approach as messing around with Things Man Wasn't Meant To Know, but it seems about as reliable as you can get given what's required. – mjr May 11 '18 at 23:28
  • @mjr - also note that this hook is thread safe - only current thread will be affected by DR, if another thread call hooked function at the same time - he will be not breaked. unlike detour, which affected all thread – RbMm May 11 '18 at 23:48
  • Excellent! I was wondering about that. It's actually very relevant to my particular scenario because the app in question is doing the child process launch from a thread. – mjr May 12 '18 at 00:12
  • @mjr *launch from a thread* ? all is executed in some thread. however think solution with `__COMPAT_LAYER` is better anyway – RbMm May 12 '18 at 00:16
  • "launch from a thread ? all is executed in some thread" - I just meant that there's a new thread per launch, so it's possible for multiple of them to be running concurrently. – mjr May 12 '18 at 00:27
  • "however think solution with __COMPAT_LAYER is better anyway" - Yeah, I think you're probably right; it's sure a heck of a lot simpler. This is Windows for you - here's this seemingly common task that seems to be impossible in the documented APIs, and it turns it's not only possible, but there are at least two completely different undocumented ways to accomplish it. – mjr May 12 '18 at 00:29