1、 漏洞简述

漏洞名称:“永恒之蓝”漏洞

漏洞编号:MS17-010,CVE-2017-0143/0144/0145/0146/0147/0148

漏洞类型:缓冲区溢出漏洞

漏洞影响:信息泄露

CVSS评分:9.3(High)

利用难度:Medium

基础权限:不需要

2、组件概述

SMB(Server Message Block)是一个协议名,它能被用于Web连接和客户端与服务器之间的信息沟通。其目的是将DOS操作系统中的本地文件接口“中断13”改造为网络文件系统。

 SMB1.0协议由于在文件共享传输过程中存在的传输效率低以及传输空间小等缺陷被人们所摒弃。为了更好的实现网络中文件的共享过程,在SMB1.0的基础上开发了新的网络文件传输协议,并将其命名为SMB2.0。

该协议在实现了文件共享传输的基本功能的基础上对文件传输的效率、文件缓存的空间以及文件并发传输等问题进行改进,使得在局域网或更高配置的网络环境下,文件传输过程的速度和效率等得到了很大的提升。

3、 漏洞影响

Windows Vista SP2; Windows Server 2008 SP2 and R2 SP1; Windows 7 SP1; Windows 8.1; Windows Server 2012 Gold and R2; Windows RT 8.1; and Windows 10 Gold, 1511, and 1607; and Windows Server 2016

以上系统打开445号端口都容易受到影响。

4、解决方案

禁用 SMBv1

对于客户端操作系统:

  1. 打开“控制面板”,单击“程序”,然后单击“打开或关闭 Windows 功能”。
  2. 在 Windows 功能窗口中,清除SMB1.0/CIFS 文件共享支持复选框,然后单击确定关闭窗口。
  3. 重新启动系统。

对于服务器操作系统:

  1. 打开服务器管理器,然后单击管理菜单并选择删除角色和功能。
  2. 在功能窗口中,清除SMB1.0/CIFS 文件共享支持复选框,然后单击确定关闭窗口。
  3. 重新启动系统。

更新Windows系统补丁:官方文档链接

漏洞复现

1、环境搭建

实验中需要用到三个机器,分别是调试机,靶机(被调试机),攻击机。

调试机环境:主机Windows 10x64专业版 1909

靶机(被调试机)环境:Windows 7x86 SP1

靶机(被调试机)配置:192.168.44.132:445

攻击机环境:Windows XPx86 SP3

攻击机配置:192.168.44.152

(1)双机内核调试

首先需要用调试机调试靶机,具体实现双机调试请看这里。

(2)安装并配置fuzzbunch

另外需要在攻击机安装fuzzbunch实现永恒之蓝漏洞:

  • 安装Python 2.6
  • 安装PyWin32需要以管理员权限进行安装
  • 下载fuzzbunch
  • 在shadowbroker-master的子目录windows中新建listeningposts文件夹,同时修改FuzzyBunch.xml文件内容,设置相应的ResourcesDir和LogDir参数值,修改结果如下图所示。(路径根据实际情况而定)

(3)靶机环境配置

  • 打开靶机445端口,具体打开方法点这里
  • 关闭防火墙:控制面板-系统和安全-Windows防火墙-打开或关闭Windows防火墙-全部选择关闭防火墙。

3、复现过程

在攻击机中启动命令行,进入fuzzbench的Windows文件夹,用python启动fuzzbench:

启动后设置靶机IP-设置击机IP-重定向no-log地址确认(无误直接回车)-0+回车(创建新的项目)-为项目命名-设置路径(Yes)-:

使用永恒之蓝建立后门:

use Eternalblue

之后一直回车到出现设置靶机系统版本,本次复现靶机为Win7,所以选择1:

下一项模式选择1,FB模式:

接下来一直回车即可,即可看到插件运行,并成功利用永恒之蓝漏洞在靶机系统中留下后门:

漏洞分析

1、基本信息

漏洞文件:srv.sys

漏洞函数:srv!SrvOs2FeaListToNt

漏洞对象:为NtFeaList分配的缓冲区

2、 背景知识

(1) SrvOs2FeaListToNt()函数

SrvOs2FeaListToNt()函数用于将FEA list转化为NTFEA list,需要分配在内核地址大非分页池分配缓冲区来存放转化后的NTFEA list,因此需要先计算转化后的NTFEA list的大小,计算大小是通过srv!SrvOs2FeaListSizeToNt()函数进行的,这个函数被SrvOs2FeaListToNt()调用。

(2)0xffdff000系统预留地址空间

0xffdff000处地址是系统预留的地址空间,用于存放时钟,版本,配置等信息,其分配的权限为可执行:

kd> !pte 0xffdff000
                    VA ffdff000
PDE at C0603FF0            PTE at C07FEFF8
contains 000000000018A063  contains 00000000001E3163
pfn 18a       ---DA--KWEV  pfn 1e3       -G-DA--KWEV

3、 详细分析

(1)静态分析、Ida分

使用Ida打开srv.sys

首先按shift + F1,再按insert键导入两个结构:

struct _FEA{
BYTE fEA;   //标记位
BYTE cbNAME;  //记录名称长度
USHORT cbValue;  //记录值的长度
}FEA;
struct _FEALIST{
ULONG cbList;  //记录Fea List总长度
_FEA list[1];
}FEALIST, *PFEALIST;

查看srv!SrvOs2FeaListSizeToNt()函数源码,右键a1,使用_FEALIST结构覆盖:

int __stdcall SrvOs2FeaListSizeToNt(_FEALIST *FeaList)
{
  _FEALIST *v1; // eax
  char *v2; // edi
  _FEA *v3; // esi
  int v4; // ebx
  int v6; // [esp+Ch] [ebp-4h]
  v1 = FeaList;                                 // a1
  v6 = 0;                                       // NtFeaList的大小
  v2 = (char *)FeaList + FeaList->cbList;       // 获取指向表结尾的指针v2
  v3 = FeaList->list;                           // 指向表的开始
  if ( FeaList->list < (_FEA *)v2 )             // 表的开始指针应该在结尾指针之前
  {
    while ( &v3[1] < (_FEA *)v2 )               // 从FeaList表的第一个元素开始,遍历整个表
    {
      v4 = v3->cbValue + v3->cbNAME;            // 获取当前FEA的长度
      if ( &v3[1].cbNAME + v4 > (BYTE *)v2 )    // 检查下一个FEA是否有效
        break;
      if ( RtlSizeTAdd(v6, (v4 + 12) & 0xFFFFFFFC, &v6) < 0 )// 增加NtFeaList的大小,每次12字节
        return 0;
      v3 = (_FEA *)((char *)v3 + v4 + 5);       // 下一个FEA,加5意思是每个FEA后面有5字节的NULL
      if ( v3 >= (_FEA *)v2 )
        return v6;
      v1 = FeaList;                             // 重置v1
    }
    LOWORD(v1->cbList) = (_WORD)v3 - (_WORD)v1;  //此处发生计算错误
  }
  return v6;
}

LOWORD(v1->cbList) = (_WORD)v3 - (_WORD)v1; 这句代码中存在一处错误,cbList长度本为4字节,这里却被强制转换为2字节,导致赋值过程中无视高2字节的内容,只赋值了低2字节的内容。

(2)补丁Diff

原来的SrvOs2FeaListSizeToNt()函数中的C代码:

LOWORD(v1->cbList) = (_WORD)v3 - (_WORD)v1;

修补后:

*(DWORD*)(v1->cbList) = v3 - (_DWORD)v1;

(v1->cbList)数据类型由WORD变为了DWORD

2、动态分析

将Windbg(需要配置好符号文件)连接靶机进行内核调试,连接成功Windbg会显示:

运行前先设置几个断点来采集运行过程中涉及的关键数据:

bp srv!SrvSmbOpen2+0x79 ".printf \"feasize: %p indatasize: %p fealist addr: %p\\",edx,ecx,eax;g;"
// 获取Fea大小和indata大小
bp srv!SrvOs2FeaListToNt+0x10 ".printf \"feasize before: %p\\",poi(edi);r $t0 = @edi;g;"
bp srv!SrvOs2FeaListToNt+0x15 ".printf \"NTFEA size: %p feasize after: %p\\",eax,poi(@$t0);g;"
// FEA List大小的前后变化,以及NTFEA List大小
bp srv!SrvOs2FeaListToNt+0x99 ".printf \"NEXT: FEA: %p NTFEA: %p\\",esi,eax;g;"
bp srv!SrvOs2FeaToNt+04d ".printf \"MOV2: dst: %p src: %p size: %p\\",ebx,eax,poi(esp+8);g;"
// 查看被分配的池
bp srv!SrvOs2FeaListToNt+0xd5
// 查看服务器返回的值

设置好断点后,在攻击机运行脚本,开始攻击。这时,Windbg会记录下攻击过程中的数据:

整理一下:

feasize: 00010000
indatasize: 000103d0
fealist addr: a1f050d8
feasize before: 00010000
NTFEA size: 00010fe8
feasize after: 0001ff5d

可以看到经过某处指令后feasize由00010000变为0001ff5d,这就是代码LOWORD(v1->cbList) = (_WORD)v3 - (_WORD)v1;产生的错误计算,其原因请看下面对应的汇编指令:

 此时esi和eax寄存器的值:

FeaEnd: esi = a4217035h

FeaStart: eax = a42070d8h

FeaSize: [eax] = 00010000

sub esi, eax 
mov word ptr[eax], si

经过sub指令后esi的值为ff5d,而在给[eax]赋值的时候使用WORD类型,导致了eax内容变成了0001ff5d。

继续查看内存池的数据:

NEXT: FEA: a1f050dc NTFEA: 85da4008
MOV2: dst: 85da4011 src: a1f050e1 size: 00000000
NEXT: FEA: a1f050e1 NTFEA: 85da4014
MOV2: dst: 85da401d src: a1f050e6 size: 00000000
......
NEXT: FEA: a1f05ca3 NTFEA: 85da5c4c
MOV2: dst: 85da5c55 src: a1f05ca8 size: 00000000
NEXT: FEA: a1f05ca8 NTFEA: 85da5c58
MOV2: dst: 85da5c61 src: a1f05cad size: 00000000
NEXT: FEA: a1f05cad NTFEA: 85da5c64
MOV2: dst: 85da5c6d src: a1f05cb2 size: 0000f383
NEXT: FEA: a1f15035 NTFEA: 85db4ff0
MOV2: dst: 85db4ff9 src: a1f1503a size: 000000a8
srv!SrvOs2FeaListToNt+0xd5:
8949263a be0d0000c0      mov     esi,0C000000Dh

分配的起始地址为85da4008,正常分配的结束地址为85da4008h + 00010fe8h = 85db4ff0h,到了Windbg中显示的最后一块池时,发生了错误,并返回STATUS_INVALID_PARAMETER(0xC000000D),查看85db4ff0h中的内容后发现这片地址属于srvnet.sys分配的池。

而这个池中存在一个很关键的数据,这个数据是一个地址,当靶机系统再接受数据的时候会将数据复制到这个地址+0x80的地方。

这个关键的地址位于_MDL结构中的MappedSystemVa成员,这个结构在srvnet池分配开始的地址+3c,我们在Windbg中找到这个结构:

kd> dt 85db503C _MDL
nt!_MDL
   +0x000 Next             : 0xffffffff _MDL
   +0x004 Size             : 0n96
   +0x006 MdlFlags         : 0n4100
   +0x008 Process          : (null)
   +0x00c MappedSystemVa   : 0xffdfef80 Void
   +0x010 StartVa          : (null)
   +0x014 ByteCount        : 0xffd00010
   +0x018 ByteOffset       : 0xffffffff

可以看到此时的这个结构的MappedSystemVa成员已经被覆盖为0xffdfef80,当靶机再次接受数据时,会向0xffdf80+0x80也就是在刚才介绍中提到的系统预留的一片可执行空间0xffdff000,此时攻击机获得一次写入shellcode的机会。那么程序是如何执行这部分shellcode的呢?

当靶机接受完这部分数据后,还会调用srvnet!SrvNetWskReceiveComplete这个函数,巧了,刚好这个函数会调用到写入的shellcode,就此,成功劫持了靶机的执行流。

查看0xffdff000地址空间内容如图所示:

0xffdff1f1处为shellcode的开始。

缓解措施

1、关闭445端口

2、更新补丁