0x00 前言
2019年5月,微软针对远程代码执行(RCE)漏洞CVE-2019-0708专门发布了带外补丁更新包,这个漏洞也就是知名的“BlueKeep”漏洞,存在于远程桌面服务(RDS)种。这是一个预身份认证漏洞,无需用户交互,因此很有可能被攻击者利用,带来破坏性风险。如果成功利用漏洞,攻击者就能以SYSTEM权限执行任意代码。根据微软安全响应中心(MSRC)发布的公告,这是一种蠕虫级漏洞,可能被攻击者用来发起类似Wannacry及EsteemAudit级别的攻击。了解到这个漏洞的严重性及可能对公众造成的潜在风险后,微软也采取了罕见的处理流程,为不再受支持的Windows XP系统推出补丁,以全面保护Windows用户。
由于该漏洞可能会带来全球范围内的灾难性后果,Palo Alto Networks Unit 42研究人员认为该漏洞非常值得研究,以便澄清RDS的内部工作原理及漏洞利用方式。我们深入研究了RDP内部实现以及如何利用这些内部工作流程在未打补丁的主机上实现代码执行。本文讨论了如何利用Bitmap Cache PDU(协议数据单元)、Refresh Rect PDU以及RDPDR Client Name Request PDU将数据写入内核内存中。
自微软在5月份发布补丁以来,该漏洞得到了计算机安全行业的广泛关注。事实上漏洞利用工具的公开并在实际攻击中使用只是一个时间问题,根据我们的研究成果,大家可以了解到存在漏洞的系统实际上会面临极大的风险。
0x01 Bitmap Cache PDU
根据MS-RDPBCGR(Remote Desktop Protocol: Basic Connectivity and Graphics Remoting)文档描述,Bitmap Cache PDU的全称为TS_BITMAPCACHE_PERSISTENT_LIST_PDU,这是一种Persistent Key List PDU Data,内嵌在Persistent Key List PDU中。Persistent Key List PDU是一种RDP Connection Sequence(连接时序)PDU,在RDP Connection Sequence的Connection Finalization(连接完成)阶段由客户端发送至服务端,如图1所示。
图1. RDP连接时序
Persistent Key List PDU头部为通用的RDP PDU头,具体格式如图2所示:tpktHeader(4字节)+x224Data(3字节)+mcsSDrq(可变字节)+securityHeader(可变字节)。
图2. Client Persistent Key List PDU
根据MS-RDPBCGR文档,TS_BITMAPCACHE_PERSISTENT_LIST_PDU结构中包含由之前会话中发送的一系列已缓存的bitmap key(位图键值),这些key与Cache Bitmap(Revision 2)Order(缓存位图序)相对应,如图3所示:
图3. Persistent Key List PDU Data(BITMAPCACHE PERSISTENT LIST PDU)
在官方设计方案中,RDP客户端可以使用Bitmap Cache PDU向服务端发送信息,表明客户端本地包含与这些key对应的位图拷贝,这意味着服务端不需要重新将位图发送给客户端。根据MS-RDPBCGR文档描述,Bitmap PDU有如下4个特点:
- RDP服务端会分配一个内核池,用来存储已缓存的bitmap key;
- RDP服务端分配的内核池大小由RDP客户端发送过来的BITMAPCACHE PERSISTENT LIST结构中的
numEntriesCacheX(X取0到4之间的值)以及totalEntriesCacheX(X取0到4之间的值)字段所控制,这两个字段大小均为2字节(WORD); - Bitmap Cache PDU可以被多次发送,因为bitmap key可以通过多个Persistent Key List PDU发送,每个PDU通过
bBitMask字段中flag的来标记; - bitmap key的数量最多为169个。
根据BITMAPCACHE PERSISTENT LIST PDU的这4个特点,如果我们能绕过bitmap key数量限制(169个),或者微软在实现RDP时没有遵循这个限制值,那么就有可能将任意数据写入内核中。
0x02 如何通过Bitmap Cache PDU将数据写入内核
根据MS-RDPBCGR文档,正常加密的BITMAPCACHE PERSISTENT LIST PDU如下所示:
f2 00 -> TS_SHARECONTROLHEADER::totalLength = 0x00f2 = 242 bytes
17 00 -> TS_SHARECONTROLHEADER::pduType = 0x0017
0x0017
= 0x0010 | 0x0007
= TS_PROTOCOL_VERSION | PDUTYPE_DATAPDU
ef 03 -> TS_SHARECONTROLHEADER::pduSource = 0x03ef = 1007
ea 03 01 00 -> TS_SHAREDATAHEADER::shareID = 0x000103ea
00 -> TS_SHAREDATAHEADER::pad1
01 -> TS_SHAREDATAHEADER::streamId = STREAM_LOW (1)
00 00 -> TS_SHAREDATAHEADER::uncompressedLength = 0
2b -> TS_SHAREDATAHEADER::pduType2 =
PDUTYPE2_BITMAPCACHE_PERSISTENT_LIST (43)
00 -> TS_SHAREDATAHEADER::generalCompressedType = 0
00 00 -> TS_SHAREDATAHEADER::generalCompressedLength = 0
00 00 -> TS_BITMAPCACHE_PERSISTENT_LIST::numEntries[0] = 0
00 00 -> TS_BITMAPCACHE_PERSISTENT_LIST::numEntries[1] = 0
19 00 -> TS_BITMAPCACHE_PERSISTENT_LIST::numEntries[2] = 0x19 = 25
00 00 -> TS_BITMAPCACHE_PERSISTENT_LIST::numEntries[3] = 0
00 00 -> TS_BITMAPCACHE_PERSISTENT_LIST::numEntries[4] = 0
00 00 -> TS_BITMAPCACHE_PERSISTENT_LIST::totalEntries[0] = 0
00 00 -> TS_BITMAPCACHE_PERSISTENT_LIST::totalEntries[1] = 0
19 00 -> TS_BITMAPCACHE_PERSISTENT_LIST::totalEntries[2] = 0x19 = 25
00 00 -> TS_BITMAPCACHE_PERSISTENT_LIST::totalEntries[3] = 0
00 00 -> TS_BITMAPCACHE_PERSISTENT_LIST::totalEntries[4] = 0
03 -> TS_BITMAPCACHE_PERSISTENT_LIST::bBitMask = 0x03
0x03
= 0x01 | 0x02
= PERSIST_FIRST_PDU | PERSIST_LAST_PDU
00 -> TS_BITMAPCACHE_PERSISTENT_LIST::Pad2
00 00 -> TS_BITMAPCACHE_PERSISTENT_LIST::Pad3
TS_BITMAPCACHE_PERSISTENT_LIST::entries:
a3 1e 51 16 -> Cache 2, Key 0, Low 32-bits (TS_BITMAPCACHE_PERSISTENT_LIST_ENTRY::Key1)
48 29 22 78 -> Cache 2, Key 0, High 32-bits (TS_BITMAPCACHE_PERSISTENT_LIST_ENTRY::Key2)
61 f7 89 9c -> Cache 2, Key 1, Low 32-bits (TS_BITMAPCACHE_PERSISTENT_LIST_ENTRY::Key1)
cd a9 66 a8 -> Cache 2, Key 1, High 32-bits (TS_BITMAPCACHE_PERSISTENT_LIST_ENTRY::Key2)
…
在RDPWD.sys内核模块中,ShareClass::SBC_HandlePersistentCacheList函数负责解析BITMAPCACHE PERSISTENT LIST PDU。当结构体中的bBitMask字段值设置为0x01时,表示当前PDU为PERSIST FIRST PDU。随后SBC_HandlePersistentCacheList会调用WDLIBRT_MemAlloc来分配一个内核池(分配内核空间)来存储持久性位图缓存键值,如图4所示。其他情况下,0x00值表示当前PDU为PERSIST MIDDLE PDU,0x02值表示当前PDU为PERSIST LAST PDU。当解析PERSIST MIDDLE PDU以及PERSIST LAST PDU时,SBC_HandlePersistentCacheList会将bitmap cache key缓存到前面分配的内存空间中,如图5所示。
图4. SBC_HandlePersistentCacheList分配池空间以及检查totalEntriesCacheLimi
图5. SBC_HandlePersistentCacheList拷贝bitmap cache key
Windows 7 x86系统中调用栈情况如图6所示,以参数形式传递给SBC_HandlePersistentCacheList函数的TS_BITMAPCACHE_PERSISTENT_LIST结构体如图7所示。
图6. SBC_HandlePersistentCacheList栈轨迹
图7. 以SBC_HandlePersistentCacheList第二个参数形式传入的TS_BITMAPCACHE_PERSISTENT_LIST结构
如图4所示,bitmapCacheListPoolLen = 0xC * (total length + 4),而total length = totalEntriesCache0 + totalEntriesCache1 + totalEntriesCache2 + totalEntriesCache3 + totalEntriesCache4。根据这个公式,我们可以设置totalEntriesCacheX=0xffff,这样bitmapCacheListPoolLen就能取得最大值。然而如图8所示,系统会对每个totalEntriesCacheX检查totalEntriesCacheLimit。totalEntriesCacheLimitX来自于TS_BITMAPCACHE_CAPABILITYSET_REV2结构体,当RDPWD调用DCS_Init时,CAPAPI_LOAD_TS_BITMAPCACHE_CAPABILITYSET_REV2函数就会初始化这个结构体。当解析Confirm Active PDU时,CAPAPI_COMBINE_TS_BITMAPCACHE_CAPABILITYSET_REV2函数就会将这些值组合起来,如图9所示。
图8. RDPWD!CAPAPI_LOAD_TS_BITMAPCACHE_CAPABILITYSET_REV2
图9. RDPWD!CAPAPI_COMBINE_TS_BITMAPCACHE_CAPABILITYSET_REV2
CAPAPI_COMBINE_TS_BITMAPCACHE_CAPABILITYSET_REV2会将服务端初始化的NumCellCaches(0x03)及totalEntriesCacheLimit[0-4](0x258、0x258、0x10000、0x0及0x0,如图9中edx寄存器所示)与客户端请求中的NumCellCaches(0x03)及totalEntriesCache[0-4](0x80000258、0x80000258、0x8000fffc、0x0及0x0,如图9中esi寄存器所示)组合起来。客户端可以控制NumCellCaches及totalEntriesCache[0-4],如图10所示,但不能超过服务端初始化的NumCellCaches(0x03)及totalEntriesCacheLimit[0-4](0x258、0x258、0x10000、0x0及0x0),如图11随时。
图10. TS_BITMAPCACHE_CAPABILITYSET_REV2
图11. CAPAPI_COMBINE_TS_BITMAPCACHE_CAPABILITYSET_REV2函数
了解这些知识后,我们可以计算出bitmapCacheListPoolLen的最大值,最大值bitmapCacheListPoolLen = 0xC * (0x10000 + 0x258 + 0x258 + 4) = 0xc3870,理论上我们可以在内核池中控制大小为0x8 * (0x10000 + 0x258 + 0x258 + 4) = 0x825a0字节数据,如图12所示。
图12. 转储出的Persistent Key List PDU内存
然而,我们发现RDP客户端并没有按我们设想的方式来控制位图缓存列表池中的所有数据。每8字节可控数据之间都有一个4字节不可控数据(索引值),这对shellcode利用来说非常不友好。此外,0xc3870字节大小的内核池不能被多次分配,因为正常情况下Persistent Key List PDU只能发送1次。然而经过多次测试后,我们发现内核池会在同一个内存地址处进行分配。此外,在分配的位图缓存列表池之前始终会有系统分配的大小为0x2b522c(x86系统)或0x2b5240(x64系统)字节的内核池,这一点对堆布局而言非常重要(尤其是在x64系统中),如图13所示。
图13. 统计Persistent Key List PDU的布局特征
0x03 Refresh Rect PDU
根据MS-RDPBCGR文档,RDP客户端可以通过Refresh Rect PDU请求服务端重绘会话屏幕中的1个或多个矩形区域。这个结构体中包含通用PDU头以及refreshRectPduData(可变)字段,如图14所示。
图14. Refresh Rect PDU Data
numberOfAreas字段是一个8比特无符号整数,用来定义areasToRefresh字段中Inclusive Rectangle结构的数量。areaToRefresh是包含TS_RECTANGLE16结构的一个数组,如图15所示。
图15. Inclusive Rectangle(TS_RECTANGLE16)
Refresh Rect PDU用来通知服务端一系列“Inclusive Rectangle”屏幕区域,以便服务端重绘会话屏幕区域中的一个或多个矩形区域。这个PDU基于默认打开的一个信道进行传输,信道ID为0x03ea(Server Channel ID)。当连接时序完成后,如图1所示,RDP服务端就可以接收/解析Refresh Rect PDU,并且这里最重要的一点在于Refresh Rect PDU可以多次发送。虽然TS_RECTANGLE16大小只有8字节,这意味着RDP客户端只能控制8字节数据,但这依然可以作为将数据写入内核的一个切入点。
0x04 如何通过Refresh Rect PDU将数据写入内核
正常解密后的Refresh Rect PDU如图16所示。
图16. 解密后的Refresh Rect PDU
RDPWD.sys内核模块中的WDW_InvalidateRect函数负责解析Refresh Rect PDU,如图17所示:
图17. RDPWD!WDW_InvalidateRect栈轨迹
如图18所示,WDW_InvalidateRect函数会解析Refresh Rect PDU数据流,从数据流中提取numberOfAreas字段值作为循环计数值。由于numberOfAreas属于1字节字段,最大值为0xFF,因此最大循环计数值也为0xFF。在循环中,WDW_InvalidateRect函数会分别提取TS_RECTANGLE16结构中的left、top、right及bottom字段值,将其放入栈上的一个结构中,然后作为WDICART_IcaChannelInput函数的第5个参数传入。这里需要提一句,WDICART_IcaChannelInput的第6个参数为常数值0x808,这个值对堆喷射来说非常有用。
图18. RDPWD!WDW_InvalidateRect函数
WDICART_IcaChannelInput最终会调用termdd.sys内核模块中的IcaChannelInputInternal函数。如图19所示,如果满足一系列判断条件,那么IcaChannelInputInternal函数就会调用ExAllocatePoolWithTag来分配大小为inputSize_6th_para + 0x20的内核池。因此,当RDPWD!WDW_InvalidateRect调用IcaChannelInputInternal函数时,inputSize_6th_para=0x808,并且内核池的大小为0x828。
图19. termdd!IcaChannelInputInternal中的ExAllocatePoolWithTag及memcpy操作
如何内核池分配成功完成,系统就会调用memcpy,将input_buffer_2拷贝到新分配的内核池内存中。当调用方为RDPWD!WDW_InvalidateRect时,memcpy所使用的参数如图20所示:
图20. 在windbg中观察termdd!IcaChannelInputInternal对应的memcpy操作
有趣的是,memcpy函数的源地址来自于RDPWD!WDW_InvalidateRect栈上的stRect结构,并且只有前3个DWORD会在RDPWD!WDW_InvalidateRect中设置,如图21所示。栈上的其他内存处于未初始化状态,因此容易导致信息泄露。此外,使用大小为0x808的内存空间来存储12字节的数据也是非常适用于堆喷射场景。
图21. RDPWD!WDW_InvalidateRect stRect结构集
根据这些信息,当RDP客户端向服务端发送一个Refresh Rect PDU,且numberOfAreas字段值为0xFF时,RDP服务端就会调用termdd!IcaChannelInputInternal 0xFF次。每次termdd!IcaChannelInputInternal调用都会分配0x828大小的内核池内存空间,将客户端可控的8字节TS_RECTANGLE16拷贝到该内核池中。因此,numberOfAreas字段值为0xFF的1个Refresh Rect PDU就可以分配0xFF个大小为0x828字节的内核池。从理论上讲,如果RDP客户端发送0x200次Refresh Rect PDU,那么RDP服务端就会分配大约0x20000个大小为0x828的非分页内核池。考虑到0x828大小的内核池会按照0x1000进行对齐,因此会同时占据非常大范围的内核池,客户端可控的8字节数据会被复制到每个0x1000内核池中0x02c固定偏移处。如图22所示,我们可以通过Refresh Rect PDU在内核中构建非常稳定的池喷射。
图22. RDPWD!WDW_InvalidateRect喷射
在某些情况下,当termdd!_IcaQueueReadChannelRequest修改某个指针时(图23中的v14变量),判断条件为False,此时ExAllocatePoolWithTag以及memcpy并不会被调用,代码也不会进入_IcaCopyDataToUserBuffer分支,这样将导致池无法成功分配。然而,当多次发送Refresh Rect PDU时,尽管某些池无法成功分配,我们还是可以成功形成内核池喷射。
此外在某些情况下,当RDP服务端使用完有些内核池后,可能会释放掉这些内核池,但内核池的数据并不会被清除,这样我们喷射到内核中的数据依然可以在漏洞利用过程中使用。
图23. termdd!IcaChannelInputInternal中的IcaCopyDataToUserBuffer
0x05 RDPDR Client Name Request PDU
根据MS-RDPEFS文档描述,RDPDR Client Name Request PDU在“Remote Desktop Protocol: File System Virtual Channel Extension”(文件系统虚拟信道扩展)中指定,该扩展运行在名为RDPDR的静态虚拟信道(static virtual channel)中。MS-RDPEFS协议的目的是将访问流从服务端重定向到客户端文件系统。Client Name Request是客户端发往服务端的第二个PDU,如图24所示。
图24. File System Virtual Channel Extension协议初始化
客户端使用Client Name Request PDU将主机名发送给服务端,如图25所示。
图25. Client Name Request(DR_CORE_CLIENT_NAME_REQ)
这里头部为4字节的RDPDR_HEADER,Component字段值为RDPDR_CTYP_CORE,PacketId字段值为PAKID_CORE_CLIENT_NAME。ComputerNameLen(4字节)是一个32位无符号整数,用来指定ComputerName字段的字节数。ComputerName字段(可变)是一个长度可变的ASCII或Unicode字符数组,具体格式由UnicodeFlag字段所决定,这个字符串用来标识客户端的计算机名。
0x06 如何通过RDPDR Client Name Request PDU将数据写入内核
典型的RDPDR Client Name Request PDU数据如下图所示。正常情况Client Name Request PDU可以被多次发送,对于每个请求,RDP服务端会分配一个内核池来存储这些信息,最重要的是:RDP客户端可以完全控制PDU的内容和长度。这也是将数据写入内核内存的一个绝佳切入点。典型的RDPDR Client Name Request PDU如图26所示。
图26. Client Name Request内存布局
当RDP服务端收到一个RDPDR Client Name Request PDU时,就会调用termdd.sys内核模块中的IcaChannelInputInternal函数来调度(dispatch)信道数据,然后调用RDPDR模块来解析Client Name Request PDU的数据部分。这里的IcaChannelInputInternal函数在处理Client Name Request PDU的代码逻辑上与对Refresh Rect PDU的处理逻辑相同。该函数会调用ExAllocatePoolWithTag(使用TSic标签)来分配内核内存,然后使用memcpy将Client Name Request数据拷贝到新分配的内核内存中,如图27所示。
图27. Client Name Request
到目前为止,我们已经知道服务端拷贝的数据内容及长度可以被RDP客户端可以控制,并且Client Name Request PDU也可以多次发送。正是因为这种灵活性及友好性,我们可以使用Client Name Request PDU来构造针对已释放内核池的UAF(释放后重用)漏洞利用场景,也可以用来将shellcode写入内核池,甚至可以用来将客户端可控的数据连续喷射到内核内存中。
如图28所示,我们成功实现了稳定的池分配,通过RDPDR Client Name Request PDU将客户端可控的数据写入内核池中。
图28. 利用Client Name Request PDU实现稳定池分配
0x07 检测及缓解
CVE-2019-0708是针对RDP的一个严重漏洞,可以被未经身份认证的攻击者所使用。根据MSRC安全公告,Windows XP、Windows 2003、Windows 7以及Windows 2008都受该漏洞影响。大家应该尽快给自己的Windows系统打上补丁,以缓解该威胁。如果条件允许,用户应当禁用或者限制通过外部接口访问RDP资源。
0x08 总结
在本文中,我们介绍了通过RDP PDU将数据写入内核的3种方法:
- Bitmap Cache PDU可以让RDP服务端在分配
0x2b5200大小的内核池后,分配0xc3870大小的内核池,并写入客户端可控的数据,然而无法多次执行分配0xc3870大小的内核池操作。 - Refresh Rect PDU可以用来喷射
0x828大小的多个内核池,这些内核池以0x1000方式对齐,并将客户端可控的8字节写入0x828大小的每个内核池中。 - RDPDR Client Name Request PDU可以用来喷射大小可控的内核池,并填充客户端可控的数据。
我们认为还有其他未公开的方式,可以让CVE-2019-0708的利用方式更加简单及稳定。用户应当采取措施,通过前面提到的缓解方法保护可能存在风险的系统。




























