
Windows カーネルプール (clfs.sys) 破損による権限昇格
==============
Windows Kernel Pool (clfs.sys) 破壊による権限昇格。(CVE-2023-36424)
このリポジトリには、技術的分析と実用的なエクスプロイトが含まれています。
著者: Nassim Asrir (@p1k4l4) || https://www.linkedin.com/in/nassim-asrir-b73a57122/
================
clfs.sys ミニフィルタードライバーにはプールオーバーフローがあります。これに関する情報は以下で読むことができます:
1 - https://googleprojectzero.blogspot.com/2021/01/hunting-for-bugs-in-windows-mini-filter.html
2 - https://www.zerodayinitiative.com/blog/2021/7/19/cve-2021-31969-underflowing-in-the-clouds
原因は、ドライバーが NTFS リパースポイントから取得したデータを十分にチェックしていないことです。
ここでは clfs.sys バージョン 10.0.22621.2134 (Windows 11 22H2 22621.2215) を対象とします。
関数 HsmFltProcessHSMControl は、クラウドフィルター FSCTL の処理を担当します。コード 0xC0000003 の操作の場合、最終的に HsmFltProcessUpdatePlaceholder を呼び出します。
いくつかの処理の後、実行フローは HsmiOpUpdatePlaceholderDirectory に到達し、最終的に HsmpRpCommitNoLock に到達します:
__int64 __fastcall HsmpRpCommitNoLock(__int64 a1, __int64 a2, struct _FILE_OBJECT *a3, char a4, char a5)
{ .....
v26 = FileObject; LODWORD(v9) = HsmpRpReadBuffer(*(PFLT_INSTANCE *)(v164 + 32), FileObject, (unsigned __int16 **)&P); // [1*]
HsmDbgBreakOnStatus((unsigned int)v9);
if ( (_DWORD)v9 == -1073741195 ) .....
goto LABEL_55; }
if ( (v9 & 0x80000000) != 0i64 )
goto LABEL_9;}
if ( (*(_DWORD *)P & 0xFFFF0FFF) != dword_1C0027650 )// Is
Cloud Reparse Tag?
{
LODWORD(v9) = 0xC000CF0B;
.....
goto LABEL_54;
}
v32 = *((unsigned __int16 *)P + 2);
v9 = (unsigned int)HsmpRpValidateBuffer((__int64)P + 8, v32); [2*]
.....
Pool2 = ExAllocatePool2(0x100i64, 0x4000i64, 'pRsH'); // [3*]
v146 = (_DWORD *)Pool2;
v13 = (void *)Pool2;
if ( Pool2 )
{
v64 = v159_10;
v65 = Pool2 + 4;
if ( v8 && *((_WORD *)v8 + 7) > 0xAu )
v64 = *((_WORD *)v8 + 7);
v9 = Pool2 + 20;
v66 = (unsigned int *)(Pool2 + 12);
*(_OWORD *)v65 = 0i64;
*(_WORD *)(Pool2 + 16) = 0;
*(_WORD *)(Pool2 + 18) = v64;
*(_DWORD *)(Pool2 + 12) = 8 * v64 + 16;
*(_DWORD *)v65 = 'pReF';
memset((void *)(Pool2 + 20), 0, 8i64 * v64);
.....
if ( v8 )
{
v127 = 10;
if ( *((_WORD *)v8 + 7) > 0xAu ) // [4*]
{
if ( WPP_GLOBAL_Control !=
(PDEVICE_OBJECT)&WPP_GLOBAL_Control
&& (HIDWORD(WPP_GLOBAL_Control->Timer) & 1) != 0
&& BYTE1(WPP_GLOBAL_Control->Timer) >= 4u )
{
WPP_SF_qiq(WPP_GLOBAL_Control->AttachedDevice, v86,
v87, a2, *(_QWORD *)(v156 + 32), FileObject);
}
while ( v127 < *((_WORD *)v8 + 7) )
{
*(_QWORD *)(v65 + 8i64 * v127 + 16) = *(_QWORD
*)&v8[8 * v127 + 16];
memmove(
(void *)(v65 + *v66),
&v8[*(unsigned int *)&v8[8 * v127 + 20]],
*(unsigned __int16 *)&v8[8 * v127 + 18]); //
[5*]
*(_DWORD *)(v65 + 8i64 * v127 + 20) = *v66;
*v66 += *(unsigned __int16 *)(v65 + 8i64 * v127++ +
18);
}
}
}
.....
}
HsmpRpReadBuffer [1*] はリパースポイントデータを取得します。このデータには、構造化アイテムの数を指定する WORD サイズの値 *((_WORD *)v8 + 7) が含まれています。各アイテムには Type フィールド、Size、およびデータフィールドへの Offset があります。
どのアイテムタイプがどの場所にあるかは厳密に事前定義されています。ただし、最初の10個のみです。例えば、最初のアイテムのタイプフィールドは 0x7 に等しい値を持つ必要があります。
ドライバーは HsmpRpValidateBuffer [2*] を実行して取得したデータを検証します。次に [3*] で固定サイズ 0x4000 バイトのページプールが割り当てられます。そして、リパースポイントデータの Count 値が10より大きい場合、10番目以降のアイテムのデータは、追加のチェックなしでこの固定サイズのプールにコピーされます [4*]。
HsmpRpValidateBuffer 内の検証は不十分です。最初の10レコードのみをチェックするためです。
__int64 __fastcall HsmpRpValidateBuffer(__int64 pBuf, unsigned int a2)
{
.....
v2 = a2 - 4;
pBuf2 = pBuf + 4;
LOBYTE(v5) = 0;
v6 = 0i64;
if ( a2 <= 4 )
v2 = 0;
v7 = 0;
v8 = *(_DWORD *)pBuf & 0xF;
if ( !v8 )
{
.....
return IsReparseBufferSupported;
}
if ( v8 > 1 )
{
....
}
v9 = 0;
v66 = 0;
if ( v2 < 0x18 )
goto ERROR_EXIT;
v9 = 1;
if ( *(_DWORD *)pBuf2 != 'pReF' )
goto ERROR_EXIT;
v9 = 2;
v10 = (unsigned int *)(pBuf + 0xC);
if ( (*(_BYTE *)(pBuf + 16) & 2) != 0 && *(_DWORD *)(pBuf +
8) != RtlComputeCrc32(0, (PUCHAR)(pBuf + 0xC), v2 - 8) )
goto ERROR_EXIT;
v11 = *v10;
v9 = 3;
if ( v2 < (unsigned int)v11 )
goto ERROR_EXIT;
v12 = *(unsigned __int16 *)(pBuf2 + 0xE);
v9 = 4;
if ( !(_WORD)v12 )
goto ERROR_EXIT;
v13 = 8 * v12 + 16;
v9 = 5;
if ( v13 >= v11 )
goto ERROR_EXIT;
v9 = 0x10000;
for ( i = 0; ; ++i )
{
v15 = *(unsigned __int16 *)(pBuf2 + 0xE);
if ( (unsigned int)v12 >= 0xA ) // [1*]
v15 = 10;
if ( i >= v15 )
break;
}
見てわかるように [1*] コードは最初の10アイテムのみを検証し、それより多くのレコードがある場合を無視します。
=================
脆弱なプールのサイズは 0x4000 です。このサイズはページの倍数であるため、セグメント割り当てが使用されます [3]。
悪用には、ここで説明されているテクニック [4] が使用されました。NtAlpcCreateResourceReserve を呼び出すと多数のハンドルが作成され、そのうちの1つを構築した偽の _KALPC_RESERVE オブジェクトへのポインターで上書きすると、任意のカーネルアドレスに書き込む機能が得られます。
メモリを準備するために、パイプ [5] を使用して 0x4000 サイズのプールを順次割り当てます。次に、1つおきのプールを解放して、脆弱なバッファーのための場所を確保します。

任意のカーネルアドレスを読み取るために、エクスプロイトはパイプを利用しました。この目的のために、PipeAttribute 構造体の AttributeValue ポインターを上書きします。

その後、システムトークンを盗み、対象プロセスのトークンを上書きできます。
お読みいただきありがとうございます。