Windows UAF 漏洞CVE-2021-34486分析

Windows UAF 漏洞CVE-2021-34486分析

原创 信创安全实验室 山石网科安全技术研究院 2022-06-30 10:48

Windows事件跟踪 (ETW) 机制允许记录内核或应用程序定义的事件以进行调试。开发人员能够启动和停止事件跟踪会话,检测应用程序以提供跟踪事件,并通过调用 ETW 用户模式 Windows API 集来使用跟踪事件。最终的请求部分都是在内核 (ntoskrnl.exe) 模块中完成。由于该功能存在于ntoskrnl.exe,部分功能可以在沙箱中访问到,尤其受攻击者关注。


EtwpUpdatePeriodicCaptureState 函数中,存在UAF漏洞,攻击者能够可控地分配一个 0x30 字节的缓冲区,释放它并随后重用该内存来执行任意代码。

我们可以通过
NtTraceControl
 系统调用来触发
 EtwpUpdatePeriodicCaptureState 
函数,输入的buffer结构如下:

typedef struct _ETW_UPDATE_PERIODIC_CAPTURE_STATE

{

  ULONG     LoggerId;

  ULONG     DueTime;    //system time units (100-nanosecond intervals)

  ULONG     NumOfGuids;

  GUID Guids[ANYSIZE_ARRAY];

} ETW_UPDATE_PERIODIC_CAPTURE_STATE, * PETW_UPDATE_PERIODIC_CAPTURE_STATE;

触发UAF需要3个步骤,下面我们具体来分析:

第一个请求中,ntoskrnl执行了这些操作,首先通过用户传入的id来获取相应的

EtwpLoggerContext
对象,同时回去验证用户传入的Guid数组是否具有

0x80 访问权限。随后调用 

ExAllocateTimer
函数创建一个定时器,定时器超时的回调函数是

PeriodicCaptureStateTimerCallback
,回调函数的参数类型是

_WORK_QUEUE_ITEM
,你可以在msdn中找到具体定义,随后定时器被激活,等待超时时间到达后触发回调函数。

__int64 __fastcall EtwpUpdatePeriodicCaptureState(unsigned int LoggerId, unsigned int DueTime, unsigned __int16 NumOfGuids, GUID *Guids)

{

     …

      // 如果没有定时器存在则直接创建定时器

      if ( !LoggerContext_->ExTimerObject )

      {

         TimerContextInfo = (CONTEXTINFO *)ExAllocatePoolWithTag(NonPagedPoolNx, 0x30ui64, 'UwtE'); // 创建回调函数参数,类型为_WORK_QUEUE_ITEM

         TimerContextInfo_ = TimerContextInfo;

         if ( !TimerContextInfo )

            goto RETURN_ERROR_C0000017;

 

         TimerContextInfo->LoggerId = LoggerId;//InBuff1.LoggerId

         TimerContextInfo->Unknown = v22;

         TimerContextInfo->WorkItem.WorkerRoutine = SendCaptureStateNotificationsWorker;   // worker的回调函数

         TimerContextInfo->WorkItem.Parameter = TimerContextInfo;         // worker回调函数的参数,也就是SendCaptureStateNotificationsWorker的参数

         TimerContextInfo->WorkItem.List.Flink = 0i64;

         LoggerContext_->ExTimerObject = ExAllocateTimer((PEXT_CALLBACK)PeriodicCaptureStateTimerCallback, TimerContextInfo, 8u); // 保存定时器

      }

 

      ExTimerObject = (PEX_TIMER)LoggerContext_->ExTimerObject;

      LoggerContext_->DueTime = 0xFFFFFFFFFF676980ui64 * DueTime;

      ExSetTimer((ULONG_PTR)ExTimerObject);     // 超时时间以秒计算,为负数

      LODWORD(LoggerContext_->ExTimerState) = 1;

      goto RETURN_1;

     …

 

RETURN_1:

   if ( (_InterlockedExchangeAdd64(pLock, 0xFFFFFFFFFFFFFFFFui64) & 6) == 2 )

      ExfTryToWakePushLock(pLock);

   KeAbPostRelease((ULONG_PTR)pLock);

 

RETURN_2:

   EtwpReleaseLoggerContext(LoggerContext_, 0i64);

   return (unsigned int)res_EtwpCheckNotificationAccess;

}

最终在
PeriodicCaptureStateTimerCallback
函数中会去调用

ExQueueWorkItem
函数将
_WORK_QUEUE_ITEM
放入系统队列中,然后最后调用

SendCaptureStateNotificationsWorker
函数。

在第二个请求中,我们可以传入一些当前用户没有权限访问的Guid,这样就会触发如下流程:

__int64 __fastcall EtwpUpdatePeriodicCaptureState(unsigned int LoggerId, unsigned int DueTime, unsigned __int16 NumOfGuids, GUID *Guids)

 {

     …

     {

        …

 FREE_POOLS_AND_RESET:

        GuidsPool = (void *)LoggerContext_->GuidsPool;

        if ( GuidsPool )

        {

            ExFreePoolWithTag(GuidsPool, 0);

            LoggerContext->GuidsPool = 0i64;

            LOWORD(LoggerContext->NumOfGuids) = 0;

        }

     …

     }

 

     if ( (_DWORD)NumOfGuids_ )

     {

        while ( 1 )  // 循环判断用户传入的guid是否具有访问权限,如果有一个不满足就进入FREE_POOLS_AND_RESET分支

        {

            res_EtwpCheckNotificationAccess = EtwpCheckNotificationAccess(

                                                &Guids[v4].Data1,

                                                (__int64)&LoggerContext_->field_0[0x124]);

            if ( res_EtwpCheckNotificationAccess < 0 )

               break;

            if ( ++v4 >= (int)NumOfGuids_ )

               goto ALL_GUIDS_HAVE_NOTIFICATION_ACCESS_OK;

        }

        res_EtwpCheckNotificationAccess = 0xC0000022;

        v8 = 0;

        goto FREE_POOLS_AND_RESET;

     }

 ALL_GUIDS_HAVE_NOTIFICATION_ACCESS_OK:

     …

 }

void __fastcall SendCaptureStateNotificationsWorker(CONTEXTINFO *TimerContextInfo)

{

   …

   if ( TimerContextInfo )

   {

     LoggerContext = (LOGGERCONTEXT *)EtwpAcquireLoggerContextByLoggerId(

                                     TimerContextInfo->Unknown,

                                     LOWORD(TimerContextInfo->LoggerId),

                                     0);

     if ( LoggerContext )

     {

        pLock = &LoggerContext->Lock;

        ExAcquirePushLockExclusiveEx((ULONG_PTR)&LoggerContext->Lock, 0i64);

        LODWORD(LoggerContext__->ExTimerState) = 0;

        if ( *(_DWORD *)&LoggerContext__->field_0[336] )

        {

            // 在第二个请求中我们已经将这个数量设置为空,所以后面会进入LABEL_31分支

            NumOfGuids = LOWORD(LoggerContext__->NumOfGuids);

 

            if ( (_WORD)NumOfGuids )

            {

            …

                if ( (int)EtwpBuildNotificationPacket(v10, v23, v15, &v19) >= 0 )

                {

                     EtwpSendDataBlock(v12, v19);

                     EtwpUnreferenceDataBlock(v19);

                }

                …

                if ( LOWORD(LoggerContext__->NumOfGuids) && !LODWORD(LoggerContext__->ExTimerState) )

                {

                   ExSetTimer(LoggerContext__->ExTimer);

                   LODWORD(LoggerContext__->ExTimerState) = 1;

                   v2 = 1;

                }

                …

            …

            }

        }

 

        if ( (_InterlockedExchangeAdd64(pLock, 0xFFFFFFFFFFFFFFFFui64) & 6) == 2 )

            ExfTryToWakePushLock(pLock);

        KeAbPostRelease((ULONG_PTR)pLock);

        EtwpReleaseLoggerContext(LoggerContext__, 0i64);

        if ( v2 )          // 这里没有进入上面分支所以不会设置为0

            return;

        goto LABEL_31;     // 

     }

   }

   // 最终释放了之前的WORK_QUEUE_ITEM参数

LABEL_31:

   ExFreePoolWithTag(TimerContextInfo, 0);

}

 

在我们第一步的syscall创建的定时器超时前,在第二个syscall调用操作中我们将
LoggerContext->NumOfGuids 
设置为0,导致回调函数超时的时候释放了
WORK_QUEUE_ITEM 

在第三步的操作中,我们还需要再次重复第一步的操作,由于定时器已经存在,在判断
LoggerContext_->ExTimerObject 
不为空后,不会再创建
WORK_QUEUE_ITEM
对象了,所以之前第一步创建的参数仍然还会被再次使用,而这个时候其实回调函数的参数已经是一个被释放的内存了,不应该被再次引用,后面的一系列操作就会导致UAF的发生。

  • 漏洞修复

这个漏洞可以认为是设计不当而导致的,一开始以为是定时器使用不当导致的,但是看过分析后发现没有那么简单,大概率是开发人员对定时器的操作编程理解不够透彻,在传入定时器超时的参数中不应该使用一个生命周期不明确的对象,必须对此类对象的使用格外的小心,猜测修复的逻辑中也要对这里的逻辑进行重新设计,后来看了修复的代码也差不多能验证自己的猜想,看看修复后的代码,我们可以看到传入定时器回调函数的参数都发生了变化,变成了
LoggerContext
,没有传递
CallbackContext
,这样这个对象就不会存在释放后重用了,同时也没有再对
LoggerContext->NumOfGuids
进行操作:

__int64 __fastcall EtwpUpdatePeriodicCaptureState(unsigned int LoggerId, unsigned int DueTime, unsigned __int16 NumOfGuids, GUID *Guids)

{

   …

   ExAcquirePushLockExclusiveEx(EtwpLoggerContext1 + 688, 0i64);

   CallbackContext1 = *(_QWORD *)(EtwpLoggerContext1 + 1080);        // 取出之前创建的Context

   if ( CallbackContext1 )                                           // 如果存在了CallbackContext则进入

      goto LABEL_31;

   if ( !(_WORD)v4 )

   {

      LABEL_24:

      if ( (_InterlockedExchangeAdd64((volatile signed __int64 *)(EtwpLoggerContext1 + 688), 0xFFFFFFFFFFFFFFFFui64) & 6) == 2)

        ExfTryToWakePushLock(EtwpLoggerContext1 + 688);

      KeAbPostRelease(EtwpLoggerContext1 + 688);

      goto LABEL_27;

   }

 

   CallbackContext = ExAllocatePool2(64i64, 72i64, 0x55777445i64);

   // 设置LoggerContext+1080位置为创建的CallbackContext

   *(_QWORD *)(EtwpLoggerContext1 + 1080) = CallbackContext;

   CallbackContext1 = CallbackContext;

 

   …

   Pool2 = (void *)ExAllocatePool2(256i64, 16 * v4, 0x55777445i64);

   *(_QWORD *)(CallbackContext1 + 24) = Pool2;

   if ( Pool2 )

   {

      *(_WORD *)(CallbackContext1 + 16) = v4;

      memmove(Pool2, (const void *)InputBuffer_kernel_C, 16 * v4);

      if ( !*(_QWORD *)(CallbackContext1 + 8) )

      {

         // 回调函数的参数变成了LoggerContext

         Timer = ExAllocateTimer((__int64)PeriodicCaptureStateTimerCallback, EtwpLoggerContext1, 8u);

         *(_QWORD *)(CallbackContext1 + 8) = Timer;

         if ( !Timer )

         {

            ExFreePoolWithTag(*(PVOID *)(CallbackContext1 + 24), 0);

            *(_QWORD *)(CallbackContext1 + 24) = 0i64;

            *(_WORD *)(CallbackContext1 + 16) = 0;

            goto LABEL_11;

         }

         *(_QWORD *)(CallbackContext1 + 56) = EtwpLoggerContext1;

         *(_QWORD *)(CallbackContext1 + 48) = SendCaptureStateNotificationsWorker;

         *(_QWORD *)(CallbackContext1 + 32) = 0i64;         // WORK_QUEUE_ITEM 的开始位置

      }

      *((_QWORD *)&v21 + 1) = 0xFFFFFFFFFFFFFFFFui64;

      v17 = *(_QWORD *)(CallbackContext1 + 8);

      v18 = 0xFFFFFFFFFF676980ui64 * a2;

      *(_QWORD *)CallbackContext1 = v18;

      ExSetTimer(v17, v18, 0i64, (__int64)&v21);

      *(_DWORD *)(CallbackContext1 + 64) = 1;

      goto LABEL_24;

   }

   …

}

 

 

void __fastcall PeriodicCaptureStateTimerCallback(__int64 Timer, __int64 EtwpLoggerContext)

{

  if ( ExAcquireRundownProtectionCacheAwareEx(

         *(PEX_RUNDOWN_REF_CACHE_AWARE *)(*(_QWORD *)(*(_QWORD *)(EtwpLoggerContext + 1096) + 448i64)

                                        + 8i64 * *(unsigned int *)EtwpLoggerContext),

         1u) )

  {

    // EtwpLoggerContext + 1080 就是CallbackContext,+32位置就是WORK_QUEUE_ITEM 的开始位置

    ExQueueWorkItem((PWORK_QUEUE_ITEM)(*(_QWORD *)(EtwpLoggerContext + 1080) + 32i64), NormalWorkQueue);

  }

}

– ## 参考链接

https://www.pixiepointsecurity.com/blog/advisory-cve-2021-34486.html