UAC 的前世今生

UAC 的基本概念

什么是 UAC

UAC 即 User Account Control(用户帐号控制),是微软从 Windows Vista 开始为提高安全性而引入的一项新技术。用户通过这项技术既可以以非管理员身份,也能够以管理员身份执行常见的任务,而不需要切换账户或者注销。在大多数的情况下,用户都是以标准用户的状态来执行日常任务,只有当需要设置系统特定资源的操作的任务才会需要用户以管理员的身份执行,以此来确保进程对系统的 “伤害” 达到最低。

为什么使用 UAC

使用 UAC 的理由很简单:保护系统资源和数据的安全。在 XP 时代,在系统安装完毕后任何新建的账户都会默认划入系统管理员组,于是用户有了安装,卸载,修改,删除系统任何地方任何数据的权限,而这正是万恶之源。而如果能够控制不同程序的权限,那么大部分的恶意软件和病毒就不能起作用了。

UAC 正是基于这种思路进行设计的:严格控制进程所能获得的权限。让一个进程无时无刻都拥有管理员权限是无法容忍的:一个恶意程序如果被自动运行于我们的系统,且肆无忌惮地执行某些会进行系统资源读写操作的代码而不为我们所知晓那是多么恐怖的事。(XP 正是这么做的)所以 UAC 的策略就是给予进程尽可能低的权限,如果程序需要管理员权限则需要知会当前用户。同时通过一系列的措施来保障程序的正确运行:

  1. 在可行的情况下,进行操作的权限将从系统管理员调整为标准用户。(给予最低权限)
  2. 利用虚拟化技术在没有获得系统管理员权限的情况下协助程序运行。(例如对注册表访问的重定向,保证旧版本程序的兼容性)
  3. 对程序进行再处理,这样用户帐户控制功能就可以知道在什么情况下需要系统管理员权限。(可执行文件的 UAC 头,来控制和判断程序所需要的权限,默认是 asInvoker 或 None)
  4. 确保在系统管理员权限下运行的程序和在标准用户权限下运行的程序是分离的(如 UIPI)

UAC 带来的影响

对普通用户而言,UAC 的引入可能并没有带来多大的影响,更多的可能只是在启动特定程序的时候会跳出提示通知用户以管理员身份运行,仅此而已。但是这个地方有个比较尴尬的问题:UAC(或者其他类似的安全措施)是基于如下假设的:

  1. 个人用户对于系统安全有一定的认识,能够分辨哪些程序是好的,哪些是坏的。但是实际上对于大多数网民来说,这个假设未必成立,即使恶意软件跳出提示要以管理员身份运行,他们往往也是点确定让它运行。那么 UAC 的意义又在哪呢?
  2. 企业用户可以对系统安全一无所知,但是考虑到企业内部会有 IT 部门帮忙进行安全属性配置和软硬件的安装,UAC 对他们来说是很有效的:只需要分配给他们标准用户帐号进行日常任务处理既可。但现实情况却是: 如果企业用户安装软件或其他需要管理员权限的事务都需要知会 IT,那整个沟通成本太高,不现实。更何况很多软件产品都有不兼容 UAC 的问题。(比如部分公司开发的程序为了“绕过”UAC,直接把程序设为必须以管理员帐户运行) 对于开发人员来说可能影响会更大。如何让自己新老程序兼容和适应 UAC 的规则是一个不大不小的课题。

开发所需要了解的 UAC

UAC 的基本实现原理

在 Windows 中有两项比较重要的概念: ACL 和 Access Token。ACL 即 Access Control List(直译成: 访问控制列表),对于 Wdinows 中的所有资源来说都会有自己的 ACL,这个列表决定了这个资源可以被具有哪些权限的用户 / 进程所访问。而 Access Token 即用户的访问令牌,这决定了用户对资源的访问属性。在 Vista 之前的系统中,如果用户使用了标准用户(如 XP 中所谓的受限用户),用户就会得到一个和之相对应的 Access Token,只能访问和修改有限的用户资源。但只要用户用了管理员组的帐号进行登入,用户就能够获取一个所谓的 “Full Access Token”,即可以获取到对任意资源的访问权。这显然是多余而且不安全的,于是从 Vista 起的 UAC 就做了如下的调整:

  1. 如果用户是标准用户,那么还是和以前一样分配给用户一个标准的访问令牌。
  2. 而如果用户是以管理员用户登入,则有所不同:系统不再是和以前一样分配个万能的访问令牌,而是生成两份访问令牌:一个完整的管理员访问令牌和一份经 “和谐” 的标准用户令牌。在默认的情况下,管理员权限会被移除(或者说被保存到某个地方,等用户主动请求),而用户只拿到了他所需要的标准用户访问权限,并通过它创建了 Explorer.exe 程序,并以其为父进程创建所有基于标准用户的进程。 具体的流程可参考如图(从 MSDN 上盗得):

UAC 影响到的资源

从上文我们已经可以知道 UAC 会使得我们的程序运行在一个尽可能低的权限下,而这个权限可能过低,而不在某些敏感资源的 ACL 允许范围。那么从技术角度来说,搞清楚哪些资源是所谓的敏感资源就很重要—- 知己知彼,百战不殆。从 Wiki 摘抄的需要 UAC 授权的操作:

	* 配置 Windows Update
	* 增加或删除用户帐户
	* 改变用户的帐户类型
	* 改变 UAC 设置
	* 安装 ActiveX
	* 安装或移除程序
	* 安装设备驱动程序
	* 设置家长控制
	* 将文件移动或复制到 Program Files 或 Windows 目录
	* 查看其他用户文件夹

基本上,只要有涉及到访问系统磁盘的根目录(例如 C:),访问 Windows 目录,Windows 系统目录,Program Files 目录,访问 Windows 安全信息以及读写系统登录数据库(Registry)的程序访问动作,都会需要通过 UAC 的认证。

UAC 带来的程序启动选项的变化和注意事项

对于普通用户来讲,UAC 最直观的感受就是在很多程序图标多了个小盾,且双击后会出来个用户账户控制的窗口。而对于技术人员来说当然更需要关心真正的内幕:启动的时候进程做了提权的动作,获取了更高权限的用户令牌。(而这又可能导致这个进程的用户相关上下文直接改变,当然这是后话)在 Vista 以后的程序在默认情况下会有 3 种启动选项: asInvoker(None),highestAvailable 和 requireAdministrator。其中 highestAvailable 最不为大家熟知:这种启动方式请求当前账户可以获取到的最高权限:如果本身是管理员组内成员,则可以得到完整的管理员访问令牌,呼风唤雨。而如果是标准用户则只能得到它这个用户能够得到的最高权限。(具体如何设置程序启动选项在下面的 Tips 中继续说)上一幅 MSDN 提供的开启 UAC 状态下程序启动的流程图:

MS 为支持 UAC 引入的新技术和注意事项

为实现 UAC 的所有功能,微软可谓煞费苦心,整了很多新的技术和新概念出来。(虽然个人觉得这个技术对于一般用户来说还是很鸡肋)下面就罗列一部分我们平常开发中可能会碰到或者遇到的技术:

1.Installer Dection

这个技术最大的作用是为了兼容以前的以前版本系统中的程序(尤其是安装程序,顾名思义嘛),在 UAC 下安装程序做的很多事情可能都是十恶不赦,需要最高权限的(如写注册表,写敏感文件目录),而旧版本的程序压根没有做任何特殊处理(或者说是只是填充了一些默认信息),所以一种行之有效的安装程序检测技术是很必要的,否则很多程序安装都不成功,更毋论运行了。 MSDN 上总结了一些 Installer Dection 的原则:

  • 文件名包含关键字:”install”,“setup”,”update”等关键字
  • 在版本资源的以下字段内包含关键字: Vendor,CompanyName,ProductName,File Description,Original Filename,Internal Name,Export Name。(这两条应该是最 SB 却又最有效的一个方法,当年闪电邮的 UpdateExec 没有做任何处理却一直要求能够以管理员权限运行的事让我迷茫了很久)
  • 可执行文件的 manifest 文件中包含关键字
  • 在链接到可执行文件的特定 String Table 中包含关键字 (这个我很迷茫,求解释)
  • 链接到可执行文件的资源文件数据包含关键属性
  • 可执行文件包含特定的字节序列(这个意思应该是用户在 manifest 中写入特定属性,然后链接到可执行文件中并填充了某个字段—- 现在基本上所有的安装程序 / 需要提权的程序都是这么做的)

2.Virtualization(虚拟化)

这是一项比较扯同时也是为了保证兼容性设计出来的技术。简单地来说 (这个只能简单来说了,具体的原理没有相应的参考资料),就是对老程序所进行的“非法” 的 访问系统敏感数据进行重定向,可以分为文件虚拟化和注册表虚拟化。当用户对一个需要管理员权限才能够访问的文件目录或者注册表项进行读写都会被重定向。

如上图,用户对 %ProgramFiles% 的读写会被定向到 %LocalAppData%VirtualStore 下,而对于 HKLMSoftware 的读写会被重定向到 HKCUSoftwareClassesVirtualStore 下。 特别需要注意的是:因为在 XP 下养成的习惯,我们对于注册表的读写很多直接就是用 KEY_ALL_ACCESS 的选项,而到了 Vista 和 Win7 后,因为分配给用户的权限低了 (即使管理员帐号登入拿到的权限也是经过“和谐” 的,上文已经提到),对注册表的访问需要按需设置,如果只是读取一些注册表项值就没必要设置 ALL_ACCESS,大多时候 READ 甚至 QUERY 的权限就够了。

3.UIPI

这是唯一一项纯粹是出于提高安全性而不是确保兼容性引进的新技术。UIPI 即 User Interface Privilege Isolation,直译过来就是用户界面特权隔离。在 XP 时代到处充斥着各种消息粉碎攻击,最典型的就是通过发送 WM_CLOSE 消息使得接收者退出或者发送 WM_SETTEXT 给其他窗口输入信息 (QQ 尾巴算是这种攻击的典型应用)。大多数程序对于这种攻击都是无能为了,很多程序(比如 QQ,POPO 之类的 IM) 往往只能自己对信息做特殊的过滤和判断来防范,很是繁琐。而 UIPI 的基本作用就是使进程可以拦截接受比自身进程 MIC 等级低的进程发来的消息。在 UIPI 开启的情况下,只要是低 MIC 等级的进程向高 MIC 等级的进程发送消息,所有高于 WM_USER 的消息都默认被拦截,而低于 WM_USER 的消息也只有部分能够被选择性地发送成功,一些比较危险的消息也是直接被拦截掉。

所谓 MIC 即 Mandatory Integrity Control,全称为强制完整性控制,是微软对 Vista 以上的系统做的安全性拓展,主要基于 Biba 模型。其核心在于达到 “no write up,no read down” 的效果。(这个 no read down 貌似在 Vista 里面反映得不是很明显,或者是没怎么注意到吧)在 Vista 和 Win7 里, MIC 共分为 6 级: 不可用,低级,中级,高级,系统级别和手保护级别。一般我们的进程包括 Explorer.exe 是中级,通过管理员身份运行的进程为高级,而值得注意的是 IE 的 MIC 级别是低级别—- 这个理由就很明显了,不赘述。

个人总结的一些关于 UAC 的 Tips

1. 何时需要提高进程的权限?

答案是:在进程启动的时候。这个问题貌似很 SB,却是很多 bug 会产生的根源。在程序运行的过程是不能再对当前进程进行提权的:如果程序执行过程中和操作系统说:哥要提权。这个时候系统是不会理你的,当然也没有相应的 API 提供。

2. 如何设置一个程序的启动选项

一种比较简单的方法就是通过 API 启动某个进程的时候带上启动选项,比如 ShellExecuteEx 有个 runas 选项 而如果需要让一个程序一直以管理员身份启动的方法就很多了:上文提到的 Installer Dection 的原则大多可以满足这个需求,让系统认为你的程序是安装程序,给加上小盾盾。不过个人感觉前面的 5 项都不太靠谱。最标准的做法是在可执行文件中嵌入 UAC 头。在 VS08 之后的工程选项 Manifest File 设置里面有了对启动等级的设置。而 05 之前则需要自己建立一个 manifest 文件,并通过 Mt.exe 向目标进程插入 manifest。详见《Create and Embed an Application Manifest (UAC)》

3.UAC 对文件系统和窗口消息的影响

因为对 HKLM 等注册表项和系统文件目录的读写会被重定向,所以尽量不要在非管理权限进程中进行这方面的读写—- 合理安排用户数据的存储,而不是像以前一样所有数据都存在程序目录下。(当然也有猥琐的方法可以绕开这个限制,但是不推荐)

启动一个需要提权的进程后需要注意这个进程的环境变量上下文:标准用户下以管理员身份启动某进程后,该进程的环境变量上下文是管理员身份相关的,而非当前标准用户的。(登入用户本身是管理员组成员不会有这个问题)

UIPI 的存在使得对于进程间窗口消息传递的控制更严格了,稍不留神一个消息可能就被吃掉了,所以进程间通信最万不得已的情况还是尽量少使用窗口消息—- 安全性和可靠性太差。(在管理员权限环境上下文中,拖曳消息会被 UIPI 给屏蔽掉……)

4. 降权的需求和实现

降权的需求来自于更新程序:更新程序为保证能够正常运行往往是以管理员身份运行,但这样有个问题, 当更新成功后更新进程启动主程序会将自己的权限传递下去,这会带来两大麻烦:

  1. 主程序获得了不该有权限,
  2. 主程序的用户环境变量上下文可能被修改了。

第一项问题可能不大,而第二项就比较要命了:主程序在更新后读取的文件路径都会变成管理员相关的而非当前用户的(通过查看进程管理器可以发现主程序也变成了管理员进程)

推荐的做法是在程序启动更新程序进行更新的同时保证有一个当前用户权限下的监控程序存在,在更新完毕后通过监控程序来启动主程序。

当然也有比较猥琐的做法,可以参考《High elevation can be bad for your application: How to start a non-elevated process at the end of the installation》,基本原理还是通过一个已存在的当前用户权限的进程来启动主程序,不同的是采用了进程内代码注入的方法,比较巧妙,但不推荐。