Introduction
When I began this project, I didn't think it would take me two weeks to complete. Well, here I am, finally, writing the article portion of the whole thing. Don't worry, I took really good notes. I am splitting this article into three major segments: The first segment is a broad overview for those who really don't care about the nitty-gritty details of UAC and just want to get to the good stuff. The second segment gets into the really nasty stuff...for those of you who enjoy information overload. The last segment covers UAC Permanent Links - load multiple elevated DLLs, execute multiple elevated functions, and start multiple processes elevated - all of that with just displaying one UAC dialog from a non-elevated process. And now, without further ado, the article.
The Big Picture
Ah. Vista. Many things have been said about it. Some good, some bad, but that doesn't really matter. Vista looks pretty, has new APIs, and has UAC (Microsoft TechNet, Wikipedia).
Ah. UAC. For the acronymically challenged, it stands for User Account Control. The bane of numerous existing software applications, and the whole reason I ended up creating the Elevate package. I initially created it for my own users, but figured the rest of you developers need it as well.
We would all like to think our applications automatically work under Vista. However, the rude awakening that follows simply plastering Vista on the end of Win95/98/Me/NT/2000/XP/2003 causes us to actually do something about it. If you are reading this, I can only assume you have run into one of several barriers to successful application deployment that UAC introduces.
The majority of this article covers how UAC operates and how CreateProcessElevated()
, half of the Elevate package, is possible. The other half of the Elevate package is to solve the major annoyance of UAC for non-elevated applications: the constant requirement to go through the UAC dialog to perform all operations that can only be done within elevated processes. If you find yourself in this position but don't want to switch your whole application to only run as an elevated process, this article is for you as well.
A few Google searches on UAC usually turns up someone's blog discussing how to use ShellExecuteEx()
with the undocumented "runas" verb to force a process to start elevated. Alternatively, a modified manifest file can result in the same thing when a user runs the executable. There is also an article here on CodeProject on how to bypass ShellExecuteEx()
by using an NT Service to do the job, but it has various issues besides the obvious system security-related ones.
But if you are like me, ShellExecuteEx()
is not an option, and I didn't want to bypass UAC either - Microsoft made UAC for a reason, and I want to play nice. I have a large library, and depend heavily on CreateProcess()
. Unfortunately, Microsoft failed to make a CreateProcessElevated()
API for people like me. Fortunately, I found a way to make a series of CreateProcess...Elevated()
APIs with nearly complete functionality.
Before I get to CreateProcessElevated()
, let's look at some code that uses ShellExecuteEx()
:
SHELLEXECUTEINFOA TempInfo = {0};
TempInfo.cbSize = sizeof(SHELLEXECUTEINFOA);
TempInfo.fMask = 0;
TempInfo.hwnd = NULL;
TempInfo.lpVerb = "runas";
TempInfo.lpFile = "C:\\TEMP\\UAC\\Test.exe";
TempInfo.lpParameters = "";
TempInfo.lpDirectory = "C:\\TEMP\\UAC\\";
TempInfo.nShow = SW_NORMAL;
::ShellExecuteExA(&TempInfo);
That is your basic, run-of-the-mill, let's force Vista to elevate this process, function call. As was stated before, the magic word here is "runas". As far as I can tell, the "runas" verb is completely undocumented in the MSDN Library. At least, I never found any reference to it during my research.
Let me take this time right now and say that, wherever possible, avoid using the "runas" verb and prefer manifest file modifications. Which brings me to, uh, manifest modifications.
If you have never used a manifest, you probably live in the console realm. Or a cave. Or both. I generally only use a manifest when I need to let the Windows loader know to load the latest Common Controls. Manifests are required to load Common Controls 6 and later (theme support) via what are called Side-by-Side Assemblies (SxS, you may have noticed the "assembly" directory in C:\WINDOWS and wondered what that is). This is Microsoft's solution to DLL Hell, and it generally works.
A manifest is a plain-ol' XML file. Let's look at your average manifest file with Common Controls 6 support:
="1.0"="UTF-8"="yes"
<assembly xmlns="urn:schemas-microsoft-com:asm.v1"
manifestVersion="1.0">
<dependency>
<dependentAssembly>
<assemblyIdentity
type="win32"
name="Microsoft.Windows.Common-Controls"
version="6.0.0.0"
processorArchitecture="X86"
publicKeyToken="6595b64144ccf1df"
language="*"
/>
</dependentAssembly>
</dependency>
</assembly>
The Internet tells us that, for administrative privileges under Vista, modify the above file to be:
="1.0"="UTF-8"="yes"
<assembly xmlns="urn:schemas-microsoft-com:asm.v1"
manifestVersion="1.0">
<dependency>
<dependentAssembly>
<assemblyIdentity
type="win32"
name="Microsoft.Windows.Common-Controls"
version="6.0.0.0"
processorArchitecture="X86"
publicKeyToken="6595b64144ccf1df"
language="*"
/>
</dependentAssembly>
</dependency>
<v3:trustInfo xmlns:v3="urn:schemas-microsoft-com:asm.v3">
<v3:security>
<v3:requestedPrivileges>
<v3:requestedExecutionLevel level="requireAdministrator" />
</v3:requestedPrivileges>
</v3:security>
</v3:trustInfo>
</assembly>
<BZZZZZZZT!>
Wrong answer! Apparently, a malformed v3 manifest file will cause Windows XP SP2 to crash. And not any ordinary crash either. A Blue Screen Of Death (BSOD), complete with reboot. Without actually taking CreateProcess()
apart, this is probably due to an XML parser crash in kernel mode (only way I know of to BSOD XP is to crash the kernel in ring 0). Also, apparently v2 manifests are significantly more stable and yet still work. So, for maximum stability, the correct format for the manifest file should be:
="1.0"="UTF-8"="yes"
<assembly xmlns="urn:schemas-microsoft-com:asm.v1"
manifestVersion="1.0">
<dependency>
<dependentAssembly>
<assemblyIdentity
type="win32"
name="Microsoft.Windows.Common-Controls"
version="6.0.0.0"
processorArchitecture="X86"
publicKeyToken="6595b64144ccf1df"
language="*"
/>
</dependentAssembly>
</dependency>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges>
<requestedExecutionLevel
level="requireAdministrator"
uiAccess="false"/>
</requestedPrivileges>
</security>
</trustInfo>
</assembly>
requestedExecutionLevel
's "level
" option can be one of 'asInvoker
', 'requireAdministrator
', or 'highestAvailable
'. I will cover the "uiAccess
" option later on in this article.
For those who are COM/DCOM/whatever addicts and heavily invested in that technology, you're probably already way ahead of me, but I'll post some redundant moniker out-of-process elevation code for you:
HRESULT CreateElevatedComObject(HWND hwnd,
REFCLSID rclsid,
REFIID riid,
__out void ** ppv)
{
BIND_OPTS3 bo;
WCHAR wszCLSID[50];
WCHAR wszMonikerName[300];
StringFromGUID2(rclsid, wszCLSID, cntof(wszCLSID));
HRESULT hr = StringCchPrintf(wszMonikerName,
cntof(wszMonikerName)),
L"Elevation:Administrator!new:%s",
wszCLSID);
if (FAILED(hr))
return hr;
memset(&bo, 0, sizeof(bo));
bo.cbStruct = sizeof(bo);
bo.hwnd = hwnd;
bo.dwClassContext = CLSCTX_LOCAL_SERVER;
return CoGetObject(wszMonikerName, &bo, riid, ppv);
}
#define cntof(a) (sizeof(a)/sizeof(a[0]))
I honestly have no idea what that code does. But apparently, it is magically delicious. I did spot the "Elevation:Administrator!new:" string in ShellExecuteEx()
, so I can only assume the code does work. I try to avoid COM wherever and whenever possible. COM is kind of like the plague.
User Interface Integration
If you are going to do process elevation, you have got to do it right and do it stylishly as well. The Vista shield icon is plastered pretty much everywhere where there is an administrative task to be performed. Task Manager, for instance, won't show all running processes from all users until you click the button with the shield icon and go through the UAC elevation process.
Fortunately, Microsoft makes it easy to plant the shield icon onto dialog buttons:
GetDlgItem(IDC_SOMEBUTTONID)->SendMessage(BCM_SETSHIELD, 0, TRUE);
Button_SetElevationRequiredState(ButtonHWnd, fRequired);
Unfortunately, however, putting the shield icon anywhere else is a pain. You can call the new Vista-only function SHGetStockIconInfo()
to extract it as an HICON
:
HICON ShieldIcon;
SHSTOCKICONINFO sii = {0};
sii.cbSize = sizeof(sii);
SHGetStockIconInfo(SIID_SHIELD, SHGFI_ICON | SHGFI_SMALLICON, &sii);
ShieldIcon = sii.hIcon;
However, unless you are designing a Vista-only application, you will want to do the whole LoadLibrary()
/GetProcAddress()
thing for SHGetStockIconInfo()
.
Alternatively, LoadIcon()
with IDI_SHIELD
can be used to get an HICON
, but that apparently loads a "low-quality" shield icon. The new LoadIconMetric()
API for Vista is a better solution because it loads a "high-quality" shield icon, which will apparently be used for high DPI displays.
UAC Weirdness
So far, I've just covered the most prominent portions of UAC, but UAC is way more than just displaying a dialog to the user. It is a way of life. Or something like that. And, as with most things in life, UAC can be downright weird.
Many of us have existing applications. Some of us even have badly written applications that write to the "Program Files" directory. And some applications are really badly behaved, and write both to the Windows directory and the HKLM registry key.
UAC treats misbehaving (non-elevated) applications as illegitimate children. There is something called the "virtual store" for each application that misbehaves. This consists of files and registry keys that are written to in places that are deemed bad to write for non-elevated processes. What happens when a write operation occurs is, the OS copies the original file to the user's virtual store and redirects all requests to that location instead. Thus, the application makes changes to the file/registry key in the virtual store, and not the actual location it was intending to make changes to. But, only for that user. Other users will see the original file or a copy in their virtual store.
But wait! It gets weirder. Let's say the application goes to delete a file it has modified in Program Files. Well, the OS redirects the request to the virtual store instead. Yet, even though the file was deleted, if the application goes back, it can see that the file still exists. Once removed from the virtual store, the OS allows the application to see the original file. However, if the application attempts to delete the file again, it will cause an error to occur. It makes sense, but it is weird.
UAC also causes a number of other minor issues to come up: Interactive Services (of the GUI variety) are completely hosed (i.e., creating windows from NT Services), administrative shares have major issues, various functions that take or create tokens (e.g., HANDLE hToken) are affected by the split token (e.g., LogonUser), and side-by-side isolation issues.
Which, of course, brings me to split tokens. Split tokens are just weird. When you log onto Windows Vista and later (obviously), you get a split token. Basically, the logon architecture (which changed...again...for Vista) takes your initially really powerful user token and creates a second token with all Administrator privileges stripped out. This second token is used to launch all applications essentially as what XP called a Limited User Account (LUA). When UAC prompts for elevation and you accept, the first token is used to create the process instead of the second token. Essentially, what UAC elevation prompts are asking is, "Do you really want me to use your super powerful administrative token to start this application?"
Another weird UAC thing is the elevation dialog. If you let it sit there for an extended period of time, it will automatically cancel itself. Found that out during my research.
The last weird thing about UAC is that once a process has been started as an elevated process, it becomes very difficult to start a process non-elevated from the elevated process. This becomes a major nuisance for authors of software installers. Us, software developer types, like to let users try out the software at the end of an installation so the user has the tendency to actually use the software and have a greater chance of buying it. There is an article that shows how to ride the Vista elevator by using a global hook and hooking Explorer.exe to create a process at a lower Integrity Level.
CreateProcess() Fails With ERROR_ELEVATION_REQUIRED
Those are the basics of UAC, all in one concise location. For many people, ShellExecuteEx()
, modified manifests, switching to HKCU, and not writing to naughty places on the hard drive, are "good enough" solutions. However, some of you want to know more. And some of you need CreateProcess()
and all the power it contains (and a few people might need ShellExecute()
/ShellExecuteEx()
with custom verbs).
Now, on to the Elevate package and CreateProcessElevated()
.
CreateProcess()
fails miserably for processes in Vista that require elevation via their manifest file. It should be noted that the manifest file doesn't really say "you have to use the administrative token to start this process". Instead, it says, "you can't use a token with fewer than these rights to start this process". So, the error message returned from CreateProcess()
, ERROR_ELEVATION_REQUIRED
(740), is somewhat misleading.
Regardless, if you are reading this, I can only assume you are desperate for a CreateProcessElevated()
solution. Which is what the Elevate package is for. The Elevate package (Elevate_BinariesAndDocs.zip) consists of two components plus comprehensive documentation: the Elevation API DLL (Elevate.dll) and the Elevation Transaction Coordinator (Elevate.exe). Both must be placed in the same directory for the elevation process to work properly. The Elevation API DLL exports the following functions:
Link_Create()
Link_CreateAsUser()
Link_CreateWithLogon()
Link_CreateWithToken()
Link_Destroy()
Link_CreateProcessA()
Link_CreateProcessW()
Link_ShellExecuteExA()
Link_ShellExecuteExW()
Link_ShellExecuteA()
Link_ShellExecuteW()
Link_LoadLibraryA()
Link_LoadLibraryW()
Link_SendData()
Link_GetData()
Link_SendFinalize()
CreateProcessElevatedA()
CreateProcessElevatedW()
CreateProcessAsUserElevatedA()
CreateProcessAsUserElevatedW()
CreateProcessWithLogonElevatedW()
CreateProcessWithTokenElevatedW()
SH_RegCreateKeyExElevatedA()
SH_RegCreateKeyExElevatedW()
SH_RegOpenKeyExElevatedA()
SH_RegOpenKeyExElevatedW()
SH_RegCloseKeyElevated()
ShellExecuteElevatedA()
ShellExecuteElevatedW()
ShellExecuteExElevatedA()
ShellExecuteExElevatedW()
IsUserAnAdmin()
You will note that the elevated APIs have very similar names to their non-elevated counterparts (e.g., CreateProcess()
and CreateProcessElevated()
). And this package includes both ANSI and Unicode versions too (A vs. W). You may note that IsUserAnAdmin()
is already a function exported from Shell32.dll, but MSDN says it may go away. IsUserAnAdmin()
currently defers to the Shell32.dll version, but can fall back to internal code if it does disappear.
Moving along. You probably have existing code that looks something like this:
Result = CreateProcess(...ParameterList...);
if (!Result) return FALSE;
...Do something with process like WaitForSingleObject()...
::CloseHandle()'s;
which works great until you try to launch a process that requires elevation. To use the Elevate package, just drop it on the system, and LoadLibrary()
/GetProcAddress()
the CreateProcessElevatedA()
function:
Result = CreateProcess(...ParameterList...); if (!Result && GetLastError() == ERROR_ELEVATION_REQUIRED)
{
HMODULE LibHandle = LoadLibrary("Elevate.dll");
if (LibHandle != NULL)
{
DLL_CreateProcessElevated =
(typecast)GetProcAddress("CreateProcessElevatedA");
if (DLL_CreateProcessElevated)
{
...Custom handle changes here *...
Result = DLL_CreateProcessElevated(...ParameterList...);
...Custom handle connections here *...
}
FreeLibrary(LibHandle);
}
}
if (!Result) return FALSE;
The "...ParameterList..." options are _nearly_ identical. The only time you have to change the parameter is when you use the STARTF_USESTDHANDLES
flag in the STARTUPINFO
structure you pass in. If you do redirection of the standard handles (stdin, stdout, stderr), things can get a bit funky. Read the documentation, but, in short, you will need to familiarize yourself with named pipes and read the rest of this article in its entirety.
Note that you must use LoadLibrary()
/GetProcAddress()
. The DLL will intentionally fail to load on any Windows OS prior to Vista.
ShellExecute...Elevated()?!
Some of you are probably raising eyebrows as to the need for a ShellExecuteElevated()
set of APIs. There are two reasons to do this. The first reason is actually a special case problem that the "runas" verb introduces. The lpVerb
/lpOperation
parameter only allows one verb to be used at a time. So, if someone needs to force a process to run elevated (that would normally run non-elevated) and, for example, use the "print" verb, the regular ShellExecute()
/ShellExecuteEx()
won't cut it.
The second reason is because someone may specifically need to run a ShellExecute()
/ShellExecuteEx()
command from within an elevated environment. There is no way to do this either.
Now, I will admit the need for the ShellExecuteElevated()
APIs is going to be rare, but I am including them for completeness.
Demo Application
For the lack of a better spot to put this, the demo application is an example of CreateProcessElevated()
in action, in all of its glory. To use the demo, first download it, and extract it to some directory you know how to get to from the Command Prompt. Then, start a non-elevated Command Prompt, and go to the directory you extracted the files to. Type in "TestParent", and press Enter. What follows should look like this:
Ignoring the obvious reference to my knowledge of cool phrases, what happens is really quite impressive. Normally, an elevated console-based program started from a non-elevated console-based program would have a separate console window. In this case, TestChild.exe (elevated) and TestParent.exe (non-elevated) share the same console. Additionally, TestChild.exe's stderr is being routed to TestParent.exe. This is possible by very carefully sifting through tons and tons of documentation and some experimentation.
Also, while the program doesn't show it, TestChild.exe shares the same environment variables as TestParent.exe.
The Nitty Gritty
Before I can discuss how Elevate.dll and Elevate.exe work to make the demo even possible, I have to cover some of the nastier details of how UAC elevation works. Bear with me, it gets pretty in-depth.
When I started this project, I wanted to avoid using ShellExecuteEx()
, so to do that, I had to figure out what made the function "tick". My first thought was, "Well, they have to call CreateProcess()
and related kin somewhere along the line. So, there's some trick to the call, right?" My first stop was the Detours traceapi.dll file. I hooked it into a test process with the ShellExecuteEx()
API, and...nada. Nothing. I thought maybe traceapi.dll was broken, so I wasted a day on figuring out that maybe ShellExecuteEx()
wasn't using CreateProcess()
at all.
So, the next step was to dig into the actual call with the Visual Studio Disassembler. I quickly realized I needed to run out to the symbol store and get the symbols for the Vista Shell32.dll and other related DLLs. I know Microsoft massages their symbols on the symbol server to trim out stuff they don't want people knowing. I was hoping UAC elevation wouldn't be part of that. Turns out, they didn't remove that information, or just simply forgot to do so. Either way, you can be eternally grateful the information was there.
Stepping through ShellExecuteEx()
is quite confusing. In fact, I'm still not sure what some of the stuff does. If you are following along with a debugger, your call stack will eventually look like this:
shell32.dll!CShellExecute::ExecuteNormal() + 0x7a
shell32.dll!ShellExecuteNormal() + 0x33
shell32.dll!_ShellExecuteExW@4() + 0x42
shell32.dll!_ShellExecuteExA@4() + 0x4a
ShellExecuteTest.exe!main() Line 18 + 0xc
ShellExecuteTest.exe!mainCRTStartup() Line 259 + 0x19
kernel32.dll!@BaseThreadInitThunk@12() + 0x12
ntdll.dll!__RtlUserThreadStart@8() + 0x27
Looks promising, right? Well, under a normal function, yeah, that's probably about where you would execute a command. In this case, however, ShellExecuteEx()
is just getting warmed up to...start a new thread. Seriously. To start a new process, a new thread is started. It is sickening. So, we step through with the debugger inside the new thread until the call stack looks like this:
shell32.dll!CExecuteApplication::Execute() + 0x22
shell32.dll!CExecuteAssociation::_DoCommand() + 0x5b
shell32.dll!CExecuteAssociation::_TryApplication() + 0x32
shell32.dll!CExecuteAssociation::Execute() + 0x30
shell32.dll!CShellExecute::_ExecuteAssoc() + 0x82
shell32.dll!CShellExecute::_DoExecute() + 0x4c
shell32.dll!CShellExecute::s_ExecuteThreadProc() + 0x25
shlwapi.dll!WrapperThreadProc() + 0x98
kernel32.dll!@BaseThreadInitThunk@12() + 0x12
ntdll.dll!__RtlUserThreadStart@8() + 0x27
Now, that looks promising, right? Well, to get here was an abysmal mess that takes something like five hours of pressing F10 and F11. You go through a huge mess of COM objects. Yup, that's right. COM is now involved in starting a new process. And that means bringing in a mess of DLLs with it. But guess what? We ain't done yet.
Inside the Execute()
method, a call to CExecuteApplication::_VerifyExecTrust()
is made. This uses COM, and takes a LOT of CPU (along with a bunch of blatant and unnecessary replication, code-wise). I sort of gave up trying to figure out what it did. My general impression was that it most likely has something to do with the pop up dialogs received when launching an EXE downloaded from the Internet (IZoneIdentifier). Probably looks for the existence of a NTFS stream called 'Zone.Identifier' so it can pop up that nifty (and annoying) dialog you see for downloaded files before executing them.
I backed out of _VerifyExecTrust()
, and went down the only remaining path. The call stack turned hilarious:
shell32.dll!AicpMsgWaitForCompletion() + 0x36
shell32.dll!AicpAsyncFinishCall() + 0x2c
shell32.dll!AicLaunchAdminProcess() + 0x2ee
shell32.dll!_SHCreateProcess() + 0x59d0
shell32.dll!CExecuteApplication::_CreateProcess() + 0xac
shell32.dll!CExecuteApplication::_TryCreateProcess() + 0x2e
shell32.dll!CExecuteApplication::_DoApplication() + 0x3c
shell32.dll!CExecuteApplication::Execute() + 0x33
shell32.dll!CExecuteAssociation::_DoCommand() + 0x5b
shell32.dll!CExecuteAssociation::_TryApplication() + 0x32
shell32.dll!CExecuteAssociation::Execute() + 0x30
shell32.dll!CShellExecute::_ExecuteAssoc() + 0x82
shell32.dll!CShellExecute::_DoExecute() + 0x4c
shell32.dll!CShellExecute::s_ExecuteThreadProc() + 0x25
shlwapi.dll!WrapperThreadProc() + 0x98
kernel32.dll!@BaseThreadInitThunk@12() + 0x12
ntdll.dll!__RtlUserThreadStart@8() + 0x27
Gotta love the names given to the functions. It sort of eggs you on to see what's next. _DoExecute()
, _Execute()
, _TryApplication()
, _DoCommand()
... Okay, now we're going to create the process. Oh, wait, never mind, now we're going to create the process. Oh, sorry about that, _now_ we're going to create the process... The marketing engine has gotten into the source code. Poor developers. I feel sorry for you guys who have to work with that mess every single day.
At any rate, the function of interest is AicLaunchAdminProcess()
. Once we get to AicpMsgWaitForCompletion()
, it will have gone too far. It took me almost two days of scratching my head to figure out how entering a MsgWaitForMultipleObjects()
could cause UAC to display the elevation dialog unchecked and then continue as if nothing had happened when Accept/Cancel was clicked.
Turns out, not only is a new thread started and a half dozen COM objects instantiated, but RPC is brought into the mix as well. RPC (Remote Procedure Call for the acronymically challenged) is a pretty old technology, but mostly just used by Microsoft for NT Services. It combines with MIDL and allows you to, uh, call remote procedures. The target function could be on another computer or the same computer. Sort of one of those unexploited security holes because it is also an obscure technology that few know how to use.
And, this is where things get interesting. The RPC call is to a brand new NT Service in Vista called AppInfo (UUID "{201ef99a-7fa0-444c-9399-19ba84f12a1a}"). AppInfo is where the magic happens. But before I get to the magic, a short discussion on Sessions and Integrity Levels is in order.
Vista Sessions and Integrity Levels
Part of the changes in Vista and how UAC works is the introduction of what are known as Sessions. They are also occasionally referred to as "Secure Desktops". Vista has two Sessions, but could technically have many more than that. Session 0 and Session 1 are the official names. Session 0 is where all NT Services reside. Session 1 is where the "WinSta0\Default" desktop resides along with Explorer and user applications. Just to clarify, Window Stations are not Sessions.
It is important to note that some documentation on Vista UAC implicitly claims each Session is supposedly completely segmented from communication with other Sessions. This is not true. Most likely, the authors are referring to window messages or an early beta. Official Microsoft documentation, Impact of Session 0 Isolation on Services and Drivers in Windows Vista, says to use RPC and Named Pipes to communicate between processes running on different Sessions. RPC, Named Pipes, and sockets are all positively confirmed Interprocess Communication (IPC) mechanisms under Vista.
The other major change in Vista with UAC is what are known as Integrity Levels (ILs). There are four Integrity Levels: System (NT Services), High (Elevated processes), Medium (most user processes), Low (processes like Protected-mode IE). Every object created has an IL associated with it, and ILs are checked before DACLs. The process/thread IL is checked against the object IL before granting access to it. It is important to note that objects created by elevated and system processes, by default, have a Medium Integrity Level. (For the observant person, that last sentence is the key to why the Elevate package works.)
AppInfo and consent.exe
AppInfo is, obviously, the key to UAC elevation. ShellExecuteEx()
forwards all elevation requests to the AppInfo NT Service via an RPC call. AppInfo then turns around and calls an executable in the SYSTEM context, called "consent.exe". As the name implies, this is the executable that brings up the dialog that the user consents to.
However, consent.exe is not your average, run-of-the-mill application. What you see when the dialog is active is not Session 1's WinSta0\Default. Nope. What you see is a desktop on Session 0. Hence the reason it is called a "secure desktop". consent.exe takes a snapshot of the screen, switches to the Session 0 desktop, plops the screenshot on the desktop, and displays the dialog. It only looks like Session 1's desktop. You can't click on anything but the dialog, because there literally isn't anything to click on. Once you accept/cancel, the desktop is switched again back to Session 1 and consent.exe exits.
AppInfo then takes the results from consent.exe and determines if it needs to start a new process (i.e., you accepted the elevation request). AppInfo then creates a process using the full administrative token (remember that split token thing?) of the logged in user on the Session 1 desktop with a High Integrity Level. If you fire up Task Manager, you can see that elevated processes indeed run as the current user. We know it also runs on the Session 1 desktop because GUI windows can be created, seen, and interacted with.
To create a process as the current user on a different desktop in a different Session is a seven stage process:
- AppInfo goes and talks to the Local Security Authority to get the elevated token of the logged in user of Session 1.
- AppInfo loads up a
STARTUPINFOEX
structure (new to Vista), and calls the brand new Vista API InitializeProcThreadAttributeList()
with room for one attribute. OpenProcess()
is called to get a handle to the process that initiated the RPC call.UpdateProcThreadAttribute()
is called with PROC_THREAD_ATTRIBUTE_PARENT_PROCESS
, and uses the handle retrieved in step 3.CreateProcessAsUser()
is called with EXTENDED_STARTUPINFO_PRESENT
and the results of steps 1 and 4.DeleteProcThreadAttributeList()
is called.- Results are gathered, and handles are cleaned up.
Once AppInfo succeeds in launching the process, it transfers some information back over the RPC interface to the application that called ShellExecuteEx()
. ShellExecuteEx()
meanders around a bit, and cleans up after itself, and eventually returns through the whole mess of function calls, closes the thread, and returns to the caller.
CreateProcessElevated() Without ShellExecuteEx()?
Once I had learned about roughly where in ShellExecuteEx()
makes its RPC call to AppInfo and how AppInfo worked, I wanted to know if it was possible to create a fully-functional CreateProcessElevated()
without making that insanely expensive call to ShellExecuteEx()
. By fully functional, I meant I wanted complete control over the STARTUPINFO
structure, with support for the STARTF_USESTDHANDLES
flag.
This process took me about three days to complete, and was quite exhausting. I eventually narrowed everything down to a few lines of assembler inside AicLaunchAdminProcess()
:
mov edi, [ebp+VarStartupInfo]
mov eax, [edi+_STARTUPINFOW.lpTitle]
mov [ebp+VarStartupInfo_Title], eax
mov eax, [edi+_STARTUPINFOW.dwX]
mov [ebp+VarStartupInfo_X], eax
mov eax, [edi+_STARTUPINFOW.dwY]
mov [ebp+VarStartupInfo_Y], eax
mov eax, [edi+_STARTUPINFOW.dwXSize]
mov [ebp+VarStartupInfo_XSize], eax
mov eax, [edi+_STARTUPINFOW.dwYSize]
mov [ebp+VarStartupInfo_YSize], eax
mov eax, [edi+_STARTUPINFOW.dwXCountChars]
mov [ebp+VarStartupInfo_XCountChars], eax
mov eax, [edi+_STARTUPINFOW.dwYCountChars]
mov [ebp+VarStartupInfo_YCountChars], eax
mov eax, [edi+_STARTUPINFOW.dwFillAttribute]
mov [ebp+VarStartupInfo_FillAttr], eax
mov eax, [edi+_STARTUPINFOW.dwFlags]
mov [ebp+VarStartupInfo_Flags], eax
mov cx, [edi+_STARTUPINFOW.wShowWindow]
mov [ebp+VarStartupInfo_ShowWindow], cx
...
push eax
push ebx
push 0FFFFFFFFh
push [ebp+hwnd]
lea eax, [ebp+VarStartupInfo_Title]
push eax
push [ebp+hMemToWinSta0_Desktop]
push [ebp+VarExpandedCurrDir]
push [ebp+ArgCreationFlags]
push [ebp+arg_8]
push [ebp+VarExpandedCommandLine]
push [ebp+VarExpandedApplicationName]
push StaticBindingHandle
lea eax, [ebp+pAsync]
push eax
call _RAiLaunchAdminProcess@52
RAiLaunchAdminProcess()
takes a whole bunch of parameters, and then routes the whole thing behind the scenes through a very nasty MIDL call. You will note the lea
(Load Effective Address) on [ebp+VarStartupInfo_Title]
. For you non-assembler gurus, this is essentially passing the address of the address of a data block from Title to ShowWindow
. So, this means only a limited amount of information from STARTUPINFO
is actually passed onto the target process. This tells me that AppInfo doesn't handle very much information.
At this point, I chucked the idea of doing my own RPC thing with AppInfo out the window, cried for two seconds for wasting a couple days, and then went to figure out how to use ShellExecuteEx()
to do what I wanted.
Routing via ShellExecuteEx()
I quickly decided to create a split DLL/EXE approach. My target audience was initially my own customer base, so it needed to be clean and easy to understand. The DLL would export functions that looked and felt a lot like Win32 APIs. The DLL would then package up each function's data and shuttle it across to the EXE, which would take that data, unpack it, and execute the correct function as an elevated process.
My initial approach was to shuttle data across using the lpParameters
member of SHELLEXECUTEINFO
. To my surprise, doing that caused an extremely meaningless error message to pop up: "The data area passed to a system call is too small." (Error code 122). It took a lot of experimentation, but apparently sending more than 2048 bytes (2K) of data in the lpParameters
member causes that error message to occur.
My solution was to pass the process ID and thread ID to Elevate.exe from Elevate.dll, and use those bits of information to open a named pipe, and connect to a named pipe server running in Elevate.dll to send the information.
About Named Pipes
But I'm getting way ahead of myself there. I got the idea for using named pipes once I learned about Integrity Levels and the fact that objects created at higher ILs only have a Medium Integrity Level...the same level as non-elevated processes.
But pipes are HANDLE
based. And, there's the "gotcha". Remember that whole roundabout mess to get a process elevated? Well, while HANDLE
s are inheritable, the process that actually starts the elevated process is AppInfo, and not the process making the ShellExecuteEx()
call. And, even if HANDLE
s were passable, there is the issue of the Session 0 to Session 1 barrier. So, passing raw HANDLE
values won't work.
Someone may point out that AppInfo's method of starting a process does actually inherit the handles of the original process and would include the Standard HANDLE
s (stdin, stdout, stderr). However, while this is true and the HANDLE
s are inheritable, AppInfo would have to populate the STARTUPINFO
structure with HANDLE
s that can't cross the Session 0 to Session 1 barrier.
The solution lies in named pipes. Named pipes have, um, names. Sometimes ANSI, sometimes Unicode, but those names are strings. And, unlike HANDLE
s, strings are passable via IPC. CreateNamedPipe()
and CreateFile()
with the same string equates to a HANDLE
that points at the same pipe. And, since both objects have a Medium Integrity Level, it works too. For security reasons and for DACL reasons, the DLL calls CreateNamedPipe()
and the EXE calls CreateFile()
to connect to the named pipe.
The MSDN Library confirms that starting a process using named pipes for redirection is possible.
How Elevate Works
Let's assume CreateProcessElevated()
is being called. Elevate.dll is loaded up, and the function is called. Just before ShellExecuteEx()
is called, one named pipe and three event objects are created. Elevate.dll starts Elevate.exe as an elevated process. Upon successful return (process started), Elevate.dll waits for Elevate.exe to finish initializing. Note that the event objects time out after 10 seconds and bail. This is so that the application doesn't hang.
Once the two processes are in sync with each other, Elevate.dll packs up the data sent to the function, and sends it as a packet of data to Elevate.exe. Elevate.exe receives the packet of data and unpacks it.
From here, Elevate.exe prepares to run the process. It attaches to the console of the process that started Elevate.exe. Also, I use the STARTF_USESTDHANDLES
flag every so often, and Elevate.exe takes named pipe names and sets up the appropriate HANDLE
s for stdin, stdout, and stderr along with building the rest of the STARTUPINFO
structure. Attaching to a console gives access to that console's stdin, stdout, and stderr HANDLE
s.
After that, the correct function is executed with correct parameters for everything. The only thing interesting to note is the use of the CREATE_SUSPENDED
flag. This flag starts the process suspended. The reason for this is so that information about the process and main thread are sent back to Elevate.dll. Remember, raw HANDLEs
can't be passed, but process and thread IDs can be. Also, keep in mind that it is possible for the started process to have such a short lifetime that it would exit before Elevate.dll could open appropriate HANDLE
s for synchronization purposes.
Elevate.dll then receives the information about the process, processes it, and then fires the event to let Elevate.exe know that it has received and processed the information.
Elevate.exe resumes the process (if applicable), and exits. Elevate.dll returns, and the caller resumes normal operations.
UAC Permanent Links
The Elevation API DLL (Elevate.dll) exports the following functions:
Link_Create()
Link_CreateAsUser()
Link_CreateWithLogon()
Link_CreateWithToken()
Link_Destroy()
Link_CreateProcessA()
Link_CreateProcessW()
Link_ShellExecuteExA()
Link_ShellExecuteExW()
Link_ShellExecuteA()
Link_ShellExecuteW()
Link_LoadLibraryA()
Link_LoadLibraryW()
Link_SendData()
Link_GetData()
Link_SendFinalize()
Now, you may be wondering, or have already wondered, what good those functions are. The term UAC Permanent Link is something I made up. Hey, there's nothing wrong with a little artistic license, right?
What exactly is a UAC Permanent Link? Suppose you have an application that you wrote and worked great on Windows XP, but required you to create a separate executable for Vista due to elevation issues. You plastered the shield icon wherever elevation would take place, and released the updated software.
Bam. Some users complained about the excessive UAC dialog use. However, you don't want to convert the whole application to require elevation to operate. This is where UAC Permanent Links come in handy.
Link_Create()
instantiates a UAC Permanent Link. It launches Elevate.exe, which shows the UAC dialog. The user clicks "Allow", and Elevate.exe starts. The HANDLE
that is returned from Link_Create()
is then able to be passed to the other Link_...()
functions. When the application is done with the UAC Permanent Link (e.g., at the end of the program), Link_Destroy()
is called. Elevate.exe exits, and all resources are cleaned up.
The possibilities for this are endless. Once the UAC dialog has been displayed and allowed, your non-elevated program has free reign in the elevated process space.
CreateProcessElevated()
and other exported functions actually instantiate a UAC Permanent Link, call Link_CreateProcess()
or whatever is appropriate, and then call Link_Destroy()
.
Instead of starting processes, consider making a DLL. You can even display dialogs that return data to the non-elevated process. It does get somewhat complicated as all data sent/received has to be serialized and unserialized, but it may be worth saving the overhead of starting multiple processes, and two-way communication with the non-elevated application could be important as well. If you take this route, I recommend looking at the source code of Elevate to get a feel for how it works.
The DLL approach also works great if you need to load one or more API hook DLLs into the elevated process space.
The nice part about UAC Permanent Links is that this approach falls completely within Microsoft's defined parameters for how UAC is to be used.
Source Code
For those of you who download the source code, please note that the source code is just the Application Layer (Safe C++ terminology). The Base Library is not included. The source code is being made available in case someone finds a bug, they can point it out and maybe even fix it. I also included it so you could more or less follow along with this article.
More .manifest Issues
As stated earlier, an associated manifest file with an executable can have a requestedExecutionLevel
's "level
" of 'asInvoker
', 'requireAdministrator
', or 'highestAvailable
'. 'asInvoker
' means a non-elevated token or higher can start the process. 'requireAdministrator
' means elevated or higher can start the process. 'highestAvailable
' means the process will start as the highest level permitted by the user's split token.
I said earlier that I would discuss the 'uiAccess
' option. 'uiAccess
' is designed to grant a select set of non-elevated processes access to elevated process user interfaces. This is specifically designed for UI automation tasks (e.g., macro recorder and playback tools). Most of the time, you will want to specify the 'uiAccess
' option as 'false'.
But what if you want to specify it as 'true'? Well, get ready to spend some money. You'll see recommendations for Verisign Code Signing Certificates, but they run ridiculous sums of green ($400/year). Alternatively, you can try one of these companies...just be sure they support Code Signing (and possibly time stamping - the more 'yes's, the better).
According to the comments, Tucows.com offers Comodo Code Signing certificates for $75/year. Very nice.
If you just need to try something out for personal use, you won't need to shell out any dough, but you will need the latest Authenticode tools and an EXE to sign that contains a manifest with uiAccess
set to 'true'. If you have that set up, run the following commands from an elevated command prompt:
1) Create a trusted root certificate:
Browse to the folder that you wish to contain a copy of the certificate.
In the command shell, execute the following commands:
makecert -r -pe -n "CN=Test Certificate" -ss PrivateCertStore testcert.cer
certmgr.exe -add testcert.cer -s -r localMachine root
2) Sign your file:
SignTool sign /v /s PrivateCertStore /n "Test Certificate"
/t http://timestamp.verisign.com/scripts/timestamp.dll
YourApp.exe
Where YourApp.exe is your application.
Note: If you sign your executable with a certificate from a trusted code signing certificate authority (CA), the orange UAC elevation dialog becomes a dull gray.
IShellExecuteHook vs. ShellExecuteWithHookElevated()
For those who use IShellExecuteHook
DLLs, you are probably well aware, Microsoft has "deprecated" this COM interface for Vista, and is not currently offering an alternative. It is turned off, by default, because of problems with older hooks crashing the Vista shell. The interface can be turned on by setting:
[HKLM or HKCU]\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer
EnableShellExecuteHooks=1 (DWORD)
An alternative to writing a IShellExecuteHook
DLL is to hook ShellExecute()
, ShellExecuteEx()
, and IsUserAnAdmin()
. When IsUserAnAdmin()
is called by those functions, rewrite the ShellExecute()
call to use the ShellExecute...Elevated()
calls. This method is not perfect because various COM interfaces bypass the APIs to get at the elevation mechanism. You also have to be really careful to not get caught in an infinite loop (the Elevate package calls ShellExecuteEx()
).
Another approach is to do a combination of the two. Write an IShellExecuteHook
DLL and then also hook IsUserAnAdmin()
. This has a better chance of catching everything that is about to do the whole UAC thing, but then you have to worry about if the COM interface will even work.
Additional References
History
- June 2007: Initial release.
- July 2007: Added
ShellExecute...Elevated()
and discussion on Hook DLLs. - August 2007: Corrected discussion on Sessions. Minor article cleanup.
- March 2008: Major rewrite of Elevate package. UAC Permanent Links. Article modifications. Fixed bugs with starting processes as another user (medium IL vs. high IL issue).
Been writing software for a really long time - something like 18 years. Started on the TI/99-4A, moved to the Tandy 1000, and somehow managed to skip all the lousy hardware/software jumps (286, 386, first Pentiums, Win95, etc.)
I now run a small software business called CubicleSoft with a few products you might be interested in. VerifyMyPC and MyUpdate Toolkit are the most popular. I'm also the author of a book called "Safe C++ Design Principles".