Share
## https://sploitus.com/exploit?id=95777ECF-009C-57FA-B296-99335273BF16
# Patch Analysis

Looking at bindiff, we can see there're 2 functions that's been modified in the patch:

![image](https://hackmd.io/_uploads/ByxXqSv2T.png)

`CClfsBaseFilePersisted::AcquireTruncateContext`:

```c
__int64 __fastcall CClfsBaseFilePersisted::AcquireTruncateContext(
        CClfsBaseFilePersisted *this,
        unsigned int *a2,
        CLFS_TRUNCATE_CONTEXT *a3,
        CLFS_TRUNCATE_RECORD_HEADER **truncateRecordHeader,
        unsigned int *a5)
{
  char v9; // si
  BOOLEAN v10; // r15
  int v11; // ebx
  CLFS_LOG_BLOCK_HEADER *truncateBlockHeader; // rdx
  ULONG truncateBlockSize; // ecx
  CLFS_CONTROL_RECORD *v14; // rcx
  unsigned int v16; // [rsp+24h] [rbp-34h]
  CLFS_CONTROL_RECORD *controlRecord; // [rsp+28h] [rbp-30h] BYREF

  controlRecord = 0i64;
  v9 = 0;
  *(_QWORD *)&a3->eTruncateState = 0i64;
  v10 = ExAcquireResourceExclusiveLite(*((PERESOURCE *)this + 4), 1u);
  v11 = CClfsBaseFile::GetControlRecord((CLFS_LOG_BLOCK_HEADER ***)this, &controlRecord);
  v16 = v11;
  if ( v11 >= 0 )
  {
    v11 = CClfsBaseFile::AcquireMetadataBlock(this, 4i64);
    v16 = v11;
    if ( v11 >= 0 )
    {
      v9 = 1;
      truncateBlockHeader = *(CLFS_LOG_BLOCK_HEADER **)(*((_QWORD *)this + 6) + 0x60i64);

      // ----- Before Patch:
      truncateBlockSize = truncateBlockHeader->SignaturesOffset - truncateBlockHeader->RecordOffsets[0];
      *a5 = truncateBlockSize;
      // ----- After Patch:
      // truncateBlockSize = RtlULongSub(truncateBlockHeader->SignaturesOffset , truncateBlockHeader->RecordOffsets[0], a5);

      if ( truncateBlockSize >= *a2 )
      {
        *truncateRecordHeader = (CLFS_TRUNCATE_RECORD_HEADER *)(*(_QWORD *)(*((_QWORD *)this + 6) + 0x60i64)
                                                              + truncateBlockHeader->RecordOffsets[0]);
        v14 = controlRecord;
        *(_QWORD *)&a3->eTruncateState = &controlRecord->cxTruncate;
        *((_QWORD *)this + 54) = v14;
        v11 = 0;
      }
      else
      {
        v11 = 0xC0000023;
      }
      v16 = v11;
    }
  }
  if ( v11 < 0 )
  {
    if ( v9 )
      CClfsBaseFile::ReleaseMetadataBlock(this, 4);
    if ( controlRecord )
      CClfsBaseFile::ReleaseMetadataBlock(this, 0);
    if ( v10 )
    {
      ExReleaseResourceForThreadLite(*((PERESOURCE *)this + 4), (ERESOURCE_THREAD)KeGetCurrentThread());
      return v16;
    }
  }
  return (unsigned int)v11;
}
```

`CClfsLogFcbPhysical::TruncateLogRewriteOwnerPages`:

```c
...
controlledBuffer = (char *)(&ownerPageBuffer->MajorVersion + ownerPageBuffer->RecordOffsets[2]);
truncateClientChange = (CLFS_TRUNCATE_CLIENT_CHANGE *)((char *)truncateRecordHeader
                                                      + truncateRecordHeader->coffClientChange);
v21 = *(_CLS_LSN **)CClfsLogFcbPhysical::GetNextBlockLsn(
                      (CClfsLogFcbPhysical *)a1,
                      (union _CLS_LSN)&v32,
                      truncateClientChange->lsnClient,
                      truncateClientChange->cLength << 9).ullOffset;
v33 = v21;
if ( ownerPageBuffer->RecordOffsets[2] > ownerPageBuffer->TotalSectorCount << 9 )
  goto LABEL_4;

// ----- Before patch:
*(_QWORD *)&controlledBuffer[16 * truncateClientChange->cidClient + 8] = v21;
v22 = &controlledBuffer[16 * truncateClientChange->cidClient];

// ----- After patch:
// ActualScratchSize = CClfsBaseFilePersisted::GetActualScratchSize(a1[85], &scratchSize);
// if ( ActualScratchSize < 0 )            // added conditon
//   break;
// v24 = 16i64 * truncateClientChange->cidClient;
// if ( &controlledBuffer[v24 + 16] > (UCHAR *)truncateRecordHeader + scratchSize )// added comparison
//   goto LABEL_4
// *(_QWORD *)&controlledBuffer[v24 + 8] = v23;

if ( v22 )
{
  if ( *(_QWORD *)v22 <= (unsigned __int64)v21 )
    goto LABEL_29;
  v23 = 1;
}
else
{
  v23 = 0;
}
if ( v23 )
  *(_QWORD *)v22 = v21;
...
```

We can clearly see that the modified code in `CClfsBaseFilePersisted::AcquireTruncateContext` hints an **Integer Overflow** bug and `CClfsLogFcbPhysical::TruncateLogRewriteOwnerPages` hints an **OOB Write**.

# Bug Triggering

Before analyzing this CVE, I've looked at some incredible researches from other people about CLFS:

[https://securelist.com/windows-clfs-exploits-ransomware/111560](https://securelist.com/windows-clfs-exploits-ransomware/111560)

[https://github.com/ionescu007/clfs-docs/tree/main](https://github.com/ionescu007/clfs-docs/tree/main)

[https://github.com/libyal/libfsclfs/blob/main/documenation/Common%20Log%20File%20System%20(CLFS).asciidoc](https://github.com/libyal/libfsclfs/blob/main/documenation/Common%20Log%20File%20System%20(CLFS).asciidoc)

[https://docs.dissect.tools/en/latest/api/dissect/clfs/c_clfs/index.html](https://docs.dissect.tools/en/latest/api/dissect/clfs/c_clfs/index.html)

After having a brief understanding about CLFS internal, I started looking for functions that use both of these vulnerable functions, and they are `CClfsLogFcbPhysical::RecoverTruncateLog` and `CClfsLogFcbPhysical::SetEndOfLog`.

At first I only analyzed `CClfsLogFcbPhysical::SetEndOfLog` because you can go directly into it using corresponding usermode api `SetEndOfLog`, or you can use `DeviceIoControl` to step directly into `CClfsLogFcbPhysical::SetEndOfLog` in kernel mode without having extra steps to check whether your input buffer is valid, of course the cons is that you have to spend extra time reversing to understand the input buffer format to construct it. But in later stage I started utilizing `CClfsLogFcbPhysical::RecoverTruncateLog` because all I need to do is to open the BLF file and the function will be instantly triggered, and also I felt like it's a bit easier to debug this function.

Either way, these functions will first parse the BLF file header and decided what to do next depends on `eTruncateState` in Truncate Context. At this point we can guess that it will read Truncate Context from `CClfsBaseFilePersisted::AcquireTruncateContext`, which is our vulnerable function.

We'll break down this function first:

```c
controlRecord = 0i64;
v9 = 0;
*(_QWORD *)&truncateContext->eTruncateState = 0i64;
v10 = ExAcquireResourceExclusiveLite(*((PERESOURCE *)this + 4), 1u);
v11 = CClfsBaseFile::GetControlRecord((CLFS_LOG_BLOCK_HEADER ***)this, &controlRecord);   // --- [1] Getting Control Record
v16 = v11;
if ( v11 >= 0 )
{
  v11 = CClfsBaseFile::AcquireMetadataBlock(this, 4i64);  // --- [2] Getting Truncate Block
  v16 = v11;
  if ( v11 >= 0 )
  {
    v9 = 1;
    truncateBlockHeader = *(CLFS_LOG_BLOCK_HEADER **)(*((_QWORD *)this + 6) + 0x60i64); // --- Result from CClfsBaseFile::AcquireMetadataBlock
    truncateBlockSize = truncateBlockHeader->SignaturesOffset - truncateBlockHeader->RecordOffsets[0];  // --- [3] Sanity check for Truncate Block size
    *a5 = truncateBlockSize;
    if ( truncateBlockSize >= *a2 ) // --- Restriction
    {
      *truncateRecordHeader = (CLFS_TRUNCATE_RECORD_HEADER *)(*(_QWORD *)(*((_QWORD *)this + 6) + 0x60i64)
                                                            + truncateBlockHeader->RecordOffsets[0]);
      v14 = controlRecord;
      *(_QWORD *)&truncateContext->eTruncateState = &controlRecord->cxTruncate;   // --- [4] Getting eTruncateState value from Control Record
      *((_QWORD *)this + 54) = v14;
      v11 = 0;
    }
    else
    {
      v11 = 0xC0000023;
    }
    v16 = v11;
  }
}
...
```

In `[1]`, it will read Control Record from our BLF file, its structure looks like this:

```c
typedef struct _CLFS_CONTROL_RECORD
{
    CLFS_METADATA_RECORD_HEADER hdrControlRecord;
    ULONGLONG ullMagicValue;
    UCHAR Version;
    CLFS_EXTEND_STATE eExtendState;
    USHORT iExtendBlock;
    USHORT iFlushBlock;
    ULONG cNewBlockSectors;
    ULONG cExtendStartSectors;
    ULONG cExtendSectors;
    CLFS_TRUNCATE_CONTEXT cxTruncate;   // <--- We can modify this
    USHORT cBlocks;
    ULONG cReserved;
    CLFS_METADATA_BLOCK rgBlocks[ANYSIZE_ARRAY];
} CLFS_CONTROL_RECORD, *PCLFS_CONTROL_RECORD;
```

Since we can control our BLF file, we can modify any values we want according to this struct as long as it passes any sanity check from the function.

In `[2]`, it will read our Truncate Block using `CClfsBaseFile::AcquireMetadataBlock` from our BLF file. A noticable thing should be point out is that the size of Truncate Block can be controlled by modifying a value from a field in BLF file. We can utilize this to come up with strategies for exploitation.

In `[3]`, it will calculate out Truncate Block buffer size by getting last signature offset minus with the start of the Truncate Block offset. Because Truncate Block Header was read from our BLF file, we can modify `truncateBlockHeader->SignaturesOffset` to be less than `truncateBlockHeader->RecordOffsets[0]`, which will lead to integer overflow! This means we can bypass any restriction in the upcoming conditional if, which could potentially lead to buffer overflow.

And finally in `[4]`, our Truncate Context will remember the value of `cxTruncate`, which is important for later stage.

After executing `CClfsBaseFilePersisted::AcquireTruncateContext`, the function will depend on the value of `cxTruncate` to decide which action to take on. This is the enum of `cxTruncate` based on previous research:

```
typedef enum _CLFS_TRUNCATE_STATE
{
    ClfsTruncateStateNone,
    ClfsTruncateStateModifyingStream,
    ClfsTruncateStateSavingOwner,
    ClfsTruncateStateModifyingOwner,
    ClfsTruncateStateSavingDiscardBlock,
    ClfsTruncateStateModifyingDiscardBlock
} CLFS_TRUNCATE_STATE, *PCLFS_TRUNCATE_STATE;
```

We can get a grasp on how this works merely just by look at the name of those enums, and everything is happening in `CClfsLogFcbPhysical::TruncateLogRewriteOwnerPages`:

```c
...
coffOwnerPage = truncateRecordHeader->coffOwnerPage;
if ( (coffOwnerPage & 7) != 0 )
  goto LABEL_4;
ownerPageBuffer = (CLFS_LOG_BLOCK_HEADER *)((char *)truncateRecordHeader + coffOwnerPage);
if ( truncateContext_arg->eTruncateState == 2 )   // --- [1] ClfsTruncateStateSavingOwner
{
  v31.ullOffset = 0i64;
  if ( !readOwnerPage )
  {
    readOwnerPage = (unsigned __int8 *)ExAllocateFromNPagedLookasideList((PNPAGED_LOOKASIDE_LIST)&CClfsLogFcbPhysical::m_laOwnerPage);
    v30 = readOwnerPage;
    if ( !readOwnerPage )
      break;
  }
  EventObject = CClfsLogFcbPhysical::ReadOwnerPage(
                  (CClfsLogFcbPhysical *)a1,
                  (CLFS_LSN *)&ullOffset,
                  0,
                  0,
                  readOwnerPage,
                  &v31);
  if ( EventObject < 0 )
    break;
  memmove(ownerPageBuffer, readOwnerPage, 0x1000ui64);  // --- noticable memmove
  EventObject = CClfsBaseFilePersisted::WriteMetadataBlock(a1[85], 4u, 1);
  if ( EventObject < 0 )
    break;
  truncateContext_arg->eTruncateState = 3;  // --- Change to ClfsTruncateStateModifyingOwner
  EventObject = CClfsBaseFilePersisted::FlushControlRecord((const void **)a1[85]);
  if ( EventObject < 0 )
    break;
}
controlledBuffer = (char *)(&ownerPageBuffer->MajorVersion + ownerPageBuffer->RecordOffsets[2]);
truncateClientChange = (CLFS_TRUNCATE_CLIENT_CHANGE *)((char *)truncateRecordHeader
                                                      + truncateRecordHeader->coffClientChange);
v21 = *(_CLS_LSN **)CClfsLogFcbPhysical::GetNextBlockLsn(
                      (CClfsLogFcbPhysical *)a1,
                      (union _CLS_LSN)&v32,
                      truncateClientChange->lsnClient,
                      truncateClientChange->cLength << 9).ullOffset;  // --- [2] Controlling v21
v33 = v21;
if ( ownerPageBuffer->RecordOffsets[2] > ownerPageBuffer->TotalSectorCount << 9 )
  goto LABEL_4;
*(_QWORD *)&controlledBuffer[16 * truncateClientChange->cidClient + 8] = v21;   // --- OOB Write
v22 = &controlledBuffer[16 * truncateClientChange->cidClient];
if ( v22 )
{
  if ( *(_QWORD *)v22 <= (unsigned __int64)v21 )
    goto LABEL_29;
  v23 = 1;
}
else
{
  v23 = 0;
}
if ( v23 )
  *(_QWORD *)v22 = v21;
...
```

In `[1]`, it'll check if `eTruncateState` is `ClfsTruncateStateSavingOwner`. As the name suggested, it'll read Owner Page using `CClfsLogFcbPhysical::ReadOwnerPage` and copy the array to `ownerPageBuffer` using memmove. Recall that Truncate Block size can be modified, `truncateRecordHeader` is inside this Block, and `ownerPageBuffer`'s position is based on `truncateRecordHeader->coffOwnerPage`, which is also inside Truncate Block, there's a Pool Overflow occuring at this `memmove` since we can make Truncate Block size smaller than 0x1000. But that's not the only bug.

In `[2]`, the value of `v21` is based on `truncateClientChange->lsnClient`, which we can control as long as the final value of `v21` has to be divisible by 0x200. After that this value will be assigned to `controlledBuffer[16 * truncateClientChange->cidClient + 8]`. Again because we can control almost anything in Truncate Block, we can modify `truncateClientChange->cidClient` so it can OOB Write to adjacent pool chunk.

We can use either bug to escalate to System privilege, but while exploiting this, I only used OOB Write, so my writeup will be focused on this bug.

# Exploit Strategies

I heavily referred to this [article](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) to exploit this CVE. I won't go to technical details about this technique; instead, I'll explain the overall strategy.

First thing is we need to deeply understand what do we have before exploiting the bug. In case of OOB Write, we can only write 8 - 16 bytes at a time, and the overwritten 8-bytes value must be divisible by 0x200. So my first thought was to create a relative write primitive, so we can easily control the overflow size and overflown value for next step, and `WNF_STATE_DATA` would be a perfect object for this.

`WNF_STATE_DATA` structure:

```c
struct _WNF_STATE_DATA
{
    struct _WNF_NODE_HEADER Header;
    ULONG AllocatedSize;
    ULONG DataSize;
    ULONG ChangeStamp;
}; 
```

Relative write can be achieved by corrupting `AllocatedSize`, and using `NtUpdateWnfStateData` to trigger this, and our OOB Write can help us corrupting both `AllocatedSize` and `DataSize` by overwriting 8 bytes to these fields.

Having this in mind, the pool spray layout I want would look like this:

First I'll try to spray `Attribute Pipe` with size 0x7c0:

`... | Attribute Pipe | Attribute Pipe | Attribute Pipe | Attribute Pipe | Attribute Pipe | Attribute Pipe | Attribute Pipe | ...`

After that free some of it to make ways for `WNF_STATE_DATA`:

`... | WNF_STATE_DATA | Attribute Pipe | WNF_STATE_DATA | Attribute Pipe | WNF_STATE_DATA | Attribute Pipe | WNF_STATE_DATA | ...`

Finally free another amount of `Attribute Pipe` to make ways for our Truncate Block chunk:

`... | WNF_STATE_DATA | CLFS | WNF_STATE_DATA | Attribute Pipe | WNF_STATE_DATA | CLFS | WNF_STATE_DATA | Attribute Pipe | ...`

With this layout, our vuln chunk (`CLFS`) will help us gain relative write through adjacent `WNF_STATE_DATA`, which will overwrite the next `Attribute Pipe`. And after that it's all exact same as the above article.

# POC Image

![image](https://hackmd.io/_uploads/HykSp5Pha.png)

# Thoughts

Although I've finally had a System Shell, it'll crash after a few seconds because some of the pools' VS header was corrupted. Also the success rate is relative high, but it still depends on the spray we make for `CLFS` pool, because if we're not lucky enough, `CLFS` pool will OOB Write to a non allocated memory, which will cause BSOD. And also for my personal curiosity, I should reverse and learn more about `Dynamic Lookaside` since in rare situations, if `Dynamic Lookaside` is not enabled, the corrupted pool will actually be freed, which will also cause BSOD.