首页
社区
课程
招聘
[原创]ms17-010 漏洞分析
发表于: 2022-7-26 18:56 19503

[原创]ms17-010 漏洞分析

2022-7-26 18:56
19503

​ 近期重新分析了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函数在进行fealistntfeallist的长度计算过程中进行了一个强制类型转换,导致了四个字节的长度只覆盖了低位的两个字节,数据在转换过程中大于申请的内存空间,从而实现溢出。此处就主要介绍一下为什么会出现四个字节转两个字节的情况?

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 v1ntml 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空间大小相似,由于Buff1Buff2的空间都属于较大的,在分页内存空间中分配大概率会前后紧挨着,分页内存在分配时又会从低地址向高地址进行分配,此时的空间布局应该是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的标志位是不是8000,不是就跳出循环
      {                                         // 注意,这里是以标志位为循环判断基础的,而本来那个损坏的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的标志位是不是8000,不是就跳出循环
      {                                         // 注意,这里是以标志位为循环判断基础的,而本来那个损坏的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){

[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

收藏
免费 3
支持
分享
最新回复 (1)
雪    币: 60
活跃值: (63)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
师傅带带带
2023-2-23 20:45
0
游客
登录 | 注册 方可回帖
返回
//