CVE-2022-21972和CVE-2022-23270漏洞分析
任意门 发表于 江苏 漏洞分析 1094浏览 · 2024-02-01 06:40

前言介绍

这两个漏洞都是出现在raspptp.sys驱动中的,为了理解这两个漏洞,我们必须首先了解内核驱动程序与套接字交互以实现网络功能的一些基础知识。(这两类漏洞都是属于条件竞争漏洞且gitub有利用的脚本但是条件竞争类漏洞成功率的因素负责,所以此处只分析成因和利用思路)
Winsock Kernel (WSK)是Windows socket API的名称,驱动程序可以使用它直接从内核创建和使用socket
如图展示了WSK的体系结构,其结构的核心是WSK子系统也就是WSK subsystem。WSK子系统是实现WSK NPI(网络编程接口)的提供程序端的网络模块。


附加到WSK子系统的是WSK应用程序;而WSK的应用程序是内核模式软件模块;用于实现WSK NPI 的客户端来去执行网络I/O操作的。WSK子系统可以调用 WSK 客户端 NPI 中的函数以向 WSK 应用程序通知异步事件。WSK 应用程序使用一组 WSK 注册函数发现并附加到 WSK 子系统。 应用程序可以使用这些函数动态检测 WSK 子系统何时可用,并交换构成 WSK NPI 的提供程序和客户端实现的调度表。
其中WSK NPI主要围绕了两种main对象类型设计:客户端和 socket。客户端对象由WSK_CLIENT结构表示;
WSK_CLIENT数据类型定义 WSK 子系统的绑定上下文,以便将其附加到 WSK 应用程序。
而socket对象表示可用于网络 I/O 的网络套接字,socket对象由 WSK_SOCKET 结构表示;WSK_SOCKET 结构为套接字定义套接字对象。

typedef struct _WSK_SOCKET {
  const VOID *Dispatch;
} WSK_SOCKET, *PWSK_SOCKET;

其中的dispatch是执向调度结构的指针;微软给出了其表结构这样能更好的了解socket对象的创建流程。

WSK API的方式是通过一组事件驱动的回调函数。当建立了socket之后,应用程序就可以提供一个调度表,其中包含一组函数指针也就是上述中的dispatch,以便为套接字相关的事件调用。为了使应用程序能够通过这些回调来维护自己的状态,驱动程序还为每个回调提供了一个上下文结构,以便可以在连接的整个生命周期中跟踪其状态。

CVE-2022-21972漏洞成因

在分析漏洞之前还需要了解一下raspptp.sys驱动是怎样使用WSK实现pptp协议;
pptp协议指定两个socket连接用于管理VPN连接的TCP-socket和用于发送和接收VPN数据的GRE-socket。
首先监听的套接字由raspptp.sys中的WskOpenSocket函数创建。该函数传递一个(此处相关联的信息我们在上述也有介绍过)WSK_CLIENT_LISTEN_DISPATCH调度表,其中WskConnAcceptEvent函数指定为WskAcceptEven处理程序。这是处理套接字接受事件的回调函数,也就是新传入的连接。
当有新客户机连接到服务器时,调用WskConnAcceptEvent函数。这个函数为新的客户端套接字分配一个新的上下文结构,并使用指定的所有事件回调函数注册一个WSK_CLIENT_CONNECTION_DISPATCH调度表。这些是WskConnReceiveEvent, wskconndisconnecteevent和WskConnSendBacklogEvent分别用于接收,断开和发送事件。
一旦接受事件被完全解析,就会调用WskAcceptCompletion并触发一个回调(CtlConnectQueryCallback),该回调完成PPTP控制连接的初始化,并创建一个专门用于跟踪客户端PPTP控制连接状态的上下文结构。
PPTP控制连接上下文结构由CtlAlloc函数分配。

在其中我们需要注意一下下述的一些结构 PptpCtlCtx—控制连接的PPTP特定上下文结构。CtlReceiveCallback - PPTP控制连接接收回调。 CtlDisconnectCallback - PPTP控制连接断开回调。CtlConnectQueryCallback - PPTP控制连接查询回调。
然后查看一下此漏洞细节是出现在raspptp.sys中是一个条件竞争的UAF漏洞;这个漏洞很大程度上取决于socket对象生命周期是如何被raspptp管理的。
还需要注意一点在raspptp中对于PptpCtlCtx结构,客户端套接字和PptpCtlCtx结构都有一个引用计数。每次创建对任何一个对象的引用时,都会增加这个引用计数。最初设置为1,当减数为0时,通过调用存储在每个结构中的自由回调来释放对象。
当raspptp.sys 接收到新数据时,WSK 就会进行处理寻找相应的事件回调调用。raspptp.sys 为所有socket注册一个名为 ReceiveData 的通用回调事件。此函数把传入数据转发到客户端socket 上下文自己的接收数据回调。
而对于 PPTP 此回调是 CtlReceiveCallback 函数实现的。在其函数下边还有一个CtlpEngine函数,其作用是负责解析传入PPTP控制数据的状态机。在这段处理过程中会发现其缺少了引用计数或PptpCtlCtx对象的锁定。

如果对比补丁来看会发现其增加的逻辑可谓不是一星半点不仅加了对于对象的锁定还有引用计数相当于脱胎换骨。

回过头接着说在回调处理的过程中没有设计到增加PptpCtlCtx的引用计数,也没有试图锁定访问以表示它正在使用;这样当我们在任何时候要减少引用计数,此时对象将被释放。但到这里还没结束接着往下看到CtlpWaitTimeout函数;其又调用了CtlpDeathTimeout函数;而在CtlpDeathTimeout函数中又调用了CtlCleanup 函数其是负责启动断开 PPTP 控制连接过程的函数。

我们来梳理一下首先将连接的状态设置为CtlStateUnknown,这样CtlpEngine函数将无法处理任何进一步的控制连接数据 。(CtlpEngine 函数中正确地使用了引用计数)所以我们要避开它这也是竞争的trick点
然后将任务推送到raspptp.sys 驱动的后台线程上,以运行类似名称的 CtlpCleanup 函数。这样CtlpCleanup 函数中的代码逻辑它将始终与 CtlpEngine 函数在不同的线程上运行。因为CtlpCleanup函数中的代码逻辑减少了 PptpCtlCtx 对象上的引用计数;这样就能实现自由回调来释放对象了。

所以我们就需要让CtlpCleanup 和 CtlpEngine 函数在单独的线程上同时运行;这样就能造成条件竞争最后触发UAF漏洞了。在CtlFree函数中用ExFreePoolWithTag就去释放lpCtlCtxToFree。

但WskCloseSocketContextAndFreeSocket 的调用就是在释放 PptpCtlCtx 结构之前关闭客户端套接字;然后
释放 PptpCtlCtx 结构时,将无法再向套接字发送新数据并触发 CtlpEngine函数了 。
但是如果当套接字关闭时,数据已经被 CtlpEngine 处理时,我们让线程在函数中停留足够长的时间,以便在 CtlFree 崩溃—UAF。
所以最后30秒超时运行 CtlCleanup,然后将CtlpCleanup 任务推送到后台工作线程队列。CtlCleanup 后台工作线程唤醒并开始从其任务队列处理 CtlpCleanup 任务。
当CtlpCleanup 函数从工作线程中释放基础 PptpCtlCtx 结构时,CtlpEngine 启动或当前正在处理 WSK 调度线程上的数据造成崩溃。

CVE-2022-23270漏洞成因

趁热打铁我们已经了解了WSK还有rsp驱动中的部分工作流程,再来看一下CVE-2022-23270漏洞
PPTP调用可以通过IncomingCallRequest或OutgoingCallRequest控制消息创建。当这些调用请求中的任何一个由连接的PPTP客户端发起时,raspptp.sys驱动程序就会创建一个调用上下文结构。
调用上下文结构被设计用于跟踪信息和缓冲调用连接的GRE数据;也就是接收VPN数据的GRE-socket。对于此漏洞通过raspptp构建对象不是关键点,重要是如何触发。
处理PPTP控制消息有两种方法可以检索调用上下文结构。这两种方法都要求客户机知道调用上下文结构的关联调用ID。这个ID是服务器在对呼入或呼出请求的回复中随机生成的。然后客户机在发送到服务器的与该特定调用相关的所有后续控制消息中使用该ID。(参考PPTP RFC (https://datatracker.ietf.org/doc/html/rfc2637))
调用上下文结构:全局可访问的呼叫ID索引数组。 PPTP控制连接上下文存储的链接列表。全局数组可以检索由任何控制连接分配的任何调用,但链表只包含跟它控制连接相关的调用。
链表访问是由EnumListEntry函数实现的其实现了遍历控制连接调用链表的每个成员变量。

EnumComplete用于结束当前循环并重置状态;其中有一个ListIterator变量是用于存储列表中已到达的当前链表项,以便在下次调用EnumListEntry时继续从该点开始继续循环。而EnumComplete函数是为了完成后重置listtiterator变量。

全局数组访问是通过CallGetCall函数;通过参数可以发现CallGetCall根据传入的Callid调用id去检索调用上下文结构存储在其中的数组;然后返回该条目的结构。

此时我们知道CallGetCall函数能够检索由任何当前连接的控制连接分配的任何调用;而且在raspptp中有几个控制连接方法是使用CallGetCall而不是引用内部控制连接链表的;如果CallGetCall函数允许访问其他控制连接调用上下文结构,并且PPTP处理过程时可以并发,那么我们可以同时在两个不同的线程中访问相同的调用上下文结构。
链表访问方法和CallGetCall函数都在全局上下文结构上引用了PptpAdapterSpinLock变量。
这是一个全局可访问的spin lock,用于防止并发访问和全局访问;使用这个锁来保护调用上下文列表访问方法
但是 要知道 CallGetCall 函数中的锁只有在我们搜索列表时才会上锁,所以一旦调用结构返回它就无了。
所以这个并不能安全的进行保护;那么利用思路就来了
当控件连接关闭时,遍历控件连接调用链表,并适当地取消初始化和释放每个调用上下文结构。通过调用函数CtlpCleanup执行。然后再去发送带有错误码的OutgoingCallReply控制消息;释放与之相关的调用结构。然后此时CallGetCall函数用于查找此控制消息处理中的调用上下文结构,此时我们就可以在控制连接关闭例程在单独的线程中运行时使用它来执行释放。如果两者连续发生,调用上下文结构被释放两次,我们就有一个Use after Free/Double Free漏洞了。
在函数CallEventCallOutReply中如果设置了OutgoingCallReply消息的状态字段,那么代码流程就会走到CallCleanup函数的调用中去,这样最终就会走到CallFree函数

而在CallFree函数中,所有子对象的指针都不会被raspptp.sys清空。那么这些对象都可能触发 double free

惨考:
https://labs.nettitude.com/blog/cve-2022-23270-windows-server-vpn-remote-kernel-use-after-free-vulnerability/

0 条评论
某人
表情
可输入 255