Click here to Skip to main content
15,884,177 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 205.9K   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 
Hello

Thanks for posting this link.
You are right: You can do it without DLLs - much easier.

But the code on the page you link to is very ugly and has SIX bugs!

I completely cleaned the code and fixed all the bugs.
I tested it on Windows 2000, XP, Vista and Windows 7.
It works perfectly.

If you compile and run this code "As Administrator" it will start the Calculator process under the account of the currently logged-in user. In Taskmanager you will see the new Calculator running under the same user account as Explorer. This is important in an installer because the new process must access the correct "Documents and Settings" folder.

Elmü

#include "stdafx.h"
#include "windows.h"

#ifndef SECURITY_MANDATORY_HIGH_RID
	#define SECURITY_MANDATORY_UNTRUSTED_RID            (0x00000000L)
	#define SECURITY_MANDATORY_LOW_RID                  (0x00001000L)
	#define SECURITY_MANDATORY_MEDIUM_RID               (0x00002000L)
	#define SECURITY_MANDATORY_HIGH_RID                 (0x00003000L)
	#define SECURITY_MANDATORY_SYSTEM_RID               (0x00004000L)
	#define SECURITY_MANDATORY_PROTECTED_PROCESS_RID    (0x00005000L)
#endif

#ifndef TokenIntegrityLevel
	#define TokenIntegrityLevel ((TOKEN_INFORMATION_CLASS)25)
#endif

#ifndef TOKEN_MANDATORY_LABEL
	typedef struct  
	{
		SID_AND_ATTRIBUTES Label;
	} TOKEN_MANDATORY_LABEL;
#endif

typedef BOOL (WINAPI *defCreateProcessWithTokenW)
		(HANDLE,DWORD,LPCWSTR,LPWSTR,DWORD,LPVOID,LPCWSTR,LPSTARTUPINFOW,LPPROCESS_INFORMATION);


// Writes Integration Level of the process with the given ID into pu32_ProcessIL
// returns Win32 API error or 0 if succeeded
DWORD GetProcessIL(DWORD u32_PID, DWORD* pu32_ProcessIL)
{
	*pu32_ProcessIL = 0;
	
	HANDLE h_Process   = 0;
	HANDLE h_Token     = 0;
	DWORD  u32_Size    = 0;
	BYTE*  pu8_Count   = 0;
	DWORD* pu32_ProcIL = 0;
	TOKEN_MANDATORY_LABEL* pk_Label = 0;

	h_Process = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, u32_PID);
	if (!h_Process)
		goto _CleanUp;

	if (!OpenProcessToken(h_Process, TOKEN_QUERY, &h_Token))
		goto _CleanUp;
				
	if (!GetTokenInformation(h_Token, TokenIntegrityLevel, NULL, 0, &u32_Size) &&
		 GetLastError() != ERROR_INSUFFICIENT_BUFFER)
		goto _CleanUp;
						
	pk_Label = (TOKEN_MANDATORY_LABEL*) HeapAlloc(GetProcessHeap(), 0, u32_Size);
	if (!pk_Label)
		goto _CleanUp;

	if (!GetTokenInformation(h_Token, TokenIntegrityLevel, pk_Label, u32_Size, &u32_Size))
		goto _CleanUp;

	pu8_Count = GetSidSubAuthorityCount(pk_Label->Label.Sid);
	if (!pu8_Count)
		goto _CleanUp;
					
	pu32_ProcIL = GetSidSubAuthority(pk_Label->Label.Sid, *pu8_Count-1);
	if (!pu32_ProcIL)
		goto _CleanUp;

	*pu32_ProcessIL = *pu32_ProcIL;
	SetLastError(ERROR_SUCCESS);

	_CleanUp:
	DWORD u32_Error = GetLastError();
	if (pk_Label)  HeapFree(GetProcessHeap(), 0, pk_Label);
	if (h_Token)   CloseHandle(h_Token);
	if (h_Process) CloseHandle(h_Process);
	return u32_Error;
}

// Creates a new process u16_Path with the integration level of the Explorer process (MEDIUM IL)
// If you need this function in a service you must replace FindWindow() with another API to find Explorer process
// The parent process of the new process will be svchost.exe if this EXE was run "As Administrator"
// returns Win32 API error or 0 if succeeded
DWORD CreateProcessMediumIL(WCHAR* u16_Path, WCHAR* u16_CmdLine)
{
	HANDLE h_Process = 0;
	HANDLE h_Token   = 0;
	HANDLE h_Token2  = 0;
	PROCESS_INFORMATION k_ProcInfo    = {0};
	STARTUPINFOW        k_StartupInfo = {0};

	BOOL b_UseToken = FALSE;

	// Detect Windows Vista, 2008, Windows 7 and higher
	if (GetProcAddress(GetModuleHandleA("Kernel32"), "GetProductInfo"))
	{
		DWORD u32_CurIL;
		DWORD u32_Err = GetProcessIL(GetCurrentProcessId(), &u32_CurIL);
		if (u32_Err)
			return u32_Err;

		if (u32_CurIL > SECURITY_MANDATORY_MEDIUM_RID)
			b_UseToken = TRUE;
	}

	// Create the process normally (before Windows Vista or if current process runs with a medium IL)
	if (!b_UseToken)
	{
		if (!CreateProcessW(u16_Path, u16_CmdLine, 0, 0, FALSE, 0, 0, 0, &k_StartupInfo, &k_ProcInfo))
			return GetLastError();

		return ERROR_SUCCESS;
	}

	defCreateProcessWithTokenW f_CreateProcessWithTokenW = 
		(defCreateProcessWithTokenW) GetProcAddress(GetModuleHandleA("Advapi32"), "CreateProcessWithTokenW");

	if (!f_CreateProcessWithTokenW) // This will never happen on Vista!
		return ERROR_INVALID_FUNCTION; 
	
	HWND h_Progman = ::FindWindow("Progman", NULL);
	if (!h_Progman) // This can only happen if Explorer has crashed (User has no desktop and no taskbar)
		return ERROR_INVALID_WINDOW_HANDLE;

	DWORD u32_ExplorerPID = 0;		
	GetWindowThreadProcessId(h_Progman, &u32_ExplorerPID);

	// ATTENTION:
	// If UAC is turned OFF all processes run with SECURITY_MANDATORY_HIGH_RID, also Explorer!
	// But this does not matter because to start the new process without UAC no elevation is required.
	h_Process = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, u32_ExplorerPID);
	if (!h_Process)
		goto _CleanUp;

	if (!OpenProcessToken(h_Process, TOKEN_DUPLICATE, &h_Token))
		goto _CleanUp;

	if (!DuplicateTokenEx(h_Token, TOKEN_ALL_ACCESS, 0, SecurityImpersonation, TokenPrimary, &h_Token2))
		goto _CleanUp;

	if (!f_CreateProcessWithTokenW(h_Token2, 0, u16_Path, u16_CmdLine, 0, 0, 0, &k_StartupInfo, &k_ProcInfo))
		goto _CleanUp;

	SetLastError(ERROR_SUCCESS);

	_CleanUp:
	DWORD u32_Error = GetLastError();
	if (h_Token)   CloseHandle(h_Token);
	if (h_Token2)  CloseHandle(h_Token2);
	if (h_Process) CloseHandle(h_Process);
	return u32_Error;
}

int main(int argc, char* argv[])
{
	DWORD u32_Err = CreateProcessMediumIL(L"C:\\Windows\\System32\\Calc.exe", NULL);

	printf("CreateProcessMediumIL() exited with error %d\r\n", u32_Err);
	Sleep(2000);
	return 0;
}

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