cssembly · 2015/03/03 10:59
0x00 前言
这是James Forshaw发表在Project Zero上的文章,主要讲了CVE-2015-0002的原理,原文链接http://googleprojectzero.blogspot.com/2015/02/a-tokens-tale_9.html。
我非常喜欢漏洞研究的过程,有时发现漏洞挖掘的难度与利用的难度之间有显著差异。在Project Zero博客中包含了很多对看似琐碎的漏洞的复杂利用过程。你可能会问,为什么我们会努力证明漏洞是可利用的,我们确实不需要这样做吗?希望在这篇博客的最后,你能更好的理解为什么我们总是花费很大努力通过开发一个poc来证明一个安全问题。
我们PoC的主要目标是供应商,但开发Poc也有其它的好处。使用供应商系统的客户可以通过PoC来测试它们的系统是否存在这一漏洞,并确保已经打上了所有的补丁。即使供应商不愿意或无法修补漏洞,安全厂商也可以利用它来开发缓解措施和漏洞签名。而不提供PoC,只有逆向补丁的人才可能了解它,他们可能没有考虑你的最佳利益。
我不希望这个博客涉及该漏洞(CVE-2015-0002)的太多技术细节。相反,我将重点放在对相对简单的漏洞的可利用性和PoC开发过程上。该PoC足够供应商对所描述的漏洞进行一个合理的评估,以减轻工作量。我也会解释我在PoC开发中采取的各种快捷方式的理由,以及为什么如此。
0x01 报告漏洞
在封闭或专有系统上进行漏洞研究的最大问题在于修复漏洞的实际报告流程。特别是在复杂的或非显而易见的漏洞的情况下。如果系统是开源的,可以开发一个补丁并提交,这代表了一个修复的机会。对于闭源系统,就不得不通过报告的流程。为了更好的理解,让我们想想典型的大型供应商在接受外部的安全漏洞报告时可能需要做的事。
这是漏洞响应处理的一个非常简单的视图,但是足以解释处理原则。对于一个在内部开发他们的大部分软件的公司,我不能够影响修补周期,但我可以影响分流周期。越容易对供应商产生影响,分流周期就越短,也就能够更快的发布补丁。除了已经在使用此漏洞的人,每个人都获得了好处。不要忘了,即使之前我不知道这个漏洞并不意味着它不被人了解。
在一个理想的漏洞研究的世界(即在其中我只需要做最少量的非科研工作),如果我发现一个bug,我需要做的只是写了一些关于它的笔记,将其发送给供应商,他们会了解系统,立即采取行动开发补丁,任务就完成了。当然,这样是行不通的,让供应商认识到这是一个安全问题是重要的第一步。这可能是从分流周期转到补丁周期的主要障碍,特别是他们通常是公司内部独立的实体。为了得到最好的可能,我可能会做两件事情:
- 1 在报告中提供足够详细的细节,以便供应商了解漏洞
- 2 开发出PoC,明确表明安全漏洞的影响
0x02 写报告
尽管在许多情况下并不够,但写报告对供应商修复安全问题是非常关键的。你可以想象,如果我写的东西类似“错误在ahcache.sys中,请修复它,lol”,这并不能真正帮助厂商。至少,我需要提供一些背景,例如漏洞影响(不影响)哪些系统,漏洞有什么影响(尽我所知)和问题存在于系统的什么地方。
只有报告为什么并不够呢?想想大型现代化的软件产品是如何开发的。它可能是团队成员分模块独立开发完成的。根据漏洞代码存在的时间,原开发者可能已经转到其他项目,或者全部离开了公司。即使是身边可交流的人写的相对较新的代码,也并不意味着他们记得代码是如何工作的。任何人在开发任何规模的软件,都会碰到一个月、一周甚至一天以前他们写的代码,但是不知道它是如何工作的。有一种真实的可能性,花费时间一条一条指令分析软件的安全研究人员可能比世界上任何人都更了解软件。
你也可以在科学意义上考虑一下报告,也就是脆弱性假说。有些漏洞是可以证明,例如缓冲区溢出,通常可以通过数学方式证明,例如想把10件物品放入只能容纳5件的空间里将不可能实现。但在很多情况下,没有什么比开发可利用的证明更好。如果正确完成,可以让报告者和供应商通过实验来验证,这就是概念证明的价值。正确开发概念证明使厂商可以观察到实验的效果,将假设变为没有人可以反驳的理论。
0x03 通过实验证明可利用性
假说假定漏洞具有真实的安全影响,我们将使用PoC来客观地证明它。为了做到这一点,我们不止需要向供应商提供证明该漏洞真实性的机理,也需要可以清楚观察到为什么这构成一个安全问题。
需要什么样的现象取决于漏洞的类型。对于内存破坏漏洞,可能只需要证明应用程序在响应某些输入时崩溃就足够了。但是并非总是如此,一些内存破坏并不提供攻击者任何有用的控制。因此需要证明可以控制当前执行流,诸如通常理想的是控制EIP寄存器。
对于逻辑漏洞,可能更细致入微,比如可以写一个文件到本不能够写的位置或者以提升的权限运行了计算器程序。没有一个适合所有情况的方法,但最起码要表现出可以客观上观察到的一些安全影响。
要了解的是我没有将PoC开发为一个可用的漏洞利用程序(从攻击者的角度来看),只是足够证明这是一个安全问题,以保证它能够被修复。遗憾的是将这两个者区分开并不容易,有时不展示本地权限提升或远程代码执行,它的严重性就不会受到应有的重视。
0x04 开发概念证明
现在来看看我在开发我发现的ahcache漏洞的PoC时遇到的挑战。我们不要忘记,花在开发PoC上的时间和漏洞被修复的几率之间要进行权衡。如果我没有花足够的时间来开发一个可用的PoC,供应商可能并不会去修复这个漏洞,另一方面,我花的时间越长,这一漏洞的存在就可能对用户有害。
0x05 漏洞的技术细节
对该漏洞有一点了解将有助于我们后面的讨论。这里(https://code.google.com/p/google-security-research/issues/detail?id=118)你可以看到这一漏洞以及附加的我发给Microsoft的PoC。漏洞存在于ahcache.sys驱动程序中,这是Windows8.1引入的,但本质上是在这驱动程序实现的Windows本地系统调用NtApphelpCacheControl中。这个系统调用是用来处理纠正在较新版本的Windows上的应用程序行为的应用程序兼容信息本地缓存的。你可以在这里(https://technet.microsoft.com/en-us/windows/jj863248)阅读更多有关应用程序兼容性的信息。
这个系统调用的一些操作是需要权限的,使驱动对当前调用的应用程序进行检查,以确保他们拥有管理员权限。这些是在函数AhcVerifyAdminContext中完成的,它看起来像下面的代码:
#!c++
BOOLEAN AhcVerifyAdminContext()
{
BOOLEAN CopyOnOpen;
BOOLEAN EffectiveOnly;
SECURITY_IMPERSONATION_LEVEL ImpersonationLevel;
PACCESS_TOKEN token = PsReferenceImpersonationToken(
NtCurrentThread(),
&CopyOnOpen,
&EffectiveOnly,
&ImpersonationLevel);
if (token == NULL) {
token = PsReferencePrimaryToken(NtCurrentProcess());
}
PSID user = GetTokenUser(token);
if(RtlEqualSid(user, LocalSystemSid) || SeTokenIsAdmin(token)) {
return TRUE;
}
return FALSE;
}
复制代码
此代码查询当前线程是否模拟其他用户。 Windows允许一个线程在系统上模拟其他用户,这样安全操作就可以正确评估。如果线程正在模拟,将返回一个指向访问令牌的指针。如果从PsReferenceImpersonationToken返回NULL,代码则查询当前进程的访问令牌。最后,代码检查访问令牌的用户是不是本地系统用户或令牌是不是Administrators组的成员。如果函数返回TRUE,则特权操作被允许继续进行。
这一切似乎很正确,问题在哪呢?虽然全模拟是仅限于具有令牌模拟特权的用户才能进行的一种特权操作,普通用户没有权限模拟其他用户执行非安全相关的功能。内核在模拟启用时,通过分配一个安全级别给令牌来区分特权和非特权模拟。要理解这个漏洞,只需要关心两个级别,SecurityImpersonation意味着模拟是特权的和SecurityIdentification是非特权的。
如果令牌被分配为SecurityIdentification,仅能进行查询令牌信息的操作,例如查询令牌的用户。如果你试图打开受保护的资源,如文件,内核将拒绝访问。这是潜在的漏洞,如果你看一下代码,PsReferenceImpersonationToken返回分配给令牌的安全级别的副本,但是代码并未验证它是否是SecurityImpersonation。这意味着能够获取本地系统访问令牌的普通用户可以在SecurityIdentification上进行模拟,仍然可以通过检查,允许对用户进行查询。
0x06 证明基本的漏洞利用
要利用该漏洞,需要捕捉本地系统的访问令牌,模拟它,然后通过适当的参数调用系统调用。这一定要通过普通用户的权限实现,否则就不是一个安全漏洞。该系统调用未公开,所以如果我们想走捷径,可能我们只需要表明我们能够捕捉令牌,只能做到这样?
其实并非如此,这个PoC将证明被文档化的可能的事情确实是可能的。即普通用户可以捕捉令牌并模拟它,作为模拟系统的设计,这不会导致安全问题。我已经知道COM支持模拟,有很多复杂的系统特权服务(例如BITS),我们可以作为普通用户与其进行交互,为了进行模拟,可以让它与我们的应用程序进行通信。这不能证明我们可以到达内核包含漏洞的AhcVerifyAdminContext方法,更别说成功地绕过检查。
所以,开始了漫长过程的逆向工程,以确定系统调用是如何工作的,你需要传递什么参数来使它做一些有用的事。这里也使用了一些来自其他研究人员的成果(如http://www.alex-ionescu.com/?p=39),但肯定没有现成可用的东西。该系统调用支持许多不同的操作,不是所有的操作所都需要复杂的参数。例如,AppHelpNotifyStart和AppHelpNotifyStop操作可以很容易进行调用,他们依赖于AhcVerifyAdminContext函数。现在可以构造PoC,通过观察系统调用的返回代码来验证对检查的绕过。
#!c++
BOOL IsSecurityVulnerability() {
ImpersonateLocalSystem();
NTSTATUS status = NtApphelpCacheControl(AppHelpNotifyStop, NULL);
return status != STATUS_ACCESS_DENIED;
}
复制代码
这足以证明漏洞可被利用吗?历史已经告诉我不能,例如这个问题(https://code.google.com/p/google-security-research/issues/detail?id=127)包含几乎完全一样的操作,即可以通过模拟绕过管理员检查。在这种情况下,除信息披露以外,我并没有足够的证据证明它是否会导致其他问题。所以它并没有被修复,即使它确实是一个安全问题。为了证明可利用性,我们需要花更多的时间在PoC上。
0x07 改进的概念证明
为了改进第一个PoC,我需要更好地了解系统调用在做什么。应用程序兼容性缓存用于存储应用程序兼容性数据库中的查询数据。这个数据库包含的规则会告诉应用程序兼容性系统什么可执行文件需要应用“shims”来实现自定义的行为,如依赖操作系统的版本号来规避不正确的检查。在进程创建的时候进行查询,如果找到合适的匹配项,它会被应用到新的进程。新的进程将从数据库中查询它需要应用的shim数据。
由于每次创建一个新的进程时都要进行这样的处理,每次查询数据库文件会带来显著的性能开销。缓存有助于降低这种影响,数据库查询可以被添加到高速缓存中。如果可执行文件稍后被创建,缓存查询可迅速消除耗时的数据库查询,同时应用或者不应用一系列的shims。
因此,我们应该能够缓存现有的查询并将其应用到任意的可执行文件。所以我花了一些时间获取系统调用的参数的格式,以便添加自己的查询缓存。对于32位Windows 8.1,结构看上去像下面这样:
#!c++
struct ApphelpCacheControlData {
BYTE unk0[0x98];
DWORD query_flags;
DWORD cache_flags;
HANDLE file_handle;
HANDLE process_handle;
UNICODE_STRING file_name;
UNICODE_STRING package_name;
DWORD buf_len;
LPVOID buffer;
BYTE unkC0[0x2C];
UNICODE_STRING module_name;
BYTE unkF4[0x14];
};
复制代码
你可以在结构中看到有非常多未知的部分。如果你想将它应用到Windows 7(其结构稍微不同)或64位(其结构大小不同),这会导致问题,但对于我们的目的来说并不重要。我们并不需要写出能在所有版本的Windows上可用的利用代码,我们需要做的只是向供应商证明这是一个安全问题。我们只要告知供应商PoC的限制(他们将注意到限制),这是可以做到的。供应商应该能够确定该PoC是否可以跨操作系统的版本使用,毕竟这是他们的产品。
所以,现在可以添加任意的缓存条目,我们真正添加的是什么呢?我只能给现有查询结果添加条目。你可以修改数据库来做类似运行时代码补丁的工作(应用程序兼容性系统也可用于修补程序),但这需要管理员权限。所以,我需要一个现有的shim以便重新利用。
我构建了SDB Explorer工具(https://github.com/evil-e/sdb-explorer)的副本,这样我可以转储现有数据库查询中的任何有用的shim。我发现对于32位程序有一个shim会导致进程启动可执行的regsvr32.exe,并传递原始命令行。此工具将加载在命令行上传递的一个DLL,执行特定的导出方法,如果我们能够控制特权进程的命令行,我们可以把它重定向来提升权限。
这又限制了PoC只对32位进程有效,但是这很好。最后一步是选择什么样的进程来进行重定向。我花了很多时间研究启动一个进程的时候并能够控制它的命令行参数的方法。我已经知道一种方式,即UAC自动提升。自动提升作为一个特性被添加到Windows 7,减少典型用户看到UAC对话框的数量。操作系统定义了允许自动提升的固定的应用程序列表,当UAC为默认设置,且用户为管理员时,请求提升这些应用程序不会显示对话框。我可以滥用这一点,为现有的自动提升的应用程序添加缓存条目(在这种情况下,我选择了ComputerDefaults.exe),并要求应用程序运行提升。被提升的应用重定向到regsvr32,并传递我们完全控制的命令行,regsvr32载入我的DLL,而我们现在得到的代码以提升的权限执行。
另外,PoC没有提供任何其他东西,不可能通过其他各种机制(如该Metasploit的模块https://github.com/rapid7/metasploit-framework/tree/master/external/source/exploits/bypassuac)来实现,但不总是这样。通过提供一个可观察的结果充分展示了这一问题(以管理员身份运行任意代码),这样微软就能够重现并解决它。
0x08 有趣的最后一点
由于很容易混淆它是否只能绕过UAC,我决定花一点时间来开发新的PoC,它可以获得本地系统权限而不依赖UAC。有时候,我喜欢写漏洞利用,只是为了证明这是可以做到的。要将原来的PoC转换为获得本地系统权限的PoC,我需要一个不同的应用来重定向。我认为最有可能的目标是注册的计划任务(scheduled task),你有时可以将任意参数传递给任务处理进程。因此,实现这一任务,有三条限制,一个普通用户必须能够启动它,它必须启动一个本地系统权限的进程,进程必须具有由用户指定的任意的命令行。经过一番搜索我找到了理想的目标,Windows应用商店维护任务(Windows Store Maintenance Task)。正如我们看到的,它作为本地系统用户来运行。
通过使用如icacls的工具查看任务文件的DACL,我们可以确定普通用户可以启动它。注意下面的截图中的项,NT AUTHORITY \Authenticated Users具有读取和执行(RX)权限。
最后,通过检查XML任务文件,我们可以检查普通用户是否可以来传递任意参数给任务。在WSTask中,它使用自定义的COM处理程序(https://msdn.microsoft.com/en-us/library/windows/desktop/aa381370(v=vs.85).aspx),但允许用户指定两个命令行参数。这将导致可执行文件c:\windows\system32\taskhost.exe作为本地系统用户执行,并且具有任意命令行参数。
这只需要修改PoC以添加taskhost.exe缓存条目,将我们DLL的路径作为参数来启动任务。这还是有一定的局限性,特别是它只能在32位Windows8.1上工作(在64位平台上没有32位taskhost.exe可供重定向)。不过我敢肯定,通过一些努力,它也能够在64位上工作。由于该漏洞已经被修复,所以我提供了新的PoC,它附加在原来的问题之后(https://code.google.com/p/google-security-research/issues/detail?id=118#c159)。
0x09 结论
我希望我已经证明,为了确保漏洞被修复,漏洞研究人员会去做的一些努力。它最终是花费在PoC开发的时间和漏洞被修复之间的一个折衷,尤其是当该漏洞是复杂的或不明显的时候。
在这种情况下,我觉得我做出了正确的权衡。尽管从我发送给Microsoft的PoC来看,表面上只是绕过了UAC,结合报告,他们就能够确定真正的严重性并开发补丁。当然,如果他们想推回,并声称这不是可利用的,我会开发一个更强大的PoC。通过对严重程度的进一步论证,我也开发了一个可用的漏洞利用,可以通过普通用户帐号获得本地系统权限。
披露PoC对用户或安全公司开发公开漏洞的缓解技术是有价值的。如果没有PoC,验证安全问题已被修补或缓解是相当困难的。它还有助于告知研究人员和开发人员,在开发一些安全敏感的应用程序时,什么类型的问题需要注意。漏洞挖掘不是Project Zero帮助软件提高安全性的唯一方法,教育也同样重要。
Project Zero的使命是解决软件漏洞,开发概念验证帮助软件厂商或者开源项目采取明智行动修复漏洞也是我们职责的重要组成部分。