April 25th, 2019

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

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.

Topics
Code

Author

Raymond has been involved in the evolution of Windows for more than 30 years. In 2003, he began a Web site known as The Old New Thing which has grown in popularity far beyond his wildest imagination, a development which still gives him the heebie-jeebies. The Web site spawned a book, coincidentally also titled The Old New Thing (Addison Wesley 2007). He occasionally appears on the Windows Dev Docs Twitter account to tell stories which convey no useful information.

15 comments

Discussion is closed. Login to edit/delete existing comments.

  • 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...

    Read more
  • 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...

    Read more
    • 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.)

  • 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...

    Read more
  • Joe Beans

    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...

    Read more
  • 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!

    • 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.

  • Mike Makarov

    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.

    • 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...

      Read more
  • 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.)

  • 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 ChenMicrosoft employee Author

      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.

  • 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.

  • 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),...

    Read more