Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

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

0.00/5 (No votes)
25 May 2007 1  
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:

    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:

    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:

    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!):

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

    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:

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

[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:

[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