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及相关的主接口,分别为IBackgroundCopyManager
及IBackgroundCopyMgr
。
“新版” 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个方法。
从Proc3
到Proc15
与官方文档中列出的方法相同,但Proc16
及Proc17
并不在官方文档中。
根据已有文档,我们知道对应的头文件为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\mountpoint
到C:\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 参考资料
- MSRC – CVE-2020-0787 | Windows Background Intelligent Transfer Service Elevation of Privilege Vulnerability
https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2020-0787 - OleViewDotNet – James Forshaw
https://github.com/tyranid/oleviewdotnet - Symbolic Link Testing Tools – James Forshaw
https://github.com/googleprojectzero/symboliclink-testing-tools - UsoDllLoader
https://github.com/itm4n/UsoDllLoader - MSDN – IBackgroundCopyManager interface
https://docs.microsoft.com/en-us/windows/win32/api/bits/nn-bits-ibackgroundcopymanager - MSDN – IBackgroundCopyQMgr interface
https://docs.microsoft.com/en-us/windows/win32/api/qmgr/nn-qmgr-ibackgroundcopyqmgr