CVE-2022-21972:windows Vpn漏洞

admin 2022年5月15日01:07:28评论126 views字数 18236阅读60分47秒阅读模式

    玄武实验室公众号推送了这个漏洞。对CVE我是敬而远之,没时间也没那个能力。看摘要发现它跟VPN相关,引起了我的兴趣。因为开发过VPN工具的缘故,就非常关注。下面的内容来自于文章的翻译,难懂,我也看得云里雾里的,多看几遍。他的前置知识还是非常不错的。

CVE-2022-21972:windows Vpn漏洞

    这是10年前开发的VPN客户端工具。开发这个软件时刚开始好像是用到了一个俄罗斯人的DPK,花了点钱买了正版,后来他停止升级,又不兼容新的windows版本。我只好转向windows的api,推倒重新架构,花了不少时间。曾一度卡在什么地方,后来在一个E语言写的代码中找到了灵感。到了2017年因为监管严就停了。很荣幸能看到一个windows vpn自身协议的漏洞。


    言归正传,CVE-2022-21972 是一个Windows VPN Use after Free (UaF)漏洞,通过对raspptp.sys内核驱动程序进行逆向工程发现。该漏洞是一个竞争条件问题,可以通过将精心设计的输入发送到易受攻击的服务器来触发。该漏洞可用于在目标系统上获得内核远程代码执行(RCE) 或本地权限提升(LPE)


    此漏洞主要基于raspptp.sys驱动程序如何管理套接字对象生命周期。为了理解该漏洞,我们必须首先了解内核驱动程序与套接字交互以实现网络功能的一些基础知识。


一、前期知识

1、Windows 内核中的套接字 – Winsock 内核 (WSK)

WSK 是 Windows 套接字 API 的名称,驱动程序可以使用它直接从内核创建和使用套接字。

通常使用 WSK API 的方式是通过一组事件驱动的回调函数。实际上,一旦设置了套接字,应用程序就可以提供一个调度表,其中包含一组函数指针,这些函数指针将被调用以用于与套接字相关的事件。为了使应用程序能够通过这些回调保持其自己的状态,驱动程序还提供了一个上下文结构,以将其提供给每个回调,以便可以在连接的整个生命周期中跟踪状态。

2、raspptp.sys 和 WSK

既然我们已经了解了内核中套接字是如何交互的基础知识,那么让我们看看 raspptp.sys 驱动程序是如何使用 WSK 来实现 PPTP 协议的。

PPTP协议规定了两个socket连接;用于管理 VPN 连接的 TCP 套接字和用于发送和接收 VPN 网络数据的 GRE(通用路由封装)套接字。TCP 套接字是我们唯一关心触发此问题的套接字,因此让我们分解一下 raspptp.sys 如何使用 WSK 处理这些连接的生命周期

  1. raspptp.sys 中的WskOpenSocket函数负责创建一个新的侦听套接字。这个函数被传递了一个WSK_CLIENT_LISTEN_DISPATCH调度表,其中WskConnAcceptEvent函数指定为WskAcceptEven处理程序。这是处理套接字接受事件的回调,也就是新的传入连接。

  2. WskConnAcceptEvent函数负责新的客户端连接到服务器。此函数为新的客户端套接字分配一个新的上下文结构,并注册一个WSK_CLIENT_CONNECTION_DISPATCH调度表,其中包含指定的所有事件回调函数。WskConnReceiveEvent、WskConnDisconnectEvent、WskConnSendBacklogEvent分别用于接收、断开连接和发送事件。


3.一旦解决了accept事件,WskAcceptCompletion就会调用并触发回调( CtlConnectQueryCallback)来完成 PPTP控制连接的初始化和创建一个专门用于跟踪客户端 PPTP 控制连接状态的上下文结构。这是我们关心这个漏洞的主要对象。

PPTP Control 连接上下文结构由CtlAlloc函数分配。这个函数的一些伪代码是:

PptpCtlCtx *__fastcall CtlAlloc(PptpAdapterContext *AdapterCtx)
{
PptpAdapterContext *lpPptpAdapterCtx;
PptpCtlCtx *PptpCtlCtx;
PptpCtlCtx *lpPptpCtlCtx;
NDIS_HANDLE lpNDISMiniportHandle;
PDEVICE_OBJECT v6;
__int64 v7;
NDIS_HANDLE lpNDISMiniportHandle_1;
NDIS_HANDLE lpNDISMiniportHandle_2;
struct _NDIS_TIMER_CHARACTERISTICS TimerCharacteristics;

lpPptpAdapterCtx = AdapterCtx;
PptpCtlCtx = (PptpCtlCtx *)MyMemAlloc(0x290ui64, 'TPTP'); // Actual name of the allocator function in the raspptp.sys code
lpPptpCtlCtx = PptpCtlCtx;
if ( PptpCtlCtx )
{
memset(PptpCtlCtx, 0, 0x290ui64);
ReferenceAdapter(lpPptpAdapterCtx);
lpPptpCtlCtx->AllocTagPTPT = 'TPTP';
lpPptpCtlCtx->CtlMessageTypeToLength = (unsigned int *)&PptpCtlMessageTypeToSizeArray;
lpPptpCtlCtx->pPptpAdapterCtx = lpPptpAdapterCtx;
KeInitializeSpinLock(&lpPptpCtlCtx->CtlSpinLock);
lpPptpCtlCtx->CtlPptpWanEndpointsEntry.Blink = &lpPptpCtlCtx->CtlPptpWanEndpointsEntry;
lpPptpCtlCtx->CtlCallDoubleLinkedList.Blink = &lpPptpCtlCtx->CtlCallDoubleLinkedList;
lpPptpCtlCtx->CtlCallDoubleLinkedList.Flink = &lpPptpCtlCtx->CtlCallDoubleLinkedList;
lpPptpCtlCtx->CtlPptpWanEndpointsEntry.Flink = &lpPptpCtlCtx->CtlPptpWanEndpointsEntry;
lpPptpCtlCtx->CtlPacketDoublyLinkedList.Blink = &lpPptpCtlCtx->CtlPacketDoublyLinkedList;
lpPptpCtlCtx->CtlPacketDoublyLinkedList.Flink = &lpPptpCtlCtx->CtlPacketDoublyLinkedList;
lpNDISMiniportHandle = lpPptpAdapterCtx->MiniportNdisHandle;
TimerCharacteristics.TimerFunction = (PNDIS_TIMER_FUNCTION)CtlpEchoTimeout;
*(_DWORD *)&TimerCharacteristics.Header.Type = 0x180197;
TimerCharacteristics.AllocationTag = 'TMTP';
TimerCharacteristics.FunctionContext = lpPptpCtlCtx;
if ( NdisAllocateTimerObject(
lpNDISMiniportHandle,
&TimerCharacteristics,
&lpPptpCtlCtx->CtlEchoTimeoutNdisTimerHandle) )
{
...
}
else
{
lpNDISMiniportHandle_1 = lpPptpAdapterCtx->MiniportNdisHandle;
TimerCharacteristics.TimerFunction = (PNDIS_TIMER_FUNCTION)CtlpWaitTimeout;
if ( NdisAllocateTimerObject(
lpNDISMiniportHandle_1,
&TimerCharacteristics,
&lpPptpCtlCtx->CtlWaitTimeoutNdisTimerHandle) )
{
...
}
else
{
lpNDISMiniportHandle_2 = lpPptpAdapterCtx->MiniportNdisHandle;
TimerCharacteristics.TimerFunction = (PNDIS_TIMER_FUNCTION)CtlpStopTimeout;
if ( !NdisAllocateTimerObject(
lpNDISMiniportHandle_2,
&TimerCharacteristics,
&lpPptpCtlCtx->CtlStopTimeoutNdisTimerHandle) )
{
KeInitializeEvent(&lpPptpCtlCtx->CtlWaitTimeoutTriggered, NotificationEvent, 1u);
KeInitializeEvent(&lpPptpCtlCtx->CtlWaitTimeoutCancled, NotificationEvent, 1u);
lpPptpCtlCtx->CtlCtxReferenceCount = 1;// Set reference count to an initial value of one
lpPptpCtlCtx->fpCtlCtxFreeFn = (__int64)CtlFree;
ExInterlockedInsertTailList(
(PLIST_ENTRY)&lpPptpAdapterCtx->PptpWanEndpointsFlink,
&lpPptpCtlCtx->CtlPptpWanEndpointsEntry,
&lpPptpAdapterCtx->PptpAdapterSpinLock);
return lpPptpCtlCtx;
}
...
}
}
...
}
if...
return 0i64;
}
PptpCtlCtx *__fastcall CtlAlloc(PptpAdapterContext *AdapterCtx)
{
PptpAdapterContext *lpPptpAdapterCtx;
PptpCtlCtx *PptpCtlCtx;
PptpCtlCtx *lpPptpCtlCtx;
NDIS_HANDLE lpNDISMiniportHandle;
PDEVICE_OBJECT v6;
__int64 v7;
NDIS_HANDLE lpNDISMiniportHandle_1;
NDIS_HANDLE lpNDISMiniportHandle_2;
struct _NDIS_TIMER_CHARACTERISTICS TimerCharacteristics;

lpPptpAdapterCtx = AdapterCtx;
PptpCtlCtx = (PptpCtlCtx *)MyMemAlloc(0x290ui64, 'TPTP'); // Actual name of the allocator function in the raspptp.sys code
lpPptpCtlCtx = PptpCtlCtx;
if ( PptpCtlCtx )
{
memset(PptpCtlCtx, 0, 0x290ui64);
ReferenceAdapter(lpPptpAdapterCtx);
lpPptpCtlCtx->AllocTagPTPT = 'TPTP';
lpPptpCtlCtx->CtlMessageTypeToLength = (unsigned int *)&PptpCtlMessageTypeToSizeArray;
lpPptpCtlCtx->pPptpAdapterCtx = lpPptpAdapterCtx;
KeInitializeSpinLock(&lpPptpCtlCtx->CtlSpinLock);
lpPptpCtlCtx->CtlPptpWanEndpointsEntry.Blink = &lpPptpCtlCtx->CtlPptpWanEndpointsEntry;
lpPptpCtlCtx->CtlCallDoubleLinkedList.Blink = &lpPptpCtlCtx->CtlCallDoubleLinkedList;
lpPptpCtlCtx->CtlCallDoubleLinkedList.Flink = &lpPptpCtlCtx->CtlCallDoubleLinkedList;
lpPptpCtlCtx->CtlPptpWanEndpointsEntry.Flink = &lpPptpCtlCtx->CtlPptpWanEndpointsEntry;
lpPptpCtlCtx->CtlPacketDoublyLinkedList.Blink = &lpPptpCtlCtx->CtlPacketDoublyLinkedList;
lpPptpCtlCtx->CtlPacketDoublyLinkedList.Flink = &lpPptpCtlCtx->CtlPacketDoublyLinkedList;
lpNDISMiniportHandle = lpPptpAdapterCtx->MiniportNdisHandle;
TimerCharacteristics.TimerFunction = (PNDIS_TIMER_FUNCTION)CtlpEchoTimeout;
*(_DWORD *)&TimerCharacteristics.Header.Type = 0x180197;
TimerCharacteristics.AllocationTag = 'TMTP';
TimerCharacteristics.FunctionContext = lpPptpCtlCtx;
if ( NdisAllocateTimerObject(
lpNDISMiniportHandle,
&TimerCharacteristics,
&lpPptpCtlCtx->CtlEchoTimeoutNdisTimerHandle) )
{
...
}
else
{
lpNDISMiniportHandle_1 = lpPptpAdapterCtx->MiniportNdisHandle;
TimerCharacteristics.TimerFunction = (PNDIS_TIMER_FUNCTION)CtlpWaitTimeout;
if ( NdisAllocateTimerObject(
lpNDISMiniportHandle_1,
&TimerCharacteristics,
&lpPptpCtlCtx->CtlWaitTimeoutNdisTimerHandle) )
{
...
}
else
{
lpNDISMiniportHandle_2 = lpPptpAdapterCtx->MiniportNdisHandle;
TimerCharacteristics.TimerFunction = (PNDIS_TIMER_FUNCTION)CtlpStopTimeout;
if ( !NdisAllocateTimerObject(
lpNDISMiniportHandle_2,
&TimerCharacteristics,
&lpPptpCtlCtx->CtlStopTimeoutNdisTimerHandle) )
{
KeInitializeEvent(&lpPptpCtlCtx->CtlWaitTimeoutTriggered, NotificationEvent, 1u);
KeInitializeEvent(&lpPptpCtlCtx->CtlWaitTimeoutCancled, NotificationEvent, 1u);
lpPptpCtlCtx->CtlCtxReferenceCount = 1;// Set reference count to an initial value of one
lpPptpCtlCtx->fpCtlCtxFreeFn = (__int64)CtlFree;
ExInterlockedInsertTailList(
(PLIST_ENTRY)&lpPptpAdapterCtx->PptpWanEndpointsFlink,
&lpPptpCtlCtx->CtlPptpWanEndpointsEntry,
&lpPptpAdapterCtx->PptpAdapterSpinLock);
return lpPptpCtlCtx;
}
...
}
}
...
}
if...
return 0i64;
}

PptpCtlCtx *__fastcall CtlAlloc(PptpAdapterContext *AdapterCtx)

{

   PptpAdapterContext *lpPptpAdapterCtx;

   PptpCtlCtx *PptpCtlCtx;

   PptpCtlCtx *lpPptpCtlCtx;

   NDIS_HANDLE lpNDISMiniportHandle;

   PDEVICE_OBJECT v6;

   __int64 v7;

   NDIS_HANDLE lpNDISMiniportHandle_1;

   NDIS_HANDLE lpNDISMiniportHandle_2;

   struct _NDIS_TIMER_CHARACTERISTICS TimerCharacteristics;


   lpPptpAdapterCtx = AdapterCtx;

   PptpCtlCtx = (PptpCtlCtx *)MyMemAlloc(0x290ui64, 'TPTP'); // Actual name of the allocator function in the raspptp.sys code

   lpPptpCtlCtx = PptpCtlCtx;

   if( PptpCtlCtx )

   {

       memset(PptpCtlCtx, 0, 0x290ui64);

       ReferenceAdapter(lpPptpAdapterCtx);

       lpPptpCtlCtx->AllocTagPTPT = 'TPTP';

       lpPptpCtlCtx->CtlMessageTypeToLength = (unsigned int *)&PptpCtlMessageTypeToSizeArray;

       lpPptpCtlCtx->pPptpAdapterCtx = lpPptpAdapterCtx;

       KeInitializeSpinLock(&lpPptpCtlCtx->CtlSpinLock);

       lpPptpCtlCtx->CtlPptpWanEndpointsEntry.Blink = &lpPptpCtlCtx->CtlPptpWanEndpointsEntry;

       lpPptpCtlCtx->CtlCallDoubleLinkedList.Blink = &lpPptpCtlCtx->CtlCallDoubleLinkedList;

       lpPptpCtlCtx->CtlCallDoubleLinkedList.Flink = &lpPptpCtlCtx->CtlCallDoubleLinkedList;

       lpPptpCtlCtx->CtlPptpWanEndpointsEntry.Flink = &lpPptpCtlCtx->CtlPptpWanEndpointsEntry;

       lpPptpCtlCtx->CtlPacketDoublyLinkedList.Blink = &lpPptpCtlCtx->CtlPacketDoublyLinkedList;

       lpPptpCtlCtx->CtlPacketDoublyLinkedList.Flink = &lpPptpCtlCtx->CtlPacketDoublyLinkedList;

       lpNDISMiniportHandle = lpPptpAdapterCtx->MiniportNdisHandle;

       TimerCharacteristics.TimerFunction = (PNDIS_TIMER_FUNCTION)CtlpEchoTimeout;

       *(_DWORD *)&TimerCharacteristics.Header.Type = 0x180197;

       TimerCharacteristics.AllocationTag = 'TMTP';

       TimerCharacteristics.FunctionContext = lpPptpCtlCtx;

       if(NdisAllocateTimerObject(

           lpNDISMiniportHandle,

           &TimerCharacteristics,

           &lpPptpCtlCtx->CtlEchoTimeoutNdisTimerHandle))

       {

       ...

       }

       else

       {

           lpNDISMiniportHandle_1 = lpPptpAdapterCtx->MiniportNdisHandle;

     TimerCharacteristics.TimerFunction = (PNDIS_TIMER_FUNCTION)CtlpWaitTimeout;

           if(NdisAllocateTimerObject(

           lpNDISMiniportHandle_1,

           &TimerCharacteristics,

           &lpPptpCtlCtx->CtlWaitTimeoutNdisTimerHandle))

           {

               ...

           }

           else

           {

               lpNDISMiniportHandle_2 = lpPptpAdapterCtx->MiniportNdisHandle;

      TimerCharacteristics.TimerFunction = (PNDIS_TIMER_FUNCTION)CtlpStopTimeout;

               if( !NdisAllocateTimerObject(

               lpNDISMiniportHandle_2,

               &TimerCharacteristics,

               &lpPptpCtlCtx->CtlStopTimeoutNdisTimerHandle))

               {

      KeInitializeEvent(&lpPptpCtlCtx->CtlWaitTimeoutTriggered, NotificationEvent, 1u);

      KeInitializeEvent(&lpPptpCtlCtx->CtlWaitTimeoutCancled, NotificationEvent, 1u);

     lpPptpCtlCtx->CtlCtxReferenceCount = 1;// Set reference count to an initial value of one

                   lpPptpCtlCtx->fpCtlCtxFreeFn = (__int64)CtlFree;

                   ExInterlockedInsertTailList(

                   (PLIST_ENTRY)&lpPptpAdapterCtx->PptpWanEndpointsFlink,

                   &lpPptpCtlCtx->CtlPptpWanEndpointsEntry,

                   &lpPptpAdapterCtx->PptpAdapterSpinLock);

                   return lpPptpCtlCtx;

               }

               ...

           }

       }

       ...

   }

   if...

       return 0i64;

}

该结构需要注意的重要部分是结构成员CtlCtxReferenceCountCtlWaitTimeoutNdisTimerHandle结构成员。这个新的上下文结构存储在新客户端套接字的套接字上下文中,然后可以为与其绑定的套接字相关的所有事件引用。

然后我们关心的套接字上下文结构的唯一部分是以下字段:

00000008 ContextPtr dq ? ; PptpCtlCtx

00000010 ContextRecvCallback dq ? ; CtlReceiveCallback

00000018 ContextDisconnectCallback dq ? ; CtlDisconnectCallback

00000020 ContextConnectQueryCallback dq ? ; CtlConnectQueryCallback


  • PptpCtlCtx– 控制连接的 PPTP 特定上下文结构。

  • CtlReceiveCallback– PPTP 控制连接接收回调。

  • CtlDisconnectCallback– PPTP 控制连接断开回调。

  • CtlConnectQueryCallback– PPTP 控制连接查询(用于在新连接完成时获取客户端信息)回调。

3、raspptp.sys 对象生命周期

在深入研究漏洞之前,我们需要了解的最后一点背景信息是 raspptp 为给定套接字保持这些上下文结构活动的方式。在PptpCtlCtx结构的情况下,客户端套接字和PptpCtlCtx结构都有一个引用计数。

每次创建对任一对象的引用时,此引用计数都会增加。这些最初设置为1,当递减到0 对象时,通过调用存储在每个结构中的空闲回调来释放对象。这显然只有在代码记得在处理相应结构时正确地增加和减少引用计数并正确锁定跨多个线程的访问时才有效。

在 raspptp.sys 中,执行引用递增和递减功能的代码通常如下所示:

// Increment code

_InterlockedIncrement(&Ctx->ReferenceCount);


// Decrement Code

if(_InterlockedExchangeAdd(&Ctx->ReferenceCount, 0xFFFFFFFF) == 1)

   ((void(__fastcall *)(CtxType *))Ctx->fpFreeHandler)(Ctx);


正如您可能已经猜到的那样,我们正在查看的漏洞确实是由于对这些引用计数及其各自的锁的错误处理造成的,所以介绍完背景知识,让我们进入下一个环节!

二、漏洞

我们在释放漏洞后使用的第一部分是处理接收客户端连接的 PPTP 控制数据的代码。当 raspptp.sys 接收到新数据时,WSK 层将调度适当的事件回调。

raspptp.sys 为所有调用的套接字注册一个通用回调ReceiveData。此函数解析来自 WSK 的传入数据结构并将传入数据转发到客户端套接字上下文自己的接收数据回调。对于 PPTP 控制连接,此回调是CtlReceiveCallback函数。

ReceiveData调用此回调的函数部分具有以下伪代码。这个片段包括所有用于保护代码免受多线程访问问题的锁定和引用增量......

_InterlockedIncrement(&ClientCtx->ConnectionContextRefernceCount);

((void(__fastcall *)(PptpCtlCtx *, PptpCtlInputBufferCtx *, _NET_BUFFER_LIST *))ClientCtx->ContextRecvCallback)(ClientCtx->ContextPtr,lpCtlBufferCtx,NdisNetBuffer);


CtlReceiveCallback函数具有以下伪代码:

__int64 __fastcall CtlReceiveCallback(PptpCtlCtx *PptpCtlCtx, PptpCtlInputBufferCtx *PptpBufferCtx, _NET_BUFFER_LIST *InputBufferList)

{

   PptpCtlCtx *lpPptpCtlCx;

   PNET_BUFFER lpInputFirstNetBuffer;

   _NET_BUFFER_LIST *lpInputBufferList;

   ULONG NetBufferLength;

   PVOID NetDataBuffer;


   lpPptpCtlCx = PptpCtlCtx;

   lpInputFirstNetBuffer = InputBufferList->FirstNetBuffer;

   lpInputBufferList = InputBufferList;

   NetBufferLength = lpInputFirstNetBuffer->DataLength;

   NetDataBuffer = NdisGetDataBuffer(lpInputFirstNetBuffer, lpInputFirstNetBuffer->DataLength, 0i64, 1u, 0);

   if( NetDataBuffer )

       CtlpEngine(lpPptpCtlCx, (uchar *)NetDataBuffer, NetBufferLength);

   ReceiveDataComplete(lpPptpCtlCx->CtlWskClientSocketCtx, lpInputBufferList);

   return 0i64;

}

CtlpEngine函数是负责解析传入的PPTP控制数据的状态机。现在这两个部分缺少一段非常重要的代码,那就是任何形式的引用计数递增或PptpCtlCtx对象锁定!

这两个回调处理程序实际上都没有增加引用计数PptpCtlCtx或尝试锁定访问以表示它正在使用中;这可能是一个漏洞,因为如果在任何时候引用计数要减少,那么对象将被释放!但是,如果情况如此糟糕,为什么不是每个 PPTP 服务器都一直在崩溃?这个问题的答案是该CtlpEngine函数实际上正确地使用了引用计数。

这就是事情变得混乱的地方。假设 raspptp.sys 驱动程序完全是单线程的,那么这个实现将是 100% 安全的,因为控制连接的接收管道的任何部分都不会在没有首先执行增量来解决它的情况下减少对象引用计数。然而,实际上,raspptp.sys 不是单线程驱动程序。回顾PptpCtlCtx对象的初始化,有一个部分特别有趣。

TimerCharacteristics.FunctionContext = PptpCtlCtx;

TimerCharacteristics.TimerFunction = (PNDIS_TIMER_FUNCTION)CtlpWaitTimeout;

if(NdisAllocateTimerObject(

   lpNDISMiniportHandle_1,

   &TimerCharacteristics,

   &lpPptpCtlCtx->CtlWaitTimeoutNdisTimerHandle))

在这里我们可以看到一个Ndis定时器对象的分配。这些计时器的实际实现并不重要,但重要的是这些计时器在一个单独的线程上向 WSK 分派回调的线程分派ReceiveData回调。另一个有趣的点是两者都使用PptpCtlCtx结构作为他们的上下文结构。

那么这个定时器回调是做什么的,什么时候发生呢?设置定时器的代码如下:

NdisSetTimerObject(newClientCtlCtx->CtlWaitTimeoutNdisTimerHandle, (LARGE_INTEGER)-300000000i64, 0, 0i64);// 30 second timeout timer

我们可以看到设置了一个 30 秒的计时器触发器,当这 30 秒结束时,CtlpWaitTimeout回调被调用。这个 30 秒的计时器可以取消,但这仅在客户端与服务器执行 PPTP 控制握手时完成,因此假设我们在 30 秒后从未发送有效的握手,回调将被调度。但这有什么作用?

CtlpWaitTimeout函数用于处理定时器回调,它具有以下伪代码:

LONG __fastcall CtlpWaitTimeout(PVOID Handle, PptpCtlCtx *Context)

{

   PptpCtlCtx *lpCtlTimeoutEvent;


   lpCtlTimeoutEvent = Context;

   CtlpDeathTimeout(Context);

   returnKeSetEvent(&lpCtlTimeoutEvent->CtlWaitTimeoutTriggered, 0, 0);

}

正如我们所见,该函数主要用于调用名称怪异的CtlpDeathTimeout函数,该函数具有以下伪代码:

void __fastcall CtlpDeathTimeout(PptpCtlCtx *CtlCtx)

{

   PptpCtlCtx *lpCtlCtx;

   __int64 Unkown;

   CHAR *v3;

   char SockAddrString;


   lpCtlCtx = CtlCtx;

   memset(&SockAddrString, 0, 65ui64);

   if...

       CtlSetState(lpCtlCtx, CtlStateUnknown, Unkown, 0);

       CtlCleanup(lpCtlCtx, 0);

}

这就是事情变得更有趣的地方。CtlCleanup函数是负责启动拆除PPTP控制连接的过程。首先将Control连接的状态被设置为CtlStateUnknown,这意味着将阻止CtlpEngine函数处理任何进一步的控制连接数据(类型)。第二步是将一个运行类似名称的CtlpCleanup函数的任务推到一个属于raspptp.sys的后台工作线程上。

CtlpCleanup函数的末尾包含以下代码,这些代码对于我们能够在free之后触发使用非常有用,因为它总是运行在与CtlpEngine函数不同的线程上

result = (unsigned int)_InterlockedExchangeAdd(&lpCtlCtxToCleanup->CtlCtxReferenceCount, 0xFFFFFFFF);

if((_DWORD)result == 1)

   result = ((__int64(__fastcall *)(PptpCtlCtx *))lpCtlCtxToCleanup->fpCtlCtxFreeFn)(lpCtlCtxToCleanup);


它会减少PptpCtlCtx对象上的引用计数,更好的是,这个超时管道的任何部分都不会以阻止调用free函数的方式增加引用计数!

所以,从理论上讲,我们需要做的就是找到一些方法,让CtlpCleanup和CtlpEngine函数在不同的线程上同时运行,这样我们就可以在 Free 后引发CtlpEngine

然而,在我们过早庆祝之前,我们应该看一下实际释放PptpCtlCtx函数的函数,因为它是另一个回调函数。fpCtlCtxFreeFn属性是一个指向CtlFree函数的回调函数指针。这个函数也做了相当数量的分解,但我们关心的是下面的行

WskCloseSocketContextAndFreeSocket(CtlWskContext);/

lpCtlCtxToFree->CtlWskClientSocketCtx = 0i64;

...

ExFreePoolWithTag(lpCtlCtxToFree, 0);


这段代码中还有更多的复杂因素这会让事情变得更困难。调用WskCloseSocketContextAndFreeSocket实际上是在释放PptpCtlCtx结构之前关闭客户端套接字。这意味着当PptpCtlCtx结构被释放时,我们将不再能够向套接字发送新数据并触发对CtlpEngine的任何调用。然而,这并不意味着我们不能触发漏洞,因为如果数据已经被处理CtlpEngine关闭套接字时我们只需要希望呆在函数的线程足够自由发生在CtlFree和繁荣——我们有UAF。

现在我们有了一个很好的老式内核竞争条件,让我们看看如何尝试触发它!

三、竞争条件

像任何良好的竞争条件一样,这个条件包含许多活动部件和增加了触发它的复杂性,这使得触发它成为一项不平凡的任务,但它仍然是可能的!让我们来看看我们需要发生什么。

  1. 触发30 秒超时并最终运行 CtlCleanup,将一个CtlpCleanup任务推入后台工作线程队列。

  2. 后台工作线程唤醒并开始从它的任务队列中处理CtlpCleanup任务。

  3. 当CtlpEngineCtlpCleanup函数从工作线程释放底层的PptpCtlCtx结构时,CtlpEngine启动或当前正在处理 WSK 调度线程上的数据

  4. 坏事发生……

触发竞态条件

这个竞争条件需要考虑的主要部分是,为了在CtlpEngine解析循环中花费尽可能多的时间,我们可以发送给服务器的数据有哪些限制,以及我们可以在不取消超时的情况下做到这一点吗?

值得庆幸的是,如前所述,取消超时的唯一方法是执行PPTP控制连接握手,这在技术上意味着,只要我们不启动握手,就可以让CtlpEngine函数处理控制连接的任何其他部分。但是,CtlpEngine中的状态机需要进行握手才能启用控制连接的任何其他部分!

在握手发生之前,仍然可以部分有效地命中CtlpEngine状态机的一部分(而不会触发错误)。这是EchoRequest控件消息类型。现在,在握手发生之前,我们实际上无法输入对消息类型的正确处理,但我们可以做的是使用它在解析循环中遍历所有发送的数据,而不会触发解析错误。这有效地形成了一种在CtlpEngine函数中旋转而不取消超时的方法,这正是我们想要的。更好的是,当CtlCleanup函数设置了CtlStateUnknown状态时,这仍然是正确的。

不幸的是,我们在一个WSK接收数据事件回调触发器中可以处理的最大数据量被限制在一个TCP数据包中可以接收的最大数据量。理论上这是65,535字节,但由于以太网帧的大小限制为1,500字节,我们在一个请求中只能发送约1,450字节(1,500减去其他网络层帧的报头)的PPTP控制消息。这相当于每个回调事件触发大约有90个EchoRequest消息。对于现代CPU来说,在跳出CtlpEngine函数之前,这并不需要做太多的工作。

另一件需要考虑的事情是,我们如何知道竞态条件是成功的还是失败的?值得庆幸的是,在这方面,超时关闭的服务器套接字对我们有利,因为当服务器关闭套接字后,如果我们试图发送更多的数据,这将导致客户端套接字异常。一旦socket被关闭,我们就知道比赛已经结束,但我们不一定知道我们是否赢得了比赛。

有了这些考虑因素,我们如何触发漏洞?它实际上变成了一个简单的概念证明。有效地,我们只是连续发送EchoRequest PPTP控制帧在90帧突发到服务器,直到超时事件发生,然后我们希望我们已经赢得了比赛。

在人们打补丁之前,我们不会发布 PoC 代码,但是当 PoC 成功时,我们可能会在目标服务器上看到这样的东西:

CVE-2022-21972:windows Vpn漏洞

因为PptpCtlCtx结构是反初始化的,有很多指针和属性包含无效值,如果在接收事件处理代码的不同部分使用,将导致以非有趣的方式如空指针遵从的崩溃。这实际上是在蓝屏死机中发生的,但CtlpEngine函数仍然处理释放的PptpCtlCtx结构。

除了简单的蓝屏,我们还能利用这个漏洞做其他事情吗?


五、利用

由于 Windows 内核中针对内存损坏漏洞利用的缓解状态以及这种竞争条件的困难性质,实现对漏洞的有效利用并不容易,尤其是在寻求获得远程代码执行 (RCE) 时。然而,这并不意味着不可能这样做。

1、释放的内存

为了评估漏洞的可利用性,我们需要查看释放的内存包含了什么,以及它在 Windows 内核堆中的位置。在 windbg 中,我们可以使用该!pool命令获取有关将在 UaF 问题中释放的已分配块的一些信息。

ffff828b17e50d20 size: 2a0 previous size: 0 (Allocated) *PTPT

我们可以看到这里,释放的内存块的大小是0x2a0或672 字节。这一点很重要,因为它将我们置于可变大小的内核堆段的分配大小范围内。这个堆段非常适合在free利用之后使用,因为可变大小的堆还维护一个已释放块的空闲列表及其大小。当分配一个新块时,将搜索这个空闲列表,如果找到一个精确或更大大小匹配的块,则将其用于新分配。由于这是内核,内核的任何其他部分如果分配这个大小或类似大小的非分页池内存分配,最终也可能使用这个被释放的slot。

那么,我们需要什么才能开始利用这个问题呢?理想情况下,我们希望在内核中找到一些可以控制其内容的已分配对象,并分配0x2a0字节的大小。这将允许我们创建一个假PptpCtlCtx对象,然后我们可以使用它来控制CtlpEngine状态机代码。找到一个精确的大小匹配分配并不是我们可以为潜在的利用准备堆的唯一方法,但它肯定是最可靠的方法。

如果我们可以控制一个PptpCtlCtx对象,我们能做什么?从利用开发的角度来看,这个漏洞最强大的地方之一是位于PptpCtlCtx结构内部的回调函数。通常称为控制流保护 (CFG) 或 扩展流保护 (XFG) 的缓解措施将阻止我们能够破坏和使用这些带有任意可执行内核地址的回调指针。但是,raspptp.sys没有启用 CFG 和 XFG,这意味着我们可以将执行指向位于内核中的任何指令。这给了我们很多可以来进行利用。需要注意的是,我们可以在一次漏洞触发中使用的这些小工具的数量有限,这意味着我们可能需要多次触发漏洞与不同的设备来实现完全利用或者至少是这种情况在当前 Windows 内核上。

2、线程

分配一个对象来填充我们释放的slot和通过一个假PptpCtlCtx对象来控制内核执行,这听起来不错,但是有一个额外限制,我们只有短时间内访问CtlpEngine使用释放对象。我们不能使用正在处理CtlpEngine的同一个线程来分配对象来填充空槽,如果我们这样做,那将是线程从CtlpEngine返回之后此时,该漏洞将不再可利用。

这意味着我们需要假对象分配发生在一个单独的线程中,希望我们可以在脆弱的内核线程仍然存在时分配一个假对象并填充我们的假对象内容CtlpEngine,从而允许我们然后开始用状态机做坏事。所有这些听起来都需要尝试在相对较小的 CPU 窗口中完成,但这是有可能实现的。任何试图这样做的漏洞攻击的问题都有可靠性,因为失败的漏洞攻击有相当高的可能使目标机器崩溃,并且重新尝试漏洞攻击将是一个缓慢且容易检测到的过程。

3、本地权限提升与远程代码执行

在受影响的 Windows 内核版本上利用 LPE 的能力比它用于 RCE 更有可能成功。即 RCE 漏洞利用需要首先使用此漏洞或另一个漏洞泄漏有关内核的信息,然后才能使用任何可能的回调破坏使用。远程可访问的内核部分也少得多,这意味着要找到一种将伪PptpCtlCtx对象远程喷射到内核堆中的方法将很难实现。

LPE 是一个更可行的利用路由的另一个原因是 localhost 套接字或 127.0.0.1 允许由每个WSK REceive事件回调处理的数据远多于我们远程获得的 1,500 字节以太网帧。这大大增加了实现成功利用的大部分变量!

六、结论

蠕虫内核远程执行代码漏洞是现代操作系统中严重的漏洞。权力越大,责任越大。虽然此漏洞的影响可能是灾难性的,但成功利用且不被发现的漏洞的技巧不可低估。内存损坏继续成为一种越来越难以掌握的艺术形式,但肯定有一些人有能力和决心充分挖掘此漏洞潜力的人。由于这些原因,CVE-2022-21972 是一个对基于Internet连接的 Microsoft 的 VPN 基础设施构成非常真实威胁的漏洞。我们建议在所有环境中优先修补此漏洞。

时间线

  • 向 Microsoft 报告的漏洞 – 2021 年 10 月 29 日

  • 漏洞确认 – 2021 年 10 月 29 日

  • 确认漏洞 – 2021 年 11 月 11 日

  • 确认补丁发布日期 – 2021 年 11 月 12 日

  • 补丁发布 – 2022 年 5 月 10 日


原文始发于微信公众号(MicroPest):CVE-2022-21972:windows Vpn漏洞

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年5月15日01:07:28
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   CVE-2022-21972:windows Vpn漏洞https://cn-sec.com/archives/1007631.html

发表评论

匿名网友 填写信息