前言
SMB协议即为指定服务器消息块协议 为数据传输协议 支持压缩数据的传输 漏洞的位置在于当传送压缩数据时 底层的实现对压缩数据的数据包头中的字段处理不当 存在整数溢出
SMB2数据包的连接顺序如下:
- 客户端建立与服务器的连接
- 在该连接上建立经过身份验证的上下文
- 发出各种请求来访问文件 打印机和命名管道以进行进程间通信
基础知识
SMB的官方解释
该协议的压缩数据包结构如下:
字节数 | 含义 | 值(该例子中) | |
---|---|---|---|
protocolID | 4 | 固定值。表示SMB协议 | fc534d42 |
OriginalSize | 4 | 表示压缩数据(compressed data)压缩前的原始大小 | 0x32(50) |
Algorithm | 2 | 压缩的算法 | 1(LZNTI压缩算法) |
Reserved | 2 | 保留 | ffff |
offset | 4 | 压缩数据在data中的偏移位置。即从data的offset处之后才是真正的压缩数据(这个字段也可能表示另外的含义length,大概是data的总长度,但这个漏洞中用的是offset的含义) | 0x64(100) |
data | 要传输的压缩数据,分为两段:offset大小的前半段和实际的compressed data。“poc”中设置orginalsize为50,偏移为0x64(100),data为500个“A”。那么实际的压缩数据为后400(500-100)个“A”,但原始大小为50,比压缩后的数据400还小,显然不是正常的数据包,所以wireshark中显示为invalid。这里只是利用“poc”发的数据包简单说明一下各字段的含义。 | 500个”A” | |
header | 0x10 | 之后的数据处理函数会常常用到0x10这个值,实际上就是指数据包的头部长度。 |
漏洞分析
由于已经有了poc文件 所以直接从poc入手 进行crash 得到程序的栈回溯
断下之后发现引用了无效内存导致蓝屏
栈回溯找到漏洞函数
利用wireshark也可看到相关信息
使用IDA静态分析该函数 该函数是对SMB压缩数据包中的压缩数据进行解压还原处理 函数的参数就是SMB 其实该a1也就是我们的可控参数 这是传输的数据包而v2取决于a1 v3取决于v2 v4取决于v3 从而导致了SrvNetAllocateBuffer函数参数可控
1 | signed __int64 __fastcall Srv2DecompressData(__int64 a1) |
SrvNetAllocateBuffer函数里面的参数是我们可以控制的
1 | SrvNetAllocateBuffer( |
OriginalCompressedSegmentSize和OffsetOrLength是我们可以控制的字段 OriginalCompressedSegmentSize是压缩前的数据大小 OffsetOrLength是压缩数据的长度或者片偏移 取决于是否设置flags变量
该字段的定义如下
回到栈回溯 我们来看srvnet的SmbCompressionDecompress函数
1 | __int64 __fastcall SmbCompressionDecompress(int a1, __int64 a2, int a3, __int64 a4, unsigned int a5, unsigned int *a6) |
然后来到RtlDecompressBufferEx2函数 该函数调用RtlDecompressBufferProcs
1 | __int64 __fastcall RtlDecompressBufferEx2(unsigned __int16 a1, __int64 a2, unsigned int a3, __int64 a4, __int64 a5, __int64 a6, __int64 a7, __int64 a8) |
最终调用到RtlDecompressBufferXpressLz函数的解压算法中出错
先看[RtlDecompressBufferEx2](RtlDecompressBufferEx2 function (ntifs.h) - Windows drivers | Microsoft Docs) 根据该函数定义可知 函数的第三个参数a5 即为UncompressedBuffer buffer大小 对应的是SmbCompressionDecompress的a5 即为r8
动态分析一下传参
在SrvNetAllocateBuffer函数之前的计算buffer大小先下断
1 | ba e1 srv2!Srv2DecompressData+0x77 |
看到buffer大小为0x3ff
下断点
1 | ba e1 srvnet!SmbCompressionDecompress |
然后我们可以看到r8的值 该值为401 为压缩数据的长度 解压数据存放位置为r9
再下断 可以看到很多数据是上面传参传来的rcx rdx r8 r9
1 | ba e1 nt!RtlDecompressBufferEx2 |
接下来再单步执行几步 即可发生崩溃 崩溃发生在函数偏移0x57处
对应的即为a3
即就是上面的r8 所以得出结论 即为传递的r8出错 导致越界读写崩溃
该函数在对数据进行解压处理是 读取第一个值时发生了越界读写
漏洞利用
通过前面的分析 我们可控的数据在SrvNetAllocateBuffer的第一个参数 即这个v4
进入SrvNetAllocateBuffer函数分析 可以看到 该函数对传进了的第一个参数进行了多次判断
首先判断是否大于0x100100就会再次判断是否大于0x1000100 大于的话直接返回 如果小于0x100100再判断是否大于0x1100 大于的话执行_BitScanReverse64 该函数在掩码数据中从最高有效位到最低有效位搜索设置位 找到length-0x1000中第一个1的位置 相等的话用最高比特位索引减去0xC 否则减去0xB 索引即为表示SrvNetBufferLookasides中相应的索引 接下来执行SrvNetBufferLookasides函数 该函数用于分配内存 如果V10偏移0x70处不为0 接下来执行PplpLazyInitializeLookasideList函数
比特位 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
---|---|---|---|---|---|---|---|---|---|
长度 | 0x1100 | 0x2100 | 0x4100 | 0x8100 | 0x10100 | 0x20100 | 0x40100 | 0x80100 | 0x100100 |
索引 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
1 | PSLIST_ENTRY __fastcall SrvNetAllocateBuffer(unsigned __int64 a1, __int64 a2) |
该函数获取的缓冲区结构如下:
函数返回SRVNET_BUFFER_HDR结构的指针 偏移0x18处存放了User Buffer指针 User Buffer区域用来存放还原的SMB
数据 解压操作其实是指向User Buffer偏移offset处释放解压数据
程序的逻辑是 再解压成功之后调用memcpy函数将raw data复制到User Buffer的起始处 解压后的数据从offset偏移处开始存放 未压缩的数据后面跟着解压后的数据 复制发数据没有超过User Buffer的范围 但是由于整数溢出 导致分配的User Buffer空间会小 User Buffer减offset剩下的空间无法容纳解压后的数据 所以我们可以综合前面的知识 构造Offset Raw Data和Compressed Data 在解压时覆盖后面Srvnet Buffer Header结构体中的User Buffer指针 从而在memcpy时向UserBuffer写入可控的数据
在动态调试过程中 还可以发现调用了SrvNetAllocateBufferFromPool函数分配内存
1 | srvnet!SrvNetAllocateBuffer+0xd597: |
该函数调用ExAllocatePoolWithTag分配内存
在根据IDA的反汇编代码可知PoolType = 0x200,NumberOfBytes = 0x1278,Tag = “LS00”
分配的内存空间起始偏移处为V12 该空间起始处偏移0x50处的值被存放在v12的偏移0x18处 v12偏移0x20处为0x1100 为UncompressedBuffer缓冲区的长度 12偏移0x38处和0x50处分别指向2个MDL结构
1 | v11 = (signed __int64)(v8 + 0x50); |
该漏洞已有提权代码 主要时使用替换Token值的方法来进行提权 利用漏洞在Token对象的0x40偏移处(privileges成员)中写入指定的值 然后提升权限 该成员包含与令牌相关的特权的所有信息 其中Present为令牌当前可用的权限 Enabled为已启用的权限 EnabledByDefault为默认情况下已启用的权限
1 |
|
然后使用任意读写覆盖Present 和 Enabled字段