Loading... > MS17-010永恒之蓝分析 --- ## PagedPool 和 NoPagedPool Windows把虚拟地址分为用户地址空间和系统地址空间,用户地址空间是给应用程序使用的,系统地址空间是给系统核心和驱动程序使用的。 系统地址空间分为分页池和非分页池。**PagedPool**是分页内存,简单来说就是物理内存不够时,会把这片内存移动到硬盘上,而**NonPagedPool**是无论物理内存如何紧缺,都绝对不把这片内存的内容移动到硬盘上。在内核里,**PagedPool**和 **NonPagedPool**都是可读可写可执行的, 而且没有类似**VirtualProtect**之类的函数。 分页池是指映射到分页文件的虚拟地址,当要使用该地址时才交换到物理内存中,由系统来调度;非分页池是指直接在物理内存中分配的内存。“页面缓冲池”就是进程占用的分页池中的虚拟内存,是进程调用某些系统功能时,由系统核心或者驱动程序分配的。如果一个程序占用的页面缓冲池内存不断增大,就是内存泄露,通常应该是创建或打开了句柄没有关闭。 系统资源主要有四种:分页池、未分页池、系统分页表和系统缓存。系统缓存容易理解,系统分页表则是用来保存所有线程使用到的堆栈(Windows所有的线程都具有自己的堆栈),分页池和未分页池则是所有程序的核心模式组件使用到的内存部分,区别只是未分页池里分配的内存是不能交换到虚拟内存上面的,分页池上的则可以(从而可能保存到磁盘上去,当程序需要这些页面的时候,再读到内存里面来)。 例如设备驱动就使用未分页池(假如放到虚拟内存并被交换到磁盘上时可能会发生灾难性的后果)。这些资源短缺的时候系统将会发生不可预料的事情,分页池吃紧的时候系统将会频繁地使用虚拟内存,从而不停读写磁盘减低性能,而未分页池吃紧的时候系统多半已经踏入鬼门关了。 总结两句: **1.NonPagedPool**的总量是有限的( 具体大小和你物理内存的大小相关), 而**PagedPool**的总量较多。申请了内存忘记释放都会造成内存泄漏,但是很明显忘记释放**NonPagedPool**的后果要严重得多; **2.**一般来说,**PagedPool**用来放数据(比如你用**ZwQuerySystemInformation**枚举内核模块,可以申请一大片**PagedPool**存放返回的数据),而**NonPagedPool**用来放代码(你写内核shellcode并需要执行时, 必须使用**NonPagedPool**存放**shellcode**)。 [**Windows kernel pool**](http://www.cnblogs.com/flycat-2016/p/5449738.html) ## 漏洞原理 MS17-010漏洞出现在Windows SMB v1中的内核态函数`srv!SrvOs2FeaListToNt`在处理FEA(File Extended attributes)转换时,在大非分页池(内核的数据结构,Large Non-Paged Kernel Pool)上存在缓冲区溢出。函数`srv!SrvOs2FeaListToNt`在将FEA list转换成NTFEA(Windows NT FEA) list前会调用`srv!SrvOs2FeaListSizeToNt`去计算转换后的FEA lsit的大小。然后会进行如下操作: 1. `srv!SrvOs2FeaListSizeToNt`会计算FEA list的大小并更新待转换的FEA list的大小。 2. 因为错误的使用WORD强制类型转换,导致计算出来的待转换的FEA list的大小比真正的FEA list大。 3. 因为原先的总大小计算错误,导致当FEA list被转化为NTFEA list时,会在非分页池导致缓冲区溢出。 在`Srv.sys`中的`SrvOs2FeaListToNt`函数中,会有以下调用关系: ```c SrvOs2FeaListToNt SrvOs2FeaListSizeToNt *****bug***** SrvOs2FeaToNt memmove *****crash***** ``` ## poc 下图所示是,调试poc时,crash之后的栈,我们根据栈回溯定位可以看到,`srv!SrvOs2FeaListToNt`调用了`srv!SrvOs2FeaToNt`,再调用`memmove`函数,这里崩掉的原因是`memmove`函数的参数过大,导致拷贝越界。 ![Snipaste_2019-03-20_12-48-09](./assets/Snipaste_2019-03-20_12-48-09.png) 其中在`SrvOs2FeaListSizeToNt`中因为有一个`DWORD转WORD`并赋值的bug,造成在`SrvOs2FeaListToNt`的一个循环中,`SrvOs2FeaToNt`被调用的次数会多于预期,而造成`SrvOs2FeaToNt`中的一个`memmove`拷贝越界。如果参数大于0x10000,因为word类型的原因,求得的返回值会变大,下图是输入的参数。 ![Snipaste_2019-03-20_15-13-32](./assets/Snipaste_2019-03-20_15-13-32.png) 这时候edi指向了payload,使用pool命令查看: ```c kd> !pool edi Pool page 9839a0d8 region is Paged pool *9839a000 : large page allocation, tag is LStr, size is 0x11000 bytes Pooltag LStr : SMB1 transaction, Binary : srv.sys ``` 所以edi指向的就是传入参数,FEA。当`srv!SrvOs2FeaListSizeToNt`执行完后,返回值变大。 ![Snipaste_2019-03-20_17-36-37](./assets/Snipaste_2019-03-20_17-36-37.png) 在求得返回值变大之后,`srv!SrvOs2FeaListToNt`会调用`srv!SrvOs2FeaToNt`对list进行遍历,而最后一次调用的参数是`0xcc00`,如下图所示。 ![Snipaste_2019-03-20_15-56-09](./assets/Snipaste_2019-03-20_15-56-09.png) 在这最后一次`srv!SrvOs2FeaToNt`中,这里就调用`memmove`从分页池的第二段payload复制到非分页池中,很明显这里的参数是肯定太大了,导致了最后的问题,分页池用完,超预期的长度,越界读取导致bsod。 ![Snipaste_2019-03-20_19-49-50](./assets/Snipaste_2019-03-20_19-49-50.png) 非分页池情况: ![Snipaste_2019-03-20_19-50-15](./assets/Snipaste_2019-03-20_19-50-15.png) ## exp 那么EternalBlue是如何利用的呢?首先发送一个SRV buffer除了最后一个数据包。这是因为大非分页池将在会话中最后一个数据包被服务端接收的时候被建立。SMB服务器会把会话中接受到的数据读取并叠加起来放入输入缓冲区中。所有的数据会在TRANS包中被标明。当接收到所有的数据后SMB服务器将会处理这些数据。数据通过CIFS(Common Internet File System)会被分发到SrvOpen2函数中来读取。 EternalBlue发送的所有数据会被SMB服务器收到后,SMB服务器会发送SMB ECHO包。因为攻击可以在网速很慢的情况下实现,所以SMB ECHO是很重要的。 在我们的分析中,即使我们发送了初始数据,存在漏洞的缓冲区仍然没有被分配在内存中。 1. FreeHole_A: EternalBlue通过发送SMB v1数据包来完成占位 2. SMBv2_1n: 发送一组SMB v2数据包 3. FreeHole_B: 发送另一个占位数据包;必须确保第一个占位的FreeHole_A被释放之前,这块内存被分配 4. FreeHole_A_CLOSE: 关闭连接,使得第一个占位的内存空间被释放。 5. SMBv2_2n: 发送一组SMB v2数据包。 6. FreeHole_B_CLOSE: 关闭连接来释放缓冲区。 7. FINAL_Vulnerable_Buffer: 发送最后的数据包,这个数据包将会被存储在有漏洞的缓冲区中。 ### windbg调试过程 exp最后的结果是`srv!SrvTransaction2DispatchTable`中的第0xe项被替换。 ```c kd> dds srv!SrvTransaction2DispatchTable 95744530 9576c56f srv!SrvSmbOpen2 95744534 95766fe4 srv!SrvSmbFindFirst2 95744538 9576706d srv!SrvSmbFindNext2 9574453c 95769a89 srv!SrvSmbQueryFsInformation 95744540 9576a2f3 srv!SrvSmbSetFsInformation 95744544 95760f65 srv!SrvSmbQueryPathInformation 95744548 95761c74 srv!SrvSmbSetPathInformation 9574454c 9576077c srv!SrvSmbQueryFileInformation 95744550 9576155d srv!SrvSmbSetFileInformation 95744554 9576a4e5 srv!SrvSmbFindNotify 95744558 9576797a srv!SrvSmbIoctl2 9574455c 9576a4e5 srv!SrvSmbFindNotify 95744560 9576a4e5 srv!SrvSmbFindNotify 95744564 957625fb srv!SrvSmbCreateDirectory2 95744568 9576cf2b srv!SrvTransactionNotImplemented(******bug******) 9574456c 9576cf2b srv!SrvTransactionNotImplemented 95744570 95753107 srv!SrvSmbGetDfsReferral 95744574 95752ff7 srv!SrvSmbReportDfsInconsistency ``` 至于为什么会替换这个,应该是最后一次SMB通信就是transaction2,当数据包发过来的时候,会调用这个表,就会执行自定义函数。 首先下断点查看是哪里在写向这个地址写值。 ```c ba w1 srv!SrvTransaction2DispatchTable+0xe*4 ``` ![Snipaste_2019-03-22_09-56-24](./assets/Snipaste_2019-03-22_09-56-24.png) 可以看到此时: 1. eip位于HAL地址空间中,显然是不对的 2. ebx指向srv!SrvTransaction2DispatchTable表头部 3. eax指向被替换的函数地址。 于是我们将整个段保存下来查看。 ```c .writemem C:\Users\zjgcj\Desktop\sc.bin ffdff000 l0x1000 ``` 可以找到shellcode的起始地址。是在`ffdff1f1`这个地方,位于HAL地址空间中。 ```c d> !address ffdff1f1 Base Address: ffdf1000 End Address: ffffffff Region Size: 0020f000 VA Type: HAL ``` ```c wrk中有定義這塊地址的作用,如下: // addressed from 0xffdf0000 - 0xffdfffff are reserved for the system // begin_ntddk begin_ntosp #define KI_USER_SHARED_DATA 0xffdf0000 #define SharedUserData ((KUSER_SHARED_DATA * const) KI_USER_SHARED_DATA) ``` ```c kd> u FFDFF1F1 ffdff1f1 31c0 xor eax,eax ffdff1f3 40 inc eax ffdff1f4 90 nop ffdff1f5 7408 je ffdff1ff ffdff1f7 e809000000 call ffdff205 ffdff1fc c22400 ret 24h ffdff1ff e8a7000000 call ffdff2ab ffdff204 c3 ret ``` 这时我们需要知道控制流是如何转移过来的,以及数据流是如何被写到HAL的地址空间中的。 ```c ba w1 FFDFF1F1 ba e1 FFDFF1F1 ``` 由于一个地址只能下一个断点,因为先写再执行,所以我们先看写的情况。 ![Snipaste_2019-03-22_10-26-04](./assets/Snipaste_2019-03-22_10-26-04.png) 可以看到,此时程序位于tcp的协议栈中,在执行memcpy的过程中,错误的将srvnet非分页缓冲池中的数据复制到HAL的地址空间中。为什么会发生这样的情况,最后再看。 然后我们再来看执行的情况。 ![Snipaste_2019-03-22_10-42-25](./assets/Snipaste_2019-03-22_10-42-25.png) 在`srvnet!SrvNetCommonReceiveHandler`这个函数中,eax的值被指向了shellcode的地址附近,之后便调用了shellcode。之后我们需要知道这个值是怎么来的。 根据网上的资料,我们可以知道如下的函数调用关系。 ```c srvnet!SrvNetWskReceiveComplete srvnet!SrvNetIndicateData srvnet!SrvNetCommonReceiveHandler ``` `srvnet!SrvNetWskReceiveComplete`这个函数是个IRP的完成例程。第三个参数Context是IRP的Context。Context 偏移0×24 处,存放了一个指针,里面存放了连接信息,我们姑且称其为Connection吧。这个Connection 会被作为第一个参数,传入`srvnet!SrvNetIndicateData`,而紧接着又会被作为第一个参数传入到`srvnet!SrvNetCommonReceiveHandler`。而Contex 由`srvnet!SrvNetAllocateBuffer`分配,类型是`SRVNET_BUFFFER` ![1496473220837](./assets/1496473220837.png) 该结构的0×24处的Connection被损坏了,被修改成了HAL中的地址空间。然后我们再下断点即可。 ```c bu srvnet!SrvNetWskReceiveComplete+17 ".if(@edi==ffdff020){} .else{gc}" ``` 然后我们可以看到如下的结果,同时我们查看esi所在的地址,是srvnet分配的非分页池。但是其实这段数据是srv中`memmove`的目的地址,所以这里其实已经越界写了。 ```c kd> dd esi 885a7010 0000ffff 00000000 00000000 00000000 885a7020 00000000 00000000 ffdff100 00000000 885a7030 00000000 [***ffdff020***] ffdff100 ffffffff 885a7040 10040060 00000000 ffdfef80 00000000 885a7050 ffd00010 ffffffff ffd00118 ffffffff 885a7060 00000000 00000000 00000000 00000000 885a7070 10040060 00000000 00000000 00000000 885a7080 ffcfff90 ffffffff 00000000 00000000 kd> !pool esi Pool page 885a7010 region is Nonpaged pool *885a7000 : large page allocation, tag is LSbf, size is 0x11000 bytes Pooltag LSbf : SMB1 buffer descriptor or srvnet allocation, Binary : srvnet.sys ``` 为什么这段数据是`memmove`拷过来的,调试一下就行了,打印出每次的地址喝参数。 ```c bp srv!srvOs2FeaToNt+4d ".printf\"memmove from %x to %x length %x\\n\", poi(@esp+4), poi(@esp), poi(@esp+8);gc" ``` 可以看到最后2次复制的长度,且最后一次的长度是a8。 ```c ba e1 srv!SrvOs2FeaToNt+0x4d ".if(poi(esp+8) != a8){gc} .else {}" ``` 正常情况是在srv.sys对象SMB buffer中,但由于长度过长导致对srvnet.sys分配的buffer越界写。 ![Snipaste_2019-03-22_14-44-24](./assets/Snipaste_2019-03-22_14-44-24.png) 至于为什么会拷贝越界就是前面进行结构体转换的时候,大小计算错误。 那么我们回过头再来看,shellcode是在SMB通信的时候写入srv中的非分页池的,在执行完`memmove`之后,越界写到了srvnet的非分页池中,但是最后shellcode执行的时候是处于HAL中,期间又发生了什么呢。再次下断点调试。 ```c ba w1 ffdff1f1 ``` ![Snipaste_2019-03-21_15-09-52](./assets/Snipaste_2019-03-21_15-09-52.png) 可以看到,`memmove`拷贝的时候,覆盖掉的不仅仅是Context->Connection。他同时也覆盖了相邻`SRVNET_BUFFER.MDL`的内容(偏移0x2c起),从而使得TCP/IP协议栈拷贝到了`ffdff1f1`内存中。在x86上,`ffdf1000`开始到`ffffffff`地址,都是保留给HAL用的。 ```c 0000ffff 00000000 00000000 00000000 00000000 00000000 ffdff100 00000000 00000000 ffdff020*****bug***** ffdff100 ffffffff 10040060 00000000 ffdfef80*****bug***** ``` Srvnet 对象buffer中包含两个重要的域: 1. 一个指向指定结构(srvnet_recv)的指针(即上图中的8834e4c0,被ffdff020覆盖),该指针将会在smb(srnet)连接结束或断开 时被用于寻址函数地址。 2. 一个用于接收缓冲区的MDL(即上图中的86546160,被ffdfef80覆盖) 因此覆盖并控制MDL将导致之后的tcp 栈实现任意写入伪造对象的操作,覆盖并控制该指针可用于将其指向一个攻击者控制的伪造对象,此时断开smb(srvnet)连接即可导致代码执行。 ## 总结 首先,歹发送SMB 的 Session Setup AndX (0×73) 命令,跟据其响应中的 Native OS 获取 目标操作系统的版本信息。 他利用了SMB.SMB_COM_NT_TRANSACT SMB_COM_TRANSACTION2_SECONDARY 在内存中精心布局,形成了一些连续的SRVNET_BUFFER内存区域。然后他关闭了一个链接,从而释放掉一个SRVNET_BUFFER,而这个释放掉的SRVNET_BUFFER空洞恰恰又会被FeaList 分配内存时重用(有图为证)。而SrvOs2FeaListToNt 中的Bug又导致了拷贝时越界,直接覆盖掉了其后的SRVNET_BUFFER,修改了MDL。于是后面的发送的数据就被错误的拷贝到了MDL 指定的内存中,也就是HAL保留的内存。而这时,歹人开始了致命的以一击,发送最后一个SMB_COM_TRANSACTION2_SECONDARY 分片,从而触发了控制转移。 ![14964738412030](./assets/14964738412030.png) ![t01d84ec3da74c87b19](./assets/t01d84ec3da74c87b19.jpg) ### 参考资料 [# MS17-010漏洞復現(x32)以及分析](https://www.twblogs.net/a/5c18b111bd9eee5e40bbfe4c) [深入剖析勒索软件传播方式](http://blog.nsfocus.net/ransomware-detail/) [狄仁杰探案之“永恒之蓝”](https://www.freebuf.com/articles/system/136298.html) [EternalBlue Shellcode详细分析](https://www.anquanke.com/post/id/86392) [WannaCry勒索软件中“永恒之蓝”漏洞利用分析](https://bbs.pediy.com/thread-217734.htm) [MS17-010深入分析“永恒之蓝”漏洞](https://www.anquanke.com/post/id/86270) [NSA Eternalblue SMB 漏洞分析](http://blogs.360.cn/post/nsa-eternalblue-smb.html) [免考实验与研究——MS17-010漏洞研究](https://www.cnblogs.com/Qujinkongyuyin/p/9266958.html) [EternalBlue工具漏洞利用细节分析](https://ti.360.net/blog/articles/detailed-analysis-of-eternalblue/) [NSA武器库之Eternalblue SMB漏洞浅析](https://blog.csdn.net/qq_32400847/article/details/72810999) ## 后记 和学姐讨论了,她的意思是在ring3上有程序直接调用了srv.sys,但是根据以下资料,我表示怀疑,而且栈回溯也看不到ring3的应用层,应该是通过os直接控制了。 [https://www.computerhope.com/](https://www.computerhope.com/) [https://www.reviversoft.com/zh-cn/processes/srv.sys](https://www.reviversoft.com/zh-cn/processes/srv.sys) [https://docs.microsoft.com/zh-cn/windows-hardware/drivers/gettingstarted/what-is-a-driver-](https://docs.microsoft.com/zh-cn/windows-hardware/drivers/gettingstarted/what-is-a-driver-) ``` ``` Last modification:January 16th, 2021 at 01:15 pm © 允许规范转载 Support 确定不打赏一下支持博主吗 ×Close Appreciate the author Sweeping payments Pay by AliPay