图2:
看到其中更改了调用_IcaBindChannel的方式,如果比较的字符串等于“MS_T120” 则此调用的第三个参数设置为31。由于只有v18为false时才会执行补丁后的逻辑,所以我认为要触发该漏洞,则v17+268应该为“MS_T120”字符串,于是向前查找v17+268的线索,在返回v16的IcaFindChannelByName函数内部找到了答案。
图3:
可以看到参数a3应为要寻找通道的名称字符串,在循环中遍历对比v9+268,而返回值v9可以认为是目标通道的结构,而v9偏移268是通道结构中的通道名称。上图中判断如果v9+268等于v3则返回v9。
通过以上分析,得出结论“MS_T120”是通道的名称,下一步就要找到如何调用这个函数,和参数的名称如何设置为“MS_T120”。
之后我在调用IcaFindChannelByName函数的地方下了一个断点,然后用正常的RDP客户端进行连接,当每次触发断点的时候检查下调用堆栈和通道的名称。
图4:
图5:
图5是第一次命中断点时,通道名称参数为“MS_T120”。后续的通道名称为CTXTW,rdpdr,rdpsnd,drdynvc,cliprdr。
然而根据图1的逻辑可知,仅当FindChannelByName函数调用成功,即通道已存在的情况下才会命中存在漏洞的代码段,命中后会创建“MS_T120“通道,所以要触发此漏洞,需要使用“MS_T120“作为通道名称再次调用IcaBindVirtualChannels。
回顾图4的调用堆栈可知最上层的调用是类似AcceptConnection的函数,根据名称猜测应该是在接受连接的时候创建通道,所以现在的目标就是寻找一个方法在连接后打开“MS_T120“通道。下一步准备用wireshark抓取RDP连接包进行分析,看看能不能找到一些线索。
win7系统默认远程桌面连接是用tls协议加密的,所以有以下两种方法解决,经过测试皆可达到目的:
1.(麻烦)直接抓取加密后的报文,再解密,方法如下
整体思路是客户端运行wireshark抓取加密的报文,再用名为mimikatz的开源工具导出RDP服务端的RSA私钥.pxf文件,再用openssl转成wireshark可识别的.pem文件格式。
但由于windowsXP及以上的系统内tls协议默认为双方协商加密算法和密钥交换算法,默认的加密算法是RSA-AES,没有问题,但默认的密钥交换算法是ECDHE椭圆曲线。基于此交换算法即使得到RSA私钥也无法解密出用于加密传输通讯内容的AES对称密钥(不了解椭圆曲线可以自行百度),所以还要win+R打开gpedit.msc组策略编辑器将ssl密码套件设置为不带DHE的弱交换算法,如图6,再重新抓包后再按如上步骤将.pem导入wireshark即可。
图6
2.(简单)修改系统设置为不使用ssl加密
在组策略编辑器中如图7所示将指定安全层改为RDP即可。
图7
简单设置完TPKT协议3389端口后,发现第二个RDP报文如下:
图8
此数据包包含传递给IcaBindVirtualChannels的六个通道中的四个,缺少“MS_T120”和CTXTW。通道在报文的顺序和在程序中打开的顺序一致。
由于报文中并未出现MS_T120和CTXTW通道,但程序中在其余通道前打开了它们,所以这两个通道应该是内部使用的通道,默认自动打开。
基于上述原因,想测试一下如果自己实现RDP协议,手动将MS_T120添加到通道数组中会不会像其他通道一样在程序中命中IcaBindVirtualChannels函数内的断点。
实现RDP协议需要用到kali中的metasploit模块,检查发现其中已经包含有RDP协议的完整Ruby模块,路径为/usr/share/Metasploit-framework/lib/msf/core/exploit/rdp.rb。在其中找到发送通道数组相关信息如图:
图9
可以看到在构造RDP连接信息的函数中已经包含了“MS_T120“通道,于是仿照现有的RDP测试模块, 创建简单ruby测试模块,然后打开RDP服务端wireshark抓包,同时kali 用msf装载测试模块测试RDP服务端。
图10
可见带有MS_T120的报文已经接到,然后我将断点移动到只有FindChannelByName成功的情况下 才命中的代码后程序成功执行到了具有漏洞部分的代码段。
目前从这个函数已经得不到更多线索了,想到既然MS_T120通道是内部使用和创建的,于是便寻找下创建的源头,在termdd模块内找到了IcaCreateChannel,创建通道的函数,在此函数上下断点,在进行RDP连接。断下后调用堆栈如下图:
图11
从上图可以看到应用层ntdll_NtCreateFile打开设备符号连接,在往下查看IcaChannelOpen 函数内容并无线索,从名字观察猜测其下的MCSCreateDomain也许有创建内部通道的逻辑。如图:
图12
果然在其中发现了以硬编码的方式用MS_T120调用IcaChannelOpen,,再深入函数内部发现第四个参数v5+36是用ZwCreateFile打开的文件句柄,这里猜想v5应该是通道结构,然后程序调用CreateIoCompletionPort创建此通道的异步IO端口,用于异步IO。而且第二个CompletionPort参数不为NULL,查文档得知会有一个处理端口IO的函数,于是对此端口句柄查找交叉引用如图:
图13
看名称先从MCSInitialize类似初始化的函数入手如图:
图14
可以看到创建完端口后用端口句柄做参数创建了新的线程,进入新线程如图:
图15
GetQueuedCompletionStatus函数检查发送到IO端口的数据,若成功接到数据则发给MCSPortData。
MCSPortData处理数据函数如下图:
注:win7 64位没有MCSPortData函数,此函数被就地内联,但代码逻辑和非内联版本一样,故不做区分。
图16
可知缓冲区偏移30的数据如果是2则会关闭通道,而ReadFile的第二个参数证明了缓冲区的数据起始地址为偏移29。为了验证这个想法,编写了一个简易的msf模块,具备在打开的通道上发送信息的功能,测试了发送“lichao”,抓包截图如下:
图17
windbg截图如下:
图18
由此可以证明,我们可以读写MS_T120通道,鉴于此漏洞为UAF类型,所以尝试发送满足关闭通道的数据看看有什么效果,即上文所述缓冲区偏移30为2,如图:
图19
出现此错误的原因看似并不是二次释放,因为msf模块中默认带有额外填充式发送数据包,从而导致释放的结构中填充了随机数据作为代码运行,而此代码运行不符合内核IRQL级别。根本原因还是在于系统试图关闭我已经关闭了的通道MS_T120,导致二次释放。
观察崩溃转储的调用堆栈信息可以发现驱动层有一个函数名为SignalBrokenConnection,属于RDPWD.sys模块的一员,如图:
图20
IcaChannelInput函数的意思是找到一个指定ID对应的通道,第三个参数31即为通道绑定的ID,这与图1中加了补丁后的绑定MS_T120通道ID强制为31的代码相对应。
分析到这里,漏洞的成因就比较清晰了,由于内部通道MS_T120第一次由系统绑定ID为31,而后在易受攻击的代码中被再次绑定到另一个ID,导致一个MS_T120通道结构存在两个引用,而且可以通过引用控制通道数据,随后通过第二次绑定的引用释放掉MS_T120通道结构,最后断开连接时系统将再次释放此结构,达成Double-Free。
下面就要寻找一些在系统内部释放通道时对此通道数据有存在关联的代码,对于UAF类型的漏洞利用大致就是释放掉MS_T120通道,然后在原来的位置上分配一个假对象,最后系统自动释放时调用伪造的对象数据完成利用。
由创建通道的代码逻辑可知通道数据结构在内核非分页内存中分配,所以就要寻找在非分页内存中写任意数据的方法,随后我们在造成崩溃的二次free中查看调用堆栈,发现IcaChannelInputInternal函数中有调用通道结构中的函数,如图:
图21
可以看到通道结构体偏移256字节是一个二重函数指针,那么就可以从v21这里劫持控制流,然后通过追踪上次发送通道消息的调用堆栈发现IcaChannelInputInternal函数在处理消息数据的代码中有些线索,如图:
图22
图23
图22的所有代码均在一个while(1)循环中,图23在while循环外面,根据逻辑可知只有命中最开始的break跳出循环才会执行图23分配内存,否则无论如何都会执行IoCompleteRequest结束IRP,返回观察图21,22,23感觉图21中v21函数疑似数据处理函数,此函数的参数都与之后分配非分页内存的大小v32,memmove的大小v6和拷贝地址v7相关,v7应该是数据指针,而且根据图23可知实际分配的内存大小v32比数据大小多56字节。
这样我们已经找到在满足break条件下可以在非分页内存分配任意大小并写入任意数据的方法,现在返回观察需要二次利用的通道结构内存是如何分配的,如图:
图24
可知每个通道结构固定分配352字节,现在的思路已经很清晰了,主动绑定MS_T120通道为非31的ID,然后发送数据包释放MS_T120通道,然后利用其他通道发送数据达到任意地址写任意数据的堆喷射占据释放的MS_T120结构,最后正常断开连接系统调用伪造的数据执行内核shellcode。
还有一些关键点,比如win7中内核内存没有DEP数据执行保护,非分页内存启始地址为
nt!MnNonPagedPoolStart,定位shellcode比较简单。
直接查看msf的bluekeep模块发现作者zerosum0x0的实现选择了RDPSND信道作为辅助发送数据通道,此通道恰好可以满足图22中break的条件,顺利分配内存。RDPSND属于音频播放功能,在win7上默认开启,server 2008 默认关闭,可以通过注册表或远程桌面配置进行修改,前文提到分配的非分页内存会比实际发送的数据多56字节,所以经过计算需要发送296字节数据,这样才会分配352字节内存,与MS_T120通道大小相同。而在设置伪造通道结构的函数指针指向shellcode时尽量选取一个较大的地址,这样的地址内存利用率较低,大概率写入shellcode,而非分页内存 启始地址在不同环境中略有区别,所以msf模块针对不同场景设置了不同的选项,选项还有为虚拟机环境设置的地址,主要由于虚拟机内存设置的可交换选项导致子系统会在非分页内存前分配大量PTE页表,导致NPP地址变化。 如图:
图25
另外一个问题是,在这种情况下命中的shellcode所在模块是termdd.sys属于内核模块,IRQL级别为DISPATCH_LEVEL,如果不做处理则shellcode只能执行相当受限的功能,所以需要方法完成R0到R3的转换,这一点和之前著名的‘永恒之蓝’的做法大同小异,msf现有的payload思路是准备两段shellcode,一段用户态,一段内核态,分两段的原因在于对通道一次发送的数据有最大限制为0x400 – 0x48 = 0x3B8 字节,其中0x48为固定大小信息头,因为不足以容纳所有的shellcode,所以作者采用两段shellcode交叉布局的方式进行堆喷,如:内核shellcode+用户态shellcode+内核shellcode+用户态shellcode 。。。其中每段分配的内存都是0x400,然后利用内核态shellcode执行APC注入spoolsrv.exe进程,并拷贝R3 shellcode到目标进程空间,完成R0到R3的转换。
用户态payload分为两个部分,第一个部分是通过“egg hunter”(根据代码特征寻找)的方式找到内核态payload,第二部分是要作为APC执行的用户态payload,如图:
图26
egg_loop是替换过变量值的目的寻找内核态shellcode,上下两个EGG是用户态shellcode的寻找标志用作分隔,最后是被注入进程要执行的R3 payload。(默认是弹 system权限的 shell连接,是MSF众多可选payload之一)
而内核态shellcode就比较长了,涉及到诸多功能,这也解释了为什么0x400的通道空间放不下的原因,主要是内核shellcode + R3 payload比较大,在开始部分作者硬编码了一些需要用到的函数和结构的hash和偏移,由于可选的测试环境都是win7/2008,所以这些值都是通用的。如图:
图27
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2019-12-16 13:27
被MSA_Li编辑
,原因: 补上不知为何丢失的图片