Vista UAC: The Definitive Guide

Windows程序权限管理实战:如何避免CreateProcess因管理员权限失败(附完整排查流程) 本文深入解析Windows程序中CreateProcess因管理员权限失败(错误740)的完整排查流程。核心在于理解UAC机制与应用程序清单(manifest)中的requestedExecutionLevel设置。文章提供了从检查项目配置、使用mt.exe诊断清单,到修改权限为asInvoker或重构架构的多种实战解决方案,帮助开发者彻底解决权限继承问题。 阅读详情

原文地址:  http://www.codeproject.com/KB/vista-security/UAC__The_Definitive_Guide.aspx

 

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

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

Collapse
<?xml version="1.0" encoding="UTF-8" standalone="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:

Collapse
<?xml version="1.0" encoding="UTF-8" standalone="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:

Collapse
<?xml version="1.0" encoding="UTF-8" standalone="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:

Collapse
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);
}

// cntof() is defined as:
#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:

Collapse
GetDlgItem(IDC_SOMEBUTTONID)->SendMessage(BCM_SETSHIELD, 0, TRUE);
// OR use the new macro:
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:

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

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

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

Collapse
Result = CreateProcess(...ParameterList...);  // Existing line of code.
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;
// Continue as usual with existing code...

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:

Screenshot - Elevate01.png

Screenshot - Elevate02.png

Screenshot - Elevate03.png

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:

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

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

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

  1. AppInfo goes and talks to the Local Security Authority to get the elevated token of the logged in user of Session 1.
  2. AppInfo loads up a STARTUPINFOEX structure (new to Vista), and calls the brand new Vista API InitializeProcThreadAttributeList() with room for one attribute.
  3. OpenProcess() is called to get a handle to the process that initiated the RPC call.
  4. UpdateProcThreadAttribute() is called with PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, and uses the handle retrieved in step 3.
  5. CreateProcessAsUser() is called with EXTENDED_STARTUPINFO_PRESENT and the results of steps 1 and 4.
  6. DeleteProcThreadAttributeList() is called.
  7. 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():

Collapse
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] ; Probably bInheritHandles
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 HANDLEs are inheritable, the process that actually starts the elevated process is AppInfo, and not the process making the ShellExecuteEx() call. And, even if HANDLEs 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 HANDLEs (stdin, stdout, stderr). However, while this is true and the HANDLEs are inheritable, AppInfo would have to populate the STARTUPINFO structure with HANDLEs 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 HANDLEs, 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 HANDLEs 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 HANDLEs.

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

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

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

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

Win10 Hook 进程创建的研究 Win7甚至在它之前开始,想要hook进程创建已经能简单的HookCreateProcessInternalW这一个函数了,因为Vista开始引入了UAC机制。如果一个进程想要通过UAC弹窗创建管理员进程,hook这个进程的CreateProcessInternalW已经起作用,我在几年前的博文《Win7/VistaHook CreateProcess 的特别之处》(http://blog 阅读详情

相关推荐

STM32实现薄膜压力传感器数据采集(标准库和HAL库实现)

薄膜压力传感器采集压力数据

XiaoCaiDaYong的博客 3万+

ShellExecuteHooks

利用Com技术实现的对ShellExecute和ShellExecuteEx的Hook.

【任何电机可使用的七段式s曲线-----包括matlab代码和simulink仿真】

S形加加速(T1)→>匀加速(T2)→减加速(T3)→>匀速(T4 )>加减速(T5)>匀减速(T6)-→>减减速(T7)

weixin_44312889的博客 6248

ShellExecuteHook

 【转:laiwuyingas 木马技术发展趋势回顾】 经常关注安全的用户或许会见到过"ShellExecuteHook",如今许多木马和恶意程序都在用户层使用它作为启动方式了,但这是一种什么技术,大家可能又会迷惑解了,究竟这个技术和木马有什么联系?其实这是一种正常的系统功能,名为"执行挂钩",操作系统厂商开发它的初衷十分简单:为程序提供一个额外的通知功能,以实现系统中任何程序启动时都提前让使用了"执行挂钩"的程序收到新程序的启动通知,简单的说,这是操作系统在出于某种程序交互需求的考虑

云里雾里的专栏 3294

UAC】Windows UAC 原理浅析

UAC理解 简而言之 用户账户控制 (User Account Control) 是Windows Vista (及更高版本操作系统)通过弹框进一步让用户确认是否授权当前可执行文件来达到阻止恶意程序的目的。 从图中看可以理解为: 想要获得管理员权限: 进程已经拥有管理员权限。 进程被用户允许通过管理员权限允许。 UAC 的实现: ACL: windows 中的所有资源都有ACL。给各个程序划定了权限。 在开启了UAC之后,如果用户是标准用户,Windows 会给用户分配一个标准Access Toke

dr0op's blog 8507

攻击技术研判|载荷隐匿与在野权限提升新手法分析

Konni将恶意代码写入文档内容末尾的方式,在躲避安全产品检测上具有一定的效果;而本地windows RPC服务滥用和父进程欺骗的结合,是对GoogleProject Zero披露的权限提升技术的改进,使得该过程更加的隐蔽。随着攻防环境的发展,此类在野攻击中涌现的新攻击手法与改进措施,值得蓝军攻击研究人员的进一步思考。

xnxqwzy的博客 185

【SQL监控】SQL完全监控的脚本

有网友询问我的这篇blog http://blog.itpub.net/26736162/viewspace-1218671/中的视图vw_sql_rubbish_monitor_lhr 的内容,我在这里列举一下,大家共同学习吧。 SET SQLBLANKLINES ON --------------------------------- 历史SQL记录 DROP TA...

小麦苗DBA宝典 1628

利用IShellExecuteHook接口对程序监控

利用IShellExecuteHook接口对程序监控

JonnyLai 5437

特许市场技术分析师(CMT)推荐材料

一级考试用书: 1) MTA Code of Ethics 2) Edwards, Robert D. and Magee, John, Technical Analysis of Stock Trends, 9th Edition, CRC Press, Taylor & Francis Group, Boca Raton, Florida, c2007, ISBN 0-8493-3772-

四纪启悟 · 数据科学 4699

断点运行的参数_利用 AicLaunchAdminProcess 参数污染 bypass UAC

本文作者:x-encounter(信安之路作者团队成员 & 信安之路病毒分析小组组长)成员招募:信安之路病毒分析小组寻找志同道合的朋友之前花了一段时间研究 UACME 这个开源项目,获益匪浅,对其中的两个方法的非常感兴趣,重点研究了一下。分别是第三十八种和第五十二种方法。同属白名单组件绕过方法。AicLaunchAdminProcess 参数污染先介绍第三十八种方法,该方法利用 ...

weixin_39640883的博客 452

PFC编程基础

PFC编程基础     概要:    这一章阐述了 PFC 编程的基本技巧,同时也告诉了你如何开始运用 PFC 编写应用程序。    设置应用程序管理器    首先建立一个PFC应用程序的第一步就是配置应用程序以及建立应用程序管理器- -n_cst_appmanager。应用程序管理器将替代原来的应用程序对象。原来在应用程序对象中编写的脚本将全部改写在应用程序管理器中。应用程序管理器中同时还通过...

ribut9225的专栏 5559

钩子函数

2016/11/19更新:Win10下创建普通进程走的是CreateProcess-&gt;CreateProcessInternalW的路,而使用Runas创建管理员进程(会弹出UAC窗口)使用本文提到的SHCreateProcess函数,而是使用了AicLaunchAdminProcess这个函数。需要注意,win10似乎把shell32.dll的部分功能实现移到了windows.stor...

hao__ran的博客 308

LDAP+SAMBA

( Lightweight Directory Access Protocol 輕量級名錄存取協定 ) 在 redhat 9 用 rpm包 安裝 因為 Samba3 預設就有 Ldap 支援,Samba 2 要先編譯再安裝比較麻煩,乾脆重新安裝 Samba3 samba 3.x 安裝之前先移除 samba 2.x 的版本 #rpm -e samba-swat #rpm -e samba-

厚嘉的it窝 7166

Vista下管理员身份启动程序

Vista下管理员身份启动程序 //以管理员身份运行 procedure RunAsAdmin(hWnd: HWND; aFile: string; aParameters: string); var   sei: TShellExecuteInfoA; begin   FillChar(sei, SizeOf(sei), 0);   sei.cbSize := SizeOf(s

surpaimb的专栏 1227

R3下 API 挂钩监视右键管理员权限启动

以前的操作系统上,挂钩 CreateProcessInternel 即可监视进程创建,在 以管理员身份启动的时候,情况发生了变化,我们的 explorer 再调用原来的路径,而是使用一个未被导出的函数 AicFindLaunchAdminProcess。我们只需要根据特征码定位该函数的入口点,并挂钩该函数,即可监视资源管理器中,那些以管理员身份启动的程序。

溡漪幽博客 347

Ring3下Dll注入方法整理汇总

1.dll劫持,粗略整理了下,可以劫持的dll有(持续更新): lpk.dll、usp10.dll、msimg32.dll、midimap.dll、ksuser.dll、comres.dll、ddraw.dll 以lpk为例,在win7下由于lpk被加入KnownDLLs且该注册表值可修改,使得lpk强制从系统目录加载, 过可以将lpk.dll加入ExcludeFromKnown...

weixin_33834628的博客 377

rpc修改为本地服务器,通过调用Windows本地RPC服务器bypass UAC

备战一个月比赛,导致近期都没啥时间更新博客,正好今天看到一篇通过调用本地RPC服务的文章,觉得非常有意思,就拿来充充博客。在1.0版本的APPINFO.DLL中的RPC服务调用接口ID为:201ef99a-7fa0-444c-9399-19ba84f12a1a用RAiLaunchAdminProcess函数调用本地RPC[uuid (201ef99a-7fa0-444c-9399-19ba84f1...

weixin_34490905的博客 931

HandleServiceCommands: system error caught. error code=1072, message = CreateService failed: unknow

HandleServiceCommands: system error caught. error code=1072, message = CreateService failed: unknown error

m0_54355172的博客 3855

焊接件表面缺陷数据集-zip

原数据集总共2300张,存在数据集标签错标及无标签文件问题,已修正; 修正数据集总共2294张,划分了458张验证集,其余做训练集;缺陷共九类包括焊缝共十类目标

上一篇: Launch your application in Vista under the local system account without the UAC popup
yanwei100
博客等级 码龄24年 9粉丝 71原创
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值