作者:360漏洞研究院 許仕杰 宋建陽 李林雙
原文鏈接:https://vul.360.net/archives/438

概述

近兩年通用日志文件系統模塊 (clfs) 成為了 Windows 平臺安全研究的熱點,本文首先會介紹一些關于 clfs 的背景知識,然后會介紹我們是如何對這個目標進行 fuzz ,最后將分享幾個漏洞案例以及我們是如何使用一種新方法實現本地提權。

背景知識

根據微軟官方文檔可以知道,通用日志文件系統 (clfs) 是 Windows Vista 引入的一種新的日志記錄機制,它負責提供一個高性能、通用的日志文件子系統,供專用客戶端應用程序使用,多個客戶端可以共享以優化日志訪問。

我們可以使用 CreateLogFile 函數創建或打開一個日志文件 (.blf)。日志名決定這個日志為單路日志還是多路日志,日志名格式為 (log :[::]) ,日志可以通過 CloseHandle 函數關閉。

CLFSUSER_API HANDLE CreateLogFile(
  [in]           LPCWSTR               pszLogFileName,
  [in]           ACCESS_MASK           fDesiredAccess,
  [in]           DWORD                 dwShareMode,
  [in, optional] LPSECURITY_ATTRIBUTES psaLogFile,
  [in]           ULONG                 fCreateDisposition,
  [in]           ULONG                 fFlagsAndAttributes
);

我們可以通過查詢微軟官方文檔或者逆向clfs.sys驅動獲取一些日志相關操作函數。

Fuzz CLFS

我們首先查閱了一些前輩的研究資料(鏈接會放在文末),可以發現攻擊面主要分為兩類

  • clfs.sys 中日志文件解析相關漏洞
  • clfs.sys 中 IoCode 處理相關漏洞

我們決定先研究 blf 日志文件格式,然后對該日志文件格式進行fuzz,最后我們總結出 blf 格式如下圖

知道日志文件格式和日志處理函數之后,我們的 fuzz 設計就很簡單,大致思路如下

  • 創建日志文件(單路、多路、是否設置 Container 容器)
  • 根據文件格式隨機數據
  • 調用函數使 clfs.sys 對日志文件進行解析

需要注意的是在每次隨機文件內容的時候,需要繞過一個 CRC 檢查,偽代碼如下

__int64 __fastcall CCrc32::ComputeCrc32(BYTE* Ptr, int Size)
{
  unsigned int Crc;

  for ( int i = 0; i < Size; i++ )
  {
    data = Ptr[i];
    Crc = (Crc >> 8) ^ CCrc32::m_rgCrcTable[(unsigned __int8)Crc ^ data];
  }

  return ~Crc;
}

在逆向過程中,我們觀察到一些以 Get 和 Acquire 開頭的函數會直接從 blf 文件中讀取數據,所以我們在隨機數據的時候重點關注這些函數即可。

漏洞分析

經過一段時間的 fuzz,我們得到了一些崩潰,這里分享其中兩個

CVE-2022-21916

第一個漏洞出現在 CClfsBaseFilePersisted::ShiftMetadataBlockDescriptor 函數,其偽代碼如下所示

CClfsBaseFilePersisted::ShiftMetadataBlockDescriptor(this,UINT iFlushBlock,UINT iExtendBlock)
{
  // ...

  NewTotalSize = -1;
  TotalSize = iExtendBlock * this->SectorSize;
  if ( TotalSize > 0xFFFFFFFF )
    return STATUS_INTEGER_OVERFLOW;
  TotalSectorSize = this->BaseMetaBlock[iFlushBlock].TotalSectorSize; // OOB read
  if ( TotalSectorSize + TotalSize >= TotalSectorSize )
    NewTotalSize = TotalSectorSize + TotalSize;
  Status = TotalSectorSize + TotalSize < TotalSectorSize ? STATUS_INTEGER_OVERFLOW : 0;
  this->BaseMetaBlock[iFlushBlock].TotalSectorSize = NewTotalSize;
  return Status;
}

該函數在解析 CLFS_CONTROL_RECORD 結構的時候出現了問題,該結構可以在 blf 文件偏移 0x70 的位置找到,其中 iFlushBlock 存在于 blf 文件的 0x8A 處,iExtendBlock 存在于文件的 0x88 處,此函數未正確對這兩個參數進行檢查導致了越界漏洞的產生。到達此函數還需要將 eExtendState 字段設置為 2,此字段存在于 blf 文件 0x84 的位置,如下所示:

Vulnerability for TianfuCup

第二個漏洞出現在 CClfsLogFcbPhysical::OverflowReferral 函數,與 CVE-2022-21916 類似,該漏洞也是在解析 blf 文件格式時出現問題,該函數主要與 ownerpage 操作相關。偽代碼如下

CClfsLogFcbPhysical::OverflowReferral(CClfsLogFcbPhysical *this, struct _CLFS_LOG_BLOCK_HEADER * LogBlockHeader)
{
  // NewOwnerPage is a Paged Pool of size 0x1000
  NewOwnerPage = &LogBlockHeader->MajorVersion + LogBlockHeader->RecordOffsets[2]; 
  OldOwnerPage = &this->OwnerPage->MajorVersion + this->OwnerPage->RecordOffsets[2];
  ClientId = CClfsBaseFile::HighWaterMarkClientId(this->CClfsBaseFilePersisted); // BaseLogRecord->cNextClient - 1
  i = 0;
  do
  {
    i = i++;
    i *= 2i64;
    *(CLFS_LSN *)&NewOwnerPage[8 * i] = CLFS_LSN_INVALID; // OOB Write
    *(_QWORD *)&NewOwnerPage[8 * i + 8] = *(_QWORD *)&OldOwnerPage[8 * i + 8];
  }
  while ( i <= ClientId ); // Overflow occurs when ClientId is greater than 0x60
}

CClfsBaseFile::HighWaterMarkClientId 函數負責獲取 blf 文件中的 ClientId 信息,可以通過修改 CLFS_BASE_RECORD_HEADER 結構中的 cNextClient 字段從而控制 ClientId ,而 cNextClient 可以直接從 blf 文件中找到并修改。 當 cNextClient 被修改為其他值的時候,它會被用作錯誤的索引,從而導致越界漏洞。下圖就是我們在文件中找到的 cNextClient

總結而言,此漏洞是一個大小為 0x1000 的分頁池溢出漏洞,它將 CLFS_LSN_INVALID 和 OldOwnerPage 數據寫入下一個池的頭部。

漏洞利用介紹

Windows 分頁池溢出的利用方式一般有如下兩種:

  • WNF
    通過溢出占用 _WNF_NAME_INSTANCE 結構的 StateData 指針可以實現有限制的任意地址讀寫
  • 命名管道
    通過溢出占用 PipeAttribute 結構的 Flink 指針可以實現任意地址讀

WNF 的利用方式有一些限制,比如 _WNF_NAME_INSTANCE 結構的大小為 0xC0 或者 0xD0,而我們的漏洞是一個大小為 0x1000 的分頁池溢出漏洞。我們無法將 0xC0 或者 0xD0 大小的分頁池分配在 0x1000 的分頁池后面,所以我們無法利用這個漏洞去溢出 WNF的_WNF_NAME_INSTANCE 結構。

但是我們可以去溢出 0x1000 大小的 _WNF_STATE_DATA 結構,通過溢出 _WNF_STATE_DATA 結構中的 AllocatedSize 字段,我們可以實現最大長度為 0x1000 的越界寫,它只能越界寫 0x1000 大小的 _WNF_STATE_DATA 結構后的數據,并且剛好只能越界寫 16 個字節。所以我們需要找到一個 0x1000 大小的分頁池結構,然后通過修改這個結構的前 16 個字節來實現任意地址寫,我們在 Windows ALPC 中找到了這個結構。

我們也找到了一種新的通用的 Windows 分頁池溢出的利用方式,通過溢出占用 _ALPC_HANDLE_TABLE 結構的 Handles 指針,我們可以實現任意地址的讀寫。

_ALPC_HANDLE_TABLE 的結構如下:

kd> dt nt!_ALPC_HANDLE_TABLE
   +0x000 Handles          : Ptr64 _ALPC_HANDLE_ENTRY
   +0x008 TotalHandles     : Uint4B
   +0x00c Flags            : Uint4B
   +0x010 Lock             : _EX_PUSH_LOCK

你可以通過調用 NtAlpcCreateResourceReserve 函數來創建一個 Reserve Blob,它會調用 AlpcAddHandleTableEntry 函數把剛創建的 Reserve Blob 的地址寫入到 _ALPC_HANDLE_TABLE 結構的Handles數組中。

當你創建alpc端口時,AlpcInitializeHandleTable 函數會被調用來初始化 HandleTable 結構。Handles 是一個初始大小為 0x80 的數組,它存放著 blob 結構的地址。當越來越多 blob 被創建時,Handles 的大小成倍增加,所以 Handles 的大小可以為 0x1000。

_KALPC_RESERVE 的結構如下所示:

kd> dt nt!_KALPC_RESERVE
   +0x000 OwnerPort        : Ptr64 _ALPC_PORT
   +0x008 HandleTable      : Ptr64 _ALPC_HANDLE_TABLE
   +0x010 Handle           : Ptr64 Void
   +0x018 Message          : Ptr64 _KALPC_MESSAGE
   +0x020 Size             : Uint8B
   +0x028 Active           : Int4B

當你溢出占用 Handles 數組中的 _KALPC_RESERVE 結構的指針時,你就可以偽造一個虛假的 Reserve Blob。因為 _KALPC_RESERVE 中存儲著 Message 的地址,所以你可以進一步偽造一個虛假的 _KALPC_MESSAGE 結構。

_KALPC_MESSAGE 的結構如下:

kd> dt nt!_KALPC_MESSAGE
   +0x000 Entry            : _LIST_ENTRY
   +0x010 PortQueue        : Ptr64 _ALPC_PORT
   +0x018 OwnerPort        : Ptr64 _ALPC_PORT
   +0x020 WaitingThread    : Ptr64 _ETHREAD
   +0x028 u1               : <anonymous-tag>
   +0x02c SequenceNo       : Int4B
   +0x030 QuotaProcess     : Ptr64 _EPROCESS
   +0x030 QuotaBlock       : Ptr64 Void
   +0x038 CancelSequencePort : Ptr64 _ALPC_PORT
   +0x040 CancelQueuePort  : Ptr64 _ALPC_PORT
   +0x048 CancelSequenceNo : Int4B
   +0x050 CancelListEntry  : _LIST_ENTRY
   +0x060 Reserve          : Ptr64 _KALPC_RESERVE
   +0x068 MessageAttributes : _KALPC_MESSAGE_ATTRIBUTES
   +0x0b0 DataUserVa       : Ptr64 Void
   +0x0b8 CommunicationInfo : Ptr64 _ALPC_COMMUNICATION_INFO
   +0x0c0 ConnectionPort   : Ptr64 _ALPC_PORT
   +0x0c8 ServerThread     : Ptr64 _ETHREAD
   +0x0d0 WakeReference    : Ptr64 Void
   +0x0d8 WakeReference2   : Ptr64 Void
   +0x0e0 ExtensionBuffer  : Ptr64 Void
   +0x0e8 ExtensionBufferSize : Uint8B
   +0x0f0 PortMessage      : _PORT_MESSAGE

當你調用 NtAlpcSendWaitReceivePort 函數發送消息時,它會將用戶傳入的數據寫入到 _KALPC_MESSAGE 結構中 ExtensionBuffer 所指向的地址,我們可以用它來實現任意地址寫入。

當你調用 NtAlpcSendWaitReceivePort 函數接收消息時,它會讀取 _KALPC_MESSAGE 結構中 ExtensionBuffer 所指向的地址處的數據,我們可以用它來實現任意地址讀取。

實現任意地址讀寫的整個過程如下:

首先,通過溢出占用 Handles 結構的 _KALPC_RESERVE 指針,我們可以構造一個虛假的 Reserve Blob,然后繼續構造一個虛假的 _KALPC_MESSAGE 結構,那么我們就可以通過 _KALPC_MESSAGE 結構中的 ExtensionBuffer 字段和 ExtensionBufferSize 字段來實現任意地址讀寫。

漏洞利用

下面來詳解一下漏洞利用具體的流程。

首先我們要結合 WNF 和 ALPC 達到漏洞利用,我們需要調用 NtUpdateWnfStateData 去噴射大量的 0x1000 大小的 _WNF_STATE_DATA 結構,然后讓它們在內存中相鄰排列。

接著我們需要調用 NtDeleteWnfStateName 函數去創建大量的內存空洞。

然后我們要創建 Ownerpage,也就是存在漏洞的池塊。我們需要創建一個 Multiplexed 類型的 blf 文件,并存在兩個 container 文件,然后向 container 中寫入大量的 record 記錄,當記錄的長度超過 0x7f000 的時候,寫入 record 時會自動創建 Ownerpage 頁到 container 中。之后我們需要調用 CreateLogFile 去打開一個 Multiplexed 類型的 blf 文件,該函數會解析 container,并在內存中創建 Ownerpage 池塊。

創建好 Ownerpage 池塊后,流程會進入到 OverflowReferral 函數,此時會導致越界寫操作,覆蓋相鄰的 WNF 池塊的內容。

我們將 WNF 的 AllocatedSize 成員覆蓋為 0xffffffff,這樣就可以通過 WNF 向下一個塊進行任意內容越界寫。

然后我們在 WNF 塊后面分配一個 Handles 結構,該結構的所有成員都是 ALPC_RESERVE 結構的指針。由于 WNF 有 0x1000 大小的寫入限制,寫入的起始位置在WNF 結構 +0x10 的偏移處,所以我們只能向下一個塊寫入16字節長度的內容,不過這已經足夠了。

我們將 Handles 原來的指針成員,替換成了我們自己的指針 0x00000282`99055970,該指針指向我們在用戶態偽造的 ALPC_RESERVE 結構。

之后我們調用 NtAlpcSendWaitReceivePort 函數,會進入到 AlpcpLookupMessage 函數內,然后再調用 AlpcReferenceBlobByHandle 函數,從 Handles 中獲取到我們偽造的用戶態地址。

ALPC_RESERVE 結構的 Message 成員是我們在用戶態偽造的 _KALPC_MESSAGE 結構。

我們在偽造 _KALPC_MESSAGE 時,需要將 Token 的地址,寫入在 _KALPC_MESSAGE+0xe0 偏移處,也就是 ExtensionBuffer 的位置。

最后當我們調用 NtAlpcSendWaitReceivePort 后,流程會進入到 AlpcpCaptureMessageDataSafe 函數,會調用 memmove 向 ExtensionBuffer (Token的地址) 寫入可控的任意內容。我們將 token 的 Privileges 全部覆蓋為0xff,獲得所有特權。

我們打開Procexp.exe 查看利用進程的權限,發現已經獲得了 SeDebugPrivilege 權限,擁有了該權限我們就可以向 Winlogon.exe 進程注入 shellcode,從而最終達到特權提升。

為什么是通用的?

我們在本次利用中到了三個結構,分別是_WNF_STATE_DATA 、Handles 和 _KALPC_MESSAGE。它們都有一個共同的特性,就是結構大小可控。根據我們的測試它們可以適配 0x30 ~ 0x11000+ 大小的池塊。

0x30 ~ 0x1000 size :

  • _WNF_STATE_DATA (0x30 ~ 0x1000)
  • _ALPC_HANDLE_TABLE->Handles (0x90、0x110、0x210、0x410 、0x810、0x1000…0x10000…)
  • _KALPC_MESSAGE (0x160 ~ 0x11000)

> 0x1000 size:

  • _ALPC_HANDLE_TABLE->Handles
  • _KALPC_MESSAGE

> 0x11000 size:

  • _ALPC_HANDLE_TABLE->Handles (0x90、0x110、0x210、0x410 、0x810、0x1000…0x10000…)

通用利用之WNF

WNF的結構可以適配 0x30 ~ 0x1000 的大小,通過修改 AllocatedSize 成員,可以達到越界寫。通過修改 DataSize 成員,可以達到越界讀。WNF 存在一個限制,就是越界讀寫的最大長度是 0x1000。

通用利用之Handles

Handles結構長度的增長規律是成倍的,例如:0x90、0x110、0x210、0x410 、0x810、0x1000…0x10000… 。超過 0x1000 以后是沒有池頭的,所以并不會被分配成 0x1010。我們可以覆蓋 Handles 的成員為我們偽造的 _KALPC_RESERVE 指針,然后再調用 NtAlpcSendWaitReceivePort 達到任意地址寫。

利用 Handles 結構比較方便,因為即使你向 Handles 結構寫入錯誤的地址,你仍然可以調用 VirtualAlloc 將錯誤的地址映射成正常的 _KALPC_RESERVE 指針地址。到了這一步利用基本就成功了。

通用利用之 _KALPC_MESSAGE

_KALPC_MESSAGE 的適配范圍在 0x160 ~ 0x11000。利用方法比較簡單,只需要通過溢出覆蓋 0xe0 偏移處的地址,然后再調用 NtAlpcSendWaitReceivePort,即可達到任意地址寫。

參考鏈接

CLFS Internals – Alex Ionescu
DeathNote of Microsoft Windows Kernel – Keen Lab
Microsoft Windows 10 CLFS.sys ValidateRegionBlocks privilege escalation vulnerability – Cisco Talos


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1920/