深入理解分析 MS17-010

henry Lv4

MS17-010

由于课程作业的需求,这里笔者重新将MS17-010进行分析,从windows内核角度来看曾经风靡一时的MS17-010漏洞形成原因以及利用过程。

一、SMB 介绍

SMB(Server Message Block)服务是一种网络文件共享协议,允许在网络上的计算机之间共享文件、打印机和其他资源,SMB 协议最初由 IBM 开发,并在 Microsoft 的 Windows 操作系统中广泛使用,它定义了如何在网络中进行文件访问、打印服务、命名管道等。

1.1 特点

  1. 文件共享

    SMB 允许用户在网络上的计算机之间共享文件和目录。用户可以在远程计算机上访问文件,就像在本地计算机上一样。

  2. 打印共享

    SMB 还支持打印机共享,使得网络上的多个用户可以共享同一台打印机。

  3. 通信协议

    SMB 提供了一种通信机制,允许应用程序在不同计算机之间进行数据交换。它使用了一种客户端/服务器模型,其中客户端请求服务,服务器响应请求。

  4. 跨平台支持

    尽管最初是 Microsoft 开发的,但 SMB 协议已经被许多操作系统和平台所支持,包括 Linux、Unix、macOS 和一些网络设备。

1.2 安全性

  • 禁用SMB 1.0

    SMB 1.0 因存在许多安全漏洞(如 永恒之蓝)而被禁用。推荐使用更高版本的 SMB 协议(如 SMB 3.0 或 SMB 3.1.1)。

  • 加密和身份验证

    最新版本的 SMB 协议提供了加密功能和更强的身份验证机制,以提高数据传输的安全性。

1.3 使用场景

  • 文件和打印机共享

    在家庭或小型办公室网络中,使用 SMB 协议共享文件和打印机。

  • 企业文件服务器

    在企业环境中,利用 SMB 协议建立文件服务器,供员工访问和共享文件。

  • 跨平台文件共享

    允许不同操作系统之间的文件和资源共享。

1.4 生命周期

一次普通的SMB周期大致如下:

	nipaste_2024-06-18_16-58-3

二、漏洞分析

调试环境

win11:主机用来远程调试

win7 64位 SP1:作为靶机

kali2023:用来作为攻击机

2.1 利用介绍

溢出发生在内存中的大非分页池,由于大非分页池没有头部。因此池和池之间的内存空间是连续的,可以在上一个池后分配一个紧密相连的池,这个池由驱动分配并含有驱动的数据。

因此,必须通过操纵池后被溢出的池,EternalBlue 使用的技术就是控制 SRVNET 驱动的缓冲区结构,为了实现这一点,两个缓冲区在内存中必须是对齐的,为了实现非分页堆的对齐,使用了内核池喷射技术。

nipaste_2024-06-17_18-24-3

利用过程

1.FreeHole_A: EternalBlue通过发送SMB v1数据包来完成占位

2.SMBv2_1n: 发送一组SMB v2数据包

3.FreeHole_B: 发送另一个占位数据包;必须确保第一个占位的FreeHole_A被释放之前,这块内存被分配

4.FreeHole_A_CLOSE: 关闭连接,使得第一个占位的内存空间被释放

5.SMBv2_2n: 发送一组SMB v2数据包

6.FreeHole_B_CLOSE: 关闭连接来释放缓冲区

7.FINAL_Vulnerable_Buffer: 发送最后的数据包,这个数据包将会被存储在有漏洞的缓冲区中

有漏洞的缓冲区(之前SRVNET创建的)被填入的数据将会覆盖部分SRVNET的缓冲区。

2.1 静态分析

漏洞代码位于 srv 驱动模块中内核态函数 SrvSmbOpen2SrvOs2FeaListToNt 的调用,SrvOs2FeaListToNt 用于将 FEAlist 转换 NTFEA list,而在这个转换过程中,存在一个转换类型错误漏洞,即函数 srv!SrvOs2FeaListToNt 在将FEA list 转换成 NTFEA(Windows NT FEA) list 前会调用 srv!SrvOs2FeaListSizeToNt 去计算转换后的 FEA lsit 的大小,从而导致可以进行缓冲区溢出。

nipaste_2024-06-17_16-18-0

其中 _FEA 和 _FEALIST 的结构体内容如上所示,下面这张图就是用来进行转换过程中的处理函数

nipaste_2024-06-18_17-19-5

在对原函数进行逆向后,注释如上图所示,同时这里进入到 SrvOs2FeaListSizeToNt 函数中来具体分析类型转换错误原因。

SrvOs2FeaListSizeToNt

nipaste_2024-06-18_17-23-5

上面红框中的位置即为漏洞点所在,由前面分析 _FEALIST 的结构体可以知道,其 cblist 成员占四个字节大小,在实际使用的过程中如果 cblist 的长度大于两个字节所能表示的最大大小,如 0x10000 就有可能导致转换过程中使用强制类型转换,从而让其高位的两个字节并不会被重置,最后形成长度溢出(具体见后面调试分析)。

nipaste_2024-06-18_17-59-0

从上图中的汇编也可以明显看到这里是使用 bx 16 位寄存器进行值传递的。

SrvOs2FeaToNt

该函数用来将每一个具体的 FEA 数据转换为 NTFEA 格式,从下图中可以看到在进入 while 循环前会将 FEALIST 中的 cblist 取出,并作为循环终止条件,如果在前面利用类型转换错误实现 FEALIST 的长度溢出,那么同理下面的while 循环次数也会增加,从而可以将恶意数据溢出到下一个非分页池当中。

nipaste_2024-06-18_18-05-4

总结

  1. srv!SrvOs2FeaListSizeToNt 会计算 FEA list 的大小并更新待转换的 FEA list 的大小

  2. 因为错误的使用 WORD 强制类型转换,导致计算出来的待转换的 FEA list 的大小比真正的 FEA list 大

  3. 因为原先的总大小计算错误,导致当 FEA list 被转化为 NTFEA list 时,会在非分页池导致缓冲区溢出

2.2 动态分析

根据前面的分析,断点设置如下

1
2
3
bp srv!SrvSmbOpen2+0xc3				;call    srv!SrvOs2FeaListToNt (fffff88006719960)
bp srv!SrvOs2FeaListToNt+0x2e
bp srv!SrvOs2FeaListToNt+0x2e+5

nipaste_2024-06-17_15-40-4

其中 FEALIST 的大小被设置为了 0x10000,一个非分页池的大小为 0x11000,数据结构体的长度满足在这个范围内,这个值被存放在 FEALIST->cbList 中。

当前函数调用栈如下所示:

1
2
3
4
5
6
7
8
9
10
3: kd> k
# Child-SP RetAddr Call Site
00 fffff880`0633b970 fffff880`03c84815 srv!SrvSmbOpen2+0xc3
01 fffff880`0633ba20 fffff880`03cc8047 srv!ExecuteTransaction+0xc5
02 fffff880`0633ba60 fffff880`03c3abb8 srv!SrvSmbTransactionSecondary+0x3f7
03 fffff880`0633baf0 fffff880`03c3aad3 srv!SrvProcessSmb+0xb8
04 fffff880`0633bb70 fffff880`03c7f78f srv!SrvRestartReceive+0xa3
05 fffff880`0633bbb0 fffff800`04122cce srv!WorkerThread+0xe9
06 fffff880`0633bc00 fffff800`03e76fe6 nt!PspSystemThreadStartup+0x5a
07 fffff880`0633bc40 00000000`00000000 nt!KiStartSystemThread+0x16

接下来将会分配一个缓冲区用来存放将FEA list转换为 NTFEA list 后的数据。这就意味着需要通过函数 srv!SrvOs2FeaListSizeToNt 计算转换后NTFEA list的大小,我们在这个函数调用前下断点来查看此时 FEA 的长度和返回后的 NTFEA list 的长度,发现 FEALIST->cbList 从0x10000增长到了0x1ff5d。

nipaste_2024-06-17_15-47-0

从调试的结果来看 FEALIST->cbList 增大了 0xff5d 的长度,在前面的静态分析中有提到过 **SrvOs2FeaListSizeToNt **。

nipaste_2024-06-17_15-57-5

即源程序 SrvOs2FeaListSizeToNt 函数这里采用了强制类型转换,导致最后的结果只对 FeaList->cbList 的低2字节进行了修改,而高位的字节仍然保留在原区域位置 。

下面在来分析当大非分页池发生溢出时候的状况,设置断点如下:

1
2
3
4
5
bp srv!SrvOs2FeaListToNt+0xa9        ;PAGE:000000000007FA09    lea     r14, [rax+rdi-5]
bp srv!SrvOs2FeaListToNt+0xc1 ;PAGE:000000000007FA21 call SrvOs2FeaToNt
bp srv!SrvOs2FeaToNt+0x38 ;PAGE:000000000007D578 call memmove1
bp srv!SrvOs2FeaToNt+0x5a ;PAGE:000000000007D59A call memmove2
bp srv!SrvOs2FeaListToNt+0x101 ;跳出循环

nipaste_2024-06-17_16-48-5

由前面可以知道这里 FeaList->cbList 已经被设置为了 0x1ff5d,在 while 循环这里下断点,从下图中看到 v14 的大小为 0x1ff5d,再次验证了通过溢出错误成功设置了 while 循环的次数。

nipaste_2024-06-17_16-48-1

设置断点如下:

1
2
3
4
5
6
7
8
bp srv!SrvOs2FeaListToNt+0xc1 ".printf \"NEXT: FEA: %p NTFEA: %p\\n\", rdx, rcx; g;"
bp srv!SrvOs2FeaToNt+5a ".printf \"memmove2: dst: %p src: %p size: %p\\n\", rcx, rdx, r8; g;"
bp srv!SrvOs2FeaListToNt+0x101

# 下面这部分用于只输出最后的一部分
bp srv!SrvOs2FeaListToNt+0xc1 ".if @rcx > fffffa803128e010 + 0x1c44 {.printf \"NEXT: FEA: %p NTFEA: %p\\n\", rdx, rcx; g;}.else{g}"
bp srv!SrvOs2FeaToNt+5a ".if @r8 > 0x000000a0 {.printf \"memmove2: dst: %p src: %p size: %p\\n\", rcx, rdx, r8; g;}.else{g}"
bp srv!SrvOs2FeaListToNt+0x101

这里将每次转换过程中的 NTFEA 和 FEA 项的转换信息输出,从下图中可以看到在前面将传输数据 size 设置为 0,每次 NTFEA 的增加值都是固定为 0xc(不包括数据部分),在最后第606次,即循环的倒数第二次,设置size 为的目标地址减去当前 NTFEA 地址之间的偏移,,这里即为要溢出的区域。

1
fffffa803315aff8 - fffffa803314bc75 = f383

nipaste_2024-06-17_18-32-5

fffffa803315b001 地址不再属于 NTFEA 的非分页池大小范围,这里实际上该地址属于 srvnet 的非分页池(见后面说明) ,但这里依然可以溢出 0xa8 的数据到 srvnet 的非分页池中,可以在 windbg 里面看溢出的具体数据信息。

nipaste_2024-06-17_18-47-3

将覆盖前和覆盖后的数据做一个比较,覆盖前的数据为正常的 SRVNET 非分页池的头部数据

nipaste_2024-06-18_11-58-2

从下图中可以看到已经成功利用溢出修改了这部分数据。

nipaste_2024-06-18_12-00-0

通过溢出非法地址之后,靶机在执行 srvnet.sys 服务时会出现蓝屏。

nipaste_2024-06-17_16-40-3

重新下断点,从下图中可以看到位于 srv 中申请的非分页池下方溢出区域正是 srvnet 的非分页池区域,这是因为在实际利用的过程中使用了堆喷和堆占位操作使得可以 srv 溢出的部分刚好覆盖到 srvnet 中数据。

nipaste_2024-06-18_12-05-1

为了了解具体的观察内存分配结构,可以在 srv.sys 和 srvnet.sys 中分配池的函数中下断点,来查看内存分配情况,断点设置如下

1
2
3
4
5
6
7
bp srvnet!SrvNetAllocatePoolWithTag+0x11
bp srv!SrvAllocateNonPagedPool+0xe7
bp srv!SrvFreeNonPagedPool+0x51

bp srvnet!SrvNetAllocatePoolWithTag+0x17 ".if @rdx > 0x0000f000 {.printf \"srvnet!SrvNetAllocatePoolWithTag Allocation Address: %p; Size: %p;\\n\",eax,esi;g}.else{g;}"
bp srv!SrvAllocateNonPagedPool+0xed ".if @rsi > 0x0000f000 {.printf \"srv!SrvAllocateNonPagedPool Allocation Address %p Size: %p;\\n\",rax,rsi;g}.else{g}"
bp srv!SrvFreeNonPagedPool+0x51 ".printf \"srv!SrvFreeNonPagedPool Free Address: %p\\n\",rcx;g;"

经测试在 win7 SP1 64位系统下, poc 在发送数据包的过程并不由 srvnet!SrvNetAllocatePoolWithTag 进行分配非分页池,而是由 srvnet!SrvNetAllocateNonPagedBufferInternal 进行分配,所以将断点打在下面这里。

1
2
3
bp srvnet!SrvNetAllocateNonPagedBufferInternal+0x68 ".if @rbx > 0x0000f000 {.printf \"srvnet!SrvNetAllocateNonPagedBufferInternal Allocation Address: %p; Size: %p;\\n\",rax,rbx;g}.else{g;}"
bp srv!SrvAllocateNonPagedPool+0xed ".if @rsi > 0x0000f000 {.printf \"srv!SrvAllocateNonPagedPool Allocation Address %p Size: %p;\\n\",rax,rsi;g}.else{g}"
bp srv!SrvFreeNonPagedPool+0x51 ".printf \"srv!SrvFreeNonPagedPool Free Address: %p\\n\",rcx;g;"

nipaste_2024-06-18_14-13-3

1
fffffa80332b9000 + 0x11000 = fffffa80332ca000

​ 从上面这里可以看到通过堆喷和堆占位让这两个非分页池实现相邻,从而可以稳定溢出到 srvnet 中的非分页池头部,大致分配策略如下图所示

nipaste_2024-06-17_19-06-3

执行流劫持

1
2
3
4
5
6
7
8
bp srvnet!SrvNetCommonReceiveHandler "kb;g;"
bp srvnet!SrvNetWskReceiveComplete "kb;g;"

r $t0 = 0
bp srvnet!SrvNetCommonReceiveHandler + b7 ".if(@r10+8 == 0xfffffa8032f3f758) {.printf \"address %p, rax = %p times = %d\\n\",r10+8,rax,$t0; r $t0 = $t0 + 1;g;}.else{}"

.text:0000000000011730 push rbx
.text:00000000000117E7 call qword ptr [r10+8]

ms17-010 永恒之蓝最后程序执行流劫持利用的是 srvnet 中的 SrvNetCommonReceiveHandler ,该函数中存在一个函数指针调用,如果能够对该指针进行劫持,则可以完成程序控制流劫持的效果,如下图所示。

nipaste_2024-06-18_15-56-4

在这里下好断点,然后输出每次函数调用地址,从下图中可以发现之前每次调用的都是固定地址fffffa8032f3f758,而在最后一次的调用过程中会发现其函数指针调用的指针已经被修改为一个恶意地址ffffffffffd001f8

nipaste_2024-06-18_15-52-5

为了只在最后调用恶意地址时断点才停下,这里设置一个条件断点:

1
2
r $t0 = 0
bp srvnet!SrvNetCommonReceiveHandler + b7 ".if(@r10+8 == 0xfffffa8032f3f758) {.printf \"address %p, rax = %p times = %d\\n\",r10+8,rax,$t0; r $t0 = $t0 + 1;g;}.else{}"

然后程序就可以成功断住,跟入就可以发现这里就是 shellcode 的入口地址,需要注意的是在 win7 中这块虚拟地址空间,即 ffffffff`ffd00000 是具有可执行权限的,为系统预留的一片区域,用于保存系统的一些信息,如时钟,版本,配置之类,注意事项在 win10 之后这块地址不再具有可执行权限。

nipaste_2024-06-18_16-13-3

至此就完成了整个 ms17-010 永恒之蓝的利用分析过程。

三、poc

eternal blue 的官方poc,经过上面的分析,相信再来看会很容易理解

https://gist.github.com/worawit/bd04bad3cd231474763b873df081c09a

四、补丁分析

4.1 winxp

winxp 由于已被微软不在维护其更新,因此其 srv.sys 驱动中漏洞仍然存在。

nipaste_2024-06-18_20-30-4

4.2 win7

这里对打过补丁的 win7 x86系统进行分析,将其 srv.sys 放到 ida 中进行分析,可以看到打过补丁之后的代码这里不在存在强制类型转换了,而是直接进行四字节大小的赋值所以在漏洞利用过程中会失败。

nipaste_2024-06-18_19-58-4

4.3 win10

Win10的早期版本使用的仍然是存在该漏洞的srv.sys驱动,该漏洞在win10下需要靶机关闭防火墙,同时需要更改组策略,才可以利用成功

nipaste_2024-06-19_09-51-3

在较新版本中的win10中,SMB3.1.1已被win10所采用,整个srv.sys驱动基本全部被重写,导致ms17-010不再能够完成利用

nipaste_2024-06-19_10-06-1

双机调试环境:

https://blog.csdn.net/huanongying131/article/details/90740286

https://blog.csdn.net/m0_63898862/article/details/132791739

参考链接:

https://www.anquanke.com/post/id/86270

https://www.52pojie.cn/thread-1196509-1-1.html

  • Title: 深入理解分析 MS17-010
  • Author: henry
  • Created at : 2024-06-19 10:41:52
  • Updated at : 2024-06-19 10:52:45
  • Link: https://henrymartin262.github.io/2024/06/19/MS17-010/
  • License: This work is licensed under CC BY-NC-SA 4.0.
 Comments