Click here to Skip to main content
15,867,453 members
Articles / Programming Languages / C++
Article

High elevation can be bad for your application: How to start a non-elevated process at the end of the installation

Rate me:
Please Sign up or sign in to vote.
4.74/5 (24 votes)
25 May 200710 min read 205K   2.4K   56   49
A reusable DLL that uses code injection to launch a non-elevated application from an InnoSetup script

Introduction

Many installation packages offer an option for the user to launch the application at the end of the installation:

Screenshot - LaunchApp.jpg

It works, except for one small problem: if installed on a Vista computer, the application started in such a way gets executed at the elevated level, with the full administrator rights. This article discusses why this is bad, and offers a reusable DLL that can be included in the existing installation packages to run the application non-elevated at the end of the installation. A sample setup script for the popular InnoSetup software is provided as well. The included source code also contains several other functions that can be of use when programming for Windows Vista.

Background

You've followed the Microsoft guidelines and carefully updated your application to run well in the context of a restricted user (non-administrator). You have added the asInvoker value to the application manifest, to make sure that when the user launches your application, it is started without the administrative privileges. You've tested it, it works well, and you think your new Vista-compatible version is ready for the release. So you create the setup package and test it, and now you are up for a nasty surprise: if you select the option to launch the application at the end of the installation, the application gets started as administrator, with full administrative privileges.

Why running your application elevated is bad? Because things may break unexpectedly. For example:

  • When started elevated, your application gets access to the folders where normally it should not get access to. Aside from the obvious security implications, it may create problems for your users: if the user saves a document into one of such folders, then she or he won't be able to open that document the next time she or he runs your application (because when the user starts the application next time, it will get executed without the administrative rights!)
  • If your application is installed by a "real" standard user (who is not a member of the Administrators group), and the installation has been authorized by the actual administrator (by entering his password in the UAC prompt "over the shoulder" of the standard user), then when your application is started by the installer, it gets access to the administrator's personal folders (My Documents, etc.), different from those of the standard user. The administrators don't like it when other users get unrestricted access to their personal folders
  • If your application needs to interact with the shell (for example, to react to the notification messages broadcasted by the shell, etc.), such interaction may break: by default, Vista UAC prevents most messages sent from the non-elevated applications (such as the shell) from reaching the elevated processes.
  • If your application creates secondary processes (for example, to show a taskbar notification icon, or run a hot-key monitor, etc.), such processes will start elevated, too. If they need to interact with the shell, such interaction may break, as well.
  • And last but not the least: you never know what kind of vulnerability might be discovered in your application that might open the way for the badware to the high elevation. A virus might be sitting on the user's computer and quietly waiting for an application like yours to start elevated, so that it could hijack your elevated process to elevate itself and start doing bad things with full administrative rights.

Why does your application run elevated when started automatically by the installer? Because the installation process runs with the full administrative rights, and when it creates child processes, such as the one for your application, such processes execute at the same elevated level, just like the setup program itself.

How to solve this problem?

If you've searched the Microsoft SDK documentation for a solution, you've undoubtedly been up for another surprise, even nastier than the first one: Microsoft has not provided for a way to start a non-elevated process from an elevated one. That's right, there is no API call, no special value to specify in the manifest, not even a flag for the ShellExecute() API to allow for that. If you've found yourself stuck in this situation, read on, this article is for you.

Let's consider the options that we have:

  1. We could create a separate helper executable that would help our main application launch a non-elevated task, when necessary. That is, it could work as follows:

    • When a user wants to install the application (by running setup.exe), she would start by launching the helper executable (helper.exe) first.

    • The helper process would start non-elevated, but it would launch setup.exe, which would start elevated, by means of the requireAdministrator value in the setup.exe's manifest.

    • After the installation is complete, setup.exe would signal back to helper.exe that the user wants to start the application (app.exe). Having received the signal, helper.exe would start app.exe on setup.exe's behalf. Since helper.exe was started non-elevated, it would start app.exe non-elevated, too.

      This method would work, but it's messy, since it requires creating a separate helper executable, as well as designing a communication protocol between the setup utility and the helper, which is not a trivial task.

  2. A simpler approach could be by making use of the capabilities of the built-in Task Scheduler of Windows Vista: our elevated process could register a task with Task Scheduler to be started at the non-elevated level immediately upon its registration. (I've described this method in detail in my previous article Vista Elevator). This method is much easier to implement than the previous one, and it works rather well when the installation is preformed by the administrator. However, if the application is installed by a standard user (with the administrator authorizing the installation "over the shoulder" of the standard user), the procedure does not work as expected: Task Scheduler is scheduling the application to start in the context of the administrator, rather than in the context of the original standard user. The application would launch not at the end of the installation, but later on, when the administrator logs on to the system, which is far from what one would expect.

    Another problem with the second method is that the target machine could have Task Scheduler disabled. In such a case, this method would fail to start the application at all.

  3. Instead of creating a helper executable to launch our application, we could find an existing non-elevated process already running on the target computer, and make it start a non-elevated process on our behalf by injecting our code into that process. The perfect candidate for the code injection is the Windows shell process: it is running non-elevated, and we can be sure it is always present on a computer running Windows Vista (when was the last time you saw a Windows computer without its shell running?).

The Solution: Code Injection in the shell process

Specifically, this method could work as follows:

  1. The elevated 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": it is responsible for displaying the user's desktop. We can call the FindWindow() API to obtain a handle to this window:

    C++
    HWND hwndShell = ::FindWindow( _T("Progman"), NULL);

  2. Our elevated process would call the RegisterWindowsMessage() API to register a unique message that we would use to communicate with the shell's Window:

    C++
    uVEMsg = ::RegisterWindowMessage( _T("VistaElevatorMsg") );

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

    C++
    hVEHook = ::SetWindowsHookEx( WH_CALLWNDPROCRET,
        (HOOKPROC)VistaElevator_HookProc_MsgRet, hModule, 0);

  4. Once the hook is installed we would send our unique message to the shell's window, and that would make our hook procedure get invoked. (That's how we inject our code into the shell process!):

    C++
    ::SendMessage( hwndShell, uVEMsg, 0, 0 );
    

  5. 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. Such a process would start non-elevated because the shell's process is not elevated, and our process would inherit the shell's elevation level:

    C++
    LRESULT CALLBACK
    VistaElevator_HookProc_MsgRet( int code, WPARAM wParam, LPARAM lParam )
    {
        if ( code >= 0 && lParam )
        {
            CWPRETSTRUCT * pwrs = (CWPRETSTRUCT *)lParam;
    
            if (pwrs->message == uVEMsg )
            {
                bVESuccess = VistaTools::MyShellExec(
                                pwrs->hwnd,
                                NULL,
                                szVE_Path,
                                szVE_Parameters,
                                szVE_Directory,
                                bVE_NeedProcessHandle ? &hVE_Process : NULL );
            }
        }
    
        return ::CallNextHookEx( hVEHook, code, wParam, lParam );
    }

  6. Finally, we would remove the hook, as we no longer need it:

    C++
    ::UnhookWindowsHookEx( hVEHook );
    

Using the code in C++ projects

The method described above is implemented as the function RunNonElevated(), defined in the file VistaTools.cxx. If you need to start a non-elevated process from your own application, you can add this file to your C++ project and call this function directly. The detailed instructions on how to use the file and this function are provided in the VistaTools.cxx file itself.

Note, however, that in order for the RunNonElevated() function to work, you must compile it in a DLL project. The reason for that is that the global hook code needs to reside in a DLL, it cannot be in an executable file. Also note that if you plan to run the code under the x64 versions of Windows as well, you need to compile a separate 64-bit version of the DLL in order for it to work as expected. The reason for this is that on the x64 versions of Windows, the shell is a native 64-bit process. In order to inject our code into it, your DLL must contain the native 64-bit code too.

Note also that VistaTools.cxx contains several other functions that you may find of use when programming for Windows Vista, such as IsVista(), GetElevationType(), and more. They are described in the file itself.

Using the code in the setup scripts

If you don't use C++, or if all you need is call the RunNonElevated() function from your setup script, then you can use the precompiled DLLs I've included in the file Redist.zip. This package contains the VistaLib32.dll and VistaLib64.dll files which export the RunNonElevated() function in such a way that you can invoke it by running the RunDll32.exe utility (which is part of the standard Windows distributions).

For example, if you use InnoSetup (a popular software installation package), then usually when you want to run your application at the end of the installation, you would include the following lines in your setup script:

C++
[Run]
Filename: "{app}\SampleApp32.EXE"; Description: "Launch application";

The above command launches the SampleApp32.EXE, that would start at the elevated level. To launch the same application at the non-elevated level, you would change the above lines to:

C++
[Run]
Filename: "RunDll32.exe"; Parameters:
"{code:AddQuotes|{app}\VistaLib32.dll},RunNonElevated 
            {code:AddQuotes|{app}\SampleApp32.EXE}";
Description: "Launch application";

Such a command would make the setup utility launch RunDll32.exe at the end of the installation. It, in turn, would load VistaLib32.dll and call its RunNonElevated entry point, passing the path to our application SampleApp32.EXE, enclosed in double quotation marks, to it. (Optional command line parameters for the application can be passed there, as well.) SampleApp32.exe is a very simple application that does nothing except that it calls a few functions exported by the VistaLib32.dll and displays the result in a message box:

Screenshot - SampleApp.jpg

To see it all in action, download the file SampleApp-setup.zip (see the link at the top of this article). It contains a pre-compiled setup utility that installs the sample application and runs it non-elevated at the end of the installation.

What about backward compatibility?

The code in VistaLib32/64.dll is backward compatible down to Windows 2000 (I did not test it with the earlier versions of Windows). If you call RunNonElevated() under Windows XP or 2000, it would simply use the regular ShellExecute() API to launch the application, as if ShellExecute() was called directly. This means that you can use the same setup script with both Vista and pre-Vista versions of Windows.

Note, however, that the setup script shown above is for the 32-bit version of Windows. To make it work with 64-bit versions, you need to modify the script to use the file VistaLib64.dll instead of VistaLib32.dll.

More information

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Web Developer
United States United States
When not busy entertaining my two cats, I run my micro-ISV business at www.winability.com

Comments and Discussions

 
QuestionRunNonElevated fails in Win8 Pin
CarstenBPoulsen27-Jun-13 23:45
CarstenBPoulsen27-Jun-13 23:45 
AnswerRe: RunNonElevated fails in Win8 Pin
Andrei Belogortseff28-Jun-13 5:09
Andrei Belogortseff28-Jun-13 5:09 
GeneralThanks! Pin
Nickmatic21-Apr-11 2:23
Nickmatic21-Apr-11 2:23 
GeneralMethod that doesn't require injection or process token changes Pin
Leo Davidson24-Feb-10 1:02
Leo Davidson24-Feb-10 1:02 
GeneralRe: Method that doesn't require injection or process token changes Pin
David Pritchard2-Dec-10 22:51
David Pritchard2-Dec-10 22:51 
GeneralWorking directory Pin
chrislong218-Sep-08 12:07
chrislong218-Sep-08 12:07 
GeneralAnother alternative to this method Pin
FaxedHead28-May-08 19:04
FaxedHead28-May-08 19:04 
NewsYes: It can be done much easier - Here is the Code: Pin
Elmue7-Nov-09 15:51
Elmue7-Nov-09 15:51 
GeneralVistaLib32.dll and VistaLib64.dll redistribution license Pin
kenny429-Apr-08 3:10
kenny429-Apr-08 3:10 
GeneralRe: VistaLib32.dll and VistaLib64.dll redistribution license Pin
Andrei Belogortseff29-Apr-08 17:54
Andrei Belogortseff29-Apr-08 17:54 
GeneralRunNonElevated is not working on Vista Pin
MForceOne4-Mar-08 8:18
MForceOne4-Mar-08 8:18 
GeneralRe: RunNonElevated is not working on Vista Pin
Andrei Belogortseff6-Mar-08 8:55
Andrei Belogortseff6-Mar-08 8:55 
GeneralCreative idea Pin
lmueller27-Aug-07 12:57
lmueller27-Aug-07 12:57 
GeneralVistaLib32.dll launches app twice Pin
sharevari15-Aug-07 1:00
sharevari15-Aug-07 1:00 
GeneralRe: VistaLib32.dll launches app twice Pin
sharevari15-Aug-07 2:06
sharevari15-Aug-07 2:06 
QuestionWhat about XP & W2K ? Pin
_Olivier_2-Aug-07 5:18
_Olivier_2-Aug-07 5:18 
AnswerRe: What about XP & W2K ? Pin
Andrei Belogortseff2-Aug-07 11:00
Andrei Belogortseff2-Aug-07 11:00 
GeneralQuestion about the High elevation solution using Rundll32.exe Pin
Tim Mayert20-Jul-07 11:00
Tim Mayert20-Jul-07 11:00 
GeneralRe: Question about the High elevation solution using Rundll32.exe Pin
Andrei Belogortseff20-Jul-07 11:10
Andrei Belogortseff20-Jul-07 11:10 
QuestionRe: Question about the High elevation solution using Rundll32.exe Pin
FaxedHead12-Apr-08 2:04
FaxedHead12-Apr-08 2:04 
Thanks your work on this Andrei.

Yes, adding /some_args after the path to the application to execute works, but I can't seem to be able to specify the working directory for starting the application using this method (rundll32). Using:

<br />
ProcessStartInfo pi = new ProcessStartInfo("rundll32.exe");<br />
pi.Arguments = "VistaLib32.dll,RunNonElevated \"" + fileName + "\" " + arguments + " " + workingDir;<br />
Process.Start(pi);<br />


The process is launched, but Process explorer shows that the arguments passed to it are {arguments workingdir} and the actual working directory is set to some default (e.g. c:\windows\system32). Is there any way to specify the working directory for the process using a call to rundll32.exe? Perhaps this has to do with RunDLL32 expecting the entrypoint function it is calling to do the parsing (as stated here http://support.microsoft.com/kb/164787)...

As an alternative I tried to call RunNonElevated directly (from C#), however my DLLImport prototype must be wrong, because I just get a messagebox entitled RunNonElevatedA with some garbage characters.

<br />
[DllImport("VistaLib32.dll", EntryPoint = "RunNonElevated")]<br />
public static extern bool RunNonElevated(<br />
	IntPtr hwnd,<br />
	[MarshalAs(UnmanagedType.LPTStr)] string filePath,<br />
	[MarshalAs(UnmanagedType.LPTStr)] string arguments,<br />
	[MarshalAs(UnmanagedType.LPTStr)] string workingDir);<br />


Can you offer any advice how to achieve this? Thanks.
GeneralRe: Question about the High elevation solution using Rundll32.exe Pin
FaxedHead12-Apr-08 18:18
FaxedHead12-Apr-08 18:18 
QuestionCreating Desktop icons for standard users. Can this be done? Pin
nagarsoft30-Jun-07 7:26
nagarsoft30-Jun-07 7:26 
AnswerRe: Creating Desktop icons for standard users. Can this be done? Pin
Andrei Belogortseff30-Jun-07 11:00
Andrei Belogortseff30-Jun-07 11:00 
GeneralRe: Creating Desktop icons for standard users. Can this be done? Pin
nagarsoft8-Jul-07 0:49
nagarsoft8-Jul-07 0:49 
GeneralNot able to build Pin
email2venki3-Jun-07 20:39
email2venki3-Jun-07 20:39 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.