How can I launch an unelevated process from my elevated process, redux

Raymond Chen

Raymond

Some time ago, I showed how you can launch an unelevated process from an elevated process by asking Explorer to launch the program on your behalf.

There’s another way which is a bit more direct, but it assumes that the thing you want to do can be done with a direct Create­Process call. In other words, if you need the system to look up the user’s file associations or default browser, then this technique is not for you.

The idea is to take advantage of PROCESS_CREATE_PROCESS access and the accompanying PROC_THREAD_ATTRIBUTE_PARENT_PROCESS process thread attribute:

PROC_THREAD_ATTRIBUTE_PARENT_PROCESS

The lpValue parameter is a pointer to a handle to a process to use instead of the calling process as the parent for the process being created. The process to use must have the PROCESS_CREATE_PROCESS access right.

Attributes inherited from the specified process include handles, the device map, processor affinity, priority, quotas, the process token, and job object. (Note that some attributes such as the debug port will come from the creating process, not the process specified by this handle.)

Basically, this lets you tell the Create­Process function, “Hey, like, um, pretend that other guy over there is creating the process.”

Here’s a Little Program to demonstrate. Remember that Little Programs do little to no error checking so that they can demonstrate the underlying technique without distractions.

int main(int, char**)
{
  HWND hwnd = GetShellWindow();

  DWORD pid;
  GetWindowThreadProcessId(hwnd, &pid);

  HANDLE process =
    OpenProcess(PROCESS_CREATE_PROCESS, FALSE, pid);

  SIZE_T size;
  InitializeProcThreadAttributeList(nullptr, 1, 0, &size);
  auto p = (PPROC_THREAD_ATTRIBUTE_LIST)new char[size];

  InitializeProcThreadAttributeList(p, 1, 0, &size);
  UpdateProcThreadAttribute(p, 0,
    PROC_THREAD_ATTRIBUTE_PARENT_PROCESS,
    &process, sizeof(process),
    nullptr, nullptr);

  wchar_t cmd[] = L"C:\\Windows\\System32\\cmd.exe";
  STARTUPINFOEX siex = {};
  siex.lpAttributeList = p;
  siex.StartupInfo.cb = sizeof(siex);
  PROCESS_INFORMATION pi;

  CreateProcessW(cmd, cmd, nullptr, nullptr, FALSE,
    CREATE_NEW_CONSOLE | EXTENDED_STARTUPINFO_PRESENT,
    nullptr, nullptr, &siex.StartupInfo, &pi);

  CloseHandle(pi.hProcess);
  CloseHandle(pi.hThread);
  delete[] (char*)p;
  CloseHandle(process);
  return 0;
}

We start by getting the shell window and using that to identify the process that is responsible for the shell.

We then use that process ID to open the process with the magic PROCESS_CREATE_PROCESS access.

We then ask the system how much memory is required to create a PROC_THREAD_ATTRIBUTE_LIST that holds one attribute, and allocate it.

We initialize the newly-allocated attribute list and update it with our process handle, saying that we want this to be the parent for the process we’re about to create.

We set up a STARTUPINFOEX structure like usual, but we take the extra step of setting the lpAttributeList to point to the attribute list.

Finally, we call Create­Process, but also set the EXTENDED_STARTUPINFO_PRESENT flag to tell it, “Hey, I gave you a STARTUPINFOEX, and if you look inside, you might find a surprise!”

A little bit of cleaning up, and we’re done.

This program runs a copy of cmd.exe using the shell process (usually explorer.exe) as its parent, which means that if the shell process is unelevated, then so too will the cmd.exe process. Of course, if the user is an administrator and has disabled UAC, then Explorer will still be elevated, and so too will be the cmd.exe. But in that case, the user wants everything to run elevated, so you’re just following the user’s preferences.

Raymond Chen
Raymond Chen

Follow Raymond   

15 comments

Comments are closed.

  • Avatar
    Joshua Hudson

    Little programs do no error checking.
    Anybody using this needs to handle GetWindowThreadProcessId failing. If it fails you won’t be able to de-elevate using this technique. I can’t say for all cases but in my experience starting the child elevated is better than not at all.
    In the now-deleted comments of the prior thread the conclusion was the best way was to start your program non-elevated, elevate a child to do whatever (using a manifest), and having the non-elevated parent start the non-elevated child. We may improve as follows: pass your own pid as a command line argument to the child being elevated and use that PID rather than the shell PID here.

  • W S
    W S

    “the shell process (usually explorer.exe)” What is your/the shell teams stance on 3rd-party shells? How to write a custom shell has never been documented and making a fully functioning one gets harder with each new Windows version. To make GetShellWindow and friends work correctly you have to call undocumented functions etc.

  • Henrik Andersson
    Henrik Andersson

    So that’s what the PROCESS_CREATE_PROCESS permission is for! The documentation team really needs to explain this stuff better. In particular, which process needs the permission. Or is it process handle? The documentation is a bit unclear.

    • Raymond Chen
      Raymond Chen

      PROCESS_CREATE_PROCESS is an access right, so it applies to handles, not processes. There is only one handle involved in PROC_THREAD_ATTRIBUTE_PARENT_PROCESS. The second sentence should say “The handle must have…” rather than “the process must have…” I’ll submit a documentation change request.

  • Avatar
    Ji Luo

    I always wondered if you could use Task Scheduler to start a process as the login user. (I also wonder what happens if the user has killed Explorer and started an elevated instance of it, or even worse as someone else, in which case asking the shell to start a program doesn’t unelevate the process.)

  • Avatar
    Михаил Макаров

    Hi Raymond, I know you sometimes write entries about VS, compilers etc, so I wondered if this story could be interesting for you. https://developercommunity.visualstudio.com/content/problem/409959/cant-order-items-with-string-collection-editor.html?childToView=547467 Check the 2nd answer from Merrie McGaw. I can hardly express without swear words all the emotions this story sparks in me. IMHO, this is beyond the definition of absurd.

    • Avatar
      Ian Yates

      This answer: https://developercommunity.visualstudio.com/comments/547467/view.html
      ?
      It sounds to me like the overriding directive was to make Visual Studio more accessible.  That’s a worthy goal. 
      Unfortunately someone, who clearly doesn’t use the dialog you’re talking about, reimplemented it to support the main directive and the complete expense of people who were already using the dialog and understood its purpose.
      What’s nice to see is that Microsoft have at least indicated they can see the problem, explained why it came about (finally!  that explanation could’ve reduced some stress on the many commenters so the change didn’t appear so random), and have promised a fix.  The fix sounds like the best of all worlds – you get the text editor back AND it’s going to be accessible.
      It does sound like it might take a while though…

  • Avatar
    Piotr Siódmak

    You’re using CreateProcessW, while passing STARTUPINFOEX, which I presume is defined as _STARTUPINFOEXA in your case since your int main takes char instead of wchar_t. Is that something to be worried about? Yay unicode!

    • Avatar
      Me Gusta

      You can tell the compiler to use the Unicode versions of the Windows API without using the wide entrypoint.
      I agree that since CreateProcessW was used explicitly then it would be better to use STARTUPINFOEXW, but as it is, if UNICODE is defined then STARTUPINFOEX will be a typedef of _STARTUPINFOEXW.

  • Avatar
    Joshua Schaeffer

    Going back to the spirit of the blog entry that led to this, is there a way we can just use the session token to create the new process exactly like an unelevated user? The only deterministic way of using the idea on this page is to always start a process chain with an unelevated stub zombie process so that you always have it available. Then inherit the full-access process handle to the “real” process that actually does the work so that you’re guaranteed to have a secure working handle without a risky OpenProcess call.

  • Avatar
    Heiko Wetzel

    As I saw your article I was very excited, because I’m looking for a working way to launch an unelevated process from an elevated process already for a long time. I used your code (enhanced with error checking) and was able to spawn an unelevated process. That’s great. But I made the observation, that the unelevated process runs in a compatibility mode, the environment variable __COMPAT_LAYER in the unelevated process has the value “Installer”. Is there a way to prevent enabling the compatibility mode (for the new process) or can you explain, why the compatibility mode ist applied in this scenario? As I don’t know about the impact of the compatibility mode on the process, I can’t decide if it is safe to ignore it. Thanks 

  • Avatar
    Gavin Lambert

    There’s a big caveat to this technique.  If the user has used “runas” or CreateProcessAsUser or similar to launch your process (or some parent of your process) under a different user account from the shell, then this would probably either fail with a permissions error or work but launch the child process under the original user account (which might annoy the actual user, since they presumably had a reason to switch user accounts in the first place).  There also might be security problems if you then try to communicate between the two processes.

    Like Joshua Hudson stated, I’ve found that the best practice is to launch unelevated, then launch an elevated child that communicates back to its still-not-elevated parent if need be, rather than trying the reverse.  Although that can still be defeated by “runas”, in this case if the user chooses to run the original process as administrator — but that probably falls into the “doing what the user requested” bucket, so it’s still better than the above.

    Otherwise, the only other solution that seems safe is if Windows provided an API to acquire the corresponding non-elevated filtered token from a process’s current token, and then use CreateProcessAsUser to launch unelevated.  (It is probably currently possible to perform a combination of existing API calls to recreate a new filtered token, but since AFAIK the difference between the elevated and unelevated tokens is not entirely specified, that seems less safe than Windows providing an explicit API to do it.)  Although as Raymond pointed out in the original blog post, this is not necessarily correct either, if elevation occurred with credentials.

    Another possibility (if you don’t want an unelevated launchpad process) is to get your parent process’s token (at startup), then later use CreateProcessAsUser with that token to launch the unelevated process.  (Caveat: I haven’t checked if this actually works — there are probably cross-user permissions issues — and there’s a race condition if your parent process terminates before you have a chance to capture its token.  It’s also a bit tricky to get your parent process without its cooperation, which makes the launchpad process more attractive anyway.)

    • Avatar
      Gavin Lambert

      Also, curse this new blog system and its lack of proper paragraph formatting!  (And its erasure of all the comments on previous blog posts, which were often very useful.)

  • dmex
    dmex

    There are issues using the PROC_THREAD_ATTRIBUTE_PARENT_PROCESS flag… Environment variables for the new process are copied from the High-IL process not the Medium-IL process which cause issues with shell functions and some directory paths (%temp%, %userprofile% etc…) The token security descriptor for the process is also created without an ACE for the current user blocking access to most resources and the token security descriptor will also have a High-IL even though the process itself has a Medium-IL… This blocks the new process from accessing any system objects (events/pipes/sections/IPC etc…) while also blocking the process from opening its own process token with TOKEN_QUERY access. This seems to be a severe bug with the API but not sure if it’ll be fixed. As a workaround you can call CreateProcess a second time but from the new child process (without the PARENT_PROCESS flag) and it’ll be created with the correct token DAC security and environment variables (this is also why the above above sample doesn’t have issues with cmd.exe since it launches child processes).