-
-
[原创]ms17-010 漏洞分析
-
2022-7-26 18:56 18177
-
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触发部分分析
- 基础知识
MDL是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 # 用来让srvnet!SrvNetFreeBuffer函数真的释放空间,防止被直接置0重复使用 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 # shellcode触发链指针 0x38 00 f1 df ff # _ 0x3c 00 00 00 00 # MDL.next 0x40 60 00 # MDL.size 0x42 04 10 # MDL.MdlFlags 0x44 00 00 00 00 # MDL.*Process 0x48 80 ef df ff # MDL.MappedSystemVa x86_addr-0x80 0x4c 00 00 00 00 # _ 这里后续本来应该是StartVa,ByteCount,ByteOffset 0x50 10 00 d0 ff ff ff ff ff # x64 MDL 0x58 10 01 d0 ff ff ff ff ff # x64 pmdl2 0x60 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x70 60 00 # MDL.size 0x72 04 10 # MDL.MdlFlags 0x74 00 00 00 00 00 00 00 00 00 00 00 00 0x80 90 ff cf ff ff ff ff ff # x64_addr-0x80 |
其中需要关注的是偏移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协议验证的命令,但是对于ntml v1 和 ntml v2却有两个不同的请求结构体,而其中两个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层进行的检测基本都能绕过,确实是个相当好的组合漏洞,对当年就能写这种工具的大佬佩服地五体投地。
[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界