聲明
以下內(nèi)容,來自先知社區(qū)的任意門作者原創(chuàng)吟宦,由于傳播公黑,利用此文所提供的信息而造成的任何直接或間接的后果和損失,均由使用者本人負(fù)責(zé)绕德,長白山攻防實(shí)驗(yàn)室以及文章作者不承擔(dān)任何責(zé)任患膛。
漏洞介紹
CVE-2021-31956是發(fā)生在NTFS.sys中一個(gè)提權(quán)漏洞,漏洞的成因是因?yàn)檎我绯鰧?dǎo)致繞過條件判斷導(dǎo)致的耻蛇。最后利用起來完成Windows提權(quán)
前置知識(shí)
在此之前可以大致了解一下關(guān)于NTFSNTFS是一個(gè)文件系統(tǒng)具備3個(gè)功能 錯(cuò)誤預(yù)警功能踪蹬,磁盤自我修復(fù)功能和日志功能 NTFS是一個(gè)日志文件系統(tǒng),這意味著除了向磁盤中寫入信息臣咖,該文件系統(tǒng)還會(huì)為所發(fā)生的所有改變保留一份日志 當(dāng)用戶將硬盤的一個(gè)分區(qū)格式化為NTFS分區(qū)時(shí)跃捣,就建立了一個(gè)NTFS文件系統(tǒng)。
漏洞點(diǎn)分析首先這個(gè)函數(shù)可以通過ntoskrnl 系統(tǒng)調(diào)用來訪問夺蛇,此外還可以控制輸出緩沖區(qū)的大小疚漆,如果擴(kuò)展屬性的大小沒有對(duì)齊,此函數(shù)將計(jì)算下一個(gè)填充刁赦,下一個(gè)擴(kuò)展屬性將存儲(chǔ)為32位對(duì)齊娶聘。(每個(gè)Ea塊都應(yīng)該被填充為32位對(duì)齊) 關(guān)于對(duì)齊的介紹于計(jì)
(padding?=?((ea_block_size??+?3)?&?0xFFFFFFFC)?-?e_block_size?
后邊用到的結(jié)構(gòu)體
typedef struct _FILE_FULL_EA_INFORMATION {
ULONG NextEntryOffset;//下一個(gè)同類型結(jié)構(gòu)的偏移,若是左后一個(gè)為0
UCHAR Flags;
UCHAR EaNameLength;//eaname數(shù)組的長度
USHORT EaValueLength;//數(shù)組中每個(gè)ea值的長度
CHAR EaName[1];
}?FILE_FULL_EA_INFORMATION,?*PFILE_FULL_EA_INFORMATION;
typedef struct _FILE_GET_EA_INFORMATION {
ULONG NextEntryOffset;
UCHAR EaNameLength;
CHAR EaName[1];
} FILE_GET_EA_INFORMATION, * PFILE_GET_EA_INFORMATION;
進(jìn)行函數(shù)的部分恢復(fù)甚脉,這樣后續(xù)確認(rèn)漏洞點(diǎn)的話就會(huì)比較明顯
_QWORD *__fastcall NtfsQueryEaUserEaList(
_QWORD *a1,
FILE_FULL_EA_INFORMATION *eas_blocks_for_file,
__int64 a3,
__int64 User_Buffer,
unsigned int User_Buffer_Length,
FILE_GET_EA_INFORMATION *UserEaList,
char a7)
{
int v8; // edi
unsigned int v9; // ebx
unsigned int padding; // r15d
FILE_GET_EA_INFORMATION *GetEaInfo; // r12
ULONG NextEntryOffset; // r14d
unsigned __int8 EaNameLength; // r13
FILE_GET_EA_INFORMATION *i; // rbx
unsigned int v15; // ebx
_DWORD *out_buf_pos; // r13
unsigned int ea_block_size; // r14d
unsigned int v18; // ebx
FILE_FULL_EA_INFORMATION *ea_block; // rdx
char v21; // al
ULONG v22; // [rsp+20h] [rbp-38h]
unsigned int ea_block_pos; // [rsp+24h] [rbp-34h] BYREF
_DWORD *v24; // [rsp+28h] [rbp-30h]
struct _STRING DesEaName; // [rsp+30h] [rbp-28h] BYREF
STRING SourceString; // [rsp+40h] [rbp-18h] BYREF
??unsigned?int?occupied_length;?//?[rsp+A0h]?[rbp+48h]
v8 = 0;
*a1 = 0i64;
v24 = 0i64;
v9 = 0;
occupied_length = 0;
padding = 0;
a1[1] = 0i64;
while ( 1 )
{ // 創(chuàng)建一個(gè)索引放入ealist成員,后續(xù)循環(huán)取值
GetEaInfo = (FILE_GET_EA_INFORMATION *)((char *)UserEaList + v9);
*(_QWORD *)&DesEaName.Length = 0i64;
DesEaName.Buffer = 0i64;
*(_QWORD *)&SourceString.Length = 0i64;
SourceString.Buffer = 0i64;
DesEaName.Length = GetEaInfo->EaNameLength;
DesEaName.MaximumLength = DesEaName.Length;
DesEaName.Buffer = GetEaInfo->EaName;
RtlUpperString(&DesEaName, &DesEaName);
if ( !(unsigned __int8)NtfsIsEaNameValid(&DesEaName) )
break;
NextEntryOffset = GetEaInfo->NextEntryOffset;
EaNameLength = GetEaInfo->EaNameLength;
v22 = GetEaInfo->NextEntryOffset + v9;
for ( i = UserEaList; ; i = (FILE_GET_EA_INFORMATION *)((char *)i + i->NextEntryOffset) )
{
if ( i == GetEaInfo )
{
v15 = occupied_length;
out_buf_pos = (_DWORD *)(User_Buffer + padding + occupied_length);// // 分配的內(nèi)核池
if ( (unsigned __int8)NtfsLocateEaByName(// 通過名字查找EA信息
eas_blocks_for_file,
*(unsigned int *)(a3 + 4),
&DesEaName,
&ea_block_pos) )
{
ea_block = (FILE_FULL_EA_INFORMATION *)((char *)eas_blocks_for_file + ea_block_pos);
ea_block_size = ea_block->EaValueLength + ea_block->EaNameLength + 9;
if ( ea_block_size <= User_Buffer_Length - padding )// 此處其實(shí)有個(gè)防止溢出的大小的檢查
{
memmove(out_buf_pos, ea_block, ea_block_size);// 緩沖區(qū)溢出的漏洞點(diǎn)
*out_buf_pos = 0;
goto LABEL_8;
}
}
else
{
ea_block_size = GetEaInfo->EaNameLength + 9;// 通過名字沒查到EA信息走的分支
if ( ea_block_size + padding <= User_Buffer_Length )
{
*out_buf_pos = 0;
*((_BYTE *)out_buf_pos + 4) = 0;
*((_BYTE *)out_buf_pos + 5) = GetEaInfo->EaNameLength;
*((_WORD *)out_buf_pos + 3) = 0;
memmove(out_buf_pos + 2, GetEaInfo->EaName, GetEaInfo->EaNameLength);
SourceString.Length = DesEaName.Length;
SourceString.MaximumLength = DesEaName.Length;
SourceString.Buffer = (PCHAR)(out_buf_pos + 2);
RtlUpperString(&SourceString, &SourceString);
v15 = occupied_length;
*((_BYTE *)out_buf_pos + GetEaInfo->EaNameLength + 8) = 0;
LABEL_8:
v18 = ea_block_size + padding + v15;
occupied_length = v18;
if ( !a7 )
{
if ( v24 )
*v24 = (_DWORD)out_buf_pos - (_DWORD)v24;
if ( GetEaInfo->NextEntryOffset )
{
v24 = out_buf_pos;
User_Buffer_Length -= ea_block_size + padding;
padding = ((ea_block_size + 3) & 0xFFFFFFFC) - ea_block_size;// padding對(duì)齊的計(jì)算
goto LABEL_26;
}
}
LABEL_12:
a1[1] = v18;
LABEL_13:
*(_DWORD *)a1 = v8;
return a1;
}
}
v21 = NtfsStatusDebugFlags;
a1[1] = 0i64;
if ( v21 )
NtfsStatusTraceAndDebugInternal(0i64, 2147483653i64, 919406i64);
v8 = -2147483643;
goto LABEL_13;
}
if ( EaNameLength == i->EaNameLength && !memcmp(GetEaInfo->EaName, i->EaName, EaNameLength) )
break;
}
if ( !NextEntryOffset )
{
v18 = occupied_length;
goto LABEL_12;
}
LABEL_26:
v9 = v22;
}
a1[1] = v9;
if ( NtfsStatusDebugFlags )
NtfsStatusTraceAndDebugInternal(0i64, 2147483667i64, 919230i64);
*(_DWORD *)a1 = -2147483629;
return a1;
}
那么三個(gè)參數(shù)是如何來的丸升,哪一個(gè)是用戶態(tài)可控的,因?yàn)槿绻鹐a_block_size可控且User_Buffer_Length可控為0就可以輕松繞過檢查牺氨,ea_block_size還可以正好導(dǎo)致溢出發(fā)生发钝。
NtfsQueryEaUserEaList函數(shù)大致會(huì)做循環(huán)遍歷文件的每個(gè)NTFS擴(kuò)展屬性(Ea-Extended the attribute index)然后從ea_block復(fù)制出來這些buffer到緩沖區(qū) (ea_block_size的值)
ea_block_size的值又是由ea_block決定的(ea_block->EaValueLength + ea_block->EaNameLength + 9)其實(shí)最后就是繞過這個(gè)檢查 具體繞過思考
參考與ncc的計(jì)算方法顿涣,用數(shù)學(xué)公式表達(dá)一下方便(注:以下是根據(jù)代碼轉(zhuǎn)換成數(shù)學(xué)公式,只是個(gè)人覺得這么理解第一次比較好理解哈)
ea_block_size <= User_Buffer_Length - padding 上邊說過是繞過這個(gè)條件判斷的檢查
首先假設(shè)幾個(gè)值
EaNameLength = x ,EaValueLength = y ,ea_block_size = z ,padding就是padding本身,User_Buffer_Length = f
那么首先能根據(jù)代碼確定幾個(gè)式子 z = x + y + 9 , 判斷條件為 z <= f - padding
首先開始第一次循環(huán)從數(shù)組里取值
假設(shè)x = 5 ,y = 4 , 所以z = 5 + 4 + 9 = 18 ,padding = 0
此時(shí)如果 設(shè)其值為30(User_Buffer_Length -= ea_block_size + padding)
那么f = 30 - z + 0 = 12
然后計(jì)算padding?=?((z?+?3)&?0xFFFFFFFC)?-?z?=?2
第二次從擴(kuò)展屬性取值,依舊 x = 5, y =4 ,z = 5 + 4 + 9=18
此時(shí)padding為2?f?=?12那么?18?<=?12?-?2?這個(gè)條件不成立,這是正常的想進(jìn)行溢出的流程
這是假設(shè)其值為30的情況也就是f稍大于z的情況,那么我們假設(shè)的值不是30是18呢
再來一遍
第一次循環(huán)取值 x = 5,y = 4 , z = 5 + 4 + 9 =18 不變,padding 依舊是0
18 <= 18 - 0這時(shí)候此時(shí)條件是滿足,接著往下進(jìn)行
設(shè)其值為18
(User_Buffer_Length -= ea_block_size + padding)
那么f = 18 - 18 + 0 =0 ,padding計(jì)算不變 因?yàn)橛X得padding的值 z 并沒有變化
padding = ((z + 3)& 0xFFFFFFFC) - z = 2
我們第二次擴(kuò)展 x = 5 , y = 99 , z = 5 + 99 + 9 = 113
z <= f - padding 也就是 113 <= 0 - 2 ,因?yàn)槭菬o符號(hào)整數(shù),最后-2就會(huì)導(dǎo)致整數(shù)溢出
從而繞過了這個(gè)條件那么超出的大小就會(huì)被覆蓋到相鄰的內(nèi)存導(dǎo)致溢出
代碼中其實(shí)可以看見其會(huì)不斷遍歷ea_block數(shù)組里邊的值,然后再根據(jù)FILE_GET_EA_INFORMATION 獲取到文件里的EA信息酝豪,通過上述的分析我們已經(jīng)知道如何過掉溢出的檢查了
分配的池空間PoolWithTag 到 NtfsQueryEaUserEaList -->User_Buffer --> out_buf_pos 最后memmove觸發(fā)
漏洞觸發(fā)利用
了解了漏洞觸發(fā)點(diǎn)之后涛碑,下一步就是驗(yàn)證。
首先需要?jiǎng)?chuàng)建一個(gè)文件然后添加EA拓展屬性=>NtSetEaFile該函數(shù)的第3個(gè)參數(shù)是一個(gè)FILE_FULL_EA_INFORM-ATION結(jié)構(gòu)的緩沖區(qū)孵淘,用來指定Ea屬性的值蒲障。所以我們可以利用EA屬性來構(gòu)造PAYLOAD, 然后使用NtQueryEaFile函數(shù)來觸發(fā)NtQueryEaFile
查詢一下能對(duì)EA 擴(kuò)展屬性進(jìn)行操作的api記一下這兩個(gè)ZwQueryEaFile , ZwSetEaFile 分別對(duì)應(yīng)NtSetEaFile , NtQueryEaFile
NTSTATUS ZwQueryEaFile(
[in] HANDLE FileHandle, //文件句柄
[out] PIO_STATUS_BLOCK IoStatusBlock,
[out] PVOID Buffer, //擴(kuò)展屬性緩沖區(qū)(FILE_FULL_EA_INFORMATION結(jié)構(gòu))
[in] ULONG Length, //緩沖區(qū)大小
[in] BOOLEAN ReturnSingleEntry,
[in, optional] PVOID EaList, //指定需要查詢的擴(kuò)展屬性
[in] ULONG EaListLength,
[in, optional] PULONG EaIndex, //指定需要查詢的起始索引
[in] BOOLEAN RestartScan
);
NTSTATUS ZwSetEaFile(
[in] HANDLE FileHandle,
[out] PIO_STATUS_BLOCK IoStatusBlock,
[in] PVOID Buffer,
[in] ULONG Length,
);
WNF是一個(gè)通知系統(tǒng)在整個(gè)系統(tǒng)中的主要任務(wù)就是用于通知瘫证,相當(dāng)于通知中心揉阎。它可以在內(nèi)核模式中使用,也可以在用戶態(tài)被調(diào)用WNF背捌。我們要明白上述的輸出緩沖區(qū)buffer是從用戶空間傳入的毙籽,同時(shí)傳入的還有這個(gè)緩沖區(qū)的長度。
這意味著我們最終會(huì)根據(jù)緩沖區(qū)的大小控制內(nèi)核空間的大小分配毡庆,觸發(fā)漏洞的話還需要觸發(fā)如上所述的溢出坑赡。我們需要進(jìn)行堆噴在內(nèi)核進(jìn)行我們想要的堆布局。
利用手法是WNF
WNF_STATE_DATA //用戶可以定義的
NtCreateWnfStateName //創(chuàng)建WNF對(duì)象實(shí)例=>WNF_NAME_INSTANCE
NtUpdateWnfStateData //寫入數(shù)據(jù)存放在WNF_STATE_DATA
NtQueryWnfStateData //讀取寫入的數(shù)據(jù)
NtDeleteWnfStateData //釋放Create創(chuàng)建的對(duì)象
有限的地址讀寫
所以首先要通過NtCreateWnfStateName創(chuàng)建一個(gè)WNF對(duì)象實(shí)例要利用漏洞溢出點(diǎn)Ntfs噴出來的堆塊去覆蓋WNF_STATE_DATA中的DataSize成員和AllocateSize成員么抗。
然后可以利用NtQueryWnfStateData去進(jìn)行讀取毅否,NtUpdateWnfStateData 去進(jìn)行修改相鄰WNF_NAME_INSTANCE數(shù)據(jù),但是此時(shí)這里完成的有限的地址讀寫蝇刀。
任意地址讀寫
利用相對(duì)內(nèi)存寫修改鄰近的 WNF_NAME_INSTANCE結(jié)構(gòu)的 StateData指針為任意內(nèi)存地址螟加,然后就可以通過NtQueryWnfStateData,NtUpdateWnfStateData來實(shí)現(xiàn)任意地址讀寫了。最后可以通過NtDeleteWnfStateData可以釋放掉這個(gè)對(duì)象吞琐。
歡迎關(guān)注長白山攻防實(shí)驗(yàn)室微信公眾號(hào)定期更新優(yōu)質(zhì)文章分享