漏洞说明

漏洞cve-2019-0708是一个Windows 系统的远程桌面服务漏洞。从应用层面看,对应的是rdp服务的3389端口。再往深了了解,这个漏洞跟一个termdd.sys的内核文件有关,具体的漏洞点在IcaBindVirtualChannels 函数中。

本来想对比下补丁前后变化,无奈系统没有更新成功。把cve-2019-0708的补丁更新上了,漏洞仍然没有修补上,termdd.sys文件也没变化。只能借用一张网上的对比差异图片了

先大致分析一下变化:在进行_IcaBindChannel前,先对比v4+88是否等于字符串MS_T120,然后再决定第三个参数为变量,还是固定值31。

漏洞触发过程

虚拟通道绑定

本地打开伪源码,直接定位到IcaBindVirtualChannels中调用_IcaBindChannel的位置。

要想给第三个、第四个参数指定值,首先要知道他们是干嘛的。往前分析,在一个IcaFindChannelByName的函数调用中用到了v6。根据这个函数名猜测,是用来根据名字找到通道,返回值大概率就是对应通道的结构体。

既然要根据通道名称找到通道结构体,肯定需要两个变量:1. 通道结构体链、2. 通道名称。第二个参数为固定值5,参数1和参数3就分别是其中之一了。

进入到函数内部,看看能不能找到线索。又碰到一个函数IcaFindChannel(找通道),用到了参数a1、a2,已知参数a2是int型整数,那么a1就是通道结构体链了。a3也就是通道名。

接着往下看,在一个while..do循环中,用到了a3,并且拿a3和一个循环体V9进行一定的偏移做对比。通过前边的分析,a3为通道名字,这里的v9就是对应的通道结构体了。也可以在最后看到,如果相等,跳出循环,返回v9。

下边调试看一下,是怎么个事。先设置断点在termdd!IcaBindVirtualChannels函数,用正常的rdp客户端去连接,成功触发断点。

从而找到调用IcaFindChannelByName函数的偏移量。

先来查看一下参数3是不是之前猜测的通道名。是一个字符串MS_T120

下边在IcaFindChannelByName调用的位置设置断点,查看通道名,依次分别为:MS_T120、CTXTW 、rdpdr、rdpsnd、cliprdr、drdynvc共6个。

正常情况下,不会重复创建通道,也就不会运行到已存在通道下边的逻辑,触发不了IcaBindChannel。

下边就需要看看通道是怎么创建的。

通过前边的测试,这些通道都是在进行RDP连接请求时创建的。下边抓一下包,看看怎么个情况。

连接的过程产生了很多包,直接找到第一个rdp数据包。可以看到,客户端请求创建了4个通道,后边的数据包也没看到另外两个的身影。

服务端返回的是通道对应的通道ID

但是在后边的通道调用中,却是6个通道ID

这里找不到更多信息了,思路回到前边猜测的可能的漏洞触发条件,把断点设到IcaFindChannelByName调用成功后才会运行的代码段。

通过正常的RDP客户端去连接,并不能触发这个断点。通过自己编写脚本,模拟RDP客户端,不同的是,向通道“MS_T120”发送绑定请求,成功触发断点。

现在成功触发了漏洞点,但是还不知道可以做什么。下边分析一下MS_T120通道的生命周期。

通道创建过程

在反编译的代码函数中,看到一个IcaCreateChannel的函数,可能跟创建通道有关系。

设置一个断点,看看调用情况

内核层面的看不到更多创建信息,找到用户层对应的代码,进一步分析。通过一步步分析,在rdpwsx中找到了有用的信息。在rdpwsx!MCSCreateDomain中,直接把MS_T120硬编码在代码中,用来调用IcaChannelOpen,如果打开成功,通过CreateIoComptionPort()函数把通道结构绑定到完成异步IO端口句柄上句柄CompletionPort。

下边再看看对CompletionPort的操作,通过交叉引用,找到这几个。前两个直接排除,第三个和后两个都是删除和清除,肯定也不是。

打开MCSInitialize函数,看看怎么个情况。

先创建 I/O 完成端口 ,将创建的 I/O 完成端口句柄保存到全局变量 CompletionPort。如果创建失败,关闭之前创建的对象,然后返回错误码 11。

如果成功,创建线程,并将创建的 I/O 完成端口句柄传递给线程,线程主函数IoThreadFunc

接着跟进函数IoThreadFunc。调用 GetQueuedCompletionStatus 函数等待从 I/O 完成端口队列中取出已完成的异步 I/O 操作。如果成功获取完成状态,会得到相关的信息,包括完成键(CompletionKey)、传输字节数(NumberOfBytesTransferred)、重叠结构指针(Overlapped)等。

指定临界区对象的所有权,也就是设置当前栈顶。这里设置为缓冲区偏移8

可以看出,缓冲区偏移64为通道地址句柄,偏移176为数据的起始地址。

如果缓冲区偏移184的值等于2,就关闭通道。通过前边的分析知道v4是读取的数据量,如果读取了32个字节,进行其他操作。

184-176 = 8
32 / 2 = 16

也就是说,向通道发送的数据,第9位等于2,并且发送16个字符,就可以触发关闭通道流程。

分析到这里,基本清楚了。下边编写代码证实一下,主要就是向MS_T120通道

漏洞点触发

发送数据,并且第9位为2

free_mst120_channel = 'A' * 8 + '\x02' + '\x00' * 7

结果win2008蓝屏了,

从debug信息来看,崩溃原因是在IcaDereferenceChannel处。从函数名来看,这是一个解引用通道的函数。我们已经关闭了一次通道,系统再次关闭通道,导致报错。

至此,整个触发过程分析结束。

但是,现在只能进行DOS攻击,并不能RCE。下边还需要分析exp构造

利用原理

通过参考文章CVE-2019-0708:远程桌面服务漏洞综合分析,及msf文档,知道漏洞rce过程:

CVE-2019-0708 BlueKeep RDP 远程 Windows 内核释放后使用

RDP,我们能够释放与 MS_T120 通道关联的数据结构。单独释放结构并没有多大用处,但控制其内容却很有用。使用 UAF(释放后使用),目标是释放一个对象,然后分配一个假对象代替它。通过用我们自己的数据替换真实对象的内容,我们可以对使用它的代码进行更广泛的控制。我们可以用我们的假通道结构做什么完全取决于该结构的用途。

能够替换的通道结构由函数 IcaFindChannel 和 IcaFindChannelByName 返回。为了找到通过操纵这个通道结构可以做什么,我需要找到它的使用位置。

虚拟通道分析

虚拟通道结构体:https://learn.microsoft.com/en-us/previous-versions/windows/embedded/aa513856(v=msdn.10)

#define CHANNEL_NAME_LEN 7
typedef struct tagCHANNEL_DEF {
  char name[CHANNEL_NAME_LEN + 1];
  ULONG options;
} CHANNEL_DEF,*PCHANNEL_DEF,**PPCHANNEL_DEF;
  • name
    包含虚拟通道名称的以 Null 结尾的字符串。 虚拟频道名称可以包含 1 到 CHANNEL_NAME_LEN 个字符。
  • options
    指定该虚拟通道的选项。 下表显示了可以组合使用的可能值。
Value Description
CHANNEL_OPTION_ENCRYPT_CS 加密客户端到服务器的数据。 0x10000000
CHANNEL_OPTION_ENCRYPT_RDP 根据远程桌面协议 (RDP) 数据加密进行加密。也就是说,如果 RDP 数据已加密,则也对此通道进行加密。 0x40000000
CHANNEL_OPTION_INITIALIZED 通道已初始化。该值由 VirtualChannelInit 或 VirtualChannelInitEx 函数设置。 0x80000000
CHANNEL_OPTION_REMOTE_CONTROL_PERSISTENT 该通道被声明为远程控制持久通道。这意味着当启动一个会话来隐藏当前会话时,或者当远程控制连接或断开连接到客户端的会话时,服务器端不会关闭通道。有关详细信息,请参阅远程控制持久虚拟通道)。 0x00100000
CHANNEL_OPTION_SHOW_PROTOCOL 影响服务器端如何接收VirtualChannelWrite)发送的数据。如果设置了该值,则每个数据块前面都有一个 CHANNEL_PDU_HEADER 结构。如果未设置此值,则数据块仅包含指定给 VirtualChannelWrite 的数据。 0x00200000
CHANNEL_OPTION_COMPRESS 保留以供将来使用。 0x00400000
CHANNEL_OPTION_COMPRESS_RDP 保留以供将来使用。 0x00800000
CHANNEL_OPTION_ENCRYPT_SC 保留以供将来使用。 0x20000000
CHANNEL_OPTION_PRI_HIGH 保留以供将来使用。 0x08000000

参考zero day initiative 对虚拟通道的分析,我们知道。客户端请求的所有虚拟通道都是使用 termdd!IcaCreateChannel()创建的。然后,指向这些通道结构的指针存储在一个表中,我们将其称为ChannelPointerTable。

当与 RDP 客户端建立连接时,包括 MS_T120 在内的所有静态虚拟通道均由 Windows RDP 服务器在内部初始化,并由 ChannelPointerTable 指向。

ChannelPointerTable 上边的通道是由IcaBindChannel写上去的。

设置断点:termdd!IcaBindChannel,看看怎么个事。从调用链可以看出来,此时是首次创建通道。

通过上篇分析,我们知道第一个参数是通道结构指针,第三个参数是下标。

在IcaFindChannelByName中我们知道,虚拟通道指针偏移268为该虚拟通道名字

十进制268转换为16进制为0x10c。16进制1f转换成10进制是31,也就是说,虚拟通道MS_T120默认在通道指针表中的下标为1f

构造了一条windbg命令,bp termdd!IcaBindChannel "da rcx + 0x10c; r r8"来查看所有的虚拟通道,以及在通道地址表中的下标,

整理如下

Index Number ChannelControlStructure Pointer
0 指针指向RDPDR
1 指针指向RDPSND
2 指针指向CLIPRDR
3 指针指向DRDYNVC
4
5
6
7 指针指向CTXTW
8
······
1f 指针指向 MS_T120

这是正常情况下的虚拟通道绑定情况,如果客户端用户单独制定某个通道,会是什么情况哪?我们利用脚本编写程序,向服务端发送3个通道绑定请求,三个通道分别是;['rdpdr', 'MS_T120', 'rdpsnd']。

可以看到两个MS_T120虚拟通道的下标并不一样,我们发送的MS_T120虚拟通道分配的下标是1。

原本的rdpsnd从1变成了2。

也就是说,"MS_T120"和"CTXTW "是服务端默认绑定的。我们发起的虚拟通道其中重复的又绑定了一次。

当客户端在“MCS Connect 初始和 GCC 创建”期间指定名称为MS_T120\x00的通道时,termdd!IcaCreateChannel() 调用termdd!IcaFindChannelByName() 并返回下标 0x1F 中现有的 MS_T120 通道结构。然后,该通道结构被视为新的虚拟通道条目,并在“MCS Attach User Request”期间存储在其他下标(在本例中为下标1)中。

Index Number ChannelControlStructure Pointer
0 指针指向RDPDR
1 指针指向 MS_T120
2 指针指向RDPSND
3
4
5
6
7 指针指向CTXTW
8
······
1f 指针指向 MS_T120

随后将精心构造的数据发送到 MS_T120 通道,termdd.sys 使用 termdd!IcaCloseChannel() 关闭通道,并清除插槽处的指针。(运行示例中的下标 1)但是,插槽 0x1F 中的同一指针并未被清除。 随后,当连接终止时,将调用 RDPWD!HandleDisconnectProviderUlt(),进而调用 termdd!IcaChannelInputInternal( ) 并尝试使用插槽 0x1F 处的指针再次销毁已释放的 MS_T120通道结构。销毁过程由通道结构中的指针指向的函数调用。这会导致释放后使用的情况。

也就是说,如果我们可以伪造通道,就可以执行任意代码。那么,怎么伪造这个通道?

堆喷射

名词解释-维基百科

堆喷射实际上并不会利用任何安全问题,但它可以用来使漏洞更容易被利用。堆喷射本身不能用来打破任何安全边界:需要一个单独的安全问题。

利用安全问题通常很困难,因为各种因素都会影响这一过程。内存和时序的偶然对齐引入了很多随机性(从攻击者的角度来看)。堆喷射可用于引入大量订单来补偿这一点并增加成功利用的机会。堆喷射利用了这样一个事实:在大多数体系结构和操作系统上,大型堆分配的起始位置是可预测的,并且连续分配大致是连续的。这意味着每次运行堆喷射时,喷射的堆将大致位于相同的位置。

漏洞利用通常使用特定字节来喷射堆,因为存储在堆上的数据具有多种作用。在利用安全问题期间,通常可以使应用程序代码从内存中的任意位置读取地址。然后代码使用该地址作为要执行的函数的地址。如果漏洞利用可以强制应用程序从喷射堆中读取该地址,则当代码使用该地址作为函数指针并将其重定向到喷射堆时,它可以控制执行流程。如果漏洞利用成功将控制流重定向到喷射的堆,则那里的字节将被执行,从而允许漏洞利用执行攻击者想要的任何操作。因此,堆上的字节仅限于表示堆喷射本身内的有效地址,保存目标体系结构的有效指令,因此应用程序不会崩溃。因此,通常使用单个字节进行喷射,该字节可转换为目标体系结构上的有效地址和 NOP) 或类似 NOP 的指令。这使得堆喷射可以充当非常大的 NOP sled(例如,0x0c0c0c0c 通常用作非规范 NOP[2])

非分页内存分配

看下这段代码,分配一块标记为0x63695354u(ciST)、大小为数据大小(v6)+0x38的非分页内存块,然后把通道的数据从偏移14处开始写入这个内存块。

在 Windows 7 及更低版本上,非分页池从固定地址 (Nt!MmNonPagedPoolStart) 开始。

我们知道非分页池从哪里开始,但我们不知道我们的 shellcode 驻留在非分页池中的哪里……但我们可以在某种程度上控制这一点。

非分页内存的大小由于缺乏分页而受到限制。因此,内核通常会避免将其用于任何不需要的地方。也就是说,内存利用率相当低。

我们能做的就是选择一个地址(例如非分页池中的 500 MB),由于内存利用率较低,该地址不太可能被取消分配。接下来,我们喷射足够的 shellcode 副本来填充非分页池,超过我们选择的地址,从而导致 shellcode 的副本很可能驻留在此处。

由于大多数在非分页池上频繁分配的对象都非常小,因此我们可以创建许多小于我们的 shellcode 大小(在所选地址之前)的空洞,以确保其他分配被分配在那里。

然后,我们可以将假通道结构中的 调用函数 指针设置为我们选择的地址,从而每当通道接收到消息时就会执行 shellcode。

EXP执行过程

先等待RDP握手完成,注册MS_T120通道,接着释放掉MS_T120虚拟通道。构造内核和用户模式shellcode,通过RDPSND虚拟通道发送到服务端,写入内存。构造虚假虚拟通道结构,替换掉调用函数指针,写入内存。关闭连接,触发对函数的调用。

RDP握手直接参照官网[MS-RDPBCGR]:远程桌面协议:基本连接和图形远程处理中规定的数据包格式,进行编写即可

注册MS_T120通道,参照服务器网络数据(TS_UD_SC_NET)构造通道列表数据包,实际exp不单单要用到MS_T120虚拟通道,还需要用到RDPSND虚拟通道。把构造好的数据包发送给服务端即可。

构造内核和用户模式shellcode,为什么要构造两个?首先漏洞触发点在内核模式,所以首先执行内核模式任意代码。内核模式任意代码不能满足我们的需求,比如要列目录,内核模式需要编写大量的代码才能实现,用户模式只需要执行dir命令即可。借助内核模式任意代码执行借助异步过程调用(APC)执行用户模式shellcode。最终返回一个用户模式的权限。

直接参照大佬写好的shellcode模板:https://github.com/RICSecLab/CVE-2019-0708/blob/master/shellcode.s,需要自行替换其中的地址和端口等信息。

shellcode前边还要拼接函数调用地址,多次向内存写入。

通过RDPSND虚拟通道发送到服务端,写入内存。这里选用RDPSND通道是有原因的,每个通道每次可携带的数据量不一样,可发送的次数也不一样。最终通过对比,RDPSND效果最好。

释放掉MS_T120虚拟通道,参照之前的分析,构造0x00*8 + 0x02 +0x00*22数据包,通过虚拟通道MS_T120发送给服务端。

因为释放了一个MS_T120通道,接着向非分页内存填充数据,

参照http://www.tssc.de/winint/Win10_17134_ntoskrnl/_ERESOURCE.htm结构体

关闭连接,触发对函数的调用。这一步断开socket连接即可。