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