CVE-2019-3969:Comodo沙箱逃逸提权漏洞分析

阅读量    101192 |   稿费 200

分享到: QQ空间 新浪微博 微信 QQ facebook twitter

 

0x00 前言

AV(反病毒软件)一直以来都是漏洞挖掘的绝佳目标,这是因为其中涉及到巨大的攻击面、复杂的解析过程以及以高权限运行的各种组件。几个月之前,我决定分析最新版的Comodo Antivirus v12.0.0.6810,最终我找到了一些有趣的信息,但这里我想跟大家分享其中一个沙箱逃逸问题,可以将权限提升至SYSTEM级别。在本文中,我们会滥用各种COM对象,绕过二进制签名检查,劫持关键服务。下面开始进入主题。

 

0x01 Comodo沙箱机制

首先我想介绍一下Comodo的沙箱技术,Comodo称之为“Containment”,在本文中我将交替使用“Containment”以及“沙箱”这两个词。这种沙箱技术可以限制不可信应用(RTATC)在类沙箱环境中运行,同时OS中还能运行其他进程。这种技术涉及到用户模式hook以及内核模式驱动,可以阻止对主机上文件或注册表的任何修改操作。该环境中可以读取文件及注册表,但一旦执行写操作,文件I/O就会被转移到沙箱文件系统,后续读取操作将与沙箱文件系统交互,使调用方误认为自己在与正常的文件系统交互。

Comodo通过过滤器驱动Cmdguard.sys来实现该功能。该驱动会使用FltRegisterFilter API注册一个_FLT_REGISTRATION结构,该结构包含与来自用户模式应用的各种IRP(比如文件访问文件写入等)对应的回调例程,并且可以执行对应的拦截/处理操作。此外,ALPC(用于各种OS组件的一种Microsoft IPC)也会被沙箱化处理,Comodo会将ALPC连接转移到“沙箱化”的svchost.exe实例,避免通过RPC/ALPC实现沙箱逃逸。这种containment技术工作原理如下图所示:

图1. Comodo Containment技术逆向分析图

Cmdguard.sys不仅可以过滤文件/注册表I/O,也会使用PsSetCreateProcessNotifyRoutine注册CREATE_PROCESS_NOTIFY_ROUTINE,跟踪正在运行的进程。当进程运行时,Comodo会将containment状态、信任等级以及其他相关属性跟踪并保存到存储在内核中的一个进程列表。Cmdguard.sys会对外提供“filter ports”,以便用户模式下的Comodo组件通信。Comodo会向cmdAuthPort filterport发送特定消息来设置containment状态,随后内核模式驱动会在该消息指定的目标进程上设置“containment”标志。

在创建沙箱化进程后,Guard64.dll(几乎每个运行的进程中都会注入该dll)负责从用户模式中发送这些containment消息。例如,Guard64.dll会hook Explorer.exeCreateProcessInternalW API,这样当用户执行不可信进程时,就会向Cmdguard.sys过滤器端口发送一个“containment”消息。现在当不可信进程启动时,驱动程序会将打上“contained”标记,阻止文件、注册表I/O操作。此外,Cmdguard.sys会将Guard64.dll注入沙箱化进程中,执行用户模式下的hook操作。

图2. Cmdguard.sys dll注入流程

Guard64.dll在用户模式下设置的hook如下图所示:

图4. Guard64.dll用户模式hook

这些hook有个相同的功能,就是避免沙箱化的进程连接非沙箱化进程创建的安全对象。为了完成该任务,Comodo会将一个!comodo_6标记附加到沙箱化进程创建或打开的每个对象名,避免对象名与系统上已有的安全对象发生名字冲突(或者关联)。

实际上,这也是RPC/ALPC沙箱隔离的工作原理。RPC/ALPC流量会被转移到沙箱化的Svchost.exe实例(参考上图),这是因为!comodo_6会附加到沙箱化进程尝试连接的端口名,而沙箱化的Svchost.exe实例也会创建附加!comodo_6的端口名。如下图所示,我们可以看到沙箱化的MSI安装程序尝试运行,发起RPC调用后最终会创建沙箱化的MSIexec.exe服务组件(父进程为cmdvirth.exe)。

图5. 沙箱化的ALPC生成沙箱化的MSIExec实例

绕过这些用户模式hook并不难,但想通过这种方式实现沙箱逃逸并不容易。如果想简单patch与ALPC相关的hook,通过WMI实现逃逸显然不可能,因为这些位置同样会被CmdAgent.exe监控并阻止。了解关于Comodo沙箱环境的基本知识后,下面我们来研究下如何实现沙箱逃逸及权限提升。

 

0x02 创建Comodo COM客户端

Comodo在各种AV组件之间使用了许多IPC机制,包括:过滤端口、共享内存、LPC以及COM。这里我们将重点关注COM。如果大家想了解COM,可以参考这篇文章。简而言之,COM的全称为“Component Object Model”,是微软提供的一种技术,允许不同模块创建由COM服务端定义的各种对象并与之交互。COM服务端可以本地部署(在当前进程中加载的一个COM服务端dll)或者远程部署,可以通过ALPC进行交互。在利用形式上,远程部署可能是更为有趣的应用场景。

我们发现Comodo可以从低权限进程(如explorer.exe,通过Context Shell Handler(当用户右键点击时出现的菜单)或者Cis.exe(Comodo客户端GUI))发起扫描任务。这些扫描任务可以通过调用CAVWP.exe中的例程来发起,而该程序以SYSTEM权限执行。

如果我们能澄清如何按照Comodo的方式连接到这个服务,那么可能我们会找到一种新的攻击面,发现除“扫描”之外更多有趣的函数。我们需要通过COM实现与CAVWP.exe的远程交互,因为从注册表中可知,CAVWP.exe是一个进程外的COM服务端:

图5. CAVWP.exe COM服务端

接下来分析Explorer.exe以及Comodo COM客户端如何通过COM远程触发这些“扫描”动作。前面提到过,Comodo在Explorer.exe的Shell Context Menu中注册了一个handler(CavShell.dll),因此低权限客户端Explorer.exe可以发起扫描动作。

图6. Explorer.exe中的Comodo Context Menu Handler

逆向分析这个shell扩展接口后,我发现其中Comodo实现了一个“扫描”客户端COM例程。理解这个函数可以帮助我们了解如何构造自己的COM客户端。

图7. Cavshell.dll(Context Menu Handler)的“Scan File”例程

该代码流程中对CoGetClassObject的调用比较有趣。CoGetClassObject会返回指向某个接口的一个指针,而该接口对应与CLSID关联的某个对象。在注册表中查找后,我们发现这个CLSID对应“Cis Gate Class”,并且很快我们意识到CAVavWp.exe与这个类没有任何关系,该类对应的实际COM服务端为CmdAgent.exe

图8. 根据CLSID(CLSID_CisGate)发现COM服务端为Cmdagent.exe

经过研究后我们发现,CmdAgent充当的是低权限COM客户端与CavWp之间的代理角色,CavWp会代表我们通过CisGate接口向CmdAgent发起扫描请求。这里我们的主要目标是理解并设置这些绑定关系,这样才能进一步利用更多的攻击面。

逆向分析客户端(以及部分CmdAgent逻辑)后,我们成功迁移了COM代码,澄清了正确的方法偏移地址,并且重新设计我们的代码,以模拟这些COM对象的操作。

图9. 模拟“伪造的”Comodo COM客户端

 

0x03 代码签名问题

然而,这段代码无法运行成功,CisClassFactoryCreateInstance会调用失败,返回E_ACCESSDENIED。这一点比较奇怪,因为我们的进程权限与Explorer.exe以及Cis.exe一样,而后者却能成功调用,原因究竟是什么?

在调试器中调试CmdAgent.exe,可以看到程序会收到我们执行的CreateInstance调用,并且进入一个自定义的E_ACCESS_DENIED消息分支。

图10. Cmdagent.exe阻止未签名进程通过COM进行交互

这意味着这个ACCESS_DENIED并不是由Windows发出,而是Comodo自己做出的决定。

这个决策实际上是基于签名检查,签名检查会验证请求实例的COM客户端是可信的、“经过签名的”程序。观察Cmdagent.exe中的签名检查例程,可以看到文件需要由Comodo或者微软签名,这一点也能够理解,因为这样只有Explorer.exe或者Cis.exe这两个客户端才能调用CmdAgent.exe中的COM方法。

图11. Cmdagent.exe签名校验类(HardMicrosoftSigner/HardComodoSigner

其实这个签名校验过程很容易被绕过,大家能不能发现问题所在?这里CmdAgent.exe会解析COM客户端的进程名,以便后续从磁盘上执行签名检查过程:

图12. Cmdagent.exe查找COM客户端的完整映像名

大家可能知道一点,GetModuleFileNameEx实际上只会查询目标进程的PEB->Ldr->InMemoryOrderModuleList,以便获取完整映像名。然而这个路径我们可以控制,因此我们可以在自己的进程中轻松修改这个特征。

还有另一种解决办法,我们可以将代码注入可信的微软程序或者Comodo程序,从中发起COM请求。然而Comodo会阻止dll注入操作,因此为了绕过这一点,我们需要对可信的Comodo程序执行Process Hollowing操作。

这个过程比操作我们自己的PEB还麻烦,但能给我们带来更大的优势。通过这种方法,Cmdguard.sys驱动就不会注入Guard64.dll,因此也不会在我们的进程中设置各种hook。如果目标进程“不可信”,那么Cmdguard.sys就会调用InjectDll例程,如下图所示。如果我们对C:\Program Files\COMODO\COMODO Internet Security\CmdVirth.exe执行Hollowing操作,那么就可以绕过IsProcessUntrusted检查,因为驱动会根据可执行文件路径来判断该程序为可信程序,因此会成功放行。

图13. 如果目标进程“可信”,Cmdguard.sys就不会注入Guard64.dll

现在我们添加了一个Process Hollowing例程,用来处理C:\Program Files\COMODO\COMODO Internet Security\CmdVirth.exe实例(该程序由Comodo签名),用我们的代码替换其中的可执行代码。现在注入的代码会执行我们前面设置的COM处理逻辑,并且能够绕过签名检查,不存在任何hook。我们重新运行这段代码,成功通过低权限进程,在设置沙箱标记的情况下触发扫描任务!

图14. 从自定义的未签名的COM客户端成功触发扫描操作

 

0x04 寻找可滥用的COM接口

可以从沙箱进程中执行扫描操作后,我们现在可以寻找CmdAgent中是否存在更多有趣的COM接口。观察我们提取到的60多个CisGate接口后,我们只找到了1个值得关注的对象,但这些接口都正确使用了CoImpersonateClient,成功阻止了我想要找到的逻辑错误。当然还有许多方法可以研究,因为我们其实能提取出更多的接口。前面提到过,我们可以使用CreateInstanceCmdAgent.exe中创建一个CisGate对象。因此我们有可能创建更多的对象,研究更多的方法,让我们回到CmdAgent。

ICisClassFactory->CreateInstance函数会创建所需的对象,调用CisGate->QueryInterface返回请求的接口指针。这里提一下,QueryInterface?source=post_page—————————————-)是IUnknown中的一个核心函数,是所有COM类的基类。简而言之,该函数会将riid(接口标识符)解析成对象接口,这样客户端(比如我们开发的客户端)就可以调用接口上的方法。了解这一点后,我们可以逆向分析CmdAgent.exeQueryInterface函数,观察其中支持哪些接口。

图15. CmdAgent.exe支持的接口

我们列出了QueryInterface支持的supported_interfaces,根据注册表中发现的信息来命名每个GUID。这里IID_ICisFacade这个riid用来返回CisGate对象,另一个比较有趣的目标是IID_IServiceProvider。查看IID_IServiceProvider?source=post_page—————————————-)官方文档,貌似该接口可以给我们提供很多信息。在Cis.exe(Comodo GUI Client)中搜索IID_IServiceProvider GUID,发现Comodo的确在使用该接口。逆向分析代码逻辑后,我们可以澄清自己如何使用该接口、Comodo想使用的服务以及想执行的操作。

 

0x05 注册表读取

Cis.exe会使用IServiceProvider来执行QueryService操作,用来获取Comodo定义的SvcRegistryAccess对象,部分代码片段如下图所示:

图16. Cis.exe获取ISvcRegistryAccess

这表明Cis.exe会从CmdAgent.exe获取SvcRegistryAccess对象,然后调用该对象中的方法读取注册表键值并返回数据。能让具备SYSTEM权限的进程帮我们读取注册表,这听起来已经是比较不错的一个攻击路径,但我感觉开发者不会只把这个SvcRegistryAccess当成一个“只读”类。让我们回到CmdAgent,观察这个COM类的实现机制。

CmdAgent中,我们可以看到被远程调用的ISvcRegistryAccess方法会直接读取注册表键值,将数据返回给客户端,整个过程不涉及到CoImpersinateClient。这意味着我们可以使用SYSTEM权限读取注册表键值,因为这也是CmdAgent所具备的权限。

图17. CmdAgent.exe ISvcRegistryAccessSYSTEM权限读取注册表,没有使用模拟机制

 

0x06 注册表写入

现在来看一下这个COM对象是否支持注册表写入。进一步研究vtable后,我们看到其中某个方法会调用RegSetValueExW

图18. CmdAgent.exe会调用更加有趣的一些方法(设置注册表键值)

图19. CmdAgent.exe ISvcRegistryAccess方法以SYSTEM权限设置注册表键值

显然,如果我们调用该方法,就能以SYSTEM权限执行注册表写入操作,因为我没有在代码中找到对任何身份模拟API的调用。我们修改了自己的COM客户端代码,获取IServiceProvider,解析ISvcRegistryAccess,然后调用这个“注册表写入”方法。如果我们观察通过调用GetRegInterface来获取regInterface的过程,就可以看到CmdAgent.exe实际上只创建了一个只读的注册表键值句柄,因此尝试调用这个“写注册表”方法就会出现ACCESS_DENIED问题。幸运的是,我发现了ISvcRegKey vtable中由另一个方法,通过传递一些额外参数,可以将我们的注册表句柄变成“可写”状态。

在原有代码中调用该方法,同时传入适当参数,这样就能获取“可写的”ISvcRegistryAccess

图20. 修改COM客户端以获取可写的注册表接口

整合在一起,我们最终得到了如下代码,能够以SYSTEM权限执行注册表写入操作。

图21. 最终的COM客户端代码

图22. 成功覆盖高权限注册表键值

 

0x07 提升至SYSTEM权限

稍微总结下,我们运行沙箱化应用,对Comodo签名的程序执行Process Hollowing操作(以便绕过CmdAgent的签名校验机制),然后通过该程序运行我们开发的COM代码,从我们“沙箱化的”进程中以SYSTEM权限执行注册表写入操作。如果想通过这种方法实现权限提升,典型的方法就是劫持已有的服务,这里我们可以选择CmdAgent.exe。通过替换注册表HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\CmdAgent对应的ImagePath值,我们就可以将CmdAgent服务替换成我们自己的程序,然后以SYSTEM权限运行。

图23. CmdAgent.exe服务

然而,如果我们想要第一时间获得SYSTEM权限,我们需要重启CmdAgent服务(不然就要等待下次重启)。幸运的是,我们有办法能够完成该任务,我找到了让CmdAgent崩溃的方法,这样服务就会自动重启,最终启动我们的程序。想让CmdAgent崩溃非常简单,该进程会对外提供结构化数据的一个Section Object(内存区对象),而EVERYONE具备该对象的写入权限:

图24. CmdAgent.exe Section Object公开可写的对象数据(SharedMemoryDictionary

CmdAgent将这个缓冲区当成一个SharedMemoryDictionary,这是共享内存中对外公开的一个类对象。我们可以在对象成员中写入错误的大小值,这样当CmdAgent尝试读取这个SharedMemoryDictionary时(CmdAgent经常会执行该操作),就会出现越界读取问题,最终导致CmdAgent崩溃。当服务恢复时,就会执行我们设置的程序,最终让我们提升至SYSTEM权限。

图25. 成功提升至SYSTEM权限

大家可以观看此处视频了解攻击过程,点击此处下载源代码。

分享到: QQ空间 新浪微博 微信 QQ facebook twitter
|推荐阅读
|发表评论
|评论列表
加载更多