Windows Vista for Developers——第四部分:用户帐号控制(User Account Control,UAC)

时间:2021-08-31 20:04:15

作者:Kenny Kerr

翻译:Dflying Chen

原文:http://weblogs.asp.net/kennykerr/archive/2006/09/29/Windows-Vista-for-Developers-_1320_-Part-4-_1320_-User-Account-Control.aspx

请同时参考《Windows Vista for Developers》系列

自从Windows 2000以来,Windows开发者一直试图为用户创造一个安全稳妥的工作环境。Windows 2000引入了一种名为“受限访问令牌(Restricted Token)”的技术,能够有效地限制应用程序的许可和权限。Windows XP则在安全方面更进一步,不过对于普通用户来讲,这种安全控制却并不是那么的深入人心……直到现在为止还是如此。不管你最初反对的理由是什么,现在用户帐号控制(User Account Control,UAC)就摆在你的面前,其实它并不像批评中所说的那样一无是处。作为开发者的我们有责任掌握这项技术,进而让我们所开发的Vista应用程序不会总是弹出那些“讨厌”的提示窗口。

《Windows Vista for Developers》系列文章的第四部分中,我们将从实际出发探索一下UAC的功能,特别是如何以编程方式使用这些特性。

什么是安全上下文(Security Context)?

安全上下文指的是一类定义某个进程允许做什么的许可和权限的集合。Windows中的安全上下文是通过登录会话(Logon Session)定义的,并通过访问令牌维护。顾名思义,登录会话表示某个用户在某台计算机上的某次会话过程。开发者可以通过访问令牌与登录会话进行交互。访问令牌所有用的许可和权限可以与登录会话的不同,但始终是它的一个子集。这就是UAC工作原理中的最核心部分。

那么UAC的工作原理是什么呢?

在Windows Vista操作系统中,有两种最主要的用户帐号:标准用户(stand user)和管理员(administrator)。你在计算机上创建的第一个用户将成为管理员,而后续用户按照默认设置将成为标准用户。标准用户用来提供给那些不信任自己能够控制整个计算机的用户,而管理员则为那些希望能够完全控制计算机的用户所准备。与先前版本的Windows不同,在Windows Vista中,你不再需要以标准用户的身份登录到系统中以便防止某些恶意代码/程序的恶意行为。标准用户和管理员的登录会话拥有同样的保护计算机安全的能力。

当一个标准用户登录到计算机时,Vista将创建一个新的登录会话,并通过一个操作系统创建的、与刚刚创建的这个登录会话相关联的shell程序(例如Windows Explorer )作为访问令牌颁发给用户。

而当一个管理员登录到计算机时,Windows Vista的处理方式却与先前版本的Windows 有所不同。虽然系统创建了一个新的登录会话,但却为该登录会话创建了两个不同的访问令牌,而不是先前版本中的一个。第一个访问令牌提供了管理员所有的许可和权限,而第二个就是所谓的“受限访问令牌”,有时候也叫做“过滤访问令牌(filtered token)”,该令牌提供了少得多的许可和权限。实际上,受限访问令牌所提供的访问权限和标准用户的令牌没什么区别。然后系统将使用该受限访问令牌创建shell应用程序。这也就意味着即使用户是以管理员身份登录的,其默认的运行程序许可和权限仍为标准用户。

若是该管理员需要执行某些需要额外许可和权限的、并不在受限访问令牌提供权限之内的操作,那么他/她可以选择使用非限制访问令牌所提供的安全上下文来运行该应用程序。在由受限访问令牌“提升”到非限制访问令牌的过程中,Windows Vista将通过给管理员提示的方式确认该操作,以其确保计算机系统的安全。恶意代码不可能绕过该安全提示并在用户不知不觉中得到对计算机的完整控制。

正如我前面提到的那样,受限访问令牌并不是Windows Vista中的新特性,但在Windows Vista中,该特性终于被无缝地集成到用户的点滴操作中,并能够实实在在地保护用户在工作(或游戏)时的安全。

受限访问令牌

虽然在通常情况下,我们不用自行创建受限访问令牌,但了解其创建的过程却非常有用,因为它可以帮助我们更好地理解受限访问令牌能够为我们做什么,进而更深入地了解我们的程序将运行于的环境。作为开发者,我们可能需要创建一个比UAC提供的更为严格的约束环境,这时了解如何创建受限访问令牌就显得至关重要了。

这个名副其实的CreateRestrictedToken 函数用来根据现有的访问令牌的约束创建一个新的访问令牌。该令牌可以用如下的方式约束访问权限:

  1. 通过指定禁用安全标示符(deny-only security identifier,deny-only SID)限制访问需要被保护的资源。
  2. 通过指定受限SID实现额外的访问检查。
  3. 通过删除权限。

UAC所使用的受限访问令牌在创建时指定了禁用SID并删除了某些权限,而并没有使用受限SID。让我们通过一个简单示例说明这一点。第一步就是得到当前正在使用的访问令牌,以便稍后进行复制并基于它删除某些权限:

CHandle processToken;
VERIFY(::OpenProcessToken(::GetCurrentProcess(),
                          TOKEN_DUPLICATE | TOKEN_ASSIGN_PRIMARY | TOKEN_QUERY,
                          &processToken.m_h));

接下来需要搞定需要禁用的SID数组,以确保这些SID不能被用来访问资源。下面的代码使用了我编写的WellKnownSid 类创建系统内建的管理员组的SID。WellKnownSid 类可以在本文的下载代码中找到。

WellKnownSid administratorsSid = WellKnownSid::Administrators();
SID_AND_ATTRIBUTES sidsToDisable[] = 
{
    &administratorsSid, 0
    // add additional SIDs to disable here
};
Next we need an array of privileges to delete. We first need to look up the privilege’s LUID value:
LUID shutdownPrivilege = { 0 };
VERIFY(::LookupPrivilegeValue(0, // local system
                              SE_SHUTDOWN_NAME,
                              &shutdownPrivilege));
LUID_AND_ATTRIBUTES privilegesToDelete[] = 
{
    shutdownPrivilege, 0
    // add additional privileges to delete here
};

然后即可调用CreateRestrictedToken 函数创建该受限访问令牌:

CHandle restrictedToken;
VERIFY(::CreateRestrictedToken(processToken,
                               0, // flags
                               _countof(sidsToDisable),
                               sidsToDisable,
                               _countof(privilegesToDelete),
                               privilegesToDelete,
                               0, // number of SIDs to restrict,
                               0, // no SIDs to restrict,
                               &restrictedToken.m_h));

这样,我们指定了SE_GROUP_USE_FOR_DENY_ONLY 标记所得到的访问令牌就将包含内建的管理员组的SID,不过这些SID是用来禁用,而不是用来允许访问的。我们还剥夺了该访问令牌的SeShutdownPrivilege 权限,保证该令牌不能重新启动、休眠或关闭计算机。

若你觉得很有意思,那么可以尝试做个小实验。将上面的代码拷贝到某个控制台应用程序中,然后添加如下的CreateProcessAsUser 函数调用,并相应地更新代码中Windows Explorer可执行程序的路径:

STARTUPINFO startupInfo = { sizeof (STARTUPINFO) };
ProcessInfo processInfo;
VERIFY(::CreateProcessAsUser(restrictedToken,
                             L"C:\\Windows\\Explorer.exe",
                             0, // cmd line
                             0, // process attributes
                             0, // thread attributes
                             FALSE, // don't inherit handles
                             0, // flags
                             0, // inherit environment
                             0, // inherit current directory
                             &startupInfo,
                             &processInfo));

现在杀掉计算机中的所有Explorer.exe 进程并运行上述代码。你会注意到再也无法重新启动、休眠或关闭计算机了。开始菜单中的这些选项也会被禁用。

最后还要介绍一个函数:IsTokenRestricted。这个函数不会告诉你该访问令牌是否是由CreateRestrictedToken 创建的,但却会告诉你该访问令牌是否包含受限SID。因此,除非你要使用受限SID,否则这个函数并没有什么太大用处。

完整性级别(Integrity levels)

UAC提供了个很少有人注意到的特性,那就是强制完整性控制(Mandatory Integrity Control)。这是一个新的添加到进程和安全描述符(security descriptor)上的授权特性。我们可以为需要安全保护的资源在其安全描述符中指定一个完整性级别。系统中的每个进程也有相应的完整性级别标记,然后即可与资源的完整性级别相互验证,并提供额外的安全保护。这不但非常简单,也是个极为有用的特性,能够帮助你简单有效地将进程的可访问资源分隔开来。

设想作为开发者的你需要开发一个应用程序,该应用程序必须处理从无法信任的源(例如Internet)中获取的数据。因为数据中可能包含有恶意代码,所以你必须想方设法保护计算机的安全,因此为你的程序添加一个“深度防御(defense in depth)”层就显得非常有用。其中一个非常有效的解决方案就是使用前面一节中描述的受限访问令牌。但是这种解决方案可能会很复杂,因为你需要明确地指出哪些资源的哪些SID可以被允许、哪些SID需要被禁用,考虑到程序本身也需要一定的权限来正常运行,你不得不做出大量的授权工作。这正是引入完整性级别的意义所在。完整性级别一般用来阻止写访问,而允许读访问和运行。而有了读和运行的权限,程序基本上即可完成大部分的工作,而阻止了写权限则可以限制其对系统的危害,例如覆盖系统文件或修改某些路径信息等。这也正是IE 7的实现方式。IE 7的部分功能运行于一个低完整性级别的独立的进程中,只允许进程修改少数几个位置的文件。

用户态进程可以设置为如下四种完整性级别:

  1. Low
  2. Medium
  3. High
  4. System

标准用户访问令牌以及受限(未经提升过)的管理员访问令牌拥有中等(Medium)的完整性级别。不受限制(经过提升)的管理员访问令牌拥有高(High)完整性级别。运行于Local System之下的帐号拥有系统(System)完整性级别。IE进程的完整性级别则为低(Low)。有一种很简单的查勘进程完整性级别的方法:用最新版本的Process Explorer,它提供了一个可选的列,可以显示出每个进程的完整性级别。

按照默认,子进程将继承父进程的完整性级别。在创建进程时我们可以更改其完整性级别,但一旦创建完毕就不能再更改。另外,我们也不能将子进程的完整性级别设置得高于父进程。这可以阻止低完整性级别的程序借机会窃取更高的完整性级别。

让我们首先看一下如何查询并设置某一进程的完整性级别,然后再讨论如何为需要保护的资源设置完整性级别。

进程完整性级别(Process integrity levels)

我们可以通过检查进程的访问令牌来取得其完整性级别信息。GetTokenInformation 函数可以返回不同种类的信息。例如,若想通过访问令牌图的当前的用户帐号,我们可以指定TokenUser 类,然后,GetTokenInformation 函数将基于该访问令牌生成一个TOKEN_USER 结构。类似地,使用TokenIntegrityLevel 类器可查询该进程的完整性级别,随后将返回TOKEN_MANDATORY_LABEL 结构。大多数GetTokenInformation 返回的结构体的长度都是可变的,因为只有GetTokenInformation 函数本身才知道到底需要多少内存空间,所以我们在调用时必须格外小心。因为大多数底层的安全相关函数均使用LocalAlloc LocalFree 来分配/释放内存,所以我使用了一个名为LocalMemory 的类模板和一个GetTokenInformation 函数模板来简化所需要的工作,该类可以在本文的下载代码中找到。这里我们先把注意力放在手头的主题上:

CHandle processToken;
VERIFY(::OpenProcessToken(::GetCurrentProcess(),
                          TOKEN_QUERY,
                          &processToken.m_h));
LocalMemory<PTOKEN_MANDATORY_LABEL>info;
COM_VERIFY(GetTokenInformation(processToken,
                               TokenIntegrityLevel,
                               info));
SID* sid = static_cast<SID*>(info->Label.Sid);
DWORD rid = sid->SubAuthority[0];
switch (rid)
{
    case SECURITY_MANDATORY_LOW_RID:
    {
        // Low integrity process
        break;
    }
    case SECURITY_MANDATORY_MEDIUM_RID:
    {
        // Medium integrity process
        break;
    }
    case SECURITY_MANDATORY_HIGH_RID:
    {
        // High integrity process
        break;
    }
    case SECURITY_MANDATORY_SYSTEM_RID:
    {
        // System integrity level
        break;
    }
    default:
    {
        ASSERT(false);
    }
}

这里我们使用了OpenProcessToken 来取得需要查询的进程的访问令牌。然后调用了我编写的GetTokenInformation 函数模版(当然,提供了适当的类的信息),并用LocalMemory 类模板指定了信息的类型。函数返回的TOKEN_MANDATORY_LABEL 结构包含了表示完整性级别的SID。分析这个SID即可得到我们想要的表示完整性级别的相对标示符(relative identifier,RID)。

设置子进程的完整性级别非常直观简单。首先复制一份父进程的访问令牌,然后使用前面实例程序中用来查询完整性级别的那些信息类和数据结构设置其完整性级别。这时即可使用SetTokenInformation 函数。最后调用CreateProcessAsUser 函数,并使用修改过的访问令牌即可创建出需要的子进程。请参考下述代码:

CHandle processToken;
VERIFY(::OpenProcessToken(::GetCurrentProcess(),
                          TOKEN_DUPLICATE,
                          &processToken.m_h));
CHandle duplicateToken;
VERIFY(::DuplicateTokenEx(processToken,
                          MAXIMUM_ALLOWED,
                          0, // token attributes
                          SecurityAnonymous,
                          TokenPrimary,
                          &duplicateToken.m_h));
WellKnownSid integrityLevelSid(WellKnownSid::MandatoryLabelAuthority,
                               SECURITY_MANDATORY_LOW_RID);
TOKEN_MANDATORY_LABEL tokenIntegrityLevel = { 0 };
tokenIntegrityLevel.Label.Attributes = SE_GROUP_INTEGRITY;
tokenIntegrityLevel.Label.Sid = &integrityLevelSid;
VERIFY(::SetTokenInformation(duplicateToken,
                             TokenIntegrityLevel,
                             &tokenIntegrityLevel,
                             sizeof (TOKEN_MANDATORY_LABEL) + ::GetLengthSid(&integrityLevelSid)));
STARTUPINFO startupInfo = { sizeof (STARTUPINFO) };
ProcessInfo processInfo;
VERIFY(::CreateProcessAsUser(duplicateToken,
                             L"C:\\Windows\\Notepad.exe",
                             0, // cmd line
                             0, // process attributes
                             0, // thread attributes
                             FALSE, // don't inherit handles
                             0, // flags
                             0, // inherit environment
                             0, // inherit current directory
                             &startupInfo,
                             &processInfo));

这个实例程序将打开记事本程序。你会注意到,虽然该记事本程序可以打开大多数位置中的文本文件,但却无法保存至任何位置,因为它的完整性级别为低。

还要说一句,我们可以使用LookupAccountSid 函数得到完整性级别的可显示名称,但该函数的返回值对用户却并不是那么友好,所以你最好另外设置一个字符串表,包含类似“低”、“中等”、“高”以及“系统”等文字。

系统为标准用户创建的访问令牌的完整性级别为中等。系统为管理员创建的受限访问令牌的完整性级别也是中等,但未受限管理员访问令牌的完整性级别为高。

现在让我们看看如何为指定的资源设置完整性级别。

资源完整性级别(Resource integrity levels)

资源的完整性级别存放在资源安全描述符的系统访问控制表(ystem access control list,SACL)中的一个特殊的访问控制条目(access control entry,ACE)中。更新该值的最简单方法就是使用SetNamedSecurityInfo 函数。Windows Vista还提供了一个新的名为AddMandatoryAce 的函数,用来将一类特殊的ACE(强制ACE)添加至ACL中。记住,安全相关的缩写词总是会让人一头雾水……认真地说,若你熟悉安全描述符相关编程的话,那么这段代码看起来将相当简单。首先使用InitializeAcl 函数准备了一个足够容纳一个单独ACE的ACL。接下来创建用SID表示的完整性级别,并使用AddMandatoryAce 函数将其添加至ACL中。最后使用SetNamedSecurityInfo 函数更新完整性级别。注意在下面的代码中,我们使用了一个新的LABEL_SECURITY_INFORMATION标记:

LocalMemory<PACL> acl;
const DWORD bufferSize = 64;
COM_VERIFY(acl.Allocate(bufferSize));
VERIFY(::InitializeAcl(acl.m_p,
                       bufferSize,
                       ACL_REVISION));
WellKnownSid sid(WellKnownSid::MandatoryLabelAuthority,
                 SECURITY_MANDATORY_LOW_RID);
COM_VERIFY(Kerr::AddMandatoryAce(acl.m_p,
                                 &sid));
CString path = L"C:\\SampleFolder";
DWORD result = ::SetNamedSecurityInfo(const_cast<PWSTR>(path.GetString()),
                                      SE_FILE_OBJECT,
                                      LABEL_SECURITY_INFORMATION,
                                      0, // owner
                                      0, // group
                                      0, // dacl
                                      acl.m_p); // sacl
ASSERT(ERROR_SUCCESS == result);

得到资源的完整性级别也非常简单,可以看到大多数资源并没有显式地设定其完整性级别。系统将没有显式声明完整性级别的资源看作带有中等完整性级别。首先使用同样的安全信息标记LABEL_SECURITY_INFORMATION调用GetNamedSecurityInfo 函数。然后即可简单地通过GetAce 函数得到指向存储了完整性级别SID的ACE的指针,随后通过读取其RID值即可判断其完整性级别。下面是一段示例:

CString path = L"C:\\SampleFolder";
LocalMemory<PSECURITY_DESCRIPTOR>descriptor;
PACL acl = 0;
DWORD result = ::GetNamedSecurityInfo(const_cast<PWSTR>(path.GetString()),
                                      SE_FILE_OBJECT,
                                      LABEL_SECURITY_INFORMATION,
                                      0,
                                      0,
                                      0,
                                      &acl,
                                      &descriptor.m_p);
ASSERT(ERROR_SUCCESS == result);
DWORD integrityLevel = SECURITY_MANDATORY_MEDIUM_RID;
if (0 != acl && 0 < acl->AceCount)
{
    ASSERT(1 == acl->AceCount);
    SYSTEM_MANDATORY_LABEL_ACE* ace = 0;
    VERIFY(::GetAce(acl,
                    0,
                    reinterpret_cast<void**>(&ace)));
    ASSERT(0 != ace);
    SID* sid = reinterpret_cast<SID*>(&ace->SidStart);
    integrityLevel = sid->SubAuthority[0];
}
ASSERT(SECURITY_MANDATORY_LOW_RID == integrityLevel);

以管理员身份运行(Run as Administrator)

目前为止,我们已经注意分析了组成UAC的各个部分,例如受限访问令牌和完整性级别等。接下来让我们看看“以管理员身份运行”是什么意思,我们如何以编程方式实现这个功能。或许你已经注意到了,在Windows Vista中你可以右键单击某个应用程序或快捷方式图标,并在弹出的上下文菜单中选择“以管理员身份运行”。无论是管理员还是标准用户,Vista都提供了这个选项。以管理员身份运行的概念可以简单地理解为作了一次“提升”或是创建一个“提升”了的进程。若想“以管理员身份运行”,那么标准用户需要输入管理员的用户名和密码,而管理员则需要在弹出对话框中进行一次确认。无论那种情况,结果都是一样的:系统将创建一个拥有不受限制管理员权限的新的进程,该进程拥有系统所有的许可和权限。

进程的“提升”显得有些复杂,但幸运的是,大多数复杂性都被隐藏在更新版本的ShellExecute(Ex) 函数中了。Windows Vista中的ShellExecute 函数通过一个非公开的COM接口使用新的应用程序信息服务(Application Information,appinfo)来执行提升操作。ShellExecute 首先调用CreateProcess ,尝试创建一个新的进程。CreateProcess 负责包括检查应用程序兼容性设置、应用程序清单(application manifest)以及运行时加载器(runtime loader)等任务。若CreateProcess 发现应用程序需要一个“提升”而其调用进程却没有提升的话,则函数调用会以ERROR_ELEVATION_REQUIRED失败告终。然后ShellExecute 调用应用程序信息服务来处理提升操作并创建被“提升”过的进程,因为调用进程显然没有执行该任务所需要的足够的权限。最后,应用程序信息服务调用CreateProcessAsUser 以获得必需的非限制的管理员访问令牌。

还有一种方法:若你只想要一个经过“提升”了的进程,而不关心使用哪个应用程序信息服务的话,那么只要在ShellExecute中使用这个鲜为人知的“runas”就可以了。无论应用程序清单或兼容性信息有多么变态,这个命令均可以实现“提升”功能。实际上,runas并不是Windows Vista中的新东西。在Windows XP和Windows 2003中就已经出现了,常用在通过shell直接创建受限访问令牌。可是在Vista中,它的行为却有了些变化,请参考如下的示例程序:

::ShellExecute(0, // owner window
               L"runas",
               L"C:\\Windows\\Notepad.exe",
               0, // params
               0, // directory
               SW_SHOWNORMAL);

想想系统在后台默默地为你做了多少工作吧!只要这么一行代码就搞定了如此复杂的功能,是不是觉得很爽呢?虽然创建一个“提升”过的进程有时候显得很合理,但若你只想暂时提升一下的话,或许这并不是最恰当的解决方案。让我们接下来看看如何通过提升COM对象完成同样的工作。

创建一个被“提升”了的COM对象

若你对COM有所造诣的话,应该知道COM支持我们在一个代理进程中创建COM服务器。现在这项技术又有了一些发展,我们可以在一个“提升”了的代理进程中创建COM服务器了。这项技术非常有用,借助于它的帮助,我们就可以在应用程序运行期间简单地创建一个COM对象,而不必去创建一个全新的进程。

使用这个技术中最难的一部分就是如何正确地注册该COM服务器,保证将其加载到一个“提升”了的代理进程中,因为COM对象需要我们显式地声明其协作方式。

我们要做的第一件事就是更新COM的注册,用来保证我们的库(DLL)服务器能够运行于一个代理进程中。只要将“DllSurrogate”添加至服务器的AppID注册表键中即可。在ATL中,只要简单地更新项目的主RGS文件,如下所示:

HKCR
{
    NoRemove AppID
    {
        '%APPID%' = s 'SampleServer'
        {
            val DllSurrogate = s ''
        }
        'SampleServer.DLL'
        {
            val AppID = s '%APPID%'
        }
    }
}

DllSurrogate 的空值表示系统提供的代理进程即刻可以使用。现在COM客户端就能够指定CLSCTX_LOCAL_SERVER 运行上下文,在该代理进程中创建COM服务器了:

CComPtr<ISampleServer>server;
COM_VERIFY(server.CoCreateInstance(__uuidof(SampleServer),
                                   0,
                                   CLSCTX_LOCAL_SERVER));

下一步就是启用该COM类的“提升”运行。这需要我们在该COM类的注册脚本中添加一些东西——一个用来表示支持“提升”的提升键,以及一个名为“LocalizedString”的值,加上用来显示在UAC确认对话框中的名称。ATL中COM类的注册脚本将类似如下所示:

HKCR
{
    SampleServer.SampleServer.1 = s 'SampleServer Class'
    {
        CLSID = s '{91C5423A-CF90-4E62-93AD-E5B922AE8681}'
    }
    SampleServer.SampleServer = s 'SampleServer Class'
    {
        CLSID = s '{91C5423A-CF90-4E62-93AD-E5B922AE8681}'
        CurVer = s 'SampleServer.SampleServer.1'
    }
    NoRemove CLSID
    {
        ForceRemove {91C5423A-CF90-4E62-93AD-E5B922AE8681} = s 'SampleServer Class'
        {
            ProgID = s 'SampleServer.SampleServer.1'
            VersionIndependentProgID = s 'SampleServer.SampleServer'
            InprocServer32 = s '%MODULE%'
            {
                val ThreadingModel = s 'Neutral'
            }
            val AppID = s '%APPID%'
            'TypeLib' = s '{A43B074B-0452-4FF4-8308-6B0BF641C3AE}'
            Elevation
            {
                val Enabled = d 1
            }
            val LocalizedString = s '@%MODULE%,-101'
        }
    }
}

不要忘记在你的字符串表中为本地化名称添加一个条目。现在该COM客户即可启动该“提升”了的COM服务器了。CoCreateInstance 不像CreateProcess那样直接创建一个“提升”了的COM对象,我们需要使用“COM提升名称(COM elevation moniker)”来完成。最简单的方法就是使用CoGetObject 函数创建这个名称(moniker )并返回最终创建好的对象的代理:

template <typename T>
HRESULT CreateElevatedInstance(HWND window,
                               REFCLSID classId,
                               T** object)
{
    BIND_OPTS3 bindOptions;
    ::ZeroMemory(&bindOptions, sizeof (BIND_OPTS3));
    bindOptions.cbStruct = sizeof (BIND_OPTS3);
    bindOptions.hwnd = window;
    bindOptions.dwClassContext = CLSCTX_LOCAL_SERVER;
    CString string;
    const int guidLength = 39;
    COM_VERIFY(::StringFromGUID2(classId,
                                 string.GetBufferSetLength(guidLength),
                                 guidLength));
    string.ReleaseBuffer();
    string.Insert(0, L"Elevation:Administrator!new:");
    return ::CoGetObject(string,
                         &bindOptions,
                         __uuidof(T),
                         reinterpret_cast<void**>(object));
}
Using the function template is just as simple as calling CoCreateInstance:
CComPtr<ISampleServer>server;
COM_VERIFY(CreateElevatedInstance(0, // window
                                  __uuidof(SampleServer),
                                  &server);

如同ShellExecute一样,UAC使用该窗体的句柄判断该提示是否会得到输入焦点,还是只在后台默默等待。

使用应用程序清单(application manifests)

还记得我曾经提到过CreateProcess 会检查应用程序兼容性设置以及应用程序清单么?确实如此,Windows Vista为了确保遗留的32位应用程序能够正常运行而做了很多努力。以往的应用程序可以轻松地完全控制文件系统以及注册表,而为了让其也能够在UAC所提供的更加严格的运行环境中继续可用,Vista作了令人难以想象的大量的模拟工作。但尽管如此,其核心理念还是尽可能地避免这些模拟。这种模拟只对那些遗留的应用程序有意义,如果你现在开发新的应用程序的话,那么请确保提供相应的应用程序清单,以避免这类不必要的模拟。微软公司也在计划在后续版本中的Windows中删除对这些模拟的支持。

应用程序清单的架构在Windows Vista下有所更新,应用程序可以在该清单中给出其需要的安全性上下文。但令人不爽的是,Visual C++ 会自动生成应用程序清单。实际上这是件好事。连接器始终对应用程序的各个依赖保持清醒,而应用程序清单则用来定义并行程序集之间的依赖。幸运的是,Visual C++ 也提供了将额外的应用程序清单与现有的清单合并的选项,并能够将合并后的整体清单一起嵌入到应用程序的可执行文件中。Visual C++ 项目的“Additional Manifest Files”设置正是为此而设。下面就是一份示例应用程序清单,其中声明了对Common Controls 6.0的依赖,并指定了其希望得到的安全性上下文:

<?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="*"
        publicKeyToken="6595b64144ccf1df"
        language="*"
        />
    </dependentAssembly>
  </dependency>
  <v3:trustInfo xmlns:v3="urn:schemas-microsoft-com:asm.v3">
    <v3:security>
      <v3:requestedPrivileges>
        <!-- level can be "asInvoker", "highestAvailable", or "requireAdministrator" -->
        <v3:requestedExecutionLevel level="highestAvailable" />
      </v3:requestedPrivileges>
    </v3:security>
  </v3:trustInfo>
</assembly>

requestedExecutionLevel可以指定为三个值:

  1. asInvoker:默认选项,新的进程将简单地继承其父进程的访问令牌。
  2. highestAvailable:应用程序会选择该用户允许范围内尽可能宽松的安全上下文。对于标准用户来说,该选项与asInvoker一样,而对于管理员来说,这就意味着请求非限制访问令牌。
  3. requireAdministrator:应用程序需要管理员的非限制访问令牌。运行该程序时,标准用户将要输入管理员的用户名和密码,而管理原则要在弹出的确认对话框中进行确认。

需要记住的是,CreateProcess 将会检查应用程序清单,如果所请求的执行级别高与其父进程的完整性级别,那么这次调用将失败。只有ShellExecute 才会使用应用程序信息服务来执行“提升”。

我真的被“提升”了么?

如果你想要知道现在是否已经被“提升”过,那么简单地调用IsUserAnAdmin 函数即可。如果你还嫌不够精确,那么也可以使用GetTokenInformation 函数,但大多数情况下似乎都有些高射炮打蚊子——大材小用了。

结论

这就使我能讲出的关于UAC的一切,希望能够对你有所帮助。这篇文章中的内容基本都不在官方文档中,因此改变也是在所难免的。

再说一句,本文示例程序中我大多使用了断言(assertion)及类似的宏来检查可能发生的异常。这么做是为了判断哪些地方应该使用异常处理。如果你想在应用程序中使用部分其中的代码,那么请确保将这些宏替换成适合你自己的处理机制,无论是异常、HRESULT还是别的什么东西。

本文的下载代码中包含了一些辅助类模板以及辅助函数,希望能对你有所帮助!