UAC即User Account Control(用户帐号控制),是微软从Windows Vista开始为提高安全性而引入的一项新技术。用户通过这项技术既可以以非管理员身份,也能够以管理员身份执行常见的任务,而不需要切换账户或者注销。在大多数的情况下,用户都是以标准用户的状态来执行日常任务,只有当需要设置系统特定资源的操作的任务才会需要用户以管理员的身份执行,以此来确保进程对系统的“伤害”达到最低。
使用UAC的理由很简单:保护系统资源和数据的安全。在XP时代,在系统安装完毕后任何新建的账户都会默认划入系统管理员组,于是用户有了安装,卸载,修改,删除系统任何地方任何数据的权限,而这正是万恶之源。而如果能够控制不同程序的权限,那么大部分的恶意软件和病毒就不能起作用了。
UAC正是基于这种思路进行设计的:严格控制进程所能获得的权限。让一个进程无时无刻都拥有管理员权限是无法容忍的:一个恶意程序如果被自动运行于我们的系统,且肆无忌惮地执行某些会进行系统资源读写操作的代码而不为我们所知晓那是多么恐怖的事。(XP正是这么做的)所以UAC的策略就是给予进程尽可能低的权限,如果程序需要管理员权限则需要知会当前用户。同时通过一系列的措施来保障程序的正确运行:
对普通用户而言,UAC的引入可能并没有带来多大的影响,更多的可能只是在启动特定程序的时候会跳出提示通知用户以管理员身份运行,仅此而已。但是这个地方有个比较尴尬的问题:UAC(或者其他类似的安全措施)是基于如下假设的:
在Windows中有两项比较重要的概念:ACL和Access Token。ACL即Access Control List(直译成:访问控制列表),对于Wdinows中的所有资源来说都会有自己的ACL,这个列表决定了这个资源可以被具有哪些权限的用户/进程所访问。而Access Token即用户的访问令牌,这决定了用户对资源的访问属性。在Vista之前的系统中,如果用户使用了标准用户(如XP中所谓的受限用户),用户就会得到一个和之相对应的Access Token,只能访问和修改有限的用户资源。但只要用户用了管理员组的帐号进行登入,用户就能够获取一个所谓的"Full Access Token",即可以获取到对任意资源的访问权。这显然是多余而且不安全的,于是从Vista起的UAC就做了如下的调整:
从上文我们已经可以知道UAC会使得我们的程序运行在一个尽可能低的权限下,而这个权限可能过低,而不在某些敏感资源的ACL允许范围。那么从技术角度来说,搞清楚哪些资源是所谓的敏感资源就很重要—-知己知彼,百战不殆。从Wiki摘抄的需要UAC授权的操作:
* 配置Windows Update
* 增加或删除用户帐户
* 改变用户的帐户类型
* 改变UAC设置
* 安装ActiveX
* 安装或移除程序
* 安装设备驱动程序
* 设置家长控制
* 将文件移动或复制到Program Files或Windows目录
* 查看其他用户文件夹
基本上,只要有涉及到访问系统磁盘的根目录(例如C:),访问Windows目录,Windows系统目录,Program Files目录,访问Windows安全信息以及读写系统登录数据库(Registry)的程序访问动作,都会需要通过UAC的认证。
对于普通用户来讲,UAC最直观的感受就是在很多程序图标多了个小盾,且双击后会出来个用户账户控制的窗口。而对于技术人员来说当然更需要关心真正的内幕:启动的时候进程做了提权的动作,获取了更高权限的用户令牌。(而这又可能导致这个进程的用户相关上下文直接改变,当然这是后话)在Vista以后的程序在默认情况下会有3种启动选项:asInvoker(None),highestAvailable和requireAdministrator。其中highestAvailable最不为大家熟知:这种启动方式请求当前账户可以获取到的最高权限:如果本身是管理员组内成员,则可以得到完整的管理员访问令牌,呼风唤雨。而如果是标准用户则只能得到它这个用户能够得到的最高权限。(具体如何设置程序启动选项在下面的Tips中继续说)上一幅MSDN提供的开启UAC状态下程序启动的流程图:
为实现UAC的所有功能,微软可谓煞费苦心,整了很多新的技术和新概念出来。(虽然个人觉得这个技术对于一般用户来说还是很鸡肋)下面就罗列一部分我们平常开发中可能会碰到或者遇到的技术:
这个技术最大的作用是为了兼容以前的以前版本系统中的程序(尤其是安装程序,顾名思义嘛),在UAC下安装程序做的很多事情可能都是十恶不赦,需要最高权限的(如写注册表,写敏感文件目录),而旧版本的程序压根没有做任何特殊处理(或者说是只是填充了一些默认信息),所以一种行之有效的安装程序检测技术是很必要的,否则很多程序安装都不成功,更毋论运行了。 MSDN上总结了一些Installer Dection的原则:
这是一项比较扯同时也是为了保证兼容性设计出来的技术。简单地来说(这个只能简单来说了,具体的原理没有相应的参考资料),就是对老程序所进行的“非法”的 访问系统敏感数据进行重定向,可以分为文件虚拟化和注册表虚拟化。当用户对一个需要管理员权限才能够访问的文件目录或者注册表项进行读写都会被重定向。
如上图,用户对%ProgramFiles%的读写会被定向到%LocalAppData%VirtualStore下,而对于HKLMSoftware的读写会被重定向到HKCUSoftwareClassesVirtualStore下。 特别需要注意的是:因为在XP下养成的习惯,我们对于注册表的读写很多直接就是用KEYALLACCESS的选项,而到了Vista和Win7后,因为分配给用户的权限低了(即使管理员帐号登入拿到的权限也是经过“和谐”的,上文已经提到),对注册表的访问需要按需设置,如果只是读取一些注册表项值就没必要设置ALL_ACCESS,大多时候READ甚至QUERY的权限就够了。
这是唯一一项纯粹是出于提高安全性而不是确保兼容性引进的新技术。UIPI即User Interface Privilege Isolation,直译过来就是用户界面特权隔离。在XP时代到处充斥着各种消息粉碎攻击,最典型的就是通过发送WMCLOSE消息使得接收者退出或者发送WMSETTEXT给其他窗口输入信息(QQ尾巴算是这种攻击的典型应用)。大多数程序对于这种攻击都是无能为了,很多程序(比如QQ,POPO之类的IM)往往只能自己对信息做特殊的过滤和判断来防范,很是繁琐。而UIPI的基本作用就是使进程可以拦截接受比自身进程MIC等级低的进程发来的消息。在UIPI开启的情况下,只要是低MIC等级的进程向高MIC等级的进程发送消息,所有高于WMUSER的消息都默认被拦截,而低于WMUSER的消息也只有部分能够被选择性地发送成功,一些比较危险的消息也是直接被拦截掉。
所谓MIC即Mandatory Integrity Control,全称为强制完整性控制,是微软对Vista以上的系统做的安全性拓展,主要基于Biba模型。其核心在于达到"no write up,no read down"的效果。(这个no read down貌似在Vista里面反映得不是很明显,或者是没怎么注意到吧)在Vista和Win7里,MIC共分为6级:不可用,低级,中级,高级,系统级别和手保护级别。一般我们的进程包括Explorer.exe是中级,通过管理员身份运行的进程为高级,而值得注意的是IE的MIC级别是低级别—-这个理由就很明显了,不赘述。
答案是:在进程启动的时候。这个问题貌似很SB,却是很多bug会产生的根源。在程序运行的过程是不能再对当前进程进行提权的:如果程序执行过程中和操作系统说:哥要提权。这个时候系统是不会理你的,当然也没有相应的API提供。
一种比较简单的方法就是通过API启动某个进程的时候带上启动选项,比如ShellExecuteEx有个runas选项 而如果需要让一个程序一直以管理员身份启动的方法就很多了:上文提到的Installer Dection的原则大多可以满足这个需求,让系统认为你的程序是安装程序,给加上小盾盾。不过个人感觉前面的5项都不太靠谱。最标准的做法是在可执行文件中嵌入UAC头。在VS08之后的工程选项Manifest File设置里面有了对启动等级的设置。而05之前则需要自己建立一个manifest文件,并通过Mt.exe向目标进程插入manifest。详见《Create and Embed an Application Manifest (UAC)》
因为对HKLM等注册表项和系统文件目录的读写会被重定向,所以尽量不要在非管理权限进程中进行这方面的读写—-合理安排用户数据的存储,而不是像以前一样所有数据都存在程序目录下。(当然也有猥琐的方法可以绕开这个限制,但是不推荐)
启动一个需要提权的进程后需要注意这个进程的环境变量上下文:标准用户下以管理员身份启动某进程后,该进程的环境变量上下文是管理员身份相关的,而非当前标准用户的。(登入用户本身是管理员组成员不会有这个问题)
UIPI的存在使得对于进程间窗口消息传递的控制更严格了,稍不留神一个消息可能就被吃掉了,所以进程间通信最万不得已的情况还是尽量少使用窗口消息—-安全性和可靠性太差。(在管理员权限环境上下文中,拖曳消息会被UIPI给屏蔽掉……)
降权的需求来自于更新程序:更新程序为保证能够正常运行往往是以管理员身份运行,但这样有个问题, 当更新成功后更新进程启动主程序会将自己的权限传递下去,这会带来两大麻烦: 1.主程序获得了不该有权限, 2.主程序的用户环境变量上下文可能被修改了。
第一项问题可能不大,而第二项就比较要命了:主程序在更新后读取的文件路径都会变成管理员相关的而非当前用户的(通过查看进程管理器可以发现主程序也变成了管理员进程)
推荐的做法是在程序启动更新程序进行更新的同时保证有一个当前用户权限下的监控程序存在,在更新完毕后通过监控程序来启动主程序。
当然也有比较猥琐的做法,可以参考《High elevation can be bad for your application: How to start a non-elevated process at the end of the installation》,基本原理还是通过一个已存在的当前用户权限的进程来启动主程序,不同的是采用了进程内代码注入的方法,比较巧妙,但不推荐。