作者:SungLin@知道创宇404实验室
时间:2019年9月18日
原文链接:https://paper.seebug.org/1035/
0x00 信道的创建、连接与释放
通道的数据包定义在MCS Connect Inittial PDU with GCC Conference Create Request中,在rdp连接过程如下图所示:
信道创建数据包格式如下:
在MCS Connect Inittial中属于Client Network Data数据段,MS_T120
将会在连接一开始的时候通过函数termdd!_IcaRegisterVcBin
创建一个虚拟通道id是0x1f大小为0x18的结构体,之后就调用termdd!IcaCreateChannel
开始创建大小为0x8c的信道结构体之后将会与虚拟通道id是0x1f绑定,也就是这个结构体将会被我们利用
信道的定义字段主要是名字加上配置,配置主要包括了优先级等
在server对MCS Connect Inittial应答包,将会依次给出对应虚拟通道的id值:
在rdp内核中依次注册的值对应应该是0、1、2、3, MS_T120信道将会通过我们发送的用户虚拟id为3的值再一次绑定,首先通过termdd!_IcaFindVcBind
找到了刚开始注册的虚拟通道id是0x1f,如下所示:
但是在termdd!_IcaBindChannel
时,却将我们自定义的id值为3与信道结构体再一次绑定在一起了,此信道结构体就是MS_T120
同时我们自己的用户id将内部绑定的0x1f给覆盖了
我们往信道MS_T120发送数据主动释放其分配的结构体,其传入虚拟通道id值为3通过函数termdd!IcaFindChannel
在channeltable中查找返回对应的信道结构体:
下图为返回的MS_T120信道结构体,其中0xf77b4300为此信道可调用的函数指针数组:
在这个函数指针数组中主要存放了三个函数,其中对应了termdd!IcaCloseChannel
、termdd!IcaReadChannel
、termdd!IcaWriteChannel
我们传入释放MS_T120信道的数据如下,字节大小为0x12,主要数据对应了0x02
之后将会进入nt! IofCompleteRequest
函数,通过apc注入后,将会通过nt! IopCompleteRequest
和nt!IopAbortRequest
进行数据请求的响应,最终在termdd!IcaDispatch
完成我们发送数据的的请求,_BYTE v2
就是我们发送的数据,所以我们发送的数据0x02将会最终调用到IcaClose函数进入IcaCloseChannel函数,最后主动释放掉了MS_T120
信道结构体
0x01 通过RDPDR信道进行数据占位
我们先来了解下rdpdr信道,首先rdpdr信道是文件系统虚拟通道扩展,该扩展在名为rdpdr的静态虚拟通道上运行。目的是将访问从服务器重定向到客户端文件系统,其数据头部将会主要是两种标识和PacketId字段组成:
在这里我们刚好利用到了rdpde客户端name响应的数据来进行池内存的占位
在完全建立连接后,将会创建rdpdr信道的结构体
在window7中,在建立完成后接收到server的rdpdr请求后,通过发送客户端name响应数据,将会调用到termdd! IcaChannelInputInternal
中的ExAllocatePoolWithTag分配非分页池内存,并且其长度是我们可以控制的,基本满足了UAF利用的需求:
可是在windowsxp中,直接发送client name request将会导致内存分配失败,直接进入termdd! _IcaCopyDataToUserBuffer
,并且在Tao Yan and Jin Chen[1]一文中也提到了通过发送client name request在触发一定的条件后将会绕过termdd!_IcaCopyDataToUserBuffer
而进入ExAllocatePoolWithTag分配我们想要的非分页内存,而打破条件如下:
我们先来看看最开始信道结构体的创建,我们可以发现从一开始创建信道结构体的时候,将会出现两个标志,而这两个标志是按照地址顺序排列的,而在上面需要打破的条件中,只要channelstruct +0x108的地址存放的是同一个地址,循环就会被break
我们发送一个正常的rdpdr的name request数据包,头部标识是0x7244和0x4e43
经过termdd!_IcaCopyDataToUserBuffer
之后,将会进入nt!IofCompleteRequest
,在响应请求后进入rdpdr!DrSession::ReadCompletion
,此函数处理逻辑如下,其将会遍历一个链表,从链表中取出对应的vftable函数数组
遍历第一次取出第一张函数数组
传入我们发送的数据后,通过函数数组调用rdpdr!DrSession::RecognizePacket
进行读取
判断头部标志是否为(RDPDR_CTYP_CORE)0x7244
接着将会读取函数vftable第二个地址,进行转发
如下图可以看到rdpdr的数据包处理逻辑
rdpdr经过一系列数据包处理后最终进入了我们关心的地方,将会传入channelstruct通过调用termdd! _IcaQueueReadChannelRequest
进行标志位的处理
最初rdpdr的channelstruct的标志位如下
经过函数termdd! _IcaQueueReadChannelRequest
对此标志的处理后变成如下,所以下一个数据依然会进入termdd!_IcaCopyDataToUserBuffer
,导致我们进行池喷射的失败
回到rdpdr头部处理函数rdpdr!DrSession::RecognizePacket
,我们发现在链表遍历失败后将会进行跳转,最后将会进入读取失败处理函数rdpdr!DrSession::ChannelIoFailed
,然后直接return了
我们构造一个头部异常的数据包发送,头部标志我们构造的是0x7240,将会导致rdpdr!DrSession::RecognizePacket
判断失败,之后将会继续遍历链表依次再取出两张函数数组
最后两个函数数组依次调用rdpdr!DrExchangeManager::RecognizePacket
和rdpdr!DrDeviceManager::RecognizePacket
,都会判断错误的头部标志0x7240,最后导致链表遍历完后进行错误跳转,直接绕过了termdd! _IcaQueueReadChannelRequest
对标志位的修改,将会打破循环
最后我们连续构造多个错误的数据包后将会进入ExAllocatePoolWithTag
,分配到我们需要的非分页内存!
0x02 win7 EXP 池喷射简要分析
首先被释放的MS_T120池大小包括是0x170,池的标志是TSic
分析Win7 exp 可以知道数据占位是用的rdpsnd信道,作者没有采用rdpdr信道,应该也和喷射的稳定性有关,rdpsnd喷射是再建立完了rdpdr初始化后开始的,在free掉MS_T120结构体前,发送了1044个数据包去申请0x170大小的池内存,这样做可以说应该是为了防止之后被free掉的内存被其他程序占用了,提高free后内存被我们占用的生存几率
占位被free的实际数据大小为0x128,利用的中转地址是0xfffffa80ec000948
之后开始池喷射,将payload喷射到可以call [rax] == 0xfffffa80ec000948的地方,喷射的payload大小基本是0x400,总共喷射了200mb的数据大小,我们先来看下喷射前带标志TSic总共占用池内存大小是58kib左右
喷射完后带TSic标志池内存大小大约就是201mb,池内存喷射基本是成功的,我的win7是sp1,总共内存大小是1GB,再喷射过程中也没有其他干扰的,所以喷射很顺利
图中可以发现基本已经很稳定的0x400大小的池喷射payload,地址越高0x400大小的内存基本就很稳定了
最后断开连接时候,被free的内存已经被我们喷射的0x128大小的数据给占用了
执行call指令后稳定跳转到了我们的payload,成功执行!
参考链接:
[0] https://github.com/rapid7/metasploit-framework/pull/12283
[1] https://unit42.paloaltonetworks.com/exploitation-of-windows-cve-2019-0708-bluekeep-three-ways-to-write-data-into-the-kernel-with-rdp-pdu/
[2]https://wooyun.js.org/drops/%E7%BE%8A%E5%B9%B4%E5%86%85%E6%A0%B8%E5%A0%86%E9%A3%8E%E6%B0%B4%EF%BC%9A%20%E2%80%9CBig%20Kids%E2%80%99%20Pool%E2%80%9D%E4%B8%AD%E7%9A%84%E5%A0%86%E5%96%B7%E6%8A%80%E6%9C%AF.html