-
-
[原创]ms17-010 漏洞分析
-
发表于: 2022-7-26 18:56 19614
-
近期重新分析了ms17-010漏洞,想写一个自己的工具,在重新分析的过程中,其实又发现了很多之前没有进行深究的问题,由于很多东西还没有弄明白,先记录一下自己的分析过程以及踩的坑,不由感慨漏洞分析和想要实际利用两者之间的差距确实挺大的。
环境:
win7 sp1 32bits srv.sys 6.1.7601.17514
srvnet.sys 6.1.7601.17514
nsa 工具集(使用教程)
PS:这两个文件在C:\Windows\System32\drivers
下
参考资料:
https://research.checkpoint.com/2017/eternalblue-everything-know/
https://github.com/3ndG4me/AutoBlue-MS17-010
https://paper.seebug.org/280/
本文将介绍以下的内容
>> 漏洞完整利用流程介绍
>> 漏洞溢出部分分析
>> 漏洞触发部分的分析
>> 漏洞的内存布局的分析
该漏洞主要是利用smb1和smb2的协议兼容问题,和windows在处理fealist
结构体和ntfeallist
结构体过程中大小计算错误导致的数据溢出漏洞。
在进行堆喷射过程中,为了实现非页内存的布局,又利用用了一个SMB_COM_SESSION_SETUP_ANDX
计算smbv1和smbv2结构体转化的漏洞,实现了任意大小的非页内存申请,从而间接利用系统的内存管理机制实现内存布局。
漏洞触发部分,在内存溢出和堆布局的基础上实现了对srvnet
头部结构的覆盖,其中对MDL指针的覆盖,使得后续发送的srvnetbuff
内容被保存到了特定可执行的内存地址(0xffdff000)中,于是在释放srvnet
链接后,处理函数会执行0xffdff000地址处的shellcode,从而实现漏洞利用。
这部分的漏洞分析是大部分文章都有写的,主要成因是由于SrvOs2FeaListSizeToNt
函数在进行fealist
到ntfeallist
的长度计算过程中进行了一个强制类型转换,导致了四个字节的长度只覆盖了低位的两个字节,数据在转换过程中大于申请的内存空间,从而实现溢出。此处就主要介绍一下为什么会出现四个字节转两个字节的情况?
SMB协议中,使用一串的命令来代表执行的操作的,当传输的数据过大时,smb通常会有一个子命令进行传输,并用传输过程中的TID,UID,PID,MID来判断是哪一个命令的后续数据。
例如,smbv1中的SMB_COM_NT_TRANSACT
命令,在传输消息过大时,便会使用SMB_COM_NT_TRANSACT_SECONDARY
来完成后续的数据传输。
而在smbv2中SMB_COM_TRANSACTION2
作为SMB_COM_NT_TRANSACT
的扩展命令,两者的请求结构体十分相似,功能也差不多,但在计算消息内容长度TotalDataCount
时,SMB_COM_TRANSACTION2
使用的是USHORT
类型(两字节),SMB_COM_NT_TRANSACT
使用的是ULONG
类型(四字节)。
NSA工具在利用该漏洞时,先传入了一个SMB_COM_NT_TRANSACT
命令的头,后续内容利用相同TID, PID, UID, MID的SMB_COM_TRANSACTION2_SECONDARY
进行传输的。没加补丁之前,windows仅通过TID, PID, UID, MID来识别命令是否一致,而消息命令的类型是由最后一个传入的命令类型确定的。这样就造成了传入NT_TRANSACT
消息,但实际上是运行的却是TRANSACTION2
命令的处理流程。
所以,在补丁修复中,除了修复SrvOs2FeaListSizeToNt
的类型强转外,还同时在 ExecuteTransaction
函数中添加了一个类型比较的判断。
用到的几个结构体的定义如下:
trans2
接受完数据后会通过dispatchtable调用srv!SrvSmbOpen2
函数对接收到的数据进行处理,函数首先会读取接收到的transion数据,然后获取其中fealist结构体的部分,将fealist结构体转成Ntfealist结构体。
在SrvOs2FeaListToNt
函数中,首先是SrvOs2FeaListSizeToNt
对于结构体长度的强转赋值,导致fealist的结构体长度是错误的,所以后续计算最后一个结构体指针的地址也是错误的。在后续循环转化ntfealist的过程中,循环是以fea结构体标志位和与最后一个结构体指针地址比较进行的条件判断,结构体标志位由用户传入,可控,指针地址错误的计算,可控,所以可以精准控制溢出字节。
在SrvOs2FeaListSizeToNt
函数中,实现了两个功能,一是计算ntfealist结构体的长度并用于申请后续空间,二是对fealist结构的长度进行重新赋值,防止由于该长度被用户输入控制导致错误,在实现第二个功能的时候,由于trans2
消息的总长度为两个字节,所以在此处进行了强转导致了最终长度计算出错。
MDL是windows内核中一个比较重要的结构,这个结构负责将用户空间中的内存通过MDL机制映射到系统地址空间。将I/O数据写入到指定的MDL指定虚拟地址中,在实际利用中client发送的数据会写入到指定的虚拟地址中,这样就可以传入可控的数据到指定的地址。
pSrvNetWskStruct
: 指向SrvNetWskStruct结构体,该结构体中存在一个函数指针HandlerFunction,该函数会在srvnet连接中断时进行调用;那么如果pSrvNetWskStruct指向的结构体是伪造的,那么就可以很顺利的触发命令执行。
heap中可执行代码的固定地址
由于我们知道倒数第二个Fea结构的value部分是f383
,之后又拷贝了个a8的长度,所以这里是在value拷贝处下断点
下面红线开始的部分为越界拷贝的那个ntfealist结构体,可以看到精准溢出的实际上是一个a8长度的字段:
越界前:
越界后:
精准覆盖的SRVNET_HEADER
部分字段含义如下:
其中需要关注的是偏移0x34处的指针,该指针正常情况下最终指向的是srv!SrvReceiveHandler,用于处理会话结束后的情况。指针调用的逻辑如下:
感兴趣的可以下断点观察:
SMB_COM_SESSION_SETUP_ANDX
消息是SMB中用来以ntml协议验证的命令,但是对于ntml v1 和 ntml v2却有两个不同的请求结构体,而其中两个WordCount的值是不一样的。
在BlockingSessionSetupAndX
函数中,由于逻辑判断的错误,我们可以发送Extended Security request(12)
附带CAP_EXTENDED_SECURITY,但不附带FLAG2_EXTENDED_SECURITY,将请求伪装成SMB_COM_SESSION_SETUP_ANDX(13)
。函数伪代码如下:
从伪代码中可以看出,这样我们会调用GetNtSecurityParameters
函数,这个函数在Extended Security request(12)
被当作SMB_COM_SESSION_SETUP_ANDX(13)
请求解析时,会将SecurityBlob解析为ByteCount的大小,并在接下来根据是否是unicode字符串来分配空间。这样就可以创造处大小可控的非分页内存空间。
首先,NSA工具集会使用匿名验证获取TID, PID, UID, MID以及系统版本信息,然后通过发送Trans2
命令,判断是否已经存在NSA后门。
然后利用Trans2
的漏洞先发送除了最后一帧外的所有数据包,这样由于最后一帧没有发送,就不会触发fealist
计算Ntfealist
的过程,不会对Ntfealist
的空间进行申请。
然后,利用SMB_COM_SESSION_SETUP_ANDX
的漏洞,构造一个稍大的内存空间,该空间主要是用来容纳需要被覆盖的那几个srvnet结构的,这里我们叫它Buff1
。
紧接着,申请了一堆srvnet的连接,这样的申请会将非分页内存空间中大小与srvnet空间大小相近的空闲空间全部占满,这样在后面再次申请空间时,就会将大块空间进行拆分,然后再次分配出去。
然后又创建了一块大空间Buff2
,这块空间的大小与转化后的Ntfealist
空间大小相似,由于Buff1
和Buff2
的空间都属于较大的,在分页内存空间中分配大概率会前后紧挨着,分页内存在分配时又会从低地址向高地址进行分配,此时的空间布局应该是Buff2
+Buff1
。
紧接着,NSA工具将Buff1
的空间进行释放,同时又申请了5块srvnet空间,5块srvnet空间大小刚好和Buff1
的空间大小接近,而前面srvnet空间大小的非分页内存又被之前申请的srvnet连接占满,所以这5块SrvnetBuff
将会被系统拆分Buff1
后分配。所以此时内存布局改变为Buff2
+SrvnetBuff
*5。
关键点来了,再又一次的网络连接判断后,NSA工具释放了Buff2
的内存空间,并且发送最后一帧Trans2
数据,触发了溢出漏洞,这样申请到的Ntfealist
的空间大概率就是Buff2
。此时的内存布局就变成了Ntfealist
+SrvnetBuff
*5,这样溢出后必定会覆盖5个SrvnetBuff
中的一个。
覆盖后,由于Srvnetbuff
的头部我们修改了PMDL结构体指针,所以再次发送数据,内容将会放到我们指定的内存空间0xffdff000 处,这个内存是块可执行的空间。
最终通过我们预先改变的DisconnectHandleFunc
指针链,我们会在srvnet!SrvNetCommonReceiveHandler+0x91
处调用传入的shellcode,shellcode的地址为0xffdff1f1。
虽然还是比较努力的分析了,但越分析越发现自己依旧有很多不明白的地方,记录下目前还遗留的坑点。
shellcode做了些什么?怎么样实现的后门驻留?
pMdl和实际读写地址的关系,实际写入shellcode的地方实在tcpip的驱动中,覆盖的pMdl指针并没有直接指向0xffdff000的部分,那么这个偏移计算的利用关系是怎么样的?
Doublepulsar如何实现的,到底做了些什么事情?
在分析过程中,我发现这个漏洞的利用其实很难在流量层进行检测,堆布局的手法可以改变,同时覆盖的指针结构数据也可以改变,非分页内存中可执行的地址也可以改变,Shellcode的具体大小没有限制,在IDS层进行的检测基本都能绕过,确实是个相当好的组合漏洞,对当年就能写这种工具的大佬佩服地五体投地。
typedef struct _FEALIST {
_ULONG( cbList );
FEA
list
[
1
];
} FEALIST;
typedef struct _FEA {
UCHAR fEA;
/
/
flag 标志位用于判断循环是否结束
UCHAR cbName;
/
/
名字长度
_USHORT( cbValue );
/
/
值长度
} FEA;
/
/
ntfealist,windows中没有直接对fealist结构进行操作而是统一使用ntfealist操作
typedef struct _FILE_FULL_EA_INFORMATION {
ULONG NextEntryOffset;
UCHAR Flags;
UCHAR EaNameLength;
USHORT EaValueLength;
CHAR EaName[
1
];
} FILE_FULL_EA_INFORMATION,
*
PFILE_FULL_EA_INFORMATION;
typedef struct _FEALIST {
_ULONG( cbList );
FEA
list
[
1
];
} FEALIST;
typedef struct _FEA {
UCHAR fEA;
/
/
flag 标志位用于判断循环是否结束
UCHAR cbName;
/
/
名字长度
_USHORT( cbValue );
/
/
值长度
} FEA;
/
/
ntfealist,windows中没有直接对fealist结构进行操作而是统一使用ntfealist操作
typedef struct _FILE_FULL_EA_INFORMATION {
ULONG NextEntryOffset;
UCHAR Flags;
UCHAR EaNameLength;
USHORT EaValueLength;
CHAR EaName[
1
];
} FILE_FULL_EA_INFORMATION,
*
PFILE_FULL_EA_INFORMATION;
SMB_TRANS_STATUS SrvSmbOpen2 (IN OUT PWORK_CONTEXT WorkContext){
/
/
...
transaction
=
WorkContext
-
>Parameters.Transaction;
/
/
...
feaList
=
(PFEALIST)transaction
-
>InData;
/
/
... Convert the FEALIST to NT style.
status
=
SrvOs2FeaListToNt(
feaList,
&ntFullEa,
&ntFullEaBufferLength,
&os2EaErrorOffset
);
}
SMB_TRANS_STATUS SrvSmbOpen2 (IN OUT PWORK_CONTEXT WorkContext){
/
/
...
transaction
=
WorkContext
-
>Parameters.Transaction;
/
/
...
feaList
=
(PFEALIST)transaction
-
>InData;
/
/
... Convert the FEALIST to NT style.
status
=
SrvOs2FeaListToNt(
feaList,
&ntFullEa,
&ntFullEaBufferLength,
&os2EaErrorOffset
);
}
unsigned
int
__stdcall SrvOs2FeaListToNt(_FEALIST
*
FeaList, _DWORD
*
NtFullEa, _DWORD
*
BufferLength, _WORD
*
EaErrorOffset)
{
int
NtBufferLen;
/
/
eax
_FEALIST
*
NtfeaAddr;
/
/
eax
FEA
*
feaLast;
/
/
ebx
FEA
*
fea;
/
/
esi
unsigned
int
v10;
/
/
esi
__int16 v11;
/
/
[esp
+
8h
] [ebp
-
4h
]
_FEALIST
*
NtFeaAddr;
/
/
[esp
+
14h
] [ebp
+
8h
]
v11
=
0
;
NtBufferLen
=
SrvOs2FeaListSizeToNt(FeaList);
/
/
计此处算长度出错
*
BufferLength
=
NtBufferLen;
if
( !NtBufferLen )
{
*
EaErrorOffset
=
0
;
return
0xC098F0FF
;
/
/
STATUS_OS2_EA_LIST_INCONSISTENT
}
NtfeaAddr
=
(_FEALIST
*
)SrvAllocateNonPagedPool(NtBufferLen,
21
);
/
/
用ntfea的length申请内存空间
*
NtFullEa
=
NtfeaAddr;
if
( NtfeaAddr )
{
/
/
问题就出现在了这里,cbList经过刚刚的赋值已经发生了改变,这个feaLast的指针地址远远大于fea最后一个结构体指针地址
feaLast
=
(FEA
*
)((char
*
)FeaList
+
FeaList
-
>cbList
-
5
);
/
/
为了保证至少有一个fea结构 FeaList
+
Feal
-
>cbList
-
sizeof(Fea)
fea
=
FeaList
-
>
list
;
if
( FeaList
-
>
list
> feaLast )
{
LABEL_13:
if
( fea
=
=
(FEA
*
)((char
*
)FeaList
+
FeaList
-
>cbList))
/
/
如果cblist长度是
0
,那么就把Ntfea的长度也设为
0
{
NtfeaAddr
-
>cbList
=
0
;
return
0
;
}
*
EaErrorOffset
=
v11
-
(_WORD)FeaList;
v10
=
0xC0000001
;
/
/
STATUS_SUCCESS
}
else
{
while
( (fea
-
>fEA &
0x7F
)
=
=
0
)
/
/
判断每个fea的标志位是不是
80
或
00
,不是就跳出循环
{
/
/
注意,这里是以标志位为循环判断基础的,而本来那个损坏的fea结构体是不在拷贝范围内的,但由于长度计算错误,会出现在拷贝的范围内。
NtFeaAddr
=
NtfeaAddr;
v11
=
(__int16)fea;
NtfeaAddr
=
(_FEALIST
*
)SrvOs2FeaToNt(NtfeaAddr, fea);
/
/
拷贝内存,导致溢出的部分
fea
=
(FEA
*
)((char
*
)fea
+
fea
-
>cbName
+
fea
-
>cbValue
+
5
);
/
/
赋值下一个fea
if
( fea > feaLast )
/
/
这个地方由于feaLast的地址计算错误,所以肯定大于fea地址
{
NtfeaAddr
=
NtFeaAddr;
goto LABEL_13;
}
}
*
EaErrorOffset
=
(_WORD)fea
-
(_WORD)FeaList;
v10
=
0xC000000D
;
/
/
STATUS_INVALID_PARAMETER
}
SrvFreeNonPagedPool(
*
NtFullEa);
return
v10;
}
if
(
*
((_BYTE
*
)WPP_GLOBAL_Control
+
29
) >
=
2u
&& (
*
((_BYTE
*
)WPP_GLOBAL_Control
+
32
) &
1
) !
=
0
&& KeGetCurrentIrql() <
2u
)
{
DbgPrint(
"SrvOs2FeaListToNt: Unable to allocate %d bytes from nonpaged pool."
,
*
BufferLength);
DbgPrint(
"\n"
);
}
return
0xC0000205
;
/
/
STATUS_INSUFF_SERVER_RESOURCES
}
unsigned
int
__stdcall SrvOs2FeaListToNt(_FEALIST
*
FeaList, _DWORD
*
NtFullEa, _DWORD
*
BufferLength, _WORD
*
EaErrorOffset)
{
int
NtBufferLen;
/
/
eax
_FEALIST
*
NtfeaAddr;
/
/
eax
FEA
*
feaLast;
/
/
ebx
FEA
*
fea;
/
/
esi
unsigned
int
v10;
/
/
esi
__int16 v11;
/
/
[esp
+
8h
] [ebp
-
4h
]
_FEALIST
*
NtFeaAddr;
/
/
[esp
+
14h
] [ebp
+
8h
]
v11
=
0
;
NtBufferLen
=
SrvOs2FeaListSizeToNt(FeaList);
/
/
计此处算长度出错
*
BufferLength
=
NtBufferLen;
if
( !NtBufferLen )
{
*
EaErrorOffset
=
0
;
return
0xC098F0FF
;
/
/
STATUS_OS2_EA_LIST_INCONSISTENT
}
NtfeaAddr
=
(_FEALIST
*
)SrvAllocateNonPagedPool(NtBufferLen,
21
);
/
/
用ntfea的length申请内存空间
*
NtFullEa
=
NtfeaAddr;
if
( NtfeaAddr )
{
/
/
问题就出现在了这里,cbList经过刚刚的赋值已经发生了改变,这个feaLast的指针地址远远大于fea最后一个结构体指针地址
feaLast
=
(FEA
*
)((char
*
)FeaList
+
FeaList
-
>cbList
-
5
);
/
/
为了保证至少有一个fea结构 FeaList
+
Feal
-
>cbList
-
sizeof(Fea)
fea
=
FeaList
-
>
list
;
if
( FeaList
-
>
list
> feaLast )
{
LABEL_13:
if
( fea
=
=
(FEA
*
)((char
*
)FeaList
+
FeaList
-
>cbList))
/
/
如果cblist长度是
0
,那么就把Ntfea的长度也设为
0
{
NtfeaAddr
-
>cbList
=
0
;
return
0
;
}
*
EaErrorOffset
=
v11
-
(_WORD)FeaList;
v10
=
0xC0000001
;
/
/
STATUS_SUCCESS
}
else
{
while
( (fea
-
>fEA &
0x7F
)
=
=
0
)
/
/
判断每个fea的标志位是不是
80
或
00
,不是就跳出循环
{
/
/
注意,这里是以标志位为循环判断基础的,而本来那个损坏的fea结构体是不在拷贝范围内的,但由于长度计算错误,会出现在拷贝的范围内。
NtFeaAddr
=
NtfeaAddr;
v11
=
(__int16)fea;
NtfeaAddr
=
(_FEALIST
*
)SrvOs2FeaToNt(NtfeaAddr, fea);
/
/
拷贝内存,导致溢出的部分
fea
=
(FEA
*
)((char
*
)fea
+
fea
-
>cbName
+
fea
-
>cbValue
+
5
);
/
/
赋值下一个fea
if
( fea > feaLast )
/
/
这个地方由于feaLast的地址计算错误,所以肯定大于fea地址
{
NtfeaAddr
=
NtFeaAddr;
goto LABEL_13;
}
}
*
EaErrorOffset
=
(_WORD)fea
-
(_WORD)FeaList;
v10
=
0xC000000D
;
/
/
STATUS_INVALID_PARAMETER
}
SrvFreeNonPagedPool(
*
NtFullEa);
return
v10;
}
if
(
*
((_BYTE
*
)WPP_GLOBAL_Control
+
29
) >
=
2u
&& (
*
((_BYTE
*
)WPP_GLOBAL_Control
+
32
) &
1
) !
=
0
&& KeGetCurrentIrql() <
2u
)
{
DbgPrint(
"SrvOs2FeaListToNt: Unable to allocate %d bytes from nonpaged pool."
,
*
BufferLength);
DbgPrint(
"\n"
);
}
return
0xC0000205
;
/
/
STATUS_INSUFF_SERVER_RESOURCES
}
ULONG SrvOs2FeaListSizeToNt (IN PFEALIST FeaList){
unsigned
int
v1;
int
Length;
PUCHAR pBody;
PUCHAR v4;
int
v5;
int
v8;
unsigned
int
v9;
v1
=
0
;
Length
=
*
(DWORD
*
)pOs2Fea;
pBody
=
pOs2Fea
+
4
;
v9
=
0
;
v4
=
pOs2Fea
+
Length;
while
(pBody < v4)
{
if
(pBody
+
4
>
=
v4
|| (v5
=
*
(BYTE
*
)(pBody
+
1
)
+
*
(WORD
*
)(pBody
+
2
),
v8
=
*
(BYTE
*
)(pBody
+
1
)
+
*
(WORD
*
)(pBody
+
2
),
v5
+
pBody
+
5
> v4))
{
/
/
此处的强转导致赋值出错
*
(WORD
*
)pOs2Fea
=
pBody
-
pOs2Fea;
return
v1;
}
if
(RtlULongAdd(v1, (v5
+
0xC
) &
0xFFFFFFFC
, &v9) <
0
)
return
0
;
v1
=
v9;
pBody
+
=
v8
+
5
;
}
return
v1;
}
ULONG SrvOs2FeaListSizeToNt (IN PFEALIST FeaList){
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
赞赏
- [原创] 在 Windows下搭建LLVM 使用环境 23931
- [原创]ms17-010 漏洞分析 19615
- [原创]php调试环境搭建 3454