windows file_exists 返回false_Windows内核逻辑漏洞:IO管理器访问模式不匹配

本文深入探讨了Windows内核中的一个逻辑漏洞,该漏洞可能导致本地权限提升。问题源于IO管理器和驱动程序在处理对象管理器符号链接时的访问模式检查错误。通过分析代码,揭示了如何通过IoCreateFile和IoCreateFileEx的选项标志组合触发漏洞,并讨论了潜在的攻击场景和修复措施。文章强调了正确理解和处理内核模式访问检查的重要性,以及与微软安全响应中心的合作过程。
摘要由CSDN通过智能技术生成

20d3808529dbcdda857613fc1d914134.gif

8114a11d57e36b9076fbc1387f05f954.png概述

本文深入介绍了Windows内核中一个有趣的逻辑漏洞,以及我与Microsoft的合作伙伴共同修复的过程。如果内核和驱动程序的开发人员在访问设备对象时未考虑IO管理器的操作方式,那么漏洞所产生的最大影响将会是本地权限提升。本篇文章重点说明了我发现漏洞的过程,并详细分析了技术背景。关于进一步调查的更多信息、修复方式和如何避免使用漏洞类编写新代码,可以参考MSRC的博客文章。

8114a11d57e36b9076fbc1387f05f954.png技术背景

我在尝试对Issue#779进行漏洞利用时,偶然发现了这个存在漏洞的类。该问题是一个文件TOCTOU绕过了自定义字体加载的缓解策略。在Windows 10中,引入了缓解策略,以限制可利用字体内存损坏漏洞的影响。通常,可以轻松使用文件和对象管理器符号链接的组合,来利用文件TOCTOU问题。使用符号链接的漏洞利用过程可以以普通用户的身份进行,而不会位于沙箱中。我并没有在这个影响较小的问题上花费太多的时间,并没有使用符号链接,而是使用了Shadow Object Directories来实现漏洞利用。Microsoft将该漏洞编号为CVE-2016-3219。我在我的待办清单中添加了一个意外行为的标注,以便日后跟进。

时间过去了一年,我决定回过头去研究一下是否存在更加深入的意外行为。失败的代码类似于以下内容:

HANDLE OpenFilePath(LPCWSTR pwzPath) {

 UNICODE_STRING Path;

 OBJECT_ATTRIBUTES ObjectAttributes;

 HANDLE FileHandle;

 NTSTATUS status;

 RtlInitUnicodeString(&Path, pwzPath);

 InitializeObjectAttributes(&ObjectAttributes,

                            &Path,

                            OBJ_KERNEL_HANDLE | OBJ_CASE_INSENSITIVE);

 status = IoCreateFile(

              &FileHandle,

              GENERIC_READ,

              &ObjectAttributes,

              // ...

              FILE_OPEN,

              FILE_NON_DIRECTORY_FILE,

              // ...

              IO_NO_PARAMETER_CHECKING | IO_FORCE_ACCESS_CHECK);

 if (NT_ERROR(status))

   return NULL;

 return FileHandle;

}

当该代码尝试在路径中打开包含对象管理器符号链接的文件时,对IoCreateFile的调用因STATUS_OBJECT_NAME_NOT_FOUND而失败。通过进一步的挖掘,我发现了ObpParseSymbolicLink中错误的来源,如下所示:

NTSTATUS ObpParseSymbolicLink(POBJECT_SYMBOLIC_LINK Object,

                             PACCESS_STATE AccessState,

                             KPROCESSOR_MODE AccessMode) {

 if (Object->Flags & SANDBOX_FLAG

   && !RtlIsSandboxedToken(AccessState->SubjectSecurityContext, AccessMode))

   return STATUS_OBJECT_NAME_NOT_FOUND;

 // ...

}

在这里,失败的检查是Microsoft在Windows 10中引入的符号链接缓解的一部分。我在沙箱中创建了符号链接,它将在对象的结构中设置SANDBOX_FLAG。在打开字体文件时,解析符号链接的过程将会进行此项检查。在设置沙箱标志后,内核还会调用RtlIsSandboxedToken来确定调用者是否仍然位于沙箱中。由于打开字体文件的调用是在沙箱进程中,因此线程RtlIsSandboxedToken应该会返回TRUE,函数将会继续执行。相反,实际上它返回的是FALSE,这使得内核认为调用是来自于权限更高的进程,并返回STATUS_OBJECT_NAME_NOT_FOUND,从而缓解任何可能的漏洞利用。

在这时,我明白了我的漏洞利用是在哪里失败的,但并不清楚为什么会失败。具体而言,我不清楚为什么RtlIsSandboxToken会返回FALSE。深入研究这个函数后,我得到了一个重要的想法:

BOOLEAN RtlIsSandboxedToken(PSECURITY_SUBJECT_CONTEXT SubjectSecurityContext,

                           KPROCESSOR_MODE AccessMode) {

 NTSTATUS AccessStatus;

 ACCESS_MASK GrantedAccess;

 if (AccessMode == KernelMode)

   return FALSE;

 if (SeAccessCheck(

        SeMediumDaclSd,

        SubjectSecurityContext,

        FALSE,

        READ_CONTROL,

        0,

        NULL,

        &RtlpRestrictedMapping,

        AccessMode,

        &GrantedAccess,

        &AccessStatus)) {

   return FALSE;

 }

 return TRUE;

}

其中,有一个重要的参数是AccessMode,它的类型为KPROCESSOR_MODE,可以设置为UserMode或KernelMode这两个值中的其中一个。如果AccessMode参数设置为值KernelMode,则该函数将自动返回FALSE,表示当前调用者不在沙箱中。在内核调试器中,我们在这个函数中设置断点,以进行确认。当从我的漏洞利用中调用时,AccessMode被设置为KernelMode。那么,RtlIsSandboxToken为什么会返回TRUE呢?为了理解内核的运行方式,我们还需要更加深入地了解AccessMode参数所代表的内容。

8114a11d57e36b9076fbc1387f05f954.png先前访问模式

Windows中的每个线程都具有与之关联的先前访问模式(Previous Access Mode)。这一先前访问模式存储在KTHREAD结构的PreviousMode成员之中。第三方使用ExGetPreviousMode访问该成员,并返回KPROCESSOR_MODE类型。如果用户模式线程由于系统调用转换而正在运行内核代码,则会将先前访问模式设置为UserMode。作为示例,下图展示了通过NTDLL中的系统调用调度存根(System Call Dispatch Stub)从用户模式应用程序到系统调用NtOpenFile的调用。

92154e519905a584a0d91a60af9b2692.png

请注意,即使代码在内核内存空间中的NtOpenFile系统调用内执行,先前模式也始终设置为UserMode。相反,如果线程是系统线程(在系统进程中),或者发生内核模式的系统调用转换,那么将设置为KernelMode。下图展示了设备驱动程序(已在内核模式中运行)调用ZwOpenFile系统调用时的转换,将导致执行NtOpenFile。

b22761d876b47173a0d3aef1ab813908.png

在图中,用户模式应用程序调用设备驱动程序内的函数,例如使用NtFsControlFile系统调用。在调用设备驱动程序期间,先前访问模式为UserMode。但是,如果设备驱动程序调用ZwOpenFile,那么内核将模拟系统调用转换,这会导致先前访问模式被更改为KernelMode,并且执行NtOpenFile系统调用代码。

从安全角度来看,先前访问模式将影响内核中两个重要且完全不同的安全检查——安全访问检查(SecAC)和内存访问检查(MemAC)。SecAC用于调用安全引用监控器公开的API,例如:SeAccessCheck或SePrivilegeCheck。这些安全API用于确定调用者是否有权访问权限。通常,API采用AccessMode参数,如果该参数为KernelMode,那么访问检查将会自动传递,如果用户通常无法访问该资源,那么可能表明其中存在着安全漏洞。我们已经在RtlIsSandboxToken中看到了这个用例,API显式检查了AccessMode为KernelMode并返回FALSE。即使没有快捷方式,通过将KernelMode传递给SeAccessCheck,无论调用者的访问令牌如何,调用都将成功,并且RtlIsSandboxToken将会返回FALSE。

MemAC用于确保用户应用程序无法将指针传递到内核地址的位置。如果AccessMode是UserMode,那么将验证传递给系统调用/操作的所有内存地址是否小于MmUserProbeAddress或者是否经过例如ProbeForRead/ProbeForWrite这样的函数。同样,如果这一检查不正确,则可能发生权限提升的问题,因为用户可能会欺骗内核读取代码或写入特权内核内存位置。请注意,并非所有的内核API都执行MemAC,举例来说,SeAccessCheck假设调用者已经检查了参数,AccessMode参数仅用于确定是否绕过安全检查。

在线程中存储先前的访问模式将会产生问题,因为无法区分内核API的SecAC和MemAC。API可能会故意禁用SecAC并意外禁用MemAC,从而导致安全问题,反之亦然。接下来,我们更加详细的了解一下IO管理器如何尝试解决这种不匹配的访问检查问题。

8114a11d57e36b9076fbc1387f05f954.pngIO管理器访问检查

在IO管理器中,公开了两个主要的API组,用于直接访问文件。第一个API是系统调用NtCreateFile/ZwCreateFile或NtOpenFile/ZwOpenFile。系统调用主要提供给用户模式应用程序使用,但如果需要的话,也可以从内核模式调用。其他API仅针对内核模式调用方IoCreateFile和IoCreateFileEx公开。

如果比较两个主要API的实现,我们可以发现,它们是围绕内部函数IopCreateFile的简单转发包装器。默认情况下,IopCreateFile使用当前线程的先前模式来确定是否执行MemAC和SecAC。例如,当从用户模式进程通过NtCreateFile调用IopCreateFile时,由于先前模式是UserMode,所以内核将执行MemAC和SecAC。如果内核模式调用ZwCreateFile,则先前模式被设置为KernelMode,并且SecAC和MemAC都将被禁用。

IoCreateFile只能从内核模式代码调用,并且不涉及系统调用转换,因此任何调用都将使用线程上设置的任何先前模式。如果从先前模式设置为UserMode的线程调用IoCreateFile,那么将执行SecAC和MemAC。强制执行MemAC会发生严重问题,因为这就意味着内核代码无法将内核模式指针传递给IoCreateFile,这会使得API非常难以使用。但是,IoCreateFile的调用者不能简单地将线程的先前模式修改为KernelMode来解决这一问题,因为SecAC将被禁用。

IoCreateFile通过指定可以通过Options参数传递的特殊标志来解决这一问题。该参数将转发到IopCreateFile,但不通过NtCreateFile系统调用公开。回到我们的问题上,WIN32K正在调用IoCreateFile,并且传递可选标志IO_NO_PARAMETER_CHECKING (INPC)和IO_FORCE_ACCESS_CHECK (IFAC)。

根据文档,INPC记录为:

2e59f6b598627897130a5e432386c258.png

[如果指定],在尝试发出创建请求之前,不应验证此调用的参数。驱动程序编写者应该谨慎使用此标志,因为某些无效参数可能会导致系统故障。有关更多信息,请参见备注。

2910f58125b40162b6c8f1c39f70cac8.png

在备注部分,做出了更进一步的解释:

2e59f6b598627897130a5e432386c258.png

如果驱动程序代表用户模式应用程序启动的操作发出内核模式创建请求,则Options的IO_NO_PARAMETER_CHECKING标志可能会非常有用。由于请求发生在用户模式上下文中,因此I/O管理器默认会探测提供的参数值。如果参数是内核模式地址,就可能导致访问冲突。该标志使得调用方可以覆盖此默认行为,并避免访问冲突。

2910f58125b40162b6c8f1c39f70cac8.png

这样一来,就使其目的明确,它禁用MemAC,允许内核代码将指针作为函数参数传递到内核内存中。其副作用是,它还会禁用大多数参数验证,例如检查不兼容的标记组合。其中,有一个单独的、没有正确记录的标志IO_CHECK_CREATE_PARAMETERS,它只返回参数标志检查,而不返回MemAC。

另一方面,IFAC将其记录为:

2e59f6b598627897130a5e432386c258.png

I/O管理器必须根据文件的安全描述符检查创建请求。

2910f58125b40162b6c8f1c39f70cac8.png

这意味着,该标志重新启用了SecAC。如果调用者是先前模式设置为KernelMode的系统线程,并且假设我们从UserMode调用,为什么还需要重新启用SecAC呢?正如我们在IopCreateFile的一些简化代码中所看到的,在这里可以找到帮助我们理解意外行为的一些蛛丝马迹。

NTSTATUS IopCreateFile(PHANDLE FileHandle, ACCESS_MASK DesiredAccess,

                      POBJECT_ATTRIBUTES ObjectAttributes, ...,

                      ULONG Options) {

 KPROCESSOR_MODE AccessMode;

 if (Options & IO_NO_PARAMETER_CHECKING) {

   AccessMode = KernelMode;

 } else {

   AccessMode = KeGetCurrentThread()->PreviousMode;

 }

 FILE_PARSE_CONTEXT ParseContext = {};

 // Initialize other values

 ParseContext->Options = Options;

 return ObOpenObjectByName(

                 ObjectAttributes,

                 IoFileObjectType,

                 AccessMode,

                 NULL,

                 DesiredAccess,

                 &ParseContext,

                 &FileHandle);

}

该代码表明,如果指定了INPC,则所有后续调用的AccessMode都将设置为KernelMode。因此,指定该选项不仅会禁用MemAC,还会禁用SecAC。值得注意的是,线程的先前模式没有改变,仍然是传递给ObOpenObjectByName的AccessMode值。IopCreateFile将指针检查交给对象管理器,因此它实现此目的的唯一方法是终止所有检查。最重要的是,这一过程中没有检查IFAC,它只在解析上下文建构中传递,这也是IO管理器要处理的另外一部分。

然而,还并没有结束,实际上也可以调用ZwCreateFile,并在OBJECT_ATTRIBUTES结构中传递特殊标志OBJ_FORCE_ACCESS_CHECK(OFAC),确保执行访问检查。在这里,先前的访问模式将设置为KernelMode。由于我们无法通过ZwCreateFile传入IFAC,并且IopCreateFile中没有检查OFAC标志,因此它必须位于ObOpenObjectByName中。实际上,这一过程要更加复杂一些,首先根据传递给ObOpenObjectByName的AccessMode处理所有参数,然后调用ObpLookupObjectName检查OFAC标志,如果设置了,就会将AccessMode强制改回UserMode。

我们现在终于可以理解为什么打开字体文件会出现意外行为。解析符号链接发生在对象管理器内,而不是IO管理器,因此并不清楚IFAC标志是什么。IopCreateFile通知对象管理器执行所有检查,就如同之前的访问模式是KernelMode一样,这是传递给ObpParseSymbolicLink的值,它将会传递到RtlIsSandboxToken,从而表明它没有在沙盒进程中运行。但是,一旦文件被实际打开,IFAC标志就会介入,并确保持续对文件执行SecAC。如果调用方也指定了OFAC,那么符号链接将会起到作用,因为在强制执行UserMode的查找操作期间,将会进行解析。

这本身就是一个有趣的结果,基本上,在对象管理器解析操作期间调用的任何操作,都信任AccessMode的值,并将禁用安全检查,除非指定了OFAC。但是,这并不是本文所要阐述的漏洞,因此我们需要更加深入地了解IFAC在IO管理器中的工作原理。

8114a11d57e36b9076fbc1387f05f954.pngIO设备解析

一旦在对象管理器命名空间中找到命名设备对象,对象管理器就会负责打开文件。对象管理器将查找设备类型的解析函数,即IopParseDevice,然后传递它知道的所有信息。其中包括:AssessMode值(已经被设置为KernelMode)、剩余的解析路径和包含Options参数的解析上下文缓冲区。IopParseDevice函数对其自身进行一些安全检查,例如检查设备遍历、分配新的IO请求包(IRP)并调用负责设备对象的驱动程序。

2f60da3e418a3e0a4c5b4a7b626dc96e.png

IRP结构中包含RequestorMode字段,该字段反映文件操作的AccessMode。具有RequestorMode字段的原因是IRP可以异步调度。处理IO操作的线程可能不是启动IO操作的线程。现在,大家可能猜测,这是不是IFAC发挥作用的地方,IO管理器是否会将RequestorMode设置为UserMode?如果在使用INPC从IoCreateFile访问时,实际在内核驱动程序中会检查这一项,但我们发现,这个字段仍然被设置为KernelMode,所以这并不是真正的原因。

正在执行的操作类型和操作的特定参数,在紧邻IRP结构之后的IO栈位置结构中传递。如果打开文件,主要操作类型是IRP_MJ_CREATE,将会使用IO_STACK_LOCATION结构的Create union字段。这是IFAC进入的位置,如果将标志指定给IopCreateFile,则在IO栈位置的Flags参数中,将设置一个新的标志SL_FORCE_ACCESS_CHECK(SFAC)。文件系统驱动程序将决定是否验证此标志,而不是依赖于将UserorMode设置为UserMode。NTFS驱动程序清楚这一点,并且包含如下代码:

KPROCESSOR_MODE NtfsEffectiveMode(PIRP Irp) {

 PIO_STACK_LOCATION loc = IoGetCurrentIrpStackLocation(Irp);

 if (loc->MajorOperation == IRP_MJ_CREATE

     && loc->Flags & SL_FORCE_ACCESS_CHECK) {

   return UserMode;

 }

 else {

   return Irp->RequestorMode;

 }

}

NtfsEffectiveMode可以由任何执行安全相关功能的操作调用。只要IFAC标志通过,即使调用者处于内核模式,它也可以确保仍然执行SecAC。NTFS文件系统驱动程序是Windows操作系统的基本组成部分,并且与IO管理器具有比较密切的交互,因此这并不奇怪。但是,在Windows上,所有驱动程序都是文件系统驱动程序,即使它们没有显式地实现文件系统。

我觉得,我们可以找出有多少微软官方或第三方驱动程序进行了正确的检查,并找出是否有驱动程序仅仅是信任RequestorMode并根据它做出安全的决策?

8114a11d57e36b9076fbc1387f05f954.png定义漏洞类

最后,我们来定义漏洞类。为了存在权限提升漏洞,需要有两个单独的组件。

1. 内核模式启动器(调用IoCreateFile或IoCreateFileEx的代码),用于设置INPC和IFAC标志,但不设置OFAC。可能位于驱动程序或内核本身。

2. 易受攻击的接收器在处理IRP_MJ_CREATE期间,使用RequestorMode进行安全性决策,但不会检查SFAC的标志。

7df6dc2b5949d0f03c2f2f88b3e5604d.png

下表总结了调用线程的先前模式设置为UserMode时的API。该表中包含输入选项的状态、INPC、IFAC、OFAC以及相应IRP的RequestorMode和SFAC标志。我已经突出表示调用是否属于一个有用的启动器。

96a900c3f4627376a9a268b6afc93c82.png

值得注意的是,任何未传递IFAC的调用都可能受到特权文件访问漏洞的影响,因为没有生成SFAC标志,所以即使是NTFS也不会执行安全检查。IoCreateFile中还有其他类似的功能,例如FltCreateFileEx,它们在特殊情况下使用,但它们都具有相似的属性。另外还有一点需要注意,表中的规则与IoCreateFile略有不同。尽管没有记录,但IoCreateFileEx总是会将INPC选项传递给IopCreateFile,因此,除非指定了OFAC标志,否则它将始终在先前访问模式设置为KernelMode的情况下运行其操作。

我们希望能有一个理想的启动器,可以从用户打开任意路径,并完全控制IoCreateFile的所有参数,将打开的文件句柄返回到用户模式的启动器。但是,由于接收器的不同,所以可能我们不需要完全控制它。

接收器可以在接收IRP时执行许多操作。文件系统驱动程序的常见方法是解析其余的文件名,并根据这一内容执行进一步的操作,例如打开另一个文件。另一种可能性是解析扩展属性(EA)块,并基于此执行某些操作。可能只是打开设备对象通常需要访问检查,其中RequestorMode到KernelMode的设置将会被绕过。

8114a11d57e36b9076fbc1387f05f954.png示例

下面是我发现的一些启动器和接收器的例子。这是基于Windows 10 1709的代码,当前最新版本(1809)与之相隔了两个版本,但1709版本的许多示例仍然存在于最新版本的Windows,包括Windows 7和8之中。下面的所有示例都是Microsoft的代码,因此第三方开发人员可能对这些行为知之甚少。

为了发现这些示例,我没有使用任何特殊的静态分析工具,只是采用手动搜索的方式。我对Microsoft开展了更加深入的调查。

1. 接收器

寻找接收器比寻找启动器可能要困难得多,因为没有导入的搜索功能可以做出一些清晰的提示。相反,我寻找导入IoCreateDevice的驱动程序,以确保驱动程序暴露了某种设备。然后,我将驱动程序导入的API过滤为采用显式AccessMode参数的API,例如SeAccessCheck或ObReferenceObjectByHandle。当然,这并没有真正限制驱动程序的数量,所以我不得不人工分析看起来最有趣的驱动程序。在我的分析过程中,我发现“真正的”文件系统驱动程序(例如:NTFS和FAT)似乎总会完成正确的工作。

1.1 WS2IFSL

尽管该驱动程序并非始终启用,但它用来创建文件对象,该文件对象使用APC将读写请求传递给用户模式应用程序。在创建新对象时,我们可以指定EA,其中包含用于创建Socket或Process文件的信息。APC根据EA中的信息在CreateProcessFile函数中设置。驱动程序使用RequestorMode,而不会进行任何进一步的检查,这将允许回调APC在内核模式下执行。创建进程文件时,会将句柄传递给线程,该线程会以THREAD_SET_CONTEXT访问权限打开,以便和APC一起使用。设置KernelMode允许使用内核句柄来调用ObReferenceObjectByHandle,但是由于线程必须在调用进程中,所以它并没有给我们带来太大的价值。

NTSTATUS DispatchCreate(DEVICE_OBJECT* DeviceObject, PIRP Irp) {

 PFILE_FULL_EA_INFORMATION ea = Irp->AssociatedIrp.SystemBuffer;

 PIO_STACK_LOCATION loc = IoGetCurrentIrpStackLocation(Irp);

 if (ea->EaNameLength != 7)

   return STATUS_INVALID_PARAMETER;

 if (!memcmp(ea->EaName, "NifsSct", 8))

   return CreateSocketFile(lock->FileObject, Irp->RequestorMode, ea);

 if (!memcmp(ea->EaName, "NifsPvd", 8))

   return CreateProcessFile(lock->FileObject, Irp->RequestorMode, ea);

 // ...

}

如果要进行漏洞利用,兼容的启动器必须能够给进程文件提供EA。然后,需要创建一个引用该进程文件的套接字文件,并且必须执行读写操作以强制执行APC。在Windows 10的最新版本中,我们还要考虑SMEP和内核CFG。如果我们将APC例程指向用户模式地址,那么内核将在KernelMode中执行APC时进行漏洞检查,如下图所示,我将使用自定义启动器来设置WS2IFSL。

332e9b2ba14d36ccf8d971389d633349.png

1.2 NPFS

当NPFS正确检查SFAC时,它会使用RequestorMode来确定是否允许调用方指定任意EA块。通常,当打开命名管道时,驱动程序会记录调用PID和会话ID。然后,可以通过诸如GetNamedPipeClientProcessId之类的API来公开此信息。如果调用方是UserMode,则不允许代码设置EA块,但如果是KernelMode,则可以使用任意EA块。这意味着,可以欺骗PID和会话ID字段。

NTSTATUS NpCreateClientEnd(PIRP Irp, ...) {

 // ...

 PFILE_FULL_EA_INFORMATION ea = Irp->AssociatedIrp.SystemBuffer;

 PVOID Data;

 SIZE_T Length;

 if (!NpLocateEa(ea, "ClientComputerName", &Data, Length))

   return STATUS_INVALID_PARAMETER;

 if (!IsValidEaString(Data, Length) || Irp->RequestorMode != KernelMode)

   return STATUS_INVALID_PARAMETER;

 NpSetAttributeInList(Irp, CLIENT_COMPUTER_NAME, Data, Length);

 NpLocateEa(ea, "ClientProcessId", Data, Length);

 NpSetAttributeInList(Irp, CLIENT_PROCESS_ID, Data, Length);

 NpLocateEa(ea, "ClientSessionId", Data, Length);

 NpSetAttributeInList(Irp, CLIENT_SESSION_ID, Data, Length);

 // ...

}

这一行为用于允许SMB驱动程序设置计算机名称字段和会话ID。如果某些服务信任此信息,就可以使用它来提升权限。要利用这一漏洞,我们需要事先将任意EA设置为KernelMode,如果想要做一些有趣的尝试,我们还需要访问打开的句柄。

2. 启动器

为了找到启动器,我在内核和驱动程序中查找了调用IoCreateFile的所有函数,并对Options和对象属性标志的调用参数进行了基本的检查。找到了合适的目标之后,我就能进行更加仔细的检查,从而确定用户可以影响哪些参数。一旦理解了漏洞类,查找启动器就相对简单了,因为只需要查找对目标方法的导入调用,这样可以让我们迅速缩小目标范围。

2.1 NTOSKRNL NtSetInformationFile FileRenameInformation类

在重命名文件时,即使文件不能与原始文件位于不同的卷上,也可以指定任意路径。调用函数IopOpenLinkOrRenameTarget首先使用传递INPC的IoCreateFileEx打开目标路径,通常是IFAC(它也设置了IO_OPEN_TARGET_DIRECTORY,但这对操作并不重要)。该启动器仅允许我们指定完整路径。

2.2 SMB v2服务器驱动程序

SMB服务器将使用IoCreateFileEx打开共享的文件,例如在Smb2CreateFile中的示例。它指定IFAC但不指定INPC,因为调用是在系统线程上进行的,所以先前访问模式已经是KernelMode。通常,当服务器将相对路径传递给打开的卷的句柄时,无法将文件创建重定向到任意NT对象管理器路径。尽管我们可以将挂载点添加到文件系统上的目录,并在本地访问服务器,但内核会故意将目标设备限制为一组有限的类型。

if (ParseContext->ReparseTag == IO_REPARSE_TAG_MOUNT_POINT) {

 switch (ParseContext->TargetDevice){

   case FILE_DEVICE_DISK:

   case FILE_DEVICE_CD_ROM:

   case FILE_DEVICE_DISK:

   case FILE_DEVICE_TAPE:

     break;

   default:

     return STATUS_IO_REPARSE_DATA_INVALID;

 }

}

在实现中,存在一个漏洞,允许我们绕过设备检查,使用本地挂载点重定向SMB服务器,从而打开任何设备文件。SMBv2驱动程序在NTFS符号链接方面具有特殊要求,它必须将链接信息返回到客户端进行处理。为了支持符号链接功能,服务器传递Options标志IO_STOP_ON_SYMLINK,如下所示。

NTSTATUS Smb2CreateFile(HANDLE VolumeHandle, PUNICODE_STRING Name, ...) {

 // ...

 int ReparseCount = 0;

 OBJECT_ATTRIBUTES ObjectAttributes;

 ObjectAttributes.RootDirectory = VolumeHandle;

 ObjectAttributes.ObjectName = Name;

 IO_STATUS_BLOCK IoStatus = {};

 do {

   status = IoCreateFileEx(

           &FileHandle,

           DesiredAccess,

           &ObjectAttributes,

           &IoStatus,

           ...

           IO_STOP_ON_SYMLINK | IO_FORCE_ACCESS_CHECK

         );

   if (status == STATUS_STOPPED_ON_SYMLINK) {

     UNICODE_STRING NewName;

     status = SrvGraftName(ObjectAttributes.ObjectName,

       (PREPARSE_DATA_BUFFER)IoStatus.Information, &NewName);

     if (status == STATUS_STOPPED_ON_SYMLINK)

       break;

     ObjectAttributes.RootDirectory = NULL;

     ObjectAttributes.ObjectName = NewName;

     continue;

   }

 } while(ReparseCount++ < MAXIMUM_REPARSE_COUNT);

 // ...

}

如果IoCreateFileEx返回STATUS_STOPPED_ON_SYMLINK,那么服务器从IO_STATUS_BLOCK中提取返回的REPARSE_DATA_BUFFER结构,并将其传递给SrvGraftName实用程序函数。重新解析缓冲区(Reparse Buffer)可以是装载点,也可以是NTFS符号链接。如果它是符号链接,那么SrvGraftName再次返回STATUS_STOPPED_ON_SYMLINK,这允许服务器将缓冲区返回给调用方。如果重新解析缓冲区是装载点,那么SrvGraftName仅仅会根据在REPARSE_DATA_BUFFER中找到的字符串来构建新的绝对路径,不会检查目标设备。服务器使用新的绝对路径重新发出打开的请求,该路径现在可以指向系统上的任何设备。

这个启动器是我在分析过程中所发现的最好的一个。我们可以指定IoCreateFileEx的几乎所有参数,包括EA缓冲区。这允许我们初始化需要EA(例如WS2IFSL)的驱动程序,从而能够打开更多的攻击面。当它在系统线程上运行时,RequestorMode和线程的先前模式都被设置为KernelMode,这可能会引入其他有趣的攻击面。

然而,这并不是理想的启动器,打开的设备需要支持某些有效的IRP,例如IRP_MJ_GET_INFORMATION_FILE,否则服务器将不会向调用方返回有效的句柄。如果没有这个检查,那么对WS2IFSL进行漏洞利用将会是非常简单的,因为我们可以执行读写操作,以使APC在内核模式下执行。即使我们可以获得文件的处理,SMB服务器也会故意限制我们可以发送的IO控制代码,这限制了我们可以使用该漏洞执行的许多关键操作。即便如此,我也认为这是一个严重的漏洞,因此我直接向MSRC报告。该漏洞被编号为CVE-2018-0749,并通过使用特殊的额外创建参数来实现修复,该参数将过滤掉除符号链接之外的所有重新解析点。

8114a11d57e36b9076fbc1387f05f954.png后续工作

尽管,我认为这是一个非常严重的漏洞类,但实际上,要找到匹配的启动器和接收器是非常困难的。在我的研究中,我没有找到任何可以直接实现特权提升的组合。我确定,一个比较好的思路就是将SMBv2的启动器与NPFS进程ID欺骗相结合。尽管我无法识别将客户端PID用于任何安全操作的服务,但实际上确实可能存在第三方服务。

在提交给MSRC的内容中,包含了我写的一个文件,其中解释了漏洞类并描述了我的一些发现。与此同时,我通过常规渠道报告了SMB服务器漏洞,这是我发现的最严重的漏洞。在发现后,我在Redmond 2017与一些Redmond团队进行了会面,并共同制定了一个计划,从而使Microsoft能够使用他们的源代码访问来发现Windows内核和驱动程序代码库中该漏洞的影响范围。我并没有直接访问源代码的权限,这部分调查被委托给了MSRC,其调查结果都在他们的博客文章中发布。

值得注意的是,尽管我针对SMB服务器漏洞采用了90天的标准披露期限,但并没有对漏洞类执行这一标准。由于它并不是一个单独的漏洞,所以研究人员要开展大量的工作,因此我们过早的披露无疑会起到不好的效果。而现在,MSRC同意我们公布有关此问题的技术细节,这也是我们为什么在提交漏洞后12个月才发表文章的原因。

8114a11d57e36b9076fbc1387f05f954.png总结

在Windows中找到一个新的漏洞类总是非常有趣的。幸运的是,这个漏洞并没有像最初认为的那么严重。尽管指定两个单独的访问模式是有意义的,一个用于MemAC,另一个用于SecAC,但这不是原始NT设计中所使用的模式。为了实现向后兼容,可能这一行为不太好改变。这个漏洞类与文档中体现的技术问题一样糟糕,不仅行为不能改变,而且对于它的文档记录也很差。

如果我们查看IoCreateFile的文档,现在可以发现一个新的评论:

“对于来源于用户模式的创建请求,如果驱动程序在IoCreateFile的Options参数中同时设置IO_NO_PARAMETER_CHECKING和IO_FORCE_ACCESS_CHECK,则它还应在ObjectAttributes参数中设置OBJ_FORCE_ACCESS_CHECK。有关此标志的信息,请参阅OBJECT_ATTRIBUTES的Attributes成员。”

这句话在最近才被添加。在GitHub上,甚至我们都能看到添加这句话的提交。

MSRC确认的任何漏洞都仅在最新版本的Windows 10中修复。因此,如果您是开发人员,则应该阅读Microsoft的博客文章,了解如何避免驱动程序中的这些问题,以及查找代码库中是否涉及这些问题。如果您是安全研究人员,在检查新的Windows内核驱动程序时要注意这一点。

最后,我要感谢MSRC的Steven Hunter和Gavin Thomas,在漏洞的提交和修复过程中我们取得了良好的合作。                                                               

3c3e4b4108fb0c2672c3efa1f2117482.png

ec55c3e1988da1356f9aab7b3a93e52a.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值