十一七 发表于 2023-8-4 23:45

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

本帖最后由 十一七 于 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
   
    ```cpp
    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<format=hex>;
      DWORD SignaturesOffset<format=hex>;
      DWORD Reserved3<format=hex>; // TODO: PADDING
    } CLFS_LOG_BLOCK_HEADER<fgcolor=cPurple>;
    ```
   
- CLFS_CONTROL_RECORD
   
    ```cpp
    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;
    } CLFS_CONTROL_RECORD<bgcolor=cLtPurple>;
    ```
   

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



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

- (https://github.com/ionescu007/clfs-docs)
- (https://github.com/fox-it/dissect.clfs)

## 漏洞成因分析

简单分析利用样本,能看到其构造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> |
| 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(详细见后)



### ClientContextOffset

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



### Symbol

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

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



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

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

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



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



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

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



### Context Part 1

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



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



在这里即下图所示



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

即覆盖`10 01` 为 `FF FF`



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



继而执行`CClfsBaseFilePersisted::FlushImage` 、`CClfsBaseFilePersisted::WriteMetadataBlock` 、`ClfsEncodeBlock` 、`ClfsEncodeBlockPrivate`

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



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



### cbSymbolZone

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



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



样本将其SymbolZone设置为`0x01114B`



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

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

参见下图



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



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



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



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

对应调用链如下图



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



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



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

- `CLFS!ClfsEarlierLsn`
   
    写edx寄存器为`0xFFFFFFFF`
   
   
   
- `nt!SeSetAccessStateGenericMapping`
   
    实际上就是将`poi(rdx)`写到`poi(poi(rcx+0x48)+0x8)`中
   
   
   

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

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

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

## 样本利用概述

- 版本识别校验
    - 指定`Token`在`_EPROCESS`中的Offset
    - 指定`PreviousMode`在`_ETHREAD`中的Offset
    - 还有一些基本的初始化
      - 进程ID、进程句柄、进程在内核中的地址
      - 线程ID、进程句柄、线程在内核中的地址
      - Windows 10 / 11 的区分
      - Win API:`NtQuerySystemInformation`、`NtWriteVirtualMemory`
- 获取进程、内核进程(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")`

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



## 样本分析

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

首先是win 10 & 11共同部分

- **版本校验**
   
    在版本校验的函数中,其不仅校验版本,而且初始化了一些其他的全局变量
   
    初始化win api
   
   
   
    获取当前进程、线程的Handle
   
   
   
    获取当前进程、线程在内核中的地址
   
   
   
    通过注册表获取UBR(修补号)
   
   
   
    通过PEB获取系统版本号,结合UBR匹配到对应Windows中`_EPROCESS`结构体中`Token`的偏移
   
   
   
    如果系统是Windows10,还会多出一个`PreviousMode` 在`_ETHREAD`中的偏移
   
   
   
    且配备了一个系统版本区分
   
   
   
- **获取进程、内核进程(System)的_EBPROCESS及Token所在地址**
    - 调用`OpenProcess`获取读取进程信息的权限
    - 调用`NtQuerySystemInformation` 获得`SystemExtendedHandleInformation`
    - 遍历`HandleInformation` 获取样本进程对应在内核中的地址
    - 与上述方法相同,遍历获取进程号为4,即`System`进程的对应在内核中的地址
   
    对应代码如下:
   
    ```jsx
    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.UniqueProcessId == CurrentProcessId) {
                HANDLE hHandle = (HANDLE)pHandleInfo->Handles.HandleValue;
                if (hHandle == hProcess) {
                  Object = pHandleInfo->Handles.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.UniqueProcessId == 4) {
                Object = pHandleInfo->Handles.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` 中



申请shellcode利用空间



堆喷,指向构造的虚表



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



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



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



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

### windows 10

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



在`CloseHandle` 时触发



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



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



当我们替换`PreviousMode`为`0`后,这意味着我们可以使用`NtReadVirtualMemory`和`NtWriteVirtualMemory`在整个内核内存中进行不受约束的RW。

随及进行Token替换



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



自此完成提权。

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



### Windows 11

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

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

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

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

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

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

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

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



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

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

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



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

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



自此完成提权。

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



## 参考

(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)

银.桑 发表于 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

膜拜大佬{:301_993:}

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

大佬,真的是大佬,只能膜拜了
页: [1] 2 3 4
查看完整版本: CVE-2022-37969 Windows 内核 CLFS 驱动漏洞分析