(This post is moved here from the TweakUAC web site, where it was first published on February 27, 2007)
Vista Elevator 2.0 is an updated version of the sample application Vista Elevator that uses a different approach to solving the problem of starting a non-elevated task from an elevated one.
The first version of Vista Elevator used a trick that created a non-elevated process by programming Vista Task Scheduler to start such a process immediately upon its registration. It worked even if the parent process was elevated. However, there were a few problems with that:
- It worked well when the process was started by an administrator (that is, by an account with a “split token”). However, if the account was of a standard user (or a Guest account) it did not work as expected: the secondary non-elevated process was created by Task Scheduler to be executed in the administrator’s context, rather than in the original context of the standard user account. The task would launch not when it was registered, but later on, when the administrator logged on to the system.
- The target machine could have Task Scheduler disabled. In such a case, this method would fail to start the secondary non-elevated task at all.
To solve these problems, a different approach is necessary. An obvious method of achieving the goal would be to have a separate helper executable that would help the main application launch a non-elevated task, when necessary. Specifically, it would work as follows:
- When a user wants to run the application (main.exe), s/he would start by launching the helper executable (helper.exe) first.
- The helper process would start non-elevated, but it would launch main.exe, and request it to start elevated (for example, by using the Run Elevated() function).
- After the administrator would have approved the launch of main.exe, the user would work with it, as usual. Helper.exe would keep running non-elevated, waiting for a signal from main.exe.
- When main.exe would need to start a non-elevated task, it would send a signal to helper.exe, using some sort of inter-process communication, and helper.exe would start a non-elevated process on main.exe’s behalf.
Such an approach would solve both problems described above: it would not require Task Scheduler to be running on the target system, and it would launch the non-elevated task in the context of the original user, whether it is an administrator, a standard user, or a guest.
What is not good about this approach, it requires a separate helper process to be running all the time, wasting the CPU cycles. It also requires to design a communication protocol between the helper and the main executable, which is not a trivial task and is subject to errors. Wouldn’t it be better if we could use some other non-elevated process already running on the target system to launch a non-elevated process on the main.exe’s behalf? Let’s see… There actually is a process that is guaranteed to run all the time while the user is logged on to the system: the Windows shell! And it runs non-elevated, just what we need. Seems like a perfect candidate for our helper process. But how can we ask Windows shell to launch a process on our behalf? Simply calling ShellExecute() or Start Process() would not work, because they would be executed by our process, not by the shell. What we need to do is inject our code into the shell process and make it launch a process on our behalf!
So, the plan of the attack could be as follows:
- Our process would find a window that belongs to the shell, and that is guaranteed to be available at any time. A good window for this purpose is Progman, that is responsible for displaying the desktop. We can call the FindWindow() API to obtain a handle to this window.
- Our process would call RegisterWindowsMessage() API to register a unique message that we would use to communicate with the shell’s window. It must be unique to avoid possible conflicts and side effects if we would have accidentally picked a message that is already used by the shell for some purpose.
- Our elevated process would call SetWindowsHookEx() API to install a global hook, to be invoked when a windows message gets processed by any process running on the system.
- Once the hook is installed we would send our unique message to the shell’s window, and that would make our hook procedure to get invoked. (That’s how we inject our code into the shell’s process!)
- When the hook procedure is called (in the context of the shell process), it would call ShellExecute() API to launch the non-elevated process that we need. The process would start non-elevated because the shell’s process is not elevated, and our process would inherit the shell’s elevation level.
- Finally, we would remove the hook, as we no longer need it and it should no longer be called and waste system resources and CPU cycles.
That’s the plan that is implemented as the RunNonElevated() function in the VistaTools.cxx file that VistaElevator 2.0 uses. To make it work, the design of the VistaElevator application had to be changed significantly:
Firstly, in order to be able to install a global hook, the hook procedure must reside in a DLL. It means that we can no longer have a single executable, we must create a DLL to go with it, as well.
Secondly, in order to be able to pass data from our process to our code injected in the shell’s process, we must set up a special code section to be shared between several processes.
Finally, to be able to use this method with both 32-bit and 64-bit versions of Vista, we must produce two separate builds, one 32-bit and another one 64-bit. The reason for that is that on a 64-bit Vista the shell is a native 64-bit process, and in order to be able to hook it, we need to use 64-bit code, too.
To see the details, use the download links below:
THIS CODE AND INFORMATION IS PROVIDED “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE.
(the compiled executables only, without the source code)
(the source code, a Visual Studio 2005 project)
Note: If you want to compile the source code on your own, make sure you have the latest Windows SDK (see msdn.microsoft.com for more information).