CVE-2020-0787:未公开RPC函数中的提权漏洞分析

 

0x00 前言

本文介绍了我在Background Intelligent Transfer Service(BITS,后台智能传输服务)中发现的一个任意文件移动漏洞,这是Windows 10中的一个特权文件操作滥用问题。这个bug创新点不多,但本身比较有趣,隐藏在未公开函数中。因此这里我将与大家分享发现该漏洞的过程,介绍挖掘逻辑缺陷中使用的逆向分析过程。

 

0x01 背景介绍

如果大家对Windows的这个功能不大了解,可以参考微软官方文档中的描述(链接):

程序员及系统管理人员可以使用BITS来下载文件,或者将文件上传至HTTP Web服务器及SMB文件共享。BITS在传输过程中会权衡传输成本以及网络使用情况,尽可能少影响用户前台工作。即使主机重启,BITS也可以处理网络中断、暂停并自动恢复传输。

该服务提供了多个COM对象,包括多个不同的“Control Class”(控制类)以及一个“Legacy Control Class”(旧版控制类),后者可以用来获取指向旧版IBackgroundCopyGroup接口的指针,该接口包含2个未公开的方法:QueryNewJobInterface()SetNotificationPointer()

如果用户调用IBackgroundCopyGroup接口(即旧版接口)的CreateJob()方法,就能得到指向旧版IBackgroundCopyJob1接口的一个指针。如果用户调用同一个接口的QueryNewJobInterface()方法,就能得到指向新IBackgroundCopyJob接口的一个指针。

这里的问题在于,目标服务在处理该调用时,并没有采用模拟(impersonation)方式。这意味着用户得到的IBackgroundCopyJob接口的指针位于NT AUTHORITY\SYSTEM上下文中。目标服务在其他方法中实现了模拟,因此这个问题的影响程度有限,但仍然会造成一些后果。

当创建传输任务、文件被添加到队列中时,服务也会创建一个临时文件。当服务完成文件写入操作后,就会调用MoveFileEx()将该文件重命名为用户指定的文件名。这里的问题在于,当使用QueryNewJobInterface()返回接口指针时,最后执行的操作并没有采用模拟机制。

因此普通用户可以利用这一点,通过挂载点(mountpoint)、机会锁(oploick)及符号链接将任意文件移动到受限区域。

 

0x02 BITS COM类工作原理

BITS提供了多个COM对象,我们可以使用OleViewDotNet来查看这些类(再次感谢James Forshaw小伙伴)。

这里我们主要关注的是Background Intelligent Transfer(BIT)Control Class 1.0、Legacy BIT Control Class及相关的主接口,分别为IBackgroundCopyManagerIBackgroundCopyMgr

“新版” BIT Control Class

BIT Control Class 1.0的工作过程如下:

1、我们首先必须使用CoCreateInstance()请求创建BIT Control Class(CLSID:4991D34B-80A1-4291-83B6-3328366B9097)的一个实例,请求指向IBackgroundCopyManager接口的一个指针。

2、调用IBackgroundCopyManager::CreateJob()创建一个“任务”,获取指向IBackgroundCopyJob接口的一个指针。

3、调用IBackgroundCopyJob::AddFile()将文件加入任务中。该调用需要两个参数:URL及本地文件路径。URL可以为UNC路径。

4、由于任务创建时处于SUSPENDED状态,因此我们必须调用IBackgroundCopyJob::Resume(),当任务状态变成TRANSFERRED时,我们需要调用IBackgroundCopyJob::Complete()

CoCreateInstance(CLSID_4991D34B-80A1-4291-83B6-3328366B9097)       -> IBackgroundCopyManager*
|__ IBackgroundCopyManager::CreateJob()                            -> IBackgroundCopyJob*
    |__ IBackgroundCopyJob::AddFile(URL, LOCAL_FILE) 
    |__ IBackgroundCopyJob::Resume() 
    |__ IBackgroundCopyJob::Complete()

虽然BITS运行在NT AUTHORITY\SYSTEM上下文中,这些操作都会模拟RPC客户端来完成,因此这里我们无法实现权限提升。

Legacy Control Class

Legacy Control Class的工作过程有点不同,多了一个步骤。

1、我们必须使用CoCreateInstance()创建Legacy BIT Control Class(CLSID:69AD4AEE-51BE-439B-A92C-86AE490E8B30)的一个实例,请求指向IBackgroundCopyQMgr接口的一个指针。

2、调用IBackgroundCopyQMgr::CreateGroup()创建一个“组”,获取指向IBackgroundCopyGroup接口的一个指针。

3、调用IBackgroundCopyGroup::CreateJob(),创建一个“任务”,获取指向IBackgroundCopyJob1接口的一个指针。

4、调用IBackgroundCopyJob1::AddFiles(),将文件添加到“任务”中,该函数的参数为一个FILESETINFO结构。

5、由于任务创建时处于SUSPENDED状态,我们需要调用IBackgroundCopyJob1::Resume(),当任务状态变为TRANSFERRED,我们需要调用IBackgroundCopyJob1::Complete()

CoCreateInstance(CLSID_69AD4AEE-51BE-439B-A92C-86AE490E8B30)       -> IBackgroundCopyQMgr*
|__ IBackgroundCopyQMgr::CreateGroup()                             -> IBackgroundCopyGroup*
    |__ IBackgroundCopyGroup::CreateJob()                          -> IBackgroundCopyJob1*
        |__ IBackgroundCopyJob1::AddFiles(FILESETINFO)
        |__ IBackgroundCopyJob1::Resume()
        |__ IBackgroundCopyJob1::Complete()

同样,虽然BITS运行在NT AUTHORITY\SYSTEM上下文中,这些操作都会模拟RPC客户端来完成,因此这里我们也找不到提权点。

MSDN上有关于这两个COM类及相关接口的使用文档(参考此处此处)。然而,当尝试理解IBackgroundCopyGroup接口的工作方式时,我注意到MSDN上的描述与实际的Proxy定义有些不同。

IBackgroundCopyGroup接口的文档可参考此处。根据该描述,这个接口包含13个方法。然而,当我们使用OleViewDotNet查看该接口的Proxy定义时,可以看到实际上该接口包含15个方法。

Proc3Proc15与官方文档中列出的方法相同,但Proc16Proc17并不在官方文档中。

根据已有文档,我们知道对应的头文件为Qmgr.h。打开该文件后,我们可以获得该接口上可用的所有方法。

这里我们的确可以看到2个未公开的方法:QueryNewJobInterface()SetNotificationPointer()

 

0x03 QueryNewJobInterface()

在OleViewDotNet的帮助下,我们可知IBackgroundCopyQMgr接口的具体实现位于qmgr.dll中。因此我们可以在IDA中打开该文件,查找关于IBackgroundCopyGroup接口的更多信息,其中我注意到了2个未公开的方法。

QueryNewJobInterface()方法需要1个参数:一个接口标识符(REFIID iid)。该方法会返回指向某个接口的一个指针(IUnknown **pUnk)。该函数的原型如下所示:

virtual HRESULT QueryNewJobInterface(REFIID iid, IUnknown **pUnk);

首先,该函数会将输入的GUID(接口ID)与硬编码的值(37668d37-507e-4160-9316-26306d150b12)进行比较(1),如果不匹配,函数会返回错误代码0x80004001(2),表示“未实现”。如果匹配,则会调用来自CJob类的GetJobExternal()函数(3).

硬编码的GUID值(37668d37-507e-4160-9316-26306d150b12)比较有趣,这个值对应的是IID_IBackgroundCopyJob。我们可以在Bits.h头文件中找到这个值。

 

0x04 任意文件移动漏洞

在进行逆向分析之前,我们可以基于已有的信息来大胆猜测,这些信息包括:

1、未公开方法名称为QueryNewJobInterface()

2、该方法由Legacy BIT Control Class的IBackgroundCopyGroup接口所提供。

3、其中涉及到“新的”IBackgroundCopyJob接口的GUID。

因此,我们可以猜测该函数的目的是从Legacy Control Class中获取指向“新的”IBackgroundCopyJob接口的接口指针。

为了验证我的猜想,我创建了一个应用程序,执行如下操作:

1、创建Legacy Control Class的一个实例,获取指向旧版IBackgroundCopyQMgr接口的一个指针。

2、调用IBackgroundCopyQMgr::CreateGroup(),创建一个新的,获取指向IBackgroundCopyGroup接口的一个指针。

3、调用IBackgroundCopyGroup::CreateJob()创建一个新的任务,获取指向IBackgroundCopyJob1接口的一个指针。

4、调用IBackgroundCopyJob1::AddFiles(),将一个文件加到任务中。

5、现在是关键步骤,应用调用IBackgroundCopyGroup::QueryNewJobInterface()方法,获取指向位置接口的一个指针,这里我们假设这是一个IBackgroundCopyJob接口。

6、应用最终会在IBackgroundCopyJob接口(而非IBackgroundCopyJob1接口)上调用Resume()Complete()方法,恢复并完成任务。

在这个应用中,目标URL为\\127.0.0.1\C$\Windows\System32\drivers\etc\hosts(我们并不需要依赖网络连接),本地文件为C:\Temp\test.txt

然后我使用Procmon来分析BITS的行为。

首先,我们可以看到服务会在目标目录中创建一个TMP文件,尝试打开以参数形式传入的本地文件,该过程会模拟当前用户来操作。

随后,一旦我们调用Resume()函数,目标服务就会开始读取目标文件\\127.0.0.1\C$\Windows\System32\drivers\etc\hosts,将该文件内容写到TMP文件C:\Temp\BITF046.tmp,这里依然会模拟当前用户。

最后,该服务会调用MoveFileEx(),将TMP文件重命名为test.txt,这里存在一个问题:执行该操作时,服务不再模拟当前用户身份,着意味着文件移动操作会在NT AUTHORITY\SYSTEM的上下文中执行。

如下图所示,SetRenameInformationFile调用源自于Win32 MoveFileEx()函数。

SYSTEM权限执行任意文件移动操作将导致本地提权(LPE)问题。普通用户可以将精心构造的一个DLL移动到System32目录,就有可能在NT AUTHORITY\SYSTEM上下文中执行任意代码,相关内容可以参考下文的“漏洞利用”部分。

 

0x05 定位缺陷

在尝试寻找QueryNewJobInterface()函数中的缺陷之前,我首先想理解一下“正常”CreateJob()方法的工作过程。

IBackgroundCopyGroup接口中的CreateJob()方法的具体实现位于服务端的COldGroupInterface类中。

由于CFG(Control Flow Guard,控制流防护)的存在,这里的逻辑不是特别清晰。如果我没弄错的话,该函数会调用同一个类的CreateJobInternal()方法。

该函数首先会调用CLockedJobWritePointer类的ValidateAccess()方法,后者会调用CJob类的CheckClientAccess()方法。

CheckClientAccess()方法中会检查用户令牌,将其应用到当前线程中,以便进行模拟。

最终,执行流会回到CreateJobInternal()方法,该方法会调用CJob类的GetOldJobExternal()方法,向客户端返回指向IBackgroundCopyJob1接口的指针。

调用过程总结如下:

(CLIENT) IBackgroundCopyGroup::CreateJob()
   |
   V
(SERVER) COldGroupInterface::CreateJob()
         |__ COldGroupInterface::CreateJobInternal()
             |__ CLockedJobWritePointer::ValidateAccess()
             |   |__ CJob::CheckClientAccess()            // Client impersonation
             |__ CJob::GetOldJobExternal()                // IBackgroundCopyJob1* returned

现在我们知道CreateJob()方法的整体工作过程,可以回到QueryNewJobInterface()方法的逆向分析上。

根据前文,如果提供的GUID匹配IID_IBackgroundCopyJob,那么目标服务将执行如下代码:

这里会调用CJob::GetExternalJob()查询新的接口指针,返回给客户端。因此,我们可以简单总结如下:

(CLIENT) IBackgroundCopyGroup::QueryNewJobInterface()
   |
   V
(SERVER) COldGroupInterface::QueryNewJobInterface()
         |__ CJob::GetJobExternal()                        // IBackgroundCopyJob* returned

现在我们可以看到问题的蛛丝马迹。从这些信息可知,当从IBackgroundCopyGroup调用QueryNewJobInterface()方法来请求新的IBackgroundCopyJob接口的指针时,服务并没有模拟客户端身份。这意味着客户端能得到指向某个接口的指针,并且该指针位于NT AUTHORITY\SYSTEM上下文中。

然而问题并没有那么简单。实际上,我注意到文件移动操作会在调用IBackgroundCopyJob::Resume()之后、调用IBackgroundCopyJob::Complete()之前执行。

调用IBackgroundCopyJob::Resume()时,简化版的调用栈如下所示:

(CLIENT) IBackgroundCopyJob::Resume()
   |
   V
(SERVER) CJobExternal::Resume()
         |__ CJobExternal::ResumeInternal()
             |__ ...
             |__ CJob::CheckClientAccess()        // Client impersonation
             |__ CJob::Resume()
             |__ ...

调用IBackgroundCopyJob::Complete()时,简化版的调用栈如下所示:

(CLIENT) IBackgroundCopyJob::Complete()
   |
   V
(SERVER) CJobExternal::Complete()
         |__ CJobExternal::CompleteInternal()
             |__ ...
             |__ CJob::CheckClientAccess()       // Client impersonation
             |__ CJob::Complete()
             |__ ...

在这两种情况下,服务都模拟了客户端身份。这意味着任务并没有由客户端完成,而是由服务端自己完成的,这可能是因为队列中没有其他文件。

因此,当调用IBackgroundCopyGroup::QueryNewJobInterface()获得IBackgroundCopyJob接口的指针时、目标服务(而非RPC客户端)会完成任务,并且没有通过模拟操作来调用最终的CFile::MoveTempFile()。我没能成功找到逻辑缺陷的确切位置,但我认为如果在COldGroupInterface::QueryNewJobInterface()中添加CJob::CheckClientAccess()检查操作,应该能解决这个问题。

简化版流程如下图所示,这些函数将导致MoveFileEx()方法在CJob对象上下文中调用:

 

0x06 漏洞利用

漏洞利用非常简单,主要思想是提供给目标服务一个文件夹路径,该路径最初将作为另一个“物理”目录的junction(连接)点。我们创建了一个新的任务,“下载”一个本地文件,并且在TMP文件上设置Oplock。恢复任务后,服务会模拟RPC客户端身份开始写入TMP文件,触发Oplock。然后,我们需要将挂载点切换至对象目录(Object Directory),创建2个符号链接。TMP文件会指向我们拥有的任意文件,而“本地”文件将指向System32目录中的一个新的DLL文件。在释放Oplock后,服务会继续写入原始的TMP文件,但会通过我们设置的2个符号链接执行最终的移动操作。

1、准备工作环境。

我们要创建具有如下结构的目录:

<DIR> C:\workspace
|__ <DIR> bait
|__ <DIR> mountpoint
|__ FakeDll.dll

mountpoint目录的作用是将指向bait目录的junction点切换为指向RPC Control对象目录的junction点。FakeDll.dll为我们希望移动到受限位置(如C:\Windows\System32\ )的目标文件。

2、创建挂载点。

我们需要创建从C:\workspace\mountpointC:\workspace\bait的挂载点。

3、创建新的任务。

我们将使用Legacy Control Class提供的接口来创建新的任务,参数如下:

目标URL: \\127.0.0.1\C$\Windows\System32\drivers\etc\hosts
本地文件: C:\workspace\mountpoint\test.txt

由于前面创建了junction点,因此本地文件的真正路径为C:\workspace\bait\test.txt

4、查找TMP文件并设置Oplock。

当将文件加入任务队列时,服务会立即创建一个TMP文件。由于文件具有“随机”名称,我们需要列出bait目录的内容来定位该文件。这里我们应当查找文件名类似BIT1337.tmp的文件,找到名称后,我们可以在该文件上设置Oplock。

5、恢复任务并等待Oplock。

前面提到过,当任务恢复时,服务会打开并写入TMP文件,从而触发Oplock。通过这种技术,我们可以暂停正常操作,轻松赢得竞争条件。

6、切换挂载点。

在执行该步骤之前:

TMP文件 = C:\workspace\mountpoint\BIT1337.tmp -> C:\workspace\bait\BIT1337.tmp
本地文件 = C:\workspace\mountpoint\test.txt -> C:\workspace\bait\test.txt

切换挂载点,创建如下符号链接:

C:\workspace\mountpoint -> \RPC Control
Symlink #1: \RPC Control\BIT1337.tmp -> C:\workspace\FakeDll.dll
Symlink #2: \RPC Control\test.txt -> C:\Windows\System32\FakeDll.dll

执行该步骤后:

TMP文件 = C:\workspace\mountpoint\BIT1337.tmp -> C:\workspace\FakeDll.dll
本地文件 = C:\workspace\mountpoint\test.txt -> C:\Windows\System32\FakeDll.dll

7、释放Oplock,完成任务。

释放Oplock后,在原始TMP文件上的CreateFile操作会返回,服务开始写入C:\workspace\bait\BIT1337.tmp。随后,由于符号链接的存在,最终的MoveFileEx()调用会被重定向。因此,我们的DLL文件会被移动到System32目录中。

由于这是一个文件移动操作,因此文件的属性得以保留。这意味着文件仍然为当前用户所有,即便已经处于受限位置,仍然可以被当前用户所修改。

8、以System权限执行代码。

为了以System权限执行代码,我通过这个任意文件移动漏洞在System32目录中创建了一个WindowsCoreDeviceInfo.dll文件,然后再利用Update Session Orchestrator服务,以System权限加载该DLL。

攻击过程如下图所示:

 

0x07 参考资料

(完)