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》,基本原理还是通过一个已存在的当前用户权限的进程来启动主程序,不同的是采用了进程内代码注入的方法,比较巧妙,但不推荐。