(CVE-2017-0143……..)【MS17-010】Windows_远程溢出漏洞

# (CVE-2017-0143……..)【MS17-010】Windows 远程溢出漏洞

====

一、漏洞简介
————

​ MS17-010(永恒之蓝)应用的不仅仅是一个漏洞,而是包含Windows SMB
远程代码执行漏洞CVE-2017-0143、CVE-2017-0144、CVE-2017-0145、CVE-2017-0146、CVE-2017-0147、CVE-2017-0148在内的6个SMB漏洞的攻击。

二、漏洞影响
————

​ 目前已知受影响的 Windows 版本包括但不限于:Windows NT,Windows
2000、Windows XP、Windows 2003、Windows Vista、Windows 7、Windows
8,Windows 2008、Windows 2008 R2、Windows Server 2012 SP0。

三、复现过程
————

### 漏洞分析

0x01 先关注MS17-010中使用的三个关键漏洞
—————————————

**第一个:漏洞即Fea list转换NT Fea list触发的overflow;通过srv
buff对象覆盖了后续的srvnet buff的结构体**

问题出现再SrvOs2FeaListSizeToNt 函数中,伪代码如下:

unsigned int __fastcall SrvOs2FeaListSizeToNt(int pOs2Fea)
{
unsigned int v1; // edi@1
int Length; // ebx@1
int pBody; // esi@1
unsigned int v4; // ebx@1
int v5; // ecx@3
int v8; // [sp+10h] [bp-8h]@3
unsigned int v9; // [sp+14h] [bp-4h]@1

v1 = 0;
Length = *(_DWORD *)pOs2Fea; // 这里以DWORD类型获取length
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更新length的低位2字节
// 初始值是0x10000,最终变成了0x1ff7E
*(_WORD *)pOs2Fea = pBody – pOs2Fea;
return v1; // 这里返回的大小为后续用于内存申请
}
if ( RtlULongAdd(v1, (v5 + 0xC) & 0xFFFFFFFC, &v9) < 0 ) return 0; v1 = v9; pBody += v8 + 5; } return v1; } 使用windbg来动态调试,先查询SrvOs2FeaListToNt中 SrvOs2FeaListSizeToNt 调用的结果: kd> p
srv!SrvOs2FeaListToNt+0x15:
a6f7a57a 8b7510 mov esi,dword ptr [ebp+10h]
kd> r
eax=00010fe8 ebx=884241e0 ecx=837a70ea edx=0000008f esi=83797008 edi=837970d8
eip=a6f7a57a esp=90b2bb70 ebp=90b2bb7c iopl=0 nv up ei pl nz ac pe nc
cs=0008 ss=0010 ds=0023 es=0023 fs=0030 gs=0000 efl=00000216
srv!SrvOs2FeaListToNt+0x15:
a6f7a57a 8b7510 mov esi,dword ptr [ebp+10h] ss:0010:90b2bb8c=90b2bba8

# 断点
bp srv!SrvOs2FeaListToNt+0x10
bp srv!SrvOs2FeaListToNt+0x33

此时获取到的大小为0x10fe8,后续变更该值进行内存申请(减去9字节)

srv!SrvOs2FeaListSizeToNt+0X5E
96759506 2bf0 sub esi,eax
96759508 668930 mov word ptr [eax],si # 这里更新size以WORD类型

调试中可以看到对应的size大小

kd> r
eax=a381b0d8 ebx=0000008f ecx=a382b0ea edx=0000008f esi=0000ff7e edi=a382b0d8
eip=96759508 esp=8ca43b54 ebp=8ca43b64 iopl=0 nv up ei pl nz ac pe nc
cs=0008 ss=0010 ds=0023 es=0023 fs=0030 gs=0000 efl=00000216
srv!SrvOs2FeaListSizeToNt+0x60:
96759508 668930 mov word ptr [eax],si ds:0023:a381b0d8=0000

si为: 0xff7e,
在SrvOs2FeaListToNt函数中获取到size值后会引用该值设置边界值,伪代码如下:

unsigned int __fastcall SrvOs2FeaListToNt(int pOs2Fea, int *pArgNtFea, int *a3, _WORD *a4)
{
__int16 v5; // bx@1
unsigned int Size; // eax@1
NTFEA *pNtFea; // ecx@3
int pOs2FeaBody; // esi@9
int v10; // edx@9
unsigned int v11; // esi@14
int v12; // [sp+Ch] [bp-Ch]@11
unsigned int v14; // [sp+20h] [bp+8h]@9

v5 = 0;
Size = SrvOs2FeaListSizeToNt(pOs2Fea); // 获取的大小为0x10fe8
*a3 = Size;
if ( !Size )
{
*a4 = 0;
return 0xC098F0FF;
}
pNtFea = (NTFEA *)SrvAllocateNonPagedPool(Size, 0x15); // 内存申请
*pArgNtFea = (int)pNtFea;
if ( pNtFea )
{
pOs2FeaBody = pOs2Fea + 4; // 后续引用该值为遍历的起始地址
v10 = (int)pNtFea;
v14 = pOs2Fea + *(_DWORD *)pOs2Fea – 5; // 这里设置边界地址size-5
if ( pOs2Fea + 4 > v14 )
{
LABEL_13:
if ( pOs2FeaBody == pOs2Fea + *(_DWORD *)pOs2Fea )
{
*(_DWORD *)v10 = 0;
return 0;
}
v11 = 0xC0000001;
*a4 = v5 – pOs2Fea;
}
else
{
while ( !(*(_BYTE *)pOs2FeaBody & 0x7F) )
{
v12 = (int)pNtFea;
v5 = pOs2FeaBody; // 起始地址
pNtFea = (NTFEA *)SrvOs2FeaToNt(pNtFea, pOs2FeaBody); // memmove触发
pOs2FeaBody += *(_BYTE *)(pOs2FeaBody + 1) + *(_WORD *)(pOs2FeaBody + 2) + 5;
// 目标地址pNtFea的大小为:0x10fe8
// 源地址pOs2FeaBody的大小为:0x1ff75
// 此时发生了越界操作
if ( pOs2FeaBody > v14 )
{
v10 = v12;
goto LABEL_13;
}
}
*a4 = pOs2FeaBody – pOs2Fea;
v11 = 0xC000000D;
}
SrvFreeNonPagedPool(*pArgNtFea);
return v11;
}
if ( BYTE1(WPP_GLOBAL_Control->Flags) >= 2u && WPP_GLOBAL_Control->Characteristics & 1 && KeGetCurrentIrql() < 2u ) { _DbgPrint("SrvOs2FeaListToNt: Unable to allocate %d bytes from nonpaged pool.", *a3, 0); _DbgPrint("\n"); } return 0xC0000205; } 动态调试以下汇编代码可以获取到用于边界判断的地址范c围: a6f7a5f1 f6067f test byte ptr [esi],7Fh a6f7a5f4 753c jne srv!SrvOs2FeaListToNt+0xcd (a6f7a632) a6f7a5f6 56 push esi // 这里为起始地址 a6f7a5f7 50 push eax a6f7a5f8 894508 mov dword ptr [ebp+8],eax a6f7a5fb 8975fc mov dword ptr [ebp-4],esi a6f7a5fe e828fcffff call srv!SrvOs2FeaToNt (a6f7a22b) a6f7a603 0fb65601 movzx edx,byte ptr [esi+1] a6f7a607 0fb74e02 movzx ecx,word ptr [esi+2] a6f7a60b 03d6 add edx,esi a6f7a60d 8d740a05 lea esi,[edx+ecx+5] a6f7a611 3bf3 cmp esi,ebx // 获取ebx即可得到边界地址 # 断点 bp srv!SrvOs2FeaListToNt+0x91 获取起始地址 bp srv!SrvOs2FeaListToNt+0xac 获取边界地址 通过边界地址-起始地址可以得到具体的大小 srv!SrvOs2FeaListToNt+0x91: a6f7a5f6 56 push esi kd> r
eax=865a5008 ebx=8ef72051 ecx=0001ff7e edx=00000000 esi=8ef520dc edi=8ef520d8
eip=a6f7a5f6 esp=8ba67b6c ebp=8ba67b7c iopl=0 nv up ei pl zr na pe nc
cs=0008 ss=0010 ds=0023 es=0023 fs=0030 gs=0000 efl=00000246
srv!SrvOs2FeaListToNt+0x91:
a6f7a5f6 56 push esi

srv!SrvOs2FeaListToNt+0xac:
a6f7a611 3bf3 cmp esi,ebx
kd> r
eax=865a5014 ebx=8ef72051 ecx=00000000 edx=8ef520dc esi=8ef520e1 edi=8ef520d8
eip=a6f7a611 esp=8ba67b6c ebp=8ba67b7c iopl=0 nv up ei ng nz na po nc
cs=0008 ss=0010 ds=0023 es=0023 fs=0030 gs=0000 efl=00000282
srv!SrvOs2FeaListToNt+0xac:
a6f7a611 3bf3 cmp esi,ebx

ebx – esi = 0x8ef72051 – 0x8ef520dc = 0x1ff75

在前面SrvOs2FeaListSizeToNt获取的大小也就是申请的内存大小为:0x10fe8,而使用的边界大小为:0x1ff75(这里为什么与size有出入是因为起始地址移动了4字节,同时边界大小也减小了5字节)

为了查看溢出覆盖的具体细节,需要定位到执行最后一次memmove的操作:

– 通过while循环跳出,在跳出位置下断点,计算循环了多少次
– 直接通过memmove设置条件断点(通过payload知道大部分的操作复制字节数都为0)

SrvOs2FeaToNt伪代码:

unsigned int __fastcall SrvOs2FeaToNt(int a1, int a2)
{
int v4; // edi@1
_BYTE *v5; // edi@1
unsigned int result; // eax@1

v4 = a1 + 8;
*(_BYTE *)(a1 + 4) = *(_BYTE *)a2;
*(_BYTE *)(a1 + 5) = *(_BYTE *)(a2 + 1);
*(_WORD *)(a1 + 6) = *(_WORD *)(a2 + 2);
_memmove((void *)(a1 + 8), (const void *)(a2 + 4), *(_BYTE *)(a2 + 1));
v5 = (_BYTE *)(*(_BYTE *)(a1 + 5) + v4);
*v5++ = 0;
_memmove(v5, (const void *)(a2 + 5 + *(_BYTE *)(a1 + 5)), *(_WORD *)(a1 + 6)); //这里产生的越界覆盖
result = (unsigned int)&v5[*(_WORD *)(a1 + 6) + 3] & 0xFFFFFFFC;
*(_DWORD *)a1 = result – a1;
return result;
}

第一种:判定while循环执行了多少次c

# 这里通过临时寄存器来计数
r $t0=0

# a6f7a5f4 753c jne srv!SrvOs2FeaListToNt+0xcd (a6f7a632)
bp srv!SrvOs2FeaListToNt+0x8f “.if (@zf=0) {} .else {gc}”

# a6f7a5f6 56 push esi // 这里为起始地址
bp srv!SrvOs2FeaListToNt+0x91 “r $t0=@$t0+1;g;”

# 查看计数
kd> r $t0
$t0=0000025b

第二种:通过srv!SrvOs2FeaToNt中的memmove下断进行定位

bp srv!SrvOs2FeaToNt+0x4d “.if (poi(esp+8) != 0) {gc} .else {}”

这里是因为payload知道了memmove前面都是进行0字节的copy。

kd> dd esp
94b1bb38 86adec31 a2e86c99 0000f3bd 86adec30

kd> dd esp
94b1bb38 86aedff9 a2e9605b 0000008f 86aedff8 // 最后一次的大小为0x8f

通过上面可以知道,目标地址为:86aedff9,复制的字节数为:0x8f;

接着查看pool信息:

kd> !pool 86aedff9
Pool page 86aedff9 region is Nonpaged pool
*86add000 : large page allocation, Tag is LSdb, size is 0x11000 bytes
Pooltag LSdb : data buffer

kd> ? 86aedff9 +8f
Evaluate expression: -2035359608 = 86aee088 // 越界后结束地址

kd> !pool 86aee088
Pool page 86aee088 region is Nonpaged pool
86aee000 size: 8 previous size: 0 (Free) ….
*86aee000 : large page allocation, Tag is LSbf, size is 0x11000 bytes
Pooltag LSbf : buffer descriptor

发生了越界覆盖

查看覆盖前的内存信息:

kd> db 86aee000 86aee000+88
86aee000 00 10 01 00 00 00 00 00-ff ff 00 00 00 00 00 00 …………….
86aee010 ff ff 00 00 c0 f0 df ff-c0 f0 df ff 00 00 00 00 …………….
86aee020 00 00 00 00 64 0b 00 00-00 f1 df ff 00 00 00 00 ….d………..
86aee030 00 00 00 00 10 e0 ae 86-00 f1 df ff 00 00 00 00 …………….
86aee040 60 00 04 10 00 00 00 00-80 ef df ff 00 00 00 00 `……………
86aee050 10 00 d0 ff ff ff ff ff-10 01 d0 ff ff ff ff ff …………….
86aee060 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 …………….
86aee070 60 00 04 10 00 00 00 00-00 00 00 00 00 00 00 00 `……………
86aee080 90 ff cf ff ff ff ff ff-fa

查看覆盖后的内存信息:

kd> db 86aee000 86aee000+88
86aee000 00 10 01 00 00 00 00 00-ff ff 00 00 00 00 00 00 …………….
86aee010 ff ff 00 00 00 00 00 00-00 00 00 00 00 00 00 00 …………….
86aee020 00 00 00 00 00 00 00 00-00 f1 df ff 00 00 00 00 …………….
86aee030 00 00 00 00 20 f0 df ff-00 f1 df ff 00 00 00 00 …. ………..
86aee040 60 00 04 10 00 00 00 00-80 ef df ff 00 00 00 00 `……………
86aee050 10 00 d0 ff ff ff ff ff-10 01 d0 ff ff ff ff ff …………….
86aee060 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 …………….
86aee070 60 00 04 10 00 00 00 00-00 00 00 00 00 00 00 00 `……………
86aee080 90 ff cf ff ff ff ff ff-fa

**第二个:发送SMB\_COM\_NT\_TRANSACT并附带FEA
LIST和多个transcation,服务端会通过
SMB\_COM\_TRANSACTION2\_SECONDARY将SMB\_COM\_NT\_TRANSACT作为SMB\_COM\_TRANSACTION2处理;通过这种方式可以传入FEA
LIST大于0xffff大小的数据,因为SMB\_COM\_NT\_TRANSACT长度字段类型为ULONG,而SMB\_COM\_TRANSACTION2为USHORT类型**

核心问题:对于
transaction类型的校验,只是以最后接收的\*\_SECONDARY类型为准,因此可以通过SMB\_COM\_NT\_TRANSACT传递payload,并以SMB\_COM\_TRANSACTION2\_SECONDARY结尾,这样就造成了错误解析,将SMB\_COM\_NT\_TRANSACT以SMB\_COM\_TRANSACTION2类型进行解析

SMB Message Structure

# The SMB_Header structure is a fixed 32-bytes in length.
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_Parameters
{
UCHAR WordCount;
USHORT Words[WordCount] (variable);
}
# SMB_Data
{
USHORT ByteCount;
UCHAR Bytes[ByteCount] (variable);
}

通过PID、MID、TID、UID来匹配是否相同,会在服务端将其组装为同一类型trancation

**第三个:在处理 SMB\_COM\_SESSION\_SETUP\_ANDX
命令时,会以13类型的请求方式处理12请求的数据;这样既可以稳定控制连续pool内存的申请和释放**

SMB\_COM\_TREE\_CONNECT\_ANDX SMB\_COM\_SESSION\_SETUP\_ANDX

# SMB_COM_SESSION_SETUP_ANDX
# LM and NTLM authentication
# NT Security request
SMB_Parameters
{
UCHAR WordCount; // 0xD = 13
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[];
} }

# extended security request
SMB_Parameters
{
UCHAR WordCount; // 0xC = 12
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[];
}
}

漏洞函数BlockingSessionSetupAndX伪代码如下:

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); // extract parameters and data to variables
SrvValidateUser(request); // do authentication
}
// …
}

发送Extended Security
request(12)附带CAP\_EXTENDED\_SECURITY,并未附带FLAG2\_EXTENDED\_SECURITY,将该请求伪装成为SMB\_COM\_SESSION\_SETUP\_ANDX(13)

这样就会将请求以NT Security
request(13)进行处理,进入函数GetNtSecurityParameters,在该函数中会通过wordcount和bytecount计算申请的内存大小,但12类型和13类型中的bytecount偏移不同,因此当12类型被作为13类型解析时,会解析到SecurityBlob作为bytecount大小

这里的主要问题是:当设定FLAGS2\_EXTENDED\_SECURITY和CAP\_EXTENDED\_SECURITY,则将请求按照Extended
Security request(12)处理,否则按照NT Security request(13)进行处理。

在payload中的利用:

sessionSetup[‘Parameters’] = smb.SMBSessionSetupAndX_Extended_Parameters()
sessionSetup[‘Data’] = pack(‘

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    请登录后查看评论内容