-
-
[原创]CVE-2022-30220:CLFS提权漏洞分析
-
2022-7-21 17:20 16639
-
原文链接:https://mp.weixin.qq.com/s/mkLjAyalo55PmdLbzMnX9g
1. 漏洞概述
项目 | 详情 |
---|---|
名称 | Windows Common Log File System Driver 提权漏洞 |
简介 | CLFS 驱动中存在堆越界写漏洞,利用该漏洞可以实现本地提权 |
影响版本 | 该漏洞影响所有受支持的 Windows 操作系统 |
编号 | CVE-2022-30220 |
2. CLFS基础
注:以下所有涉及的数据结构根据参考资料整理得到,由于没有官方文档,所以可能存在未知或同名字段,但不影响此次漏洞分析。
2.1 概念
CLFS(Common Log File System) 是一个日志框架,其他应用程序可以通过 CLFS 创建、保存和读取日志数据。“日志(log)”既可以表示一个抽象的概念,也可以表示一个物理上的文件,CLFS 对一条日志以及保存它的物理存储空间做了区分,因此物理空间的管理和日志的管理是分开的。CLFS 通过使用流(stream)和容器(container)在 NTFS 之上构建了一个虚拟的抽象层。
2.2 日志的存储
一个 CLFS 日志的存储由两部分组成:
包含元数据(metadata)的 base log file(BLF)
BLF文件大小通常为 64KB,但是可以根据需要增长,其中包含了日志存储所需的一些元信息,例如日志的起始位置、容器的大小和路径、日志名称、客户端信息等。考虑到日志恢复的需要,BLF 文件中包含了一份元数据的拷贝,使用 dump count 参数来识别哪份信息是最新的。最多 1023 个包含真正数据的容器文件
容器是一个活动的日志流在空间上的基础分配单元,同一日志中的容器大小一致,是 512KB(一个扇区的大小)的倍数,最大为 4GB。CLFS 客户端通过增加和删除容器实现日志流的扩大和缩小。在实现时,CLFS 把容器当作 BLF 所在卷上的一个连续文件,通过在逻辑上将多个容器串在一起,形成包含一条日志的单个逻辑顺序磁盘区。初始化的时候一条日志至少要分配两个容器。
可以把整个日志存储看作是文件系统中一个卷的概念
2.2.1 Log Blocks
CLFS 使用日志块对记录进行组织管理,每个日志块由多个 512 字节的扇区组成,之后对日志数据的读取和写入操作都在日志块上进行。
每个日志块的开头有一个 _CLFS_LOG_BLOCK_HEADER
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | typedef struct _CLFS_LOG_BLOCK_HEADER { UCHAR MajorVersion; UCHAR MinorVersion; UCHAR Usn; UCHAR StreamNum; USHORT TotalSectorCount; USHORT ValidSectorCount; ULONG Padding; ULONG Checksum; ULONG Flags; ULONG Unknown2; CLFS_LSN CurrentLsn; CLFS_LSN NextLsn; ULONG RecordOffsets[ 16 ]; ULONG SignaturesOffset; ULONG Unknown3; } CLFS_LOG_BLOCK_HEADER; |
当日志写入硬盘的时候,日志块会进行编码,编码状态的日志块中,每个扇区结尾处有一个 2 字节的扇区签名(sector signature):
1 | [Sector Block Type ][Usn] |
由于在编码阶段每个扇区的最后两个字节被改写成了扇区签名,因此需要对原始数据进行备份。日志块的最后一个扇区结尾处,有一个签名数组,这个数组中就保存了原始数据,由 _CLFS_LOG_BLOCK_HEADER
中的 SignaturesOffset
表示。
在写入日志的时候,可以比较每个扇区结尾签名位置的数据是否和签名数组中的数据一致来判断写入是否成功。
2.2.2 BLF文件
BLF 由六种不同的元数据块组成,每种元数据块只包含对应类型的数据,其中 shadow block 中保存的是对应元数据块的备份,同样使用 dump count 参数说明数据块的新旧。
Control Block:包含了有关布局(layout)、扩展(extend)区域以及截断(truncate)区域的信息
Base Block:包含了符号表信息,其中包括该BLF有关的客户端、容器和安全上下文信息
Truncate Block:包含了因为截断操作而需要对扇区进行更改的客户端信息,以及具体更改的扇区字节
Control Block Shadow
Base Block Shadow
Truncate Block Shadow
这里主要关注其中的 Control Block,这个日志块中的记录遵循以下数据结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | typedef struct _CLFS_CONTROL_RECORD { CLFS_METADATA_RECORD_HEADER hdrControlRecord; ULONGLONG ullMagicValue; ULONG Version; CLFS_EXTEND_STATE eExtendState; USHORT iExtendBlock; USHORT iFlushBlock; ULONG cNewBlockSectors; ULONG cExtendStartSectors; ULONG cExtendSectors; CLFS_TRUNCATE_CONTEXT cxTruncate; ULONG cBlocks; ULONG cReserved; CLFS_METADATA_BLOCK rgBlocks[ 6 ]; } CLFS_CONTROL_RECORD; |
其中的 cBlocks
表示整个文件中包含的日志块数量:
rgBlocks
中保存了每个日志块的大小信息:
1 2 3 4 5 6 7 8 | typedef struct _CLFS_METADATA_BLOCK { ULONGLONG pbImage; / / 指向内存中数据的指针,在BLF文件中为 0 ULONG cbImage; / / 日志块大小 ULONG cbOffset; / / 偏移 CLFS_METADATA_BLOCK_TYPE eBlockType; ULONG Padding; } CLFS_METADATA_BLOCK; |
3. 补丁对比
对比 clfy.sys 文件发现只有 CClfsBaseFilePersisted::ReadImage
函数存在不同,仔细检查代码,发现修改主要位于 cBlocks > 6
的情况:
修复之后,如果 BLF 文件中 cBlocks
数值大于 6,ReadImage
函数会直接出错返回。
4. 漏洞调试分析
4.1 漏洞原理
根据补丁分析结果,我们生成一个 BLF 文件,并手动将 cBlocks
修改成一个较大值 0x19(后期调试发现大于等于 0x20 无法通过检查),加载 BLF 文件后,系统崩溃(需要多次测试)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | kd> g KDTARGET: Refreshing KD connection KDTARGET: Refreshing KD connection * * * Fatal System Error: 0x00000050 ( 0xFFFF800C3905118E , 0x0000000000000000 , 0xFFFFF8006B2A8140 , 0x0000000000000000 ) Driver at fault: * * * CLFS.SYS - Address FFFFF8006B2A8140 base at FFFFF8006B2A0000, DateStamp 0c6e6b39 . Break instruction exception - code 80000003 (first chance) A fatal system error has occurred. Debugger entered on first try ; Bugcheck callbacks have not been invoked. A fatal system error has occurred. nt!DbgBreakPointWithStatus: fffff800 * 6b804c70 cc int 3 |
查看此时的函数调用栈:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | 3 : kd> kb # RetAddr : Args to Child : Call Site 00 fffff800 * 6b918032 : ffffa780 * aec28ad0 fffff800 * 6b782480 fffff800 * 6b2a0000 00000000 * 00000000 : nt!DbgBreakPointWithStatus 01 fffff800 * 6b917616 : fffff800 * 00000003 ffffa780 * aec28ad0 fffff800 * 6b811d10 ffffa780 * aec29020 : nt!KiBugCheckDebugBreak + 0x12 02 fffff800 * 6b7fced7 : 00000000 * 00000000 00000000 * 00000000 0000f288 * 00000000 ffff800c * 3905118e : nt!KeBugCheck2 + 0x946 03 fffff800 * 6b85352f : 00000000 * 00000050 ffff800c * 3905118e 00000000 * 00000000 ffffa780 * aec293c0 : nt!KeBugCheckEx + 0x107 04 fffff800 * 6b6ab960 : ffffa780 * aec29300 00000000 * 00000000 ffffa780 * aec29440 00000000 * 00000000 : nt!MiSystemFault + 0x189b7f 05 fffff800 * 6b80af5e : 00000000 * 00000000 00000000 * 00000000 00000000 * 00000000 00000000 * 00000001 : nt!MmAccessFault + 0x400 06 fffff800 * 6b2a8140 : 00000000 * 00000000 00000000 * 00000000 00000000 * 00000000 ffffb90d * 58a5b0f0 : nt!KiPageFault + 0x35e 07 fffff800 * 6b2a802d : 00000000 * 00004210 ffff800c * 39050190 00000000 * 00000002 00000000 * 00848400 : CLFS!ClfsEncodeBlockPrivate + 0xe0 08 fffff800 * 6b2eedff : 00000000 * 00000000 00000000 * 00000200 00000000 * 00000200 ffff800c * 373e5dd0 : CLFS!ClfsEncodeBlock + 0x1d 09 fffff800 * 6b2fa139 : ffff800c * 38ba02a0 ffffb90d * 00ffffff 00000000 * 00000200 ffffa881 * ebebcfb0 : CLFS!CClfsBaseFileSnapshot::CopyImage + 0xc3 0a fffff800 * 6b2f39a2 : ffffb90d * 538672b0 00000000 * 00000200 ffffb90d * 538672b0 ffffb90d * 53867201 : CLFS!CClfsLogCcb::ReadArchiveMetadata + 0x85 0b fffff800 * 6b2d0e82 : ffffb90d * 53df89e0 00000000 * 00000000 ffffb90d * 53dfae50 ffffb90d * 53dfad80 : CLFS!CClfsRequest::ReadArchiveMetadata + 0xfe 0c fffff800 * 6b2d0987 : ffffb90d * 53df89e0 fffff800 * 6b616131 ffffb90d * 53552330 00000058 * a7c00fb0 : CLFS!CClfsRequest::Dispatch + 0x35e 0d fffff800 * 6b2d08d7 : ffffb90d * 53dfad80 ffffb90d * 53dfad80 00000000 * 00000000 ffffdd00 * 2c53dff8 : CLFS!ClfsDispatchIoRequest + 0x87 0e fffff800 * 6b653565 : ffffb90d * 53dfad80 ffffb90d * 5652ca10 00000000 * 00000021 ffffb90d * 5253d080 : CLFS!CClfsDriver::LogIoDispatch + 0x27 0f fffff800 * 6ba12b18 : ffffb90d * 53dfad80 00000000 * 00000000 00000000 * 00000000 00000000 * 0000d55b : nt!IofCallDriver + 0x55 10 fffff800 * 6ba13af7 : 00000000 * 00000002 00000000 * 00000000 00000000 * 00000000 ffffa780 * aec29b80 : nt!IopSynchronousServiceTail + 0x1a8 11 fffff800 * 6ba12e76 : 00007ffa * 7b746308 00000000 * 00000000 00000000 * 00000000 00000000 * 00000000 : nt!IopXxxControlFile + 0xc67 12 fffff800 * 6b80e7b5 : 00000000 * 00000000 00000000 * 00000000 00000000 * 00000000 00000000 * 00000000 : nt!NtDeviceIoControlFile + 0x56 13 00007ffa * 800ece24 : 00007ffa * 7da4b0bb 00000058 * a7bfed64 00000000 * 00000000 00000000 * 00000078 : nt!KiSystemServiceCopyEnd + 0x25 14 00007ffa * 7da4b0bb : 00000058 * a7bfed64 00000000 * 00000000 00000000 * 00000078 00000000 * 00000034 : ntdll!NtDeviceIoControlFile + 0x14 15 00000058 * a7bfed64 : 00000000 * 00000000 00000000 * 00000078 00000000 * 00000034 00000058 * a7bfeda0 : 0x00007ffa * 7da4b0bb 16 00000000 * 00000000 : 00000000 * 00000078 00000000 * 00000034 00000058 * a7bfeda0 00000000 * 80076856 : 0x00000058 * a7bfed64 |
可以看到异常发生在 CLFS!ClfsEncodeBlockPrivate
函数中,在 IDA 中查看该函数:
合理猜测,clfs.sys 在通过 BLF 文件获取到 Block 的数量之后并没有进行验证,而是直接使用该值对后面的空间进行读取。
为验证该猜测,我们跟随函数调用栈到达函数 CClfsBaseFileSnapshot::CopyImage
,从代码看这里在对堆块进行循环编码:
1 2 3 4 5 6 7 | while ( idx < * (this + 20 ) && * a5 < size ) / / * (this + 20 )保存了cBlocks { blockHeader = * ( * (this + 6 ) + 24 * idx); ClfsEncodeBlock(blockHeader, blockHeader - >TotalSectorCount << 9 , blockHeader - >Usn, 0x10u , 1u ); ... idx = idx + 1 ; } |
在这个函数设置断点并重新开始调试,当代码执行到循环判断时:
1 2 3 4 5 6 7 8 9 10 11 | 1 : kd> r rax = 0000000000000001 rbx = 0000000000000000 rcx = ffff92816d5e8180 rdx = 0000000000000000 rsi = 0000000000000200 rdi = 0000000000000000 rip = fffff8047537edb7 rsp = ffff8381808b35c0 rbp = 0000000000000002 r8 = 0000000000000000 r9 = ffff92816f707500 r10 = 0000000000000000 r11 = ffff8381808b3668 r12 = 0000000000000000 r13 = ffffe60fcaf944a0 r14 = 0000000000ffffff r15 = ffff8381808b3738 iopl = 0 nv up ei ng nz na pe nc cs = 0010 ss = 0018 ds = 002b es = 002b fs = 0053 gs = 002b efl = 00040282 CLFS!CClfsBaseFileSnapshot::CopyImage + 0x7b : fffff804 * 7537edb7 410fb74528 movzx eax,word ptr [r13 + 28h ] ds: 002b :ffffe60f * caf944c8 = 0019 |
这里在获取保存的 cBlocks
数值,可以看到系统在通过 ReadImage
读取 BLF 文件中存储的 cBlocks
数值之后,该数值并没有发生过更改,并直接在 CopyImage
中被使用。
4.2 越界写的位置与内容
现在已知漏洞是由于没有对 cBlocks
进行验证,但对于发生堆越界写时,写入的位置和内容还不清楚。
如果继续向下调试,到达获取 blockHeader
地址的位置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | 2 : kd> p CLFS!CClfsBaseFileSnapshot::CopyImage + 0x9f : fffff804 * 7537eddb 488b3cc8 mov rdi,qword ptr [rax + rcx * 8 ] 2 : kd> r rax = ffffe60fc7415030 rbx = 0000000000000000 rcx = 0000000000000000 rdx = 0000000000000000 rsi = 0000000000000200 rdi = 0000000000000000 rip = fffff8047537eddb rsp = ffff8381808b35c0 rbp = 0000000000000002 r8 = 0000000000000000 r9 = ffff92816f707500 r10 = 0000000000000000 r11 = ffff8381808b3668 r12 = 0000000000000000 r13 = ffffe60fcaf944a0 r14 = 0000000000ffffff r15 = ffff8381808b3738 iopl = 0 nv up ei ng nz na po cy cs = 0010 ss = 0018 ds = 002b es = 002b fs = 0053 gs = 002b efl = 00040287 CLFS!CClfsBaseFileSnapshot::CopyImage + 0x9f : fffff804 * 7537eddb 488b3cc8 mov rdi,qword ptr [rax + rcx * 8 ] ds: 002b :ffffe60f * c7415030 = ffffe60fc86bbbb0 2 : kd> dq ffffe60fc7415030 ffffe60f * c7415030 ffffe60f * c86bbbb0 00000000 * 00000400 ffffe60f * c7415040 00000000 * 00000000 ffffe60f * c86bbbb0 ffffe60f * c7415050 00000400 * 00000400 00000000 * 00000001 ffffe60f * c7415060 ffffe60f * cc319000 00000800 * 00007a00 ffffe60f * c7415070 00000000 * 00000002 ffffe60f * cc319000 ffffe60f * c7415080 00008200 * 00007a00 00000000 * 00000003 ffffe60f * c7415090 ffffe60f * c8c43a10 0000fc00 * 00000200 ffffe60f * c74150a0 00000000 * 00000004 ffffe60f * c8c43a10 |
可以看到地址 0xffffe60fc7415030
的位置存储了一个列表,根据数据内容判断,保存的就是 rgBlocks
信息。
在 CClfsBaseFilePersisted::ReadImage
函数中,如果 cBlocks
大于6,修复之前的函数会继续向下执行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | if ( cBlocks > 6u ) { pool1 = ExAllocatePoolWithTag( 512 , 24 * cBlocks, 'sflC' ); / / 分配大小为 24 * cBlocks的空间 pool2 = ExAllocatePoolWithTag( 512 , 2 * cBlocks, 'sflC' ); / / 分配大小为 2 * cBlocks的空间 if ( pool1 ) { if ( pool2 ) { memmove(pool1, * (this + 6 ), 24i64 * * (this + 20 )); memmove(pool2_, * (this + 7 ), 2i64 * * (this + 20 )); ExFreePoolWithTag( * (this + 6 ), 0 ); ExFreePoolWithTag( * (this + 7 ), 0 ); * (this + 20 ) = cBlocks; / / 注意保存的位置 * (this + 6 ) = pool1; * (this + 7 ) = pool2; } } } for ( i = 0 ; i < cBlocks; + + i ) { controlRecord = * pControlRecord; pool1 = * ((_QWORD * )this + 6 ); * (_OWORD * )(pool1_ + 24 * i) = * (_OWORD * )((char * ) * pControlRecord + 24 * i + 0x50 ); / / 从BLF文件中复制数据,rgBlocks * (_QWORD * )(pool1_ + 24 * i + 16 ) = * ((_QWORD * )controlRecord_ + 3 * i + 0xC ); * (_QWORD * )( * ((_QWORD * )this + 6 ) + 8 * v15) = 0i64 ; } * * (this + 6 ) = v13; / / 第一个堆块的pbImage字段赋值 * ( * (this + 6 ) + 24i64 ) = v13; / / 第二个堆块的pbImage字段赋值 |
代码根据新的 cBlocks
数值分配了两个空间,大小为 24 x cBlocks
和 2 x cBlocks
,并分别放在了*(this+6)
和*(this+7)
的位置,除此之外还将 cBlocks
保存到了*(this+20)
。之后将 BLF 文件中的 rgBlocks
列表复制到 pool1 所在空间,此时 pbImage
字段为 0。
但是 pool1 的地址并不是崩溃发生时 rgBlocks
列表所在的地址 0xffffe60fc7415030
,因此在 ReadImage
中,this 指针指向的是一个 CClfsBaseFilePersisted
结构,而崩溃发生时,this 指针指向的是一个 CClfsBaseFileSnapshot
结构,*(this+6)
中存储的空间地址并不相同。
根据结构名,找到函数 CClfsBaseFileSnapshot::InitializeSnapshot
,这里的 this 指针指向的同样是 CClfsBaseFileSnapshot
结构,同时发现了相同结构的一段代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | if ( cBlocks ) { * (this + 6 ) = ExAllocatePoolWithTag(PagedPool, 24i64 * cBlocks, 'SflC' ); pool2 = ExAllocatePoolWithTag(PagedPool, 2i64 * cBlocks, 'sflC' ); * (this + 7 ) = pool2; pool1 = * (this + 6 ); if ( pool1 && pool2 ) { memmove(pool1, * a3, 24i64 * cBlocks); memset(pool2, 0 , 2i64 * cBlocks); for ( i = 0 ; i < cBlocks; + + i ) / / 清空所有pbImage * ( * (this + 6 ) + 24i64 * i) = 0i64 ; for ( j = 0 ; j < cBlocks; j + = 2 ) { * ( * (this + 6 ) + 24i64 * j) = ExAllocatePoolWithTag(PagedPool, * ( * (this + 6 ) + 24i64 * j + 8 ), 'sflC' ); / / 根据cbImage字段分配空间,并赋值给pbImage pool1_ = * ( * (this + 6 ) + 24i64 * j); if ... memmove(pool1_, * ( * a3 + 24i64 * j), * ( * a3 + 24i64 * j + 8 )); * ( * (this + 7 ) + 2i64 * j) = 1 ; if ( cBlocks > 1u ) { v18 = j + 1 ; if ( v18 < cBlocks ) * ( * (this + 6 ) + 24 * v18) = * ( * (this + 6 ) + 24i64 * j); / / shadow block } } .... } .... } |
这里根据对应日志块的 cbImage
循环分配了 cBlocks
个空间,由于 cBlocks
比实际值大,且 BLF 文件除 cBlocks
值之外没有做任何修改,因此系统认为后续 19 个日志块的大小为 0,ExAllocatePoolWithTag
分配了大小为 0 的空间,memmove
复制了大小为 0 的数据,最终保存到 pbImage
字段的地址是堆中一个未初始化空间的地址。
因此在系统执行到 ClfsEncodeBlockPrivate
时,blockHeader 指向的实际上是一块未初始化且大小为 0 的空间。
通过调试获取到分配的13 个日志块地址(剩余的 12 个日志块地址重复):
1 2 3 4 5 6 7 8 9 10 11 12 13 | ffffd30c417903c0 / / control block ffffd30c43f83000 / / base block ffffd30c3fc134b0 / / truncate block ffffd30c42cdebd0 ffffd30c42cdec30 ffffd30c42cdecf0 ffffd30c42cdfbd0 ffffd30c42cdfbf0 ffffd30c42cdf950 ffffd30c42cdf9d0 ffffd30c42cdf9f0 ffffd30c42cdfa10 ffffd30c42cdfa30 |
其中前三个是正常的日志块,我们主要看后 10 个日志块的地址,使用 !pool
命令查看堆块情况,截取部分输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | 2 : kd> !pool ffffd30c42cdfbd0 1 Pool page ffffd30c42cdfbd0 region is Paged pool ... ffffd30c42cdf940 size: 20 previous size: 0 (Allocated) Clfs ffffd30c42cdf950 00000000 00000000 26e5c558 00007ff8 ... ffffd30c42cdf9c0 size: 20 previous size: 0 (Allocated) Clfs ffffd30c42cdf9d0 00000101 10000000 00002000 00007ff8 ffffd30c42cdf9e0 size: 20 previous size: 0 (Allocated) Clfs ffffd30c42cdf9f0 0000001d 00000000 80000000 00000000 ffffd30c42cdfa00 size: 20 previous size: 0 (Allocated) Clfs ffffd30c42cdfa10 00000101 10000000 00002000 00007ff8 ffffd30c42cdfa20 size: 20 previous size: 0 (Allocated) Clfs ffffd30c42cdfa30 001e001c 00000000 26e5c558 00007ff8 ... * ffffd30c42cdfbc0 size: 20 previous size: 0 (Allocated) * Clfs Pooltag Clfs : CLFS General buffer , or owner page lookaside list , Binary : clfs.sys ffffd30c42cdfbd0 00000101 10000000 00002000 00007ff8 ffffd30c42cdfbe0 size: 20 previous size: 0 (Allocated) Clfs ffffd30c42cdfbf0 00000000 10000000 00002000 00007ff8 |
可以看到,虽然调用时请求分配的空间大小是 0,但实际上分配的是大小为 32 字节(包含头部)的堆块,且堆块都位于连续的一块空间中,且堆块头部数据相对固定:
1 2 3 4 5 6 7 8 9 10 11 12 13 | 2 : kd> dt _POOL_HEADER ffffd30c42cdfbc0 nt!_POOL_HEADER + 0x000 PreviousSize : 0y00000000 ( 0 ) + 0x000 PoolIndex : 0y00000000 ( 0 ) + 0x002 BlockSize : 0y00000010 ( 0x2 ) + 0x002 PoolType : 0y00000011 ( 0x3 ) + 0x000 Ulong1 : 0x3020000 + 0x004 PoolTag : 0x73666c43 + 0x008 ProcessBilled : (null) + 0x008 AllocatorBackTraceIndex : 0 + 0x00a PoolTagHash : 0 2 : kd> db ffffd30c42cdfbc0 l10 ffffd30c * 42cdfbc0 00 00 02 03 43 6c 66 73 - 00 00 00 00 00 00 00 00 ....Clfs........ |
也就是说,当相对于 blockHeader
进行取值时,如果偏移范围在 0x10-0x1F、0x30-0x3F、0x50-0x5F...,那么取到的数据就来自相对固定的头部数据,否则就来自未初始化的数据空间。
再次回到发生崩溃的函数,看一下涉及到的数据来源:
注:以下代码删除了一些细节,主要关注相对于blockHeader
的偏移量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | __int64 __fastcall ClfsEncodeBlockPrivate(struct _CLFS_LOG_BLOCK_HEADER * blockHeader, unsigned int allSectorSize, char usn, unsigned __int8 a4) { nSector = * (blockHeader + 4 ); / / TotalSectorCount if ( !nSector || * (blockHeader + 6 ) < nSector || nSector << 9 > allSectorSize ) / / ValidSectorCount return 0xC01A000Ai64 ; if ( ( * (blockHeader + 0x10 ) & 1 ) ! = 0 ) / / Flags return 0xC01A000Ai64 ; signatureOffset = * (blockHeader + 0x68 ); / / SignaturesOffset * (blockHeader + 2 ) = usn; / / Usn signatureArray = blockHeader + signatureOffset; if ( ULongAdd(signatureOffset, 2 * (allSectorSize >> 9 ), v21) < 0 || 0 > allSectorSize ) { return 0xC01A000Ai64 ; } idx = 0 ; if ( nSector ) { do { signatureArray + = 2i64 ; flag1 = 0x20 ; flag2 = 0x40 ; flag = flag2 | flag1; curSectorSize = idx << 9 ; LOBYTE(v22) = a4 | flag; + + idx; * (signatureArray - 2 ) = * (curSectorSize + blockHeader + 0x1FE ); / / 异常发生位置 / / 获取当前sector结尾处的signature并写入signature array * (curSectorSize + blockHeader + 0x1FE ) = v22; } while ( idx < nSector); } return 0i64 ; } |
越界写的位置是 signatureArray - 2
,主要受到偏移 0x68 位置的 signatureOffset
的影响;越界写的数据是 *(curSectorSize + blockHeader+ 0x1FE)
,主要受到偏移 0x4 位置的 nSector
的影响,就是因为这里的数值太大才导致了崩溃的发生。
还有一些偏移位置的数据会影响程序流程,这里暂时不关注。
总结:
写入位置:
blockHeader + *(blockHeader+0x68) + idx * 2
写入数据:
*(blockHeader + idx*0x200 + 0x1FE)
根据上面对于堆块结构的分析,如果 blockHeader+【offset】
中 offset % 16
的结果是奇数,那么数据来源就是相对固定的堆块头部,否则数据来源就是未初始化的数据空间。
所以如果不考虑超出这块内存空间(即内部全部是0x10堆块的空间)的情况:写入数据所在的位置正位于堆块头部偏移 0x0E 的位置,该位置的数据绝大部分情况下均为 0x0000 ;写入位置可以通过类似堆喷射的方式进行控制。
5. 总结
通过以上分析可知,CVE-2022-30220 漏洞是由于对保存在堆块头部中的 cBlocks
字段的检查不够充分造成的,通过修改 BLF 文件中该字段数值,可以导致堆越界写的发生。由于堆空间结构的限制,越界写的位置与写入数据受到限制,如果想要进行漏洞利用,可能需要对堆空间结构更详细的分析,并通过修改BLF文件内容以及类似堆喷射的方式对数据进行控制。
6. 参考资料
https://github.com/libyal/libfsclfs/blob/main/documenation/Common%20Log%20File%20System%20(CLFS).asciidoc
Windows Internal editon 6th