| 1 | + | # 0-day Root Cause Analysis Template |
| 2 | + | |
| 3 | + | |
| 4 | + | # CVE-2022-24521: Windows Common Log File System (CLFS) Logical-Error Vulnerability |
| 5 | + | Sergey Kornienko (@b1thvn_) of PixiePoint Security |
| 6 | + | |
| 7 | + | ## The Basics |
| 8 | + | |
| 9 | + | **Disclosure or Patch Date:** April 12, 2022 |
| 10 | + | |
| 11 | + | **Product:** Microsoft Windows |
| 12 | + | |
| 13 | + | **Advisory:** https://msrc.microsoft.com/update-guide/vulnerability/CVE-2022-24521 |
| 14 | + | |
| 15 | + | **Affected Versions:** Before security updates of April 12, 2022, for Windows 7, 8.1, 10, 11 and Windows Server 2008, 2012, 2016, 2019, 2022 |
| 16 | + | |
| 17 | + | **First Patched Version:** Security updates of April 12, 2022, for CVE-2022-24521 |
| 18 | + | |
| 19 | + | **Issue/Bug Report:** N/A |
| 20 | + | |
| 21 | + | **Patch CL:** N/A |
| 22 | + | |
| 23 | + | **Bug-Introducing CL:** N/A |
| 24 | + | |
| 25 | + | **Reporter(s):** Sergey Kornienko (@b1thvn_) of PixiePoint Security |
| 26 | + | |
| 27 | + | ## The Code |
| 28 | + | |
| 29 | + | **Proof-of-concept:** N/A |
| 30 | + | |
| 31 | + | **Exploit sample:** N/A |
| 32 | + | |
| 33 | + | **Did you have access to the exploit sample when doing the analysis?** No |
| 34 | + | |
| 35 | + | ## The Vulnerability |
| 36 | + | |
| 37 | + | **Bug class:** Logical error (lack of indirect-call validation) |
| 38 | + | |
| 39 | + | **Vulnerability details:** |
| 40 | + | |
| 41 | + | As per the CLFS format, the array of signatures intersects with the container or client context. |
| 42 | + | |
| 43 | + | 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. |
| 44 | + | |
| 45 | + | 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`: |
| 46 | + | |
| 47 | + | ```c |
| 48 | + | __int64 __fastcall CClfsBaseFilePersisted::RemoveContainer(CClfsBaseFilePersisted *this, unsigned int a2) |
| 49 | + | { |
| 50 | + | ... |
| 51 | + | v11 = CClfsBaseFilePersisted::FlushImage((PERESOURCE *)this); |
| 52 | + | v9 = v11; |
| 53 | + | v16 = v11; |
| 54 | + | if ( v11 >= 0 ) |
| 55 | + | { |
| 56 | + | pContainer = *((_QWORD *)containerContext + 3); |
| 57 | + | if ( pContainer ) |
| 58 | + | { |
| 59 | + | *((_QWORD *)containerContext + 3) = 0i64; |
| 60 | + | ExReleaseResourceForThreadLite(*((PERESOURCE *)this + 4), (ERESOURCE_THREAD)KeGetCurrentThread()); |
| 61 | + | v4 = 0; |
| 62 | + | (*(void (__fastcall **)(__int64))(*(_QWORD *)pContainer + 0x18i64))(pContainer); // remove method |
| 63 | + | (*(void (__fastcall **)(__int64))(*(_QWORD *)pContainer + 8i64))(pContainer); // release method |
| 64 | + | v9 = v16; |
| 65 | + | goto LABEL_20; |
| 66 | + | } |
| 67 | + | goto LABEL_19; |
| 68 | + | } |
| 69 | + | ... |
| 70 | + | } |
| 71 | + | ``` |
| 72 | + | |
| 73 | + | To ensure that the user cannot pass any `FAKE_pContainer` pointer to the kernel, before any indirect call this field is set to zero: |
| 74 | + | |
| 75 | + | ```c |
| 76 | + | v44 = *((_DWORD *)containerContext + 5); // to trigger RemoveContainer one should set this field to -1 |
| 77 | + | if ( v44 == -1 ) |
| 78 | + | { |
| 79 | + | *((_QWORD *)containerContext + 3) = 0i64; // pContainer is set to NULL |
| 80 | + | v20 = CClfsBaseFilePersisted::RemoveContainer(this, v34); |
| 81 | + | v72 = v20; |
| 82 | + | if ( v20 < 0 ) |
| 83 | + | goto LABEL_134; |
| 84 | + | v23 = v78; |
| 85 | + | v34 = (unsigned int)(v34 + 1); |
| 86 | + | v79 = v34; |
| 87 | + | } |
| 88 | + | ``` |
| 89 | + | |
| 90 | + | 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: |
| 91 | + | |
| 92 | + | ```c |
| 93 | + | ... |
| 94 | + | // Obtain all container contexts represented in blf |
| 95 | + | // save pContainer class pointer for each valid container context |
| 96 | + | for ( i = 0; i < 0x400; ++i ) |
| 97 | + | { |
| 98 | + | v20 = CClfsBaseFile::AcquireContainerContext(this, i, &v22); |
| 99 | + | v15 = (char *)this + 8 * i; |
| 100 | + | if ( v20 >= 0 ) |
| 101 | + | { |
| 102 | + | v16 = v22; |
| 103 | + | *((_QWORD *)v15 + 56) = *((_QWORD *)v22 + 3); // for each valid container save pContainer |
| 104 | + | *((_QWORD *)v16 + 3) = 0i64; // and set the initial pContainer to zero |
| 105 | + | CClfsBaseFile::ReleaseContainerContext(this, &v22); |
| 106 | + | } |
| 107 | + | else |
| 108 | + | { |
| 109 | + | *((_QWORD *)v15 + 56) = 0i64; |
| 110 | + | } |
| 111 | + | } |
| 112 | + | // Stage [1] enode block, prepare it for writing |
| 113 | + | ClfsEncodeBlock( |
| 114 | + | (struct _CLFS_LOG_BLOCK_HEADER *)v9, |
| 115 | + | *(unsigned __int16 *)(v9 + 4) << 9, |
| 116 | + | *(_BYTE *)(v9 + 2), |
| 117 | + | 0x10u, |
| 118 | + | 1u); |
| 119 | + | // write modified data |
| 120 | + | v10 = CClfsContainer::WriteSector( |
| 121 | + | *((CClfsContainer **)this + 19), |
| 122 | + | *((struct _KEVENT **)this + 20), |
| 123 | + | 0i64, |
| 124 | + | *(void **)(*((_QWORD *)this + 6) + 24 * v8), |
| 125 | + | *(unsigned __int16 *)(v9 + 4), |
| 126 | + | &v23); |
| 127 | + | ... |
| 128 | + | if ( v7 ) |
| 129 | + | { |
| 130 | + | // Stage [2] Decode file again for futher processing in clfs.sys |
| 131 | + | ClfsDecodeBlock((struct _CLFS_LOG_BLOCK_HEADER *)v9, *(unsigned __int16 *)(v9 + 4), *(_BYTE *)(v9 + 2), 0x10u, &v21); |
| 132 | + | // optain new pContainer class pointer |
| 133 | + | v17 = (_QWORD *)((char *)this + 448); |
| 134 | + | do |
| 135 | + | { |
| 136 | + | // Stage [3] for each valid container |
| 137 | + | // update pContainer field |
| 138 | + | if ( *v17 && (int)CClfsBaseFile::AcquireContainerContext(this, v6, &v22) >= 0 ) |
| 139 | + | { |
| 140 | + | *((_QWORD *)v22 + 3) = *v17; |
| 141 | + | CClfsBaseFile::ReleaseContainerContext(this, &v22); |
| 142 | + | } |
| 143 | + | ++v6; |
| 144 | + | ++v17; |
| 145 | + | } |
| 146 | + | while ( v6 < 0x400 ); |
| 147 | + | } |
| 148 | + | ... |
| 149 | + | ``` |
| 150 | + | |
| 151 | + | 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. |
| 152 | + | |
| 153 | + | **Patch analysis:** |
| 154 | + | |
| 155 | + | The patch diffing of CLFS.sys reveals eight changed and two new functions. Of these, new logical block has been added to the `LoadContainerQ` function: |
| 156 | + | |
| 157 | + | ```c |
| 158 | + | ... |
| 159 | + | containerArray = (_DWORD *)((char *)BaseLogRecord + 0x328); // *CLFS_CONTAINER_CONTEXT->rgContainers |
| 160 | + | ... |
| 161 | + | v22 = CClfsBaseFile::ContainerCount(this); |
| 162 | + | ... |
| 163 | + | while ( containerIndex < 0x400 ) |
| 164 | + | { |
| 165 | + | v17 = (CClfsContainer *)containerIndex; |
| 166 | + | if ( containerArray[containerIndex] ) |
| 167 | + | ++v24; |
| 168 | + | v89 = ++containerIndex; |
| 169 | + | } |
| 170 | + | ... |
| 171 | + | if ( v24 == v22 ) |
| 172 | + | { |
| 173 | + | if ( (unsigned int)Feature_Servicing_38197806__private_IsEnabled() ) |
| 174 | + | { |
| 175 | + | v25 = (_OWORD *)((char *)v19 + 0x138); |
| 176 | + | v26 = (unsigned int *)operator new(0x11F0ui64, PagedPool); |
| 177 | + | rgObject = v26; |
| 178 | + | if ( !v26 ) |
| 179 | + | { |
| 180 | + | goto LABEL_135; |
| 181 | + | } |
| 182 | + | memmove(v26, containerArray, 0x1000ui64); |
| 183 | + | v28 = rgObject + 0x400; |
| 184 | + | v29 = 3i64; |
| 185 | + | ... |
| 186 | + | v20 = CClfsBaseFile::ValidateRgOffsets(this, rgObject); |
| 187 | + | v72 = v20; |
| 188 | + | operator delete(rgObject); |
| 189 | + | } |
| 190 | + | ``` |
| 191 | + | |
| 192 | + | In fact, this block is a wrapper for `CClfsBaseFile::ValidateRgOffsets`: |
| 193 | + | |
| 194 | + | ```c |
| 195 | + | __int64 __fastcall CClfsBaseFile::ValidateRgOffsets(CClfsBaseFile *this, unsigned int *rgObject) |
| 196 | + | { |
| 197 | + | ... |
| 198 | + | LogBlockPtr = *(_QWORD *)(*((_QWORD *)this + 6) + 48i64); // * _CLFS_LOG_BLOCK_HEADER |
| 199 | + | ... |
| 200 | + | signatureOffset = LogBlockPtr + *(unsigned int *)(LogBlockPtr + 0x68); // PCLFS_LOG_BLOCK_HEADER->SignaturesOffset |
| 201 | + | ... |
| 202 | + | qsort(rgObject, 0x47Cui64, 4ui64, CompareOffsets); // sort rgObject array |
| 203 | + | while ( 1 ) |
| 204 | + | { |
| 205 | + | currObjOffset = *rgObject2; // obtain offset from rgObject |
| 206 | + | if ( *rgObject2 - 1 <= 0xFFFFFFFD ) |
| 207 | + | { |
| 208 | + | pObjContext = CClfsBaseFile::OffsetToAddr(this, currObjOffset); // Obtain in-memory representation |
| 209 | + | // of the object's context structure |
| 210 | + | ... |
| 211 | + | unkn = currObjOffset - 0x30; |
| 212 | + | v13 = rgIndex * 4 + v5 + 0x30; |
| 213 | + | if ( v13 < v5 || v5 && v13 > unkn ) |
| 214 | + | break; |
| 215 | + | v5 = unkn; |
| 216 | + | if ( *pObjContext == 0xC1FDF008 ) // CLFS_NODE_TYPE_CLIENT_CONTEXT |
| 217 | + | { |
| 218 | + | rgIndex = 0xC; |
| 219 | + | } |
| 220 | + | else |
| 221 | + | { |
| 222 | + | if ( *pObjContext != 0xC1FDF007 ) // CLFS_NODE_TYPE_CONTAINER_CONTEXT |
| 223 | + | return 0xC01A000D; |
| 224 | + | rgIndex = 0x22; |
| 225 | + | } |
| 226 | + | criticalRange = &pObjContext[rgIndex]; // get the address of context + 0x30 |
| 227 | + | if ( criticalRange < pObjContext || (unsigned __int64)criticalRange > signatureOffset ) // comapre with sig offset |
| 228 | + | break; |
| 229 | + | } |
| 230 | + | ++i; |
| 231 | + | ++rgObject2; |
| 232 | + | if ( i >= 0x47C ) |
| 233 | + | return ret; |
| 234 | + | } |
| 235 | + | return 0xC01A000D; |
| 236 | + | } |
| 237 | + | ``` |
| 238 | + | |
| 239 | + | 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`. |
| 240 | + | |
| 241 | + | **Thoughts on how this vuln might have been found _(fuzzing, code auditing, variant analysis, etc.)_:** |
| 242 | + | |
| 243 | + | Code auditing |
| 244 | + | |
| 245 | + | **(Historical/present/future) context of bug:** |
| 246 | + | |
| 247 | + | https://msrc.microsoft.com/update-guide/vulnerability/CVE-2022-24521 |
| 248 | + | |
| 249 | + | ## The Exploit |
| 250 | + | |
| 251 | + | (The terms *exploit primitive*, *exploit strategy*, *exploit technique*, and *exploit flow* are [defined here](https://googleprojectzero.blogspot.com/2020/06/a-survey-of-recent-ios-kernel-exploits.html).) |
| 252 | + | |
| 253 | + | **Exploit strategy (or strategies):** |
| 254 | + | |
| 255 | + | Similar procedure to overwrite process token with pipe objects as outlined in the |
| 256 | + | [SSTIC2020: Scoop the Windows 10 pool!](https://www.sstic.org/media/SSTIC2020/SSTIC-actes/pool_overflow_exploitation_since_windows_10_19h1/SSTIC2020-Article-pool_overflow_exploitation_since_windows_10_19h1-bayet_fariello.pdf) paper. |
| 257 | + | |
| 258 | + | **Exploit flow:** |
| 259 | + | |
| 260 | + | 1. Create pipe objects and add pipe attributes. The attributes are a key-value pair and stored in a linked list, and the `PipeAttribute` object is allocated in the Paged Pool. |
| 261 | + | 2. Use `NtQuerySystemInformation` to leak kernel virtual address of pipe objects in big pool. |
| 262 | + | 3. Allocate `fake_pipe_attribute` object. It will be used later to inject its address to an original doubly linked list. |
| 263 | + | 4. Obtain selected gadget-module base address using `NtQuerySystemInformation`. |
| 264 | + | 5. Trigger CLFS bug which allows us to call a module-gadget performing arbitrary data modification to achieve an arbitrary read primitive which can be used to obtain `EPROCESS` address. |
| 265 | + | 6. Trigger CLFS bug to overwrite usermode process token to elevate to system privileges. |
| 266 | + | |
| 267 | + | **Known cases of the same exploit flow:** |
| 268 | + | |
| 269 | + | N/A |
| 270 | + | |
| 271 | + | **Part of an exploit chain?** |
| 272 | + | |
| 273 | + | N/A |
| 274 | + | |
| 275 | + | ## The Next Steps |
| 276 | + | |
| 277 | + | ### Variant analysis |
| 278 | + | |
| 279 | + | **Areas/approach for variant analysis (and why):** N/A |
| 280 | + | |
| 281 | + | **Found variants:** N/A |
| 282 | + | |
| 283 | + | ### Structural improvements |
| 284 | + | |
| 285 | + | What are structural improvements such as ways to kill the bug class, prevent the introduction of this vulnerability, mitigate the exploit flow, make this type of vulnerability harder to exploit, etc.? |
| 286 | + | |
| 287 | + | **Ideas to kill the bug class:** N/A |
| 288 | + | |
| 289 | + | **Ideas to mitigate the exploit flow:** N/A |
| 290 | + | |
| 291 | + | **Other potential improvements:** N/A |
| 292 | + | |
| 293 | + | ### 0-day detection methods |
| 294 | + | |
| 295 | + | What are potential detection methods for similar 0-days? Meaning are there any ideas of how this exploit or similar exploits could be detected **as a 0-day**? |
| 296 | + | |
| 297 | + | ## Other References |
| 298 | + | |
| 299 | + | - More information about the affected versions can be found on the [Microsoft Advisory](https://msrc.microsoft.com/update-guide/vulnerability/CVE-2022-24521) web site. |
| 300 | + | - More details about the exploitation can be found on the [CVE-2022-24521: Analysing and Exploiting the Windows Common Log File System (CLFS) Logical-Error Vulnerability](https://www.pixiepointsecurity.com/blog/nday-cve-2022-24521.html) blog post. |