Why don’t elevated processes inherit their environment variables from their non-elevated parent?


As a general rule, child processes inherit the environment of their parent. But if the parent is non-elevated and the child is elevated, then this inheritance does not happen. Why not?

There are two answers to this question. For the kernel-color glasses answer, I defer to Chris Jackson, the App Compat Guy. It's interesting to see how it all works, but it doesn't explain why the mechanism was designed to block environment variable inheritance.

The reason for the design is that allowing an elevated process to inherit the PATH from a non-elevated process creates an attack vector.

The non-elevated process sets its PATH to put some attacker-controlled directories ahead of the directories the elevated application actually expects. For example, suppose the elevated application links to C:\Program Files\Common Files\Contoso\Contoso­Grid­Control.dll. It arranges for this by setting the system PATH to include the C:\Program Files\Common Files\Contoso directory. Or maybe the program calls Load­Library on a DLL that might not exist, and it handles the case that the call fails by disabling some optional feature. (Whether this is a good idea or not is beside the point.)

The attacker changes the PATH to read \\rogue\server;C:\Program Files\Common Files\Contoso, so that the library search finds the evil copy on the rogue server before finding the expected version in the Common Files directory (or in the case of a DLL that may not exist, it finds the evil copy on the rogue server instead of failing outright).

Bingo, the attacker has injected arbitrary code into an elevated process. Game over.

For similar reasons, the current directory is reset to the system directory when a non-elevated program launches an elevated program.

If the environment and current directory were inherited, then malware could ask to elevate Program X with a custom current directory or environment. The user will merely be asked if they want to run Program X elevated, unaware that it is being run in a nonstandard manner, using an execution environment that did not receive administrator approval. As a result, the malware would be able to sneak into the administrator account under sheep's clothing (the sheep being Program X).

What if you want to run another program elevated, and with a custom current directory or environment?

Write a wrapper program which sets the current directory and environment, then launches the desired target process. Then ask the user for permission to run the wrapper elevated.

Comments (23)
  1. Joshua Ganes says:

    I find your articles on security issues intriguing. This is another example of an issue I wouldn't have considered on the face of it, but makes perfect sense once explained. It's a wonder that anyone can make secure software given the wide range of exploits that come to light. I wonder if, in hindsight, there was a better initial design that could have prevented this type of exploit in the first place.

  2. Adam Rosenfield says:

    I wonder how many security vulnerabilities on Unix systems have resulted from the fact the setuid binaries do inherit their environment and current working directory from their parent processes.

    Unix's sudo(1) does clear the environment by default (except for a few whitelisted environment variables), but you can override it and ask it to preserve the environment with the -E option.

  3. Joker_vD says:

    "To improve security, it's a good idea revert all implicit path-resolving settings to safe and boring defaults for elevated child processes." I guess that's the point of education — that you get told simple yet not immediately apparent things. When you are told it, it makes perfect sense, but before being told, very few would think about it off the top of their heads.

  4. Joshua says:

    Wake me when the pipeline can be preserved during elevation. I can fix the environment variables. I can't fix the pipeline.

  5. @Joshua:

    Even if you are sarcastic about it or whatever, isn't it obvious that the pipe would also not work across elevation?

  6. Maurits says:

    @Crescens2k it's not obvious, at least to me. Why shouldn't the pipe work across elevation?

  7. Joshua says:

    @Maurits: Because elevation happens via WinExec or ShellExecute but not CreateProcess.

  8. @Marutis [MSFT]:

    It should be obvious because of the affect the integrity level has on inter-process communication.

    As far as I am aware, Windows doesn't do any fiddling with the handles, so the high integrity process will have its end open with the default high integrity level, and the lower integrity process will have its end open with a lower integrity level.

    This means that the lower integrity process will be WRITING to stdout, and the higher integrity process will be reading from stdin. The writing is capitalised there because the default integrity level associated with a process has the MANDATORY_LEVEL_NO_WRITE_UP mask set in the access mask of the ACE. The pipe is doing exactly that, writing to a higher level process, therefore writes get blocked.

    I am also pretty sure that this is done via named pipes in Windows.

    For the lower integrity process to be able to write to stdout in this case, the pipe would have to have a lower integrity level assigned to it.

  9. Maurits says:

    Can the elevated process voluntarily lower the integrity level on its input pipe? I assume so.

  10. Joshua says:

    @Crescens2k: You know even the MAC checks are only done at open time, do you not?

    It's also nontrivial to get the exit code of the elevated process to the point where I don't currently know how to do it (I only know how to wait for it by searching the process table for it, with the obvious race condition).

  11. Myria says:

    I wish that elevated processes could more easily start non-elevated children.  There are so many programs out there whose installer asks at the end whether you want to run the program you just installed.  When you click yes, the program runs, but because the installer was running elevated, the child is elevated, too.  This can result in data files created during this first execution being inaccessible to subsequent executions.  It has caused us tech support issues, sadly.

    A hack I came up with is to use Explorer's token.  I feel bad about using it, and thankfully have not in a real application.  GetShellWindow to find Explorer, GetWindowThreadProcessId to get its PID, OpenProcess, OpenProcessToken, DuplicateTokenEx, CreateProcessAsUser.  You don't need SeAssignPrimaryTokenPrivilege because a non-elevated version of your token is considered a restricted version of your token for CreateProcessAsUser.  If you want Explorer to be the parent rather than yourself, use STARTUPINFOEX with PROC_THREAD_ATTRIBUTE_PARENT_PROCESS.  One downside to this is that the child still gets the elevated process's environment, rather than your user environment, the point of this article. =)

    [There's no need to hack it this way. There's a completely documented and supported solution. I have an article in the queue on this topic, but you can also find it if you just look through the SDK samples. -Raymond]
  12. Lars Skovlund says:

    Adam Rosenfeld: The sudo(1) manpage on my Linux system states:

    "The -E (preserve environment) option indicates to the security policy that the user wishes to preserve their existing environment variables.  The security policy may return an error if the -E option is specified and the user does not have permission to preserve the environment."

    And there are indeed settings in the sudoers file to control this.

  13. Myria says:

    @Maurits [MSFT]: Yes, but it's a bit painful in the way I know.  Make a new SACL with a single entry containing a SYSTEM_MANDATORY_LABEL_ACE, specifying the well-known SID WinLowLabelSid or whatever, and mask = SYSTEM_MANDATORY_LABEL_NO_WRITE_UP.  Initialize a security descriptor with nothing in it but a SetSecurityDescriptorSacl.  Then call SetKernelObjectSecurity with LABEL_SECURITY_INFORMATION.

    This can also be done at the time of pipe creation by specifying a security descriptor with this SACL.  (In both cases, ACCESS_SYSTEM_SECURITY is apparently not required for lowering the label despite being in the SACL.)

  14. "(In both cases, ACCESS_SYSTEM_SECURITY is apparently not required for lowering the label despite being in the SACL.)"

    Yes, you only need the rights that standard users have. This is why you use the LABEL_SECURITY_INFORMATION constant instead of the SACL one. This allows even standard users to lower the integrity level of objects and processes they spawn.

  15. Maurits says:

    *Somewhere* there's a CreateProcess.

  16. mappu says:

    [ I wish that elevated processes could more easily start non-elevated children. – Myria ]

    The hack i used in my most recent installer-project is to shell out to schtasks.exe, to register a scheduled task for the target executable that runs as the current interactive user, don't add a schedule, invoke it immediately and then unregister it.

    It seems like an absurd solution but i have seen it mentioned on MSDN somewhere, although i can't find the citation right now (so feel free to throw things at me for being part of the eventual Win32 compatibility problem instead of using the Right Solution)

  17. command prompt says:

    well that explains why elevated command prompts start in system32 then

  18. Neil says:

    But an elevated app still has "my" %TEMP% so presumably I can access its temporary files (if it has any).

  19. floyd says:

    @Neil: Since %TEMP% has no effect on the Dynamic-Link Library Search Order it cannot be abused to cause an elevated process to run an attacker's module's code.

    .f

  20. DWalker59 says:

    @Myria, @mappu: "There are so many programs out there whose installer asks at the end whether you want to run the program you just installed."

    Personally, I think the easiest solution is to NOT have the installer ask if you want to run the program.  

     – It's not a huge feature (I personally don't care if the program gives me a way to run the program when it's done)

     – I have to know how to run the program using the "normal" method anyway, or the program will not be useful

     – It only happens once (at the end of the install)

     – Some programmers are apparently having trouble with it

    Why go to a lot of trouble for a tiny feature?

  21. Harry Johnston says:

    @DWalker: it's not even a feature, in my book, it's an anti-feature.  But I suspect the marketing folk want it.

  22. Gabe says:

    DWalker: It's not an entirely useless feature. There have been plenty of times I've want to run some trivial utility program that requires installation before I can use it. If I have to go through the annoying install process, it's nice to not have to then go and try to find what I just installed.

    When you're installing an app to make it permanently available on your computer (like Office), it's have it run the app when the installer's done. When you're trying to run a program, and installation is just an impediment to using it, offering to run the program at the end of the install is the least they can do.

  23. mappu says:

    Some other times when running the application post-install is useful:

    – The software has an always-on component running as the user (e.g. a tray icon)

    – The installation was an upgrade, and the software was open prior to the installation – restarting it would simply be the polite thing to do (especially if the installation was run silently in the background, or initiated from within the application itself)

    – If configuration needs to be performed on first application launch, then it makes sense to continue straight into it once installation is complete

Comments are closed.