再谈永恒之蓝
前言
文章本来想写双脉冲星后门,但是在调试过程中,发现永恒之蓝这个调试过程有点长,所以单独成文,之后有时间再看双脉冲星后门。
在分析过程中,可能有些结构或者过程和网上的不太一致,如果存在错误或者不准确的地方,还请海涵。如果在调试过程中存在疑惑,十分欢迎与小弟进行交流。
漏洞背景
永恒之蓝是美国国家安全局开发的漏洞利用程序,于2017年4月14日被影子经纪人泄漏。该工具利用445/TCP端口的文件分享协议的漏洞进行散播,无需用户任何操作,只要开机上网,就能在电脑中植入恶意程序
前提知识
SMB
首先在讲SMB之前,先要知道什么是SMB,以及SMB连接过程发生了什么事情,虽然网上已经有很多,但纸上得来终觉浅,所以就边操作变写这篇文章。
先来百科一下SMB:
SMB( Server Message Block ),服务消息块,是 IBM 公司在 80 年代中期发明的一种文件共享协议,电脑上的网上邻居就是靠它实现的 。它只是系统之间通信的一种方式(协议),并不是一款特殊的软件。
SMB 协议被设计成为允许计算机通过本地局域网(LAN)在远程主机上读写文件。远程主机上通过 SMB 协议开放访问的目录称为 共享文件夹。
生命周期
一次普通的SMB会话一般经历以下六方面
SMB协议协商(Negotiate)
在一个SMB还没有开始的时候,由客户端率先发出一个协商请求。在请求中,客户端会列出所有它所支持协议版本以及所支持的一些特性(比如加密Encryption、持久句柄Persistent Handle、客户端缓存Leasing等等)。而服务端在回复中则会指定一个SMB版本且列出客户端与服务端共同支持的特性。
建立SMB会话(Session Setup)
客户端选择一个服务端支持的协议来进行用户认证,可以选择的认证协议一般包括NTLM、Kerberos等。按照选择的认证协议的不同,这个阶段可能会进行一次或多次SESSION_SETOP请求/回复的网络包交换。至于NTLM或Kerberos认证协议的细节,我们会另文再叙。
连接一个文件分享(Tree Connect)
在会话建立之后,客户端会发出连接文件分享的请求。源于文件系统的树形结构,该请求被命名为树连接(Tree Connect)。以SMB协议的阿里云NAS为例,一般的SMB挂载命令为:
net use z: \\XXX.nas.aliyuncs.com\myshare
其中的“ \XXX.nas.aliyuncs.com\myshare”便是我们将要连接的那个文件分享,也便是那棵“树”。
如果在“myshare”中创建有子目录“abc”,那直接连接“abc”这棵子树也是可以的:
net use z: \\XXX.nas.aliyuncs.com\myshare\abc
文件系统操作
在文件分享连接成功之后,用户通过SMB客户端进行真正的对于目标文件分享的业务操作。这个阶段可以用到的指令有CREATE、CLOSE、FLUSH、READ、WRITE、SETINFO、GETINFO等等。
断开文件分享连接(Tree Disconnect)
当一个SMB会话被闲置一定时间之后,Windows会自动断开文件分享连接并随后中止SMB会话。这个闲置时间可以通过Windows注册表进行设定。当然,用户也可以主动发起断开连接请求。
终止SMB会话(Logoff)
当客户端发出会话中止请求并得到服务端发回的中止成功的回复之后,这个SMB会话至此便正式结束了。
下图为SMB2,其过程和SMB类似
利用WireShark流量分析
SMB结构
在分析之前,先看一下SMB数据包的格式,如果需要更进一步的详情,可查看官网
SMB消息可分为三个部分:
固定长度的标头
可变长度参数块
可变长度数据块
标头标记消息标识为SMB消息,指定要执行的命令并提供上下文。在响应消息中,标头还包含状态信息,该状态信息指示命令是成功还是失败。
参数块是两字节值(字)的简短数组
数据块是最大64 KB的数组。这些块的结构和内容就是不同的SMB消息。
具体结构
SMB_Header结构的长度固定为32个字节。
SMB_Header
{
UCHAR Protocol[4];
UCHAR Command;
SMB_ERROR Status;
UCHAR Flags;
USHORT Flags2;
USHORT PIDHigh;
UCHAR SecurityFeatures[8];
USHORT Reserved;
USHORT TID;
USHORT PIDLow;
USHORT UID;
USHORT MID;
}
SMB 参数
SMB最初被设计为远程过程调用协议,参数定义为WordCount大小的Short数组。SMB_Parameters. Words结构的格式是为每个命令消息单独定义的。
参数块的一般格式如下。
SMB_Parameters
{
UCHAR WordCount;
USHORT Words[WordCount] (variable);
}
SMB 数据块,数据块的一般结构与参数块的结构相似,除了缓冲区部分的长度以字节为单位。
SMB_Data
{
USHORT ByteCount;
UCHAR Bytes[ByteCount] (variable);
}
一个典型的结构如下
抓包分析
- 协商
首先由客户端向服务端发送数据包,并且包含其支持的所有协议版本
服务端接收请求之后,选择客户端所支持的最高版本,返回给客户端
- 认证
协议确定后, 开始进行认证,客户端通过发送用户名/密码进行认证
服务端返回数据包们进行应答是否连接成功
-
连接
此命令用于建立到服务器共享的客户端连接。共享由名称标识,连接一旦建立,就由返回给客户端的TID标识。
发送一个Tree connect rerquest SMB数据报并列出它想访问网络资源的名称
回复包提供回复连接成功或者拒绝,并且列出的操作的权限
之后就是文件的读写,具体可以去官网查询具体功能命令,就不一一描述了
最后就是断开分享连接并终止会话,释放资源和锁
调试使用
调试环境
调试环境
Win7 32 SP1
srv.sys 6.1.7601.17514
srvnet 6.1.7601.17514
工具
Shadow Brokers工具包 (https://github.com/misterch0c/shadowbroker)
python2.6 ( https://www.python.org/download/releases/2.6/ )
pywin32 ( url=https://sourceforge.net/projects/pywin32/files/pywin32/Build%20212/]https://sourceforge.net/projects ... ywin32/Build%20212/[/url(https://sourceforge.net/projects/pywin32/files/pywin32/Build 212/) )
调试分析
我们运行工具包,利用WireShark抓包,我们可以看到
首先会发送 NT_Trans命令,然后后续发送一系列的Trans2 Secondary数据包,这里涉及到一个消息类型转化Bug
通常
命令后面必须SMB_COM_TRANSACTION_SECONDARY命令
SMB_COM_TRANSACTION2命令必须是SMB_COM_TRANSACTION2_SECONDARY命令
SMB_COM_NT_TRANS命令必须 跟随SMB_COM_NT_TRANS_SECONDARY命令。
但是如果第一个SMB消息要发送的事务数据还没有完成,此时服务器并不做检测,我们就 可以发送任何类型的消息(只要TID,UID,PID和MID匹配)来完成这个事务。
由上基础知识可知服务器使用最后一个SMBCOM * _ SECONDARY命令来确定事务类 型。因此,我们可以将任何事务类型转换为SMB_COM_TRANSACTION或 SMB_COM_TRANSACTION2。
我们查看以上的数据包发现确实具有相同的TID,UID,PID和MID,所以,我们断定该过程就是利用了上边的BUG。
我们查看这两个消息的结构
SMB_COM_NT_TRANSACT对应结构体
SMB_Parameters
{
UCHAR WordCount;
Words
{
UCHAR MaxSetupCount;
USHORT Reserved1;
ULONG TotalParameterCount;
ULONG TotalDataCount; //四个字节
ULONG MaxParameterCount;
ULONG MaxDataCount;
ULONG ParameterCount;
ULONG ParameterOffset;
ULONG DataCount;
ULONG DataOffset;
UCHAR SetupCount;
USHORT Function;
USHORT Setup[SetupCount];
}
}
SMB_Data
{
USHORT ByteCount;
Bytes
{
UCHAR Pad1[];
UCHAR NT_Trans_Parameters[ParameterCount];
UCHAR Pad2[];
UCHAR NT_Trans_Data[DataCount];
}
}
SMB_COM_TRANSACTION2_SECONDARY
SMB_Parameters
{
UCHAR WordCount;
Words
{
USHORT TotalParameterCount;
USHORT TotalDataCount; //两个字节
USHORT ParameterCount;
USHORT ParameterOffset;
USHORT ParameterDisplacement;
USHORT DataCount;
USHORT DataOffset;
USHORT DataDisplacement;
USHORT FID;
}
}
SMB_Data
{
USHORT ByteCount;
Bytes
{
UCHAR Pad1[];
UCHAR Trans2_Parameters[ParameterCount];
UCHAR Pad2[];
UCHAR Trans2_Data[DataCount];
}
}
其中我们看到数据结构中的TotalDataCount一个是四个字节,一个是两个字节,当TotalDataCount为103d0时,FEA_LIST的长度为0x10000,但是如果是正常情况,两个字节是怎么也不会有0x10000大小的FEA_LIST。
这就导致FEA_LIST的SizeOfListInBytes大小高位为1.并且之后没有检查和重新赋值。
然后接下来因为第一个BUG引发的第二个BUG
在传输过程中,FEA_LIST需要转化为NT_FEA_LIST
FEA_LIST结构体
SMB_FEA
{
UCHAR ExtendedAttributeFlag;
UCHAR AttributeNameLengthInBytes;
USHORT AttributeValueLengthInBytes;
UCHAR AttributeName[AttributeNameLengthInBytes + 1];
UCHAR AttributeValue[AttributeValueLengthInBytes];
}
SMB_FEA_LIST
{
ULONG SizeOfListInBytes;
UCHAR FEAList[];
}
定位到函数SrvOs2FeaListSizeToNt
我们看到这里在重新写入FEA_LIST的SizeOfListInBytes值时,是将si写入到[eax]中,如果eax地址处的值大于0XFFFF则会导致错误
接下来我们按照我们的猜测,进行windbg调试
下断点到关键位置
bp SrvOs2FeaListSizeToNt+0x60 ".printf \"Fea_List_Originsize=%p, Fea_List_si=%p\\n\",poi(eax),esi;g"
bp srv!SrvOs2FeaListSizeToNt+0x63 ".printf \"Fea_List_Fix_Size=%p\\n\",poi(eax);g"
bp SrvOs2FeaListToNt+0x15 ".printf \"Nt_Fea_list_size=%p\\n\",eax;g"
我们可以看到由于originsize没有清零,直接赋值fixsize,得到大小为0x0001ff5d,是远远大于NT_Fea_List_Size 0x00010fe8的。
接着向下看,根据NT_Fea_List_Size 大小申请一块大非分页池空间,然后对FEA_LIST象NT_FEA_LIST的转化,
在转化过程中存在memmove操作
我们下断点分别对申请的空间大小和转化过程中的memmove进行下断点查看
bp SrvOs2FeaListToNt+38 ".printf \"pool=%p\\n\",eax;g"
bp srv!SrvOs2FeaToNt+2e ".printf \"MOV1: dst: %p src: %p size: %p\\n\",ebx,eax,poi(esp+8);g;"
bp srv!SrvOs2FeaToNt+4d ".printf \"MOV2: dst: %p src: %p size: %p\\n\",ebx,eax,poi(esp+8);g;"
看到pool起始为0x85f43008,大小为0x00010fe8,范围就是0x85f43008~0x85F53FF0
以MOV1和MOV2一对为一次,我们可以看到前边前605次都是0,在第606次开始大小为f383
可以看到最后几次拷贝已经是越界拷贝了,不过第一次MOV1的size大小为0,看第二次拷贝情况,发现此时0x85F53FF9此时已经超过0x85F53FF0
如果想查看最后一次内存情况也可下断点查看
bp srv!SrvOs2FeaToNt+0x4d ".if(poi(esp+8) != a8){g} .else {}"
覆盖前
覆盖后
我们查看其流量进行分析
算一下,0x3cc+0x805=0xBD1, 0xBD1/5=605次正好是前边的复制次数
之后存在数据的传输的SMB_FEA_LIST大小
最后一次传输,也就是溢出的SMB_FEA_LIST传输
知道了溢出点,如何利用?也就是我们必须控制溢出内存的地址,使其可控。
我们查看其溢出点位置。
依旧下断点
bp srv!SrvOs2FeaToNt+0x4d ".if(poi(esp+8) != a8){g} .else {}"
利用!pool ebx查看池
然后!pool ebx+7查看池
可以看到在复制的时候是横跨两个池的,那么查看这两个池所在的模块,这里需要看一下pooltag
为了看一下现在的内存情况,利用同样的方法我们可以看一下前后其他内存块,最后查看内存块排列基本如下
可以看到这两个池分别在srv.sys和srvnet.sys中
通过观察内存结构及其特征,我们可以下断点
bp srvnet!SrvNetAllocatePoolWithTag+0x1b ".if @esi > 0x0000f000 {.printf \"srvnet!SrvNetAllocatePoolWithTag Allocation Address: %p; Size: %p;\\n\",eax,esi;g}.else{g;}"
bp srv!SrvAllocateNonPagedPool+0xe3 ".if @esi > 0x0000f000 {.printf \"srv!SrvAllocateNonPagedPool Allocation Address %p Size: %p;\\n\",eax,esi;g}.else{g}"
其中两个申请srv池,可以看到申请的地址一致,并且第二个值就是溢出的池的地址,所以中间应该有释放空间
所以重新下断点进行查看
bp srvnet!SrvNetAllocatePoolWithTag+0x1b ".if @esi > 0xf000 {.printf \"srvnet!SrvNetAllocatePoolWithTag Allocation Address: %p; Size: %p;\\n\",eax,esi;g}.else{g;}"
bp srv!SrvAllocateNonPagedPool+0xe3 ".if @esi > 0x0000f000 {.printf \"srv!SrvAllocateNonPagedPool Allocation Address %p Size: %p;\\n\",eax,esi;g}.else{g}"
bp SrvFreeNonPagedPool+0x3 ".printf\"SrvFreeNonPagedPool free Nopage:%p\\n\",eax;g;"
这样就可以控制srvnet在srv池的后边了
在通过wireshark抓包分析
首先是一系列的srvnet池申请,该池是通过TCP发送SMB消息进行传输的,具体的内容可以查看官方文档
https://docs.microsoft.com/en-us ... e-9a71-f854e24aeee6
那么接下来我们还需要存在申请一段内存,并且该内存之后需要进行释放,我们主要看一下,在其中存在这样的一段流量信息,这里是创建会话的过程,如果失败,此时就会自动释放相关资源,正好符合我们的预想。
为了探究这个是如何申请非分页的大池的,我们还是下断点
bp srv!SrvAllocateNonPagedPool+0xe3 ".if @esi > 0x0000f000 {.printf \"srv!SrvAllocateNonPagedPool Allocation Address %p Size: %p;\\n\",eax,esi;g}.else{g}"
在srv申请空间的时候断下,栈回溯
发现过程主要在srv!BlockingSessionSetupAndX中
该函数的主要流程如下
并且我们查找SMB_COM_SESSION_SETUP_ANDX 请求,存在有两种格式:
LAN Manager 1.0
SMB_Parameters
{
UCHAR WordCount; 0XD
Words
{
UCHAR AndXCommand;
UCHAR AndXReserved;
USHORT AndXOffset;
USHORT MaxBufferSize;
USHORT MaxMpxCount;
USHORT VcNumber;
ULONG SessionKey;
USHORT OEMPasswordLen;
USHORT UnicodePasswordLen;
ULONG Reserved;
ULONG Capabilities;
}
}
SMB_Data
{
USHORT ByteCount;
Bytes
{
UCHAR OEMPassword[];
UCHAR UnicodePassword[];
UCHAR Pad[];
SMB_STRING AccountName[];
SMB_STRING PrimaryDomain[];
SMB_STRING NativeOS[];
SMB_STRING NativeLanMan[];
}
}
[NTLMv2](https://docs.microsoft.com/en-us ... 2-8c59-a334b92e01c0])
SMB_Parameters
{
UCHAR WordCount; 0XC
Words
{
UCHAR AndXCommand;
UCHAR AndXReserved;
USHORT AndXOffset;
USHORT MaxBufferSize;
USHORT MaxMpxCount;
USHORT VcNumber;
ULONG SessionKey;
USHORT SecurityBlobLength;
ULONG Reserved;
ULONG Capabilities;
}
}
SMB_Data
{
USHORT ByteCount;
Bytes
{
UCHAR SecurityBlob[SecurityBlobLength];
SMB_STRING NativeOS[];
SMB_STRING NativeLanMan[];
}
}
通过构建数据包,即smb中的Flag不包含 FLAGS2_EXTENDED_SECURITY 标志,则会进入 GetNtSecurityParameters 流程中,此时的解析就会按照0xD进行解析,此时SMB_DATA大小不会再为0x16而是0x87f8
还是进行调试进行说明
bp BlockingSessionSetupAndX+62E
经过错误的解析结构体,返回错误的空间。
通过 add esi,esi 和 SrvAllocateNonPagedPool 获取大非分页池
接下来就看如何去执行恶意代码,这时候就需要看之前池越界内容了。
另外我们可以看到在后边流量数据,显示就断开连接了。
所以我们猜测srvnet模块的越界内存空间应该是在释放前后执行的代码,首先我们再看一下他覆盖的空间内存,着重注意此值
我们下断点 SrvNetFreeBuffer,进行栈回溯,查找srvnet模块中是否存在相关的执行链
ba e1 SrvNetFreeBuffer "kb;g"
之前想过几种方法来确定代码的之后的执行shellcode的方法
- 利用 ba r4 addr 因为在溢出中的值中之后肯定会被读取,会断下来
- 利用shellcode的代码进行回溯
- 利用流程下API断点,这个就需要比较熟悉代码了,如知道断开连接会调用那些函数,直接去相关模块去查找
- 根据已知条件去合理的猜,如我上边的方法
- 根据补丁去回溯,这个应该是比较常用的方法,但这里并不是补丁的地方,而是利用链中的一个地方。
- 网上参考,就像很多文章,也不去验证,反正文章说偏移+1就+1,加2就加2,这对于需要快速分析的,也不失为一种方法,但是如果想调试一手的漏洞代码,就不能只是抄网上的
尝试下断点
bp SrvNetIndicateData
进行调试
之后我们就可以进一步下更准确的断点
bp SrvNetIndicateData ".if(edi != 0xffdff020){g} .else {}"
当然可以回溯到更上层
bp srvnet!SrvNetWskReceiveComplete+0x13 ".if(poi(esi+0x24) == 0xffdff020) {} .else {gc}"
然后就可以找到执行shellcode代码,即双脉冲星后门
其栈调用
接下来再看一下shellcode是如何写入的
我们在上边知道了shellcode,这段代码是如何写入的,就可以很轻松的回溯了。
直接写断点
ba w4 0xffdff194
可以看一下栈环境极其值,这里是什么对象就不去细细探究了。
最后的最后,说一下为什么覆盖的值是ffdf0000,经过资料
// 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)
这块内存是系统预留的,里面保存了系统的一些信息,像时钟,版本,配置之类。注意这个地址在win10下是不可以执行的。所以这个利用方法在win10下是不可用的。
参考
永恒之蓝
SMB协议
SMB
eternalblue-everything-know