ms17-010 漏洞分析
0x00 前言
近期重新分析了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/
0x01 简介
本文将介绍以下的内容
>> 漏洞完整利用流程介绍
>> 漏洞溢出部分分析
>> 漏洞触发部分的分析
>> 漏洞的内存布局的分析
0x02 漏洞完整利用流程
该漏洞主要是利用smb1和smb2的协议兼容问题,和windows在处理fealist
结构体和ntfeallist
结构体过程中大小计算错误导致的数据溢出漏洞。
在进行堆喷射过程中,为了实现非页内存的布局,又利用用了一个SMB_COM_SESSION_SETUP_ANDX
计算smbv1和smbv2结构体转化的漏洞,实现了任意大小的非页内存申请,从而间接利用系统的内存管理机制实现内存布局。
漏洞触发部分,在内存溢出和堆布局的基础上实现了对srvnet
头部结构的覆盖,其中对MDL指针的覆盖,使得后续发送的srvnetbuff
内容被保存到了特定可执行的内存地址(0xffdff000)中,于是在释放srvnet
链接后,处理函数会执行0xffdff000地址处的shellcode,从而实现漏洞利用。
0x03 溢出部分分析
这部分的漏洞分析是大部分文章都有写的,主要成因是由于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
函数中添加了一个类型比较的判断。
用到的几个结构体的定义如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
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;
|
trans2
接受完数据后会通过dispatchtable调用srv!SrvSmbOpen2
函数对接收到的数据进行处理,函数首先会读取接收到的transion数据,然后获取其中fealist结构体的部分,将fealist结构体转成Ntfealist结构体。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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
);
}
|
在SrvOs2FeaListToNt
函数中,首先是SrvOs2FeaListSizeToNt
对于结构体长度的强转赋值,导致fealist的结构体长度是错误的,所以后续计算最后一个结构体指针的地址也是错误的。在后续循环转化ntfealist的过程中,循环是以fea结构体标志位和与最后一个结构体指针地址比较进行的条件判断,结构体标志位由用户传入,可控,指针地址错误的计算,可控,所以可以精准控制溢出字节。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
|
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
}
|
在SrvOs2FeaListSizeToNt
函数中,实现了两个功能,一是计算ntfealist结构体的长度并用于申请后续空间,二是对fealist结构的长度进行重新赋值,防止由于该长度被用户输入控制导致错误,在实现第二个功能的时候,由于trans2
消息的总长度为两个字节,所以在此处进行了强转导致了最终长度计算出错。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
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;
}
|
0x04 shellcode触发部分分析
是windows内核中一个比较重要的结构,这个结构负责将用户空间中的内存通过MDL机制映射到系统地址空间。将I/O数据写入到指定的MDL指定虚拟地址中,在实际利用中client发送的数据会写入到指定的虚拟地址中,这样就可以传入可控的数据到指定的地址。
pSrvNetWskStruct
: 指向SrvNetWskStruct结构体,该结构体中存在一个函数指针HandlerFunction,该函数会在srvnet连接中断时进行调用;那么如果pSrvNetWskStruct指向的结构体是伪造的,那么就可以很顺利的触发命令执行。
heap中可执行代码的固定地址
1
2
3
4
5
|
/
/
win7
0xffdff000
/
/
32
位
0xffffffffffd00010
/
/
64
位
/
/
win8\win10
0xffffffffffd04000
/
/
64
位
|
由于我们知道倒数第二个Fea结构的value部分是f383
,之后又拷贝了个a8的长度,所以这里是在value拷贝处下断点
1
|
kd> ba e1 srv!SrvOs2FeaToNt
+
0x4d
".if(poi(esp+8) != a8){gc} .else {}"
|
下面红线开始的部分为越界拷贝的那个ntfealist结构体,可以看到精准溢出的实际上是一个a8长度的字段:
越界前:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
85774000
00011000
00000000
00000000
00000000
85774010
8b6f0008
871b5e60
871b5e60
85774160
85774020
00010ea0
00000080
8577403c
00000000
85774030
0000fff7
85774010
857740a4
00000000
85774040
10040060
00000000
85774160
85774000
85774050
00010ea0
00000160
0003fd74
0003fd75
85774060
0003fd76
0003fd77
0003fd78
0003fd79
85774070
0003fd7a
0003fd7b
0003fd7c
0003fd7d
85774080
0003fd7e
0003fd7f
0003fd80
0003fd81
85774090
0003fd82
0003fd83
0003fd84
48be015c
857740a0
7447dbac
00000000
00000064
00020004
857740b0
00020000
00000000
00010ea0
00000fff
857740c0
00000000
00000000
00000000
00000000
857740d0
8b701820
005c003a
00650044
00690076
857740e0
00650063
0048005c
00720061
00640064
857740f0
00730069
0056006b
006c006f
006d0075
85774100
00310065
0050005c
006f0072
00720067
85774110
4e4e4e4e
4e4e4e4e
4e4e4e4e
4e4e4e4e
85774120
4e4e4e4e
4e4e4e4e
4e4e4e4e
4e4e4e4e
85774130
4e4e4e4e
4e4e4e4e
4e4e4e4e
4e4e4e4e
|
越界后:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
85774000
00000000
00000000
0000ffff
00000000
85774010
0000ffff
00000000
00000000
00000000
85774020
00000000
00000000
ffdff100
00000000
85774030
00000000
ffdff020 ffdff100 ffffffff
85774040
10040060
00000000
ffdfef80
00000000
85774050
ffd00010 ffffffff ffd00118 ffffffff
85774060
00000000
00000000
00000000
00000000
85774070
10040060
00000000
00000000
00000000
85774080
ffcfff90 ffffffff
00000000
00000000
85774090
00001080
00000000
00000000
00000000
857740a0
7447db76
00000000
00000064
00020004
857740b0
00020000
00000000
00010ea0
00000fff
857740c0
00000000
00000000
00000000
00000000
857740d0
8b701820
005c003a
00650044
00690076
857740e0
00650063
0048005c
00720061
00640064
857740f0
00730069
0056006b
006c006f
006d0075
85774100
00310065
0050005c
006f0072
00720067
85774110
4e4e4e4e
4e4e4e4e
4e4e4e4e
4e4e4e4e
85774120
4e4e4e4e
4e4e4e4e
4e4e4e4e
4e4e4e4e
85774130
4e4e4e4e
4e4e4e4e
4e4e4e4e
4e4e4e4e
|
精准覆盖的SRVNET_HEADER
部分字段含义如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
chunk
80
00
a8
00
00
00
00
00
00
00
00
00
0x00
00
00
00
00
00
00
00
00
ff ff
00
00
00
00
00
00
0x10
ff ff
00
00
0x14
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
0x24
00
00
00
00
0x28
00
f1 df ff
00
00
00
00
00
00
00
00
0x34
20
f0 df ff
0x38
00
f1 df ff
0x3c
00
00
00
00
0x40
60
00
0x42
04
10
0x44
00
00
00
00
0x48
80
ef df ff
0x4c
00
00
00
00
0x50
10
00
d0 ff ff ff ff ff
0x58
10
01
d0 ff ff ff ff ff
0x60
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
0x70
60
00
0x72
04
10
0x74
00
00
00
00
00
00
00
00
00
00
00
00
0x80
90
ff cf ff ff ff ff ff
|
其中需要关注的是偏移0x34处的指针,该指针正常情况下最终指向的是srv!SrvReceiveHandler,用于处理会话结束后的情况。指针调用的逻辑如下:
1
2
3
4
5
6
7
8
9
10
|
/
/
srvnet!SrvNetWskReceiveComplete
+
13
mov edi, [esi
+
24h
]
/
/
srvnet!SrvNetIndicateData
+
17
mov ebx, dword ptr [ebp
+
8
]
/
/
srvnet!SrvNetCommonReceiveHandler
+
13
mov esi, dword ptr [ebp
+
8
]
/
/
srvnet!SrvNetCommonReceiveHandler
+
64
mov eax, dword ptr [esi
+
16Ch
]
/
/
srvnet!SrvNetCommonReceiveHandler
+
0x91
call dword ptr [eax
+
4
]
/
/
该处为shellcode执行的地方
|
感兴趣的可以下断点观察:
1
2
|
kd> ba e1 ffdff1f1
kd> bu srvnet!SrvNetWskReceiveComplete
+
17
"r $t0=poi(esi+24h);r $t1=poi(@$t0+16c);.if(@$t1 !=0x00000000){.printf \"srvnet!SrvNetWskReceiveComplete+17 addr: %p val:%p val+16c:%p *(val+16c):%p func:%p\\n\",esi+24h,@$t0,@$t0+16c,@$t1,poi(@$t1+0x4);gc;} .else {gc;}"
|
0x05 漏洞的内存布局实现
SMB_COM_SESSION_SETUP_ANDX
消息是SMB中用来以ntml协议验证的命令,但是对于 和 却有两个不同的请求结构体,而其中两个WordCount的值是不一样的。
在BlockingSessionSetupAndX
函数中,由于逻辑判断的错误,我们可以发送Extended Security request(12)
附带CAP_EXTENDED_SECURITY,但不附带FLAG2_EXTENDED_SECURITY,将请求伪装成SMB_COM_SESSION_SETUP_ANDX(13)
。函数伪代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
BlockingSessionSetupAndX(request, smbHeader)
{
/
/
check word count
if
(! (request
-
>WordCount
=
=
13
|| (request
-
>WordCount
=
=
12
&& (request
-
>Capablilities & CAP_EXTENDED_SECURITY))) ) {
/
/
error
and
return
}
/
/
...
if
((request
-
>Capablilities & CAP_EXTENDED_SECURITY) && (smbHeader
-
>Flags2 & FLAGS2_EXTENDED_SECURITY)) {
/
/
this request
is
Extend Security request
GetExtendSecurityParameters(request);
/
/
extract parameters
and
data to variables
SrvValidateSecurityBuffer(request);
/
/
do authentication
}
else
{
/
/
this request
is
NT Security request
GetNtSecurityParameters(request,&smbInformationLength);
/
/
extract parameters
and
data to variables
SrvValidateUser(request);
/
/
do authentication
}
/
/
...
length
=
isUnicode ? smbInformationLength :smbInformationLength
*
sizeof( WCHAR );
infoBuffer
=
ALLOCATE_NONPAGED_POOL(length,BlockTypeDataBuffer);
}
|
从伪代码中可以看出,这样我们会调用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。
0x06 小结
虽然还是比较努力的分析了,但越分析越发现自己依旧有很多不明白的地方,记录下目前还遗留的坑点。
shellcode做了些什么?怎么样实现的后门驻留?
pMdl和实际读写地址的关系,实际写入shellcode的地方实在tcpip的驱动中,覆盖的pMdl指针并没有直接指向0xffdff000的部分,那么这个偏移计算的利用关系是怎么样的?
Doublepulsar如何实现的,到底做了些什么事情?
在分析过程中,我发现这个漏洞的利用其实很难在流量层进行检测,堆布局的手法可以改变,同时覆盖的指针结构数据也可以改变,非分页内存中可执行的地址也可以改变,Shellcode的具体大小没有限制,在IDS层进行的检测基本都能绕过,确实是个相当好的组合漏洞,对当年就能写这种工具的大佬佩服地五体投地。