前言
EternalBlue(永恒之蓝)据称是方程式组织在其漏洞利用框架中一个针对SMB服务进行攻击的模块,由于其涉及漏洞的影响广泛性及利用稳定性,在被公开以后为破坏性巨大的勒索蠕虫 WannaCry所用而名噪一时。360威胁情报中心对于WannaCry的活动持续地进行着监控,我们看到的趋势是 WannaCry的感染量还在增加,说明作为蠕虫主要传播手段的EternalBlue相应的漏洞还大量存在着。但是,对于EternalBlue 这个攻击利器本身的技术分析在公开渠道上看到的讨论其实并不充分,本文尝试通过一个较完全的分析梳理相关的细节,揭示其成因和相应的利用技巧。
测试环境
对于EternalBlue的分析是在一个相对简单的环境中进行的,执行攻击的系统为一个Win7机器,目标机器也是 Win7 32位系统,没有安装EternalBlue相关的补丁,srv.sys文件的版本为 6.1.7601.17514,srvnet.sys的版本为 6.1.7601.17514 。本文中所有的调试器中代码截图都对应上述的版本,不同版本的文件在代码本身或偏移可能不同,但整体的执行逻辑应该差不多。
漏洞
根据我们的分析,EternalBlue达到其攻击目的事实上利用了3个独立的漏洞:第一个也就是 CVE-2017-0144被用于引发越界内存写;第二个漏洞则用于绕过内存写的长度限制;第三个漏洞被用于攻击数据的内存布局。下面重点介绍一下前两个漏洞,第三个漏洞会在内存布局的过程中提到。
漏洞1
首先是EternalBlue工具中使用到的主体漏洞,该漏洞也是Eternalblue的核心部分,编号为 CVE-2017-0144。漏洞通过SMB协议的SMB_COM_TRANSACTION2 命令触发,该命令说明如下所示:
当该数据包中包含对应的FEA LIST时,SMB服务中会将 FEA LIST转换为对应的NTFEA LIST,其对应的数据结构并不公开,如下所示为趋势团队分析出的对应的FEALIST 结构。
入口处理函数为SrvSmbOpen2,其中漏洞出现在函数SrvOs2FeaListToNt 中:
如下所示为对应的漏洞函数SrvOs2FeaListToNt,用于实现 FEA LIST转换为对应的NTFEA LIST,函数调用SrvOs2FeaListSizeToNt 计算 FEALIST的长度,但是该函数存在漏洞导致在特定的情况下,攻击者可以伪造超长的size,从而导致在之后的 SrvOs2FeaToNt 转换中导致 pool溢出。
进入导致漏洞的SrvOs2FeaListSizeToNt函数,该函数会计算对应的 FEA LIST的长度并随后对长度进行更新,该长度一开始为DWORD类型的,之后的长度更新代码中计算出的size 拷贝回去的时候是按 WORD进行的拷贝,此时只要原变量a中的初始值大于FFFF ,即为 10000+,该函数的计算结果就会增大。
该赋值中如下所示esi 变成了si,此时如果eax高位中的数据不为零,则将返回的超长的size 。
如下图所示为对应发送的该数据包,可以看到该请求数据包的长度为 103d0,其中对应的FEALIST的长度为10000。
如下图所示, eax为链表的开头,其指向了FEA LIST的总长度,即10000, esi为遍历之后的链表尾部,eax-esi=ff5d,为实际对应的长度,但是更新长度的mov 指令中 esi变成了si,由于eax 中的值为 10000 ,原本应该被赋值为ff5d的eax ,变成了 1ff5d 。
之后在紧接着的函数 SrvOsFeaToNt中,由于使用了错误的长度进行memmove从而导致溢出。
下图为其中的复制导致越界写,长度为a8,可以看到正常请求应该是在86535000这个srv.sys对象SMB buffer中,由于长度过长导致对srvnet.sys分配的buffer越界写。
Enternalblue 中通过内存布局,将srvnet对象buffer稳定的分配到srv 拷贝对象 buffer之后,如下图所示为越界写时的内存情况。
Srvnet 对象buffer中包含两个重要的域:
-
一个指向指定结构(srvnet_recv)的指针(即上图中的8834e4c0,被ffdff020覆盖),该指针将会在smb(srnet)连接结束或断开时被用于寻址函数地址。
-
一个用于接收缓冲区的MDL(即上图中的86546160,被ffdfef80覆盖)
因此覆盖并控制MDL将导致之后的tcp 栈实现任意写入伪造对象的操作,覆盖并控制该指针可用于将其指向一个攻击者控制的伪造对象,此时断开smb(srvnet)连接即可导致代码执行。
如下图所示,MDL复写为ffd2 />
GetNtSecurityParameters会检查对应的请求中的参数,函数参数中的v70为通过 wordcount 和Bytecount计算出来的一个size。
GetNtSecurityParameters函数中的计算如下所示:
该参数返回后作为SrvAllocateNonPagedPool的参数分配一段pool 。
因此利用该漏洞将12类型的请求包通过13类型进行处理,由于两种类型的请求包格式不一致,通过控制请求包指定偏移的数据,即可以控制SrvAllocateNonPagedPool创建的pool的大小,可以使用以下的断点监控该过程:
bp GetNtSecurityParameters+0x1AC".printf\"GetNtSecurityParameters1\\n\";r;.echo;?cx-si+bx+1d;g;"
bp SrvAllocateNonPagedPool+0×10″.printf\"SrvAllocateNonPagedPool NonPageSize:%p\\n\",ecx;g;"
bp SrvAllocateNonPagedPool+0x15C".printf\"SrvAllocateNonPagedPool allocNopage:%p\\n\",eax;g;"
bp BlockingSessionSetupAndX+0x7C0″.printf\"BlockingSessionSetupAndX double\\n\";g;"
如下图所示即为通过断点监控到的非法size生成的过程,通过构造畸形数据包,包含数据87f8,漏洞触发后识别出该错误的偏移,计算最后会分配一段大小为 10fec大小的pool。
通过断开对应的该命令请求,可以导致之前分配的10fec大小的pool被释放,从而在地址空间中生成一个hole,该hole之后会被srv对象buffer来填充。
现在知道了如何在内存中稳定的spray一段连续的srvnet的对象buffer,以及如何开辟并释放一段指定大小的空间,内存布局的基本条件已经具备,可以看到具体的布局流程到最后的触发执行过程如下:
通过SMB_COM_NT_TRANSACT发送一段FEA LIST长度满足0×10000的数据包
发送后续的SMB_COM_TRANSACTION2_SECONDARY,这将导致smb服务将SMB_COM_NT_TRANSACT当做SMB_COM_TRANSACTION2处理,但是最后一个SMB_COM_TRANSACTION2_SECONDARY留置最后。
通过smb 2协议进行srvnet对象的spray
通过SMB_COM_SESSION_SETUP_ANDX漏洞在srvnet对象之后分配一段大小和srv对象大小几乎一致的pool内存
通过smb 2协议继续进行srvnet对象的spray,以确保srvnet位于srv对象之后
断开连接导致第4步开辟的pool内存释放,生成一个hole
发送最后一个SMB_COM_TRANSACTION2_SECONDARY,由于大小一致,该数据包会填补生成的hole,并触发漏洞导致之后的srvnet对象buffer中的MDL和指针被修改,此时后续发送的数据将拷贝到ffdff000的位置。
断开所有连接,触发srvnet_recv指向的shellcode执行
可以通过以下断点监控利用时内存的释放和分配(主要是srv,srvnet对象):
bp SrvAllocateNonPagedPool+0×10″.printf\"SrvAllocateNonPagedPoolNonPageSize:%p\\n\",ecx;g;"
bp SrvAllocateNonPagedPool+0x15C".printf\"SrvAllocateNonPagedPool allocNopage:%p\\n\",eax;g;"
bp SrvFreeNonPagedPool+0×3″.printf\"SrvFreeNonPagedPool free Nopage:%p\\n\",eax;g;"
bp BlockingSessionSetupAndX".printf\"BlockingSessionSetupAndX\\n\";g;"
bp SrvNetAllocateNonPagedBufferInternal".printf\"AllocateNonPagedNonPagedBufferSize:%p\\n\",poi(esp+8);g;"
bpSrvNetAllocateNonPagedBufferInternal+0×179 ".printf\"AllocateNonPagedNonPagedBufferAddress:%p\\n\",eax;g;"
bp SrvNetFreeNonPagedBufferInternal".printf\"SrvNetFreeNonPagedBufferInternal freeNonPageBufferAddress:%p\\n\",poi(esp+4);g;"
ba e1 srvnet!SrvNetWskReceiveComplete+0×13″.if(poi(esi+0×24) == ffdff020) {} .else {gc}"
如下图所示即为整体监控到的数据包于内存中的布局情况,其中867bb000处为对应的srv buffer对象,之后867cc000上的srvnet buffer对象将会被覆盖如下所示:
以上为EternalBlue利用过程中内存布局及对应发送数据包的一个概述,但是其内部其实还有一些细节可供深入挖掘。由于作者水平有限,有什么错误欢迎大家指正。
参考资料
http://bobao.360.cn/learning/detail/3738.html
https://github.com/worawit/MS17-010
http://blog.trendmicro.com/trendlabs-security-intelligence/ms17-010-eternalblue/