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.exe
的CreateProcessInternalW
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 代码签名问题
然而,这段代码无法运行成功,CisClassFactory
的CreateInstance
会调用失败,返回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
,成功阻止了我想要找到的逻辑错误。当然还有许多方法可以研究,因为我们其实能提取出更多的接口。前面提到过,我们可以使用CreateInstance
在CmdAgent.exe
中创建一个CisGate对象。因此我们有可能创建更多的对象,研究更多的方法,让我们回到CmdAgent。
ICisClassFactory
->CreateInstance
函数会创建所需的对象,调用CisGate
->QueryInterface
返回请求的接口指针。这里提一下,QueryInterface?source=post_page—————————————-)是IUnknown
中的一个核心函数,是所有COM类的基类。简而言之,该函数会将riid(接口标识符)解析成对象接口,这样客户端(比如我们开发的客户端)就可以调用接口上的方法。了解这一点后,我们可以逆向分析CmdAgent.exe
的QueryInterface
函数,观察其中支持哪些接口。
图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
ISvcRegistryAccess
以SYSTEM
权限读取注册表,没有使用模拟机制
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
权限