Quantcast
Channel: DEVCORE 戴夫寇爾
Viewing all 145 articles
Browse latest View live

Streaming vulnerabilities from Windows Kernel - Proxying to Kernel - Part I

$
0
0

English Version, 中文版本

Over the past few decades, vulnerabilities in the Windows Kernel have emerged frequently. The popular attack surface has gradually shifted from Win32k to CLFS (Common Log File System). Microsoft has continuously patched these vulnerabilities, making these targets increasingly secure. However, which component might become the next attack target? Last year, MSKSSRV (Microsoft Kernel Streaming Service) became a popular target for hackers. However, this driver is tiny and can be analyzed in just a few days. Does this mean there might not be new vulnerabilities?

This research will discuss an overlooked attack surface that allowed us to find more than ten vulnerabilities within two months. Additionally, we will delve into a proxy-based logical vulnerability type that allows us to bypass most validations, enabling us to successfully exploit Windows 11 in Pwn2Own Vancouver 2024.

(This research will be divided into several parts, each discussing different bug classes and vulnerabilities. This research was also presented at HITCON CMT 2024.)

Start from MSKSSRV

For vulnerability research, looking at historical vulnerabilities is indispensable.

Initially, we aimed to challenge Windows 11 in Pwn2Own Vancouver 2024. Therefore, we began by reviewing past Pwn2Own events and recent in-the-wild Windows vulnerabilities, searching for potential attack surfaces. Historical trends show that Win32K, primarily responsible for handling GDI-related operations, has always been a popular target, with numerous vulnerabilities still emerging. Since 2018, CLFS (Common Log File System) has also gradually become a popular target. Both components are extremely complex, suggesting that there are likely still many vulnerabilities. However, becoming familiar with these components requires significant time, and many researchers are already examining them. Therefore, we did not choose to analyze them first.

Last year, after Synacktiv successfully exploited a vulnerability in MSKSSRV to compromise Windows 11 during Pwn2Own 2023, many researchers began to focus on this component. Shortly thereafter, a second vulnerability ,CVE-2023-36802, was discovered. At this time, chompie also published an excellent blog post detailing this vulnerability and its exploitation techniques. Given that this component is very small, with a file size of approximately 72 KB, it might only take a few days of careful examination to fully understand it. Therefore, we chose MSKSSRV for historical vulnerability analysis, with the hopeful prospect of identifying other vulnerabilities.

We will briefly discuss these two vulnerabilities but not go into much detail.

CVE-2023-29360 - logical vulnerability

The first one is the vulnerability used by Synacktiv in Pwn2Own Vancouver 2023.

This is a logical vulnerability in the MSKSSRV driver. When MSKSSRV uses MmProbeAndLockPages to lock user-specified memory address as framebuffer, the AccessMode was not set correctly. This leads to a failure to check whether the user-specified address belongs to the user space. If the user provides a kernel address, it will map the specified kernel address to user space for the user to use. Ultimately, this allows the user to write data to any address in the kernel. The exploitation is simple and very stable, making it become one of the most popular vulnerabilities.

For more details, please refer to Synacktiv’s presentation at HITB 2023 HKT and Nicolas Zilio(@Big5_sec)‘s blog post.

CVE-2023-36802 - type confusion

This vulnerability was discovered shortly after CVE-2023-29360 was released. It was already being exploited when Microsoft released their patches. This is a very easily discovered vulnerability. It uses the objects (FSContextReg, FSStreamReg) stored in FILE_OBJECT->FsContext2 for subsequent processing. However, there is no check on the type of FsContext2, leading to type confusion. For detailed information, you can refer to the IBM X-Force blog, which provides a very thorough explanation.

Since then, there have been very few vulnerabilities related to MSKSSRV. Due to its relatively small size, MSKSSRV is quickly reviewed, and gradually, fewer and fewer people pay attention to it.

But is that the end of it ?

However, does this mean there are no more vulnerabilities?

In fact, the entire Kernel Streaming looks like the diagram below:

MSKSSRV is just the tip of the iceberg. In fact, there are many other potential components, and those listed in the diagram above are all part of Kernel Streaming. After delving into this attack surface, numerous vulnerabilities were eventually discovered, flowing like a stream.

By the way, while I was writing this blog, chompie also published about the vulnerability she used in this year’s Pwn2Own Vancouver 2024, CVE-2024-30089, which is also a vulnerability in MSKSSRV. The vulnerability lies in handling the reference count, requiring a lot of attention and thought to discover. It is also quite interesting, but I won’t discuss it here. I highly recommend reading this one.

Brief overview of Kernel Streaming

So, what is Kernel Streaming? In fact, we use it very frequently.

On Windows systems, when we open the webcam, enable sound, and activate audio devices such as microphones, the system needs to write or read related data such as your voice and captured images from your devices into RAM. It is essential to read data into your computer more efficiently during this process. Microsoft provides a framework called Kernel Streaming to handle these data, which primarily operates in kernel mode. It features low latency, excellent scalability, and a unified interface, making handling streaming data more convenient and efficient.

In Microsoft’s Kernel Streaming, three multimedia class driver models are provided: port class, AVStream, and stream class. We will briefly introduce port class and AVStream, as stream class is less common and more outdated and will not be discussed here.

Port class

This type of driver is mostly used for PCI and DMA-based audio device hardware drivers. Currently, most audio-related processing, such as volume control or microphone-related processing, falls into this category. The main component library used would be portcls.sys.

AVStream

AVStream is a multimedia driver provided by Microsoft that primarily supports video-only and integrated audio/video streaming. Currently, most video-related processing, such as your webcam, capture card, etc., is associated with this category.

In fact, the use of Kernel Streaming is also very complex. We will only provide a brief description. For more detailed information, please refer to Microsoft Learn.

Interact with device

When we want to interact with audio devices or webcams, we need to open the device just like with any other device. Essentially, it interacts with the device driver in the same way. So, what would the names of these types of devices be? These names are not typically like \Device\NamedPipe, but rather something like the following:

\\?\hdaudio#subfunc_01&ven_8086&dev_2812&nid_0001&subsys_00000000&rev_1000#6&2f1f346a&0&0002&0000001d#{6994ad04-93ef-11d0-a3cc-00a0c9223196}\ehdmiouttopo

Enumerate device

Here you can use APIs such as SetupDiGetClassDevs to enumerate devices. Generally, KS series devices are registered under KSCATEGORY*, such as audio devices which are registered under KSCATEGORY_AUDIO.

Additionally, you can use the APIs provided by KS, such as KsOpenDefaultDevice, to obtain the handle of the first matching PnP device in that category. Actually, it is just a wrapper around SetupDiGetClassDevs and CreateFile.

hr=KsOpenDefaultDevice(KSCATEGORY_VIDEO_CAMERA,GENERIC_READ|GENERIC_WRITE,&g_hDevice)

Kernel Streaming object

After we open these devices, Kernel Streaming will create some Kernel Streaming related instances, the most important of which are KS Filters and KS Pins.

These will be used during the Kernel Streaming process. We will only provide a brief introduction here. We will use audio filters as an example, as most others are quite similar.

KS filters

Each KS Filter typically represents a device or a specific function of a device. When we open an audio device, it usually corresponds to a KS filter object. When we read data from the audio device, this data is first processed through this KS Filter.

Conceptually, as shown in the diagram below, the large box in the middle represents a KS filter for the audio device. When we want to read data from the audio device, it is read into the filter from the left, processed through several nodes, and then output from the right.

(From: https://learn.microsoft.com/en-us/windows-hardware/drivers/audio/audio-filters)

KS pins

In the figure above, the points for reading and outputting data are called pins. The kernel also has corresponding KS pin Objects to describe the Pins, recording whether the Pin is a sink or a source, the data format for input and output, and so on. When we use it, we must open a pin on the filters to create an instance to read from or write to the device.

KS property

Each of these KS objects will have its own property, and each property corresponds to a specific feature. For instance, the data format mentioned earlier in the Pin, the volume level, and the status of the device are all properties. These properties typically correspond to a set of GUIDs. We can set these properties through IOCTL_KS_PROPERTY.

This greatly simplifies the development of multimedia drivers and ensures consistency and scalability across different devices.

Read streams from webcam

Here is a simple example illustrating how an application can read data from a webcam.

The most basic flow is roughly shown in this diagram:

  1. Open the device to obtain the device handle.
  2. Use this device handle to create an instance of the Pin on this filter and obtain the Pin handle.
  3. Use IOCTL_KS_PROPERTY to set the device state of the Pin to RUN.
  4. Finally, you can use IOCTL_KS_READ_STREAM to read data from this Pin.

Kernel Streaming architecture

For vulnerability research, we must first understand its architecture and consider the potential attack surfaces.

After gaining a preliminary understanding of the functionalities and operations of Kernel Streaming, we need to understand the architecture to find vulnerabilities. It’s crucial to know how Windows implements these functions and what components are involved. This way, we can identify which sys files to analyze and where to start.

After our analysis, the overall architecture looks approximately like this diagram:

In the Kernel Streaming components, the most important ones are ksthunk.sys and ks.sys. Almost all functionalities are related to them.

ksthunk (Kernel Streaming WOW Thunk Service Driver)

ksthunk.sys is the entry point in Kernel Streaming. Its function is quite simple: it converts 32-bit requests from the WoW64 process into 64-bit requests, allowing the underlying driver to handle the requests without additional processing for 32-bit structures.

ks (Kernel Connection and Streaming Architecture Library)

ks.sys is one of the core components of Kernel Streaming. It is the library of Kernel Streaming responsible for forwarding requests such as IOCTL_KS_PROPERTY to the corresponding device driver, and it also handles functions related to AVStream.

The work flow of IOCTL_KS_*

IOCTL_KS_PROPERTY will be used as an example here. When calling DeviceIoControl, as shown in the figure below, the user’s request will be sequentially passed to the corresponding driver for processing.

At step 6, ks.sys will determine which driver and handler to hand over your request based on the KSPROPERTY you requested.

Finally, forward it to the corresponding driver, as shown in the figure above, where it is ultimately forwarded to the handler in portcls to operate the audio device.

You should now have a preliminary understanding of the architecture and process of Kernel Streaming. Next, it’s time to look for vulnerabilities.

Based on the existing elements, which attack surfaces are worth examining?

From attacker’s view

 Before digging for vulnerabilities, if you can carefully consider under what circumstances they are likely to occur, you can achieve twice the result with half the effort.

From a vulnerability researcher’s perspective, there are a few key points to consider:

1. Property handler in each device

KS object for each device has its own properties, and each property has its own handler. Some properties are prone to issues during handling.

2. ks and ksthunk

ks and ksthunk have not had vulnerabilities for a long time, but they are the most accessible entry points and might be good targets. The last vulnerabilities(CVE-2020-16889 and CVE-2020-17045) were found in 2020 by @nghiadt1098.

3. Each driver handles a part of the content

In some functionalities of Kernel Streaming, certain drivers handle parts of the input individually, which may lead to inconsistencies.

After reviewing Kernel Streaming from the above perspectives, we quickly identified several relatively easy-to-discover vulnerabilities.

  • portcls.sys
    • CVE-2024-38055 (out-of-bounds read when set dataformat for Pin)
    • CVE-2024-38056
  • ksthunk
    • CVE-2024-38054 (out-of-bounds write)
    • CVE-2024-38057

However, we will not be explaining these vulnerabilities one by one. Most of these are obvious issues such as unchecked length or index leading to out-of-bounds access. @Fr0st1706 also wrote an exploit for CVE-2024-38054 recently. We might slowly explain these in subsequent parts in the future. We will leave this for the readers to study for now.

During the review process, we discovered some interesting things. Do you think the following code snippet is really fine?

__int64 __fastcall CKSThunkDevice::CheckIrpForStackAdjustmentNative(__int64 a1, struct _IRP *irp, __int64 a3, int *a4)
{

    if ( irp->RequestorMode )
    {
        v14 = 0xC0000010;
    }
    else
    {
        UserBuffer = (unsigned int *)irp->UserBuffer;
        ...
        v14 = (*(__int64 (__fastcall **)(_QWORD, _QWORD, __int64 *))    (Type3InputBuffer + 0x38))(// call Type3InputBuffer+0x38
                *UserBuffer,
                0LL,
               v19);
    }
}

Seeing this code reminds me of CVE-2024-21338. Initially, the vulnerability had no checks, but after patching, ExGetPreviousMode was added. However, the check here uses the RequestorMode in IRP for validation. Generally, the RequestorMode from a user-called IOCTL will be UserMode(1), so there shouldn’t be any issues.

At this point, I also recall James Forshaw’s article Windows Kernel Logic Bug Class: Access Mode Mismatch in IO Manager.

The overlooked bug class

First, we need to mention a few terms and concepts. If you are already familiar with Previous Mode and RequestorMode, you can skip to A logical bug class section.

PreviousMode

The first one is PreviousMode. In an Application, if a user operates on a device or file through Nt* System Service Call, upon entering the kernel, it will be marked as UserMode(1) in _ETHREAD’s PreviousMode, indicating that this System Service Call is from the user. Conversely, if it is called from kernel mode, such as a device driver invoking the Zw* System Service Call, it will be marked as KernelMode(2).

RequestorMode

Another similar field is the RequestorMode in the IRP. This field records whether your original request came from UserMode or KernelMode. In kernel driver code, this is a very commonly used field, typically derived from PreviousMode.

It is often used to decide whether to perform additional checks on user requests, such as Memory Access Check or Security Access Check. In the example below, if the request comes from UserMode, it will check the user-provided address. If it comes from the Kernel, no additional checks are performed to increase efficiency.

But in reality, this has also led to some issues.

A logical bug class

James Forshaw’s Windows Kernel Logic Bug Class: Access Mode Mismatch in IO Manager mentions a type of Bug Class.

What would happen if a user calls a System Service Call like NtDeviceIoControlFile, and then the driver handling it uses user-controllable data as parameters for ZwOpenFile?

After the driver calls ZwOpenFile, PreviousMode will switch to KernelMode, and when it uses NtOpenFile processing, most checks will be skipped due to PreviousMode being KernelMode. Subsequently, Irp->RequestorMode will become KernelMode, bypassing the Security Access Check and Memory Access Check. However, this largely depends on how the subsequent driver implements these checks. There might be issues if it relies solely on RequestorMode to decide whether to perform checks The actual situation is slightly more complex and related to the flags of CreateFile. For details, refer to the following articles:

These studies mainly focused on the Zw* series of System Service Call. Are there other similar situations that could also cause this kind of logical vulnerability?

The new bug pattern

Actually, it is possible. When the device driver uses IoBuildDeviceIoControlRequest to create a DeviceIoControl IRP, it is easy to encounter such issues if not careful. This API is primarily used by kernel drivers to call IOCTL, and it helps you build the IRP. Subsequently, calling IofCallDriver allows you to call IOCTL within the kernel driver. On Microsoft Learn, there is a particular passage worth noting.

By default, if you do not explicitly set the RequestorMode, it will directly call IOCTL with KernelMode.

Following this approach, we revisited Kernel Streaming and discovered an intriguing aspect.

The function where IoBuildDeviceIoControlRequest is used in Kernel Streaming is in ks!KsSynchronousIoControlDevice and it obviously involves calling IOCTL in the kernel using the aforementioned method. However, it appears that Irp->RequestorMode is properly set here, and different values are assigned based on the parameters of KsSynchronousIoControlDevice. This will be a convenient library for kernel streaming driver developers.

However…

ks!CKsPin::GetState

ks!SerializePropertySet

ks!UnserializePropertySet

We found that in Kernel Streaming, all functions using KsSynchronousIoControlDevice consistently use KernelMode(0). At this point, we can carefully inspect whether the places it is used have any security issues. Therefore, we convert the bug pattern in Kernel Streaming into the following points:

  1. Utilized KsSynchronousIoControlDevice
  2. Controllable InputBuffer & OutputBuffer
  3. The second processing of IOCTL relies on RequestorMode for security checks

Following this pattern, we quickly found the first vulnerability.

The vulnerability & exploitation

CVE-2024-35250

This vulnerability is also the one we used in Pwn2Own Vancouver 2024. In the IOCTL_KS_PROPERTY of Kernel Streaming, to increase efficiency, KSPROPERTY_TYPE_SERIALIZESET and KSPROPERTY_TYPE_UNSERIALIZESET requests are provided to allow users to operate on multiple properties through a single call. These types of requests will be broken down into multiple calls by the KsPropertyHandler. For more details, refer to this one.

It is implemented in ks.sys

When handling property in ks.sys, if the KSPROPERTY_TYPE_UNSERIALIZESET flag is provided, ks!UnserializePropertySet will handle your request.

Let’s take a look at UnserializePropertySet.

unsigned__int64__fastcallUnserializePropertySet(PIRPirp,KSIDENTIFIER*UserProvideProperty,KSPROPERTY_SET*propertyset_){...New_KsProperty_req=ExAllocatePoolWithTag(NonPagedPoolNx,InSize,0x7070534Bu);...memmove(New_KsProperty_req,CurrentStackLocation->Parameters.DeviceIoControl.Type3InputBuffer,InSize);//------[1] ...status=KsSynchronousIoControlDevice(CurrentStackLocation->FileObject,0,CurrentStackLocation->Parameters.DeviceIoControl.IoControlCode,New_KsProperty_req,InSize,OutBuffer,OutSize,&BytesReturned);//-----------[2]...}

You can see that during the processing, the original request is first copied into a newly allocated buffer at [1]. Subsequently, this buffer is used to call the new IOCTL using KsSynchronousIoControlDevice at [2]. Both New_KsProperty_req and OutBuffer are contents provided by the user.

The flow when calling UnserializePropertySet is roughly as illustrated below:

When calling IOCTL, as shown in step 2 of the diagram, the I/O Manager will set Irp->RequestorMode to UserMode(1). Until step 6, it will check if the requested property by the user exists in the KS object. If the property exists in the KS object and is set with KSPROPERTY_TYPE_UNSERIALIZESET, UnserializePropertySet will be used to handle the specified property.

Next, in step 7, KsSynchronousIoControlDevice will be used to perform the IOCTL again. At this point, the new Irp->RequestorMode will become KernelMode(0), and the subsequent processing will be the same as a typical IOCTL_KS_PROPERTY. This part will not be elaborated further.

As a result, we now have a primitive that allows us to perform arbitrary IOCTL_KS_PROPERTY operations. Next, we need to look for places where it might be possible to achieve EoP (Elevation of Privilege).

The EoP

The first thing you will likely notice is the entry point ksthunk.sys.

Let’s take a look at ksthunk!CKSThunkDevice::DispatchIoctl.

__int64__fastcallCKSThunkDevice::DispatchIoctl(CKernelFilterDevice*a1,IRP*irp,unsignedinta3,NTSTATUS*a4){...if(IoIs32bitProcess(irp)&&irp->RequestorMode)//------[3]{//Convert 32-bit requests to 64-bit requests}elseif(CurrentStackLocation->Parameters.DeviceIoControl.IoControlCode==IOCTL_KS_PROPERTY){returnCKSThunkDevice::CheckIrpForStackAdjustmentNative((__int64)a1,irp,v11,a4)//-----[4];}}

You can see that ksthunk will first determine whether the request is from a WoW64 Process. If it is, it will convert the original 32-bit Requests into 64-bit at [3]. If the original request is already 64-bit, it will call CKSThunkDevice::CheckIrpForStackAdjustmentNative at [4] to pass it down.

__int64__fastcallCKSThunkDevice::CheckIrpForStackAdjustmentNative(__int64a1,struct_IRP*irp,__int64a3,int*a4){...if(*(_OWORD*)&Type3InputBuffer->Set==*(_OWORD*)&KSPROPSETID_DrmAudioStream&&!type3inputbuf.Id&&(type3inputbuf.Flags&2)!=0)//-----[5]   {if(irp->RequestorMode)//-------[6]{v14=0xC0000010;}else{UserBuffer=(unsignedint*)irp->UserBuffer;...v14=(*(__int64(__fastcall**)(_QWORD,_QWORD,__int64*))(Type3InputBuffer+0x38))(// call Type3InputBuffer+0x38*UserBuffer,0LL,v19);//------------[7]}}}

We can notice that if we give the property set as KSPROPSETID_DrmAudioStream, there is additional processing at [5]. At [6], we can see that it first checks whether Irp->RequestorMode is KernelMode(0). If it is called from user, it will directly return an error.

It should be pretty obvious here that if we call IOCTL with KSPROPERTY_TYPE_UNSERIALIZESET and specify the KSPROPSETID_DrmAudioStream property, it will be KernelMode(0) here. Additionally, it will directly use the input provided by the user as a function call at [7]. Even the first parameter is controllable.

After writing the PoC, we confirmed our results.

Some people might wonder, under what device or situation would have KSPROPSETID_DrmAudioStream? Actually, most audio devices will have it, primarily used for setting DRM-related content.

Exploitation

Once arbitrary calls are achieved, accomplishing EoP is not too difficult. Although protections such as kCFG, kASLR, and SMEP will be encountered, the only protection that needs to be dealt with under Medium IL is kCFG.

  • kCFG
  • kASLR
    • NtQuerySystemInformation
  • SMEP
    • Reuse Kernel Code

Bypass kCFG

Our goal here is straightforward: to create an arbitrary write primitive from a legitimate function, which can then be used to achieve EoP through typical methods like replacing the current process token with system token or abusing the token privilege.

It is intuitive to look directly for legitimate functions with names containing set, as they are more likely to do arbitrary writing. We directly take the export functions from ntoskrnl.exe to see if there are any good gadgets, as these functions are generally legitimate.

We quickly found RtlSetAllBits.

It is a very useful gadget and a legitimate function in kCFG. Additionally, it only requires controlling the first parameter _RTL_BITMAP.

struct_RTL_BITMAP{ULONGSizeOfBitMap;ULONG*Buffer;};

We can assign the buffer to any address and specify the size, allowing us to set up a range of bits entirely. At this point, we are almost done. As long as Token->Privilege is fully set up, we can use the Abuse Privilege method to achieve EoP.

However, before the Pwn2Own event, we created a new Windows 11 23H2 VM on Hyper-V and ran the exploit. The result was a failure. It failed at the open device stage.

After investigation, we discovered that Hyper-V does not have an audio device by default, causing the exploit to fail.

In Hyper-V, only MSKSSRV is present by default. However, MSKSSRV does not have the KSPROPSETID_DrmAudioStream property, which prevents us from successfully exploiting this EoP vulnerability. Therefore, we must find other ways to trigger it or discover new vulnerabilities. We decided to review the entire process again to see if there were any other exploitable vulnerabilities.

CVE-2024-30084

After re-examining, it was found that IOCTL_KS_PROPERTY uses Neither I/O to transmit data, which means it uses the input buffer for data processing directly. Generally speaking, this method is not recommended as it often leads to double fetch issues.

From the above figure of KspPropertyHandler, we can see that after the user calls IOCTL, the Type3InputBuffer is directly copied into a newly allocated buffer, containing the KSPROPERTY structure. This GUID in the structure is then used to check if the property set exists in the device. If it does, it will proceed to call UnserializePropertySet.

Let’s take another look at UnserializePropertySet.

It once again copies the user-provided data from Type3InputBuffer as the input for the new IOCTL. Clearly, there exists a double fetch vulnerability here, so we have modified the entire process, as shown in the following diagram.

When we initially send IOCTL_KS_PROPERTY, we use the existing property KSPROPSETID_Service of MSKSSRV for subsequent operations. As shown in step 6 of the diagram, a copy of KSPROPERTY is first made into the SystemBuffer, and then this property is used to check whether it is in the support list of the KS object. Since MSKSSRV supports it, it will then call UnserializePropertySet.

After calling UnserializePropertySet, due to the double fetch vulnerability, we can change KSPROPSETID_Service with KSPROPSETID_DrmAudioStream between the check and use phases. Subsequently, KSPROPSETID_DrmAudioStream will be used as the request to send IOCTL, thereby triggering the CVE-2024-35250 mentioned above logic flaw. This makes the vulnerability exploitable in any environment.

Finally, we successfully compromised Microsoft Windows 11 during Pwn2Own Vancouver 2024.

After the Pwn2Own event, our investigation revealed that this vulnerability has existed since Windows 7 for nearly 20 years. Moreover, it is highly reliable and has a 100% success rate in exploitation. We strongly recommend that everyone update to the latest version as soon as possible.

To be continued

This article focuses on how we identified the vulnerabilities used in this year’s Pwn2Own and the attack surface analysis of Kernel Streaming. After discovering this vulnerability, we continued our research on this attack surface and found another exploitable vulnerability along with other interesting findings.

Stay tuned for Part II, expected to be published in October of this year.

Reference


Streaming vulnerabilities from Windows Kernel - Proxying to Kernel - Part I

$
0
0

English Version, 中文版本

在過去的幾十年中 Windows Kernel 的漏洞層出不窮,熱門的攻擊面逐漸從 Win32k 慢慢轉移到 CLFS (Common Log File System) 上。微軟也持續且積極地修補這些漏洞,使得這些元件越來越安全。而下一個熱門的目標會是哪個元件呢?去年開始,MSKSSRV (Microsoft Kernel Streaming Service) 成為駭客喜愛的目標之一。這個驅動程式小到可以在幾天內完成分析。這是否意味著可能不太會有新的漏洞了?

在這篇研究將講述一個長期被忽視的攻擊面,讓我們在兩個月內就找出了超過 10 個漏洞。此外,也將深入探討了一種 Proxy-Based 的邏輯漏洞類型,使我們可以忽略掉大多數的檢查,最終成功在 Pwn2Own Vancouver 2024 中,攻下 Windows 11 的項目。

這份研究將分成數個部分來撰寫,分別講述不同的漏洞類型及漏洞型態,亦發表於 HITCON CMT 2024中。

Start from MSKSSRV

對於一項漏洞研究來說,從歷史的漏洞看起,是不可或缺的。

起初,我們為了挑戰 Pwn2Own Vancouver 2024 中 Windows 11 的項目,開始從過去的 Pwn2Own 以及近期 in-the-wild 的漏洞中開始審視,尋找可能的攻擊面。沿著歷史軌跡可以得知,過去主要負責 GDI 相關操作的 Win32K 一直是個很熱門的目標,從 2018 年以來,CLFS (Common Log File System) 也漸漸成為了熱門目標之一。這兩個元件都非常複雜,並且直到現在仍然有不少新漏洞出現,但要熟悉這兩個元件需要花不少時間,同時也有許多研究員在看這兩個元件,所以最終我們沒有先選擇分析他們。

去年 Synacktiv在 Pwn2Own 2023 中,使用 MSKSSRV 的漏洞成功攻下 Windows 11 後,便有不少人往這個元件開始看起,短時間內就又出現了第二個漏洞 CVE-2023-36802,這時 chompie也發表了一篇非常詳細的文章,講述這個漏洞成因及其利用細節。由於這個元件非常的小,只看檔案大小約略只有 72 KB,可能認真看個幾天就可以全部看完,因此我們便挑了 MSKSSRV 來做歷史漏洞分析,看看是否有機會抓出其他漏洞。

接下來我們會提一下這兩個漏洞,但不會著墨過多。

CVE-2023-29360 - logical vulnerability

第一個是 Synacktiv 在 Pwn2Own 2023 中所使用的漏洞 :

這是一個邏輯上的漏洞。當 MSKSSRV 使用 MmProbeAndLockPages鎖定使用者給的記憶體位置作為 FrameBuffer 時,並沒有設置正確的 AccessMode,導致沒有檢查使用者指定的位置是否屬於 User space。如果使用者給的是 Kernel space 中的位置,它就會把指定的 Kernel 位置映射到 User space 給使用者用,最終導致使用者可以對 Kernel 中的任意位置寫入,利用上簡單且非常穩定,成為了受歡迎的漏洞之一

更多細節可以參考 Synacktiv 在 HITB 2023 HKT 的演講Nicolas Zilio(@Big5_sec)部落格文章

CVE-2023-36802 - type confusion

這個漏洞則是在 CVE-2023-29360 出來後沒多就被許多人發現,並且在微軟發佈更新時,就已經偵測到利用,是個非常容易被發現的漏洞。MSKSSRV 會先將內部使用的物件(FSContextReg、FSStreamReg)存放在 FILE_OBJECT的 FsContext2 中,然而後續使用時並沒有對 FsContext2 的型態做檢查,導致 type confusion,詳細內容可參考 IBM X-Force 的部落格

至此之後,就很少有關於 MSKSSRV 的相關漏洞了。

But is that the end of it ?

然而是否這樣就沒洞了呢?

而我要更準確地回答,No!

實際上整個 Kernel Streaming 就像下面這張圖這樣 :

MSKSSRV 只是冰山一角而已,實際上還有不少潛在的元件,上圖中所寫的都是屬於 Kernel Streaming 的一部分。實際往這方向挖掘之後,最終也在這個攻擊面上取得不少漏洞,就如同流水般的流出漏洞來。

順帶一提,我在寫這篇部落格時,chompie 也發表了有關於他在今年 Pwn2Own Vancouver 2024 中所使用的漏洞 CVE-2024-30089。這個漏洞也在 MSKSSRV 中,該漏洞發生在 Reference Count 的處理,其成因也很有趣,不過這邊就不多談,詳細內容可參考她發表的文章

Brief overview of Kernel Streaming

那麼,什麼是 Kernel Streaming 呢? 事實上,我們正常使用電腦情況下就會用到 :

在 Windows 系統上,當我們打開鏡頭、開啟音效以及麥克風等音訊設備時,系統需要從這些設備讀取你的聲音、影像等相關資料到 RAM 中。為了更高效地完成這些資料的傳輸,微軟提供了一個名為 Kernel Streaming的框架,用來處理這些資料。這個框架主要在 Kernel mode 下運行,具有低延遲、良好的擴充性和統一介面等特性,使你能更方便、更高效地處理串流(Stream)資料。

Kernel Streaming 中,提供了三種多媒體驅動模型:port class、AVStream 和 stream class。這裡將主要介紹 port class 和 AVStream,而 stream class 因為較為罕見且過時,不會多加討論。

Port Class

大多數用於 PCI 和 DMA 型音效裝置的硬體驅動程式,它處理與音訊相關的數據傳輸,例如音量控制、麥克風輸入等等,主要會使用到的元件函式庫會是 portcls.sys。

AVStream

AVStream 則是由微軟提供的多媒體類驅動程式,主要支援僅限影片的串流和整合音訊/影片串流,目前跟影像有關的處理多數都跟這類別有關,例如你的視訊鏡頭、擷取卡等等。

實際上 Kernel Streaming 的使用很複雜,因此這裡只會簡單的敘述一下,更多詳細內容可以參考微軟官方文件

Interact with device

在我們想要與音訊設備或是視訊鏡頭等設備互動時該怎麼做呢?其實就跟一般設備互動一樣,可以透過 CreateFile 函數來開啟一個設備。那麼這類型的設備,名稱又會是甚麼呢?其實這邊不太會像是 \Devcie\NamedPipe這類型的名稱,而是會像下面這樣的路徑 :

\\?\hdaudio#subfunc_01&ven_8086&dev_2812&nid_0001&subsys_00000000&rev_1000#6&2f1f346a&0&0002&0000001d#{6994ad04-93ef-11d0-a3cc-00a0c9223196}\ehdmiouttopo

Enumerate device

每台電腦都可能不一樣,必須使用 SetupDiGetClassDevs等 API 去列舉設備,一般來說 KS 系列的設備都會註冊在 KSCATEGORY*底下,像是音訊設備就會註冊在 KSCATEGORY_AUDIO中。

你也可以使用 KS 所提供的 KsOpenDefaultDevice獲得該類別中第一個符合的 PnP 裝置的 Handle,實際上來說也只是 SetupDiGetClassDevs 和 CreateFile 的封裝而已。

hr=KsOpenDefaultDevice(KSCATEGORY_VIDEO_CAMERA,GENERIC_READ|GENERIC_WRITE,&g_hDevice)

Kernel Streaming object

我們在開啟這些設備之後,Kernel Streaming 會在 Kernel 中建立一些相關的 Instance,其中最為重要的就是 KS FiltersKS Pins。在 Kernel Streaming 的使用過程中,這些 Instance 會被頻繁使用,它們主要用來封裝設備的硬體功能,方便開發者透過統一的介面進行串流的處理。

這邊先以 Audio Filters作為例子,其他多數大同小異,我們也只會簡單介紹,其他細節請自行參考微軟官方文件。

KS filters

每個 KS Filter 通常代表一個設備或設備的特定功能,在我們打開一個音訊設備後,大部分情況下會對應到一個 Kernel Filter,當我們從音訊設備讀取資料時,這些資料就會先通過這個 KS Filter 進行處理。

概念上如下圖所示,中間的大框表示一個代表音訊設備的 KS filter。而我們想要從音訊設備中讀取資料時,會從左邊讀入 Filter,經過幾個節點進行處理後,從右邊輸出。

(From: https://learn.microsoft.com/en-us/windows-hardware/drivers/audio/audio-filters)

KS pins

上圖中,讀取及輸出資料的點稱為 Pin,Kernel 也有相對應的 KS Pin Object,用於描述這些 Pin 的行為,例如 Pin 是輸入端還是輸出端、支援的格式有哪些等。我們使用時必須在 Filters 上,開啟一個 Pin來建立 Instance,才能從設備讀取或輸出資料。

KS Property

這些 KS Object 都會有自己的 Property,每個 Property 都會有相對應的功能,前面所提到的 Pin 中的資料格式、音量大小及設備的狀態等等,這些都是一個 Property,通常會對應到一組 GUID,我們可以透過 IOCTL_KS_PROPERTY來讀取或設定這些 Property。

這大大簡化了多媒體驅動程式的開發,並且確保了不同設備之間的一致性和可擴展性。

Read streams from webcam

這邊就用個簡單的範例來介紹一下 Application 如何從視訊鏡頭讀取資料

其最簡單的流程大概如這張圖所示 :

  1. 開啟設備後獲得設備 Handle
  2. 使用這個 Handle 在這個 Filter 上建立 Pin 的 Instance 並獲得 Pin handle
  3. 使用 IOCTL_KS_PROPERTY 設置 Pin 的狀態到 RUN
  4. 最後就可以使用 IOCTL_KS_READ_STREAM從這個 Pin 中讀資料進來

Kernel Streaming architecture

對漏洞研究而言,我們必須先了解其架構,思考有哪些可能的攻擊面

在初步了解 Kernel Streaming 有哪些功能和操作後,為了找尋漏洞必須先了解一下架構,了解 Windows 是怎麼實作這些功能、分別有哪些元件等等,才知道應該要分析哪些 sys,從哪邊下手會比較好。

經過我們分析後,整個架構約略會像這張圖所示 :

在 Kernel Stearming 元件中,最為核心的就是 ksthunk.sys 及 ks.sys,幾乎所有功能都會與它們有關。

ksthunk (Kernel Streaming WOW Thunk Service Driver)

Application 呼叫 DeviceIoControl 後,在 Kernel Streaming 中的入口點,但它功能很簡單,負責將 WoW64 process 中 32-bit 的 requests 轉換成 64-bit 的 requests,使得下層的 driver 就可以不必為 32 位元的結構另外處理。

ks (Kernel Connection and Streaming Architecture Library)

Kernel Streaming 的核心元件之一,它是 Kernel Streaming 的函示庫,負責及轉發 IOCTL_KS_PROPERTY 等 requests 到對應設備的 driver 中,同時也會負責處理 AVStream 的相關功能。

The work flow of IOCTL_KS_*

而在呼叫 DeviceIoControl 時,就會像下圖一樣,將使用者的 requests 依序給相對應的 driver 來處理

而到第 6 步時 ks.sys 就會根據你 requests 的 Property來決定要交給哪個 driver 及 handler 來處理你的 request。

最終再轉發給相對應的 Driver,如上圖中最後轉發給 portcls 中的 handler 來操作音訊設備。

到這邊應該對 Kernel Streaming 的架構及流程有初步概念了,接下來就是找洞的時刻。依照現有的元素來看,哪些是值得一看的攻擊面呢?

From attacker’s view

在挖掘漏洞前,如果能仔細思考怎樣的情況下容易有洞,可以達到事半功倍的效果

從一個漏洞研究員的角度來說,大概會有下列這幾個點

  1. 每個設備中的 Property handler 每個設備中的 KS Object 都有各自的 Property,而且每個 Property 都有各自的實作,有些 Property 處理起來容易出問題。

  2. ks 及 ksthunk ks 及 ksthunk 已經有很長一段時間沒有漏洞,但卻是個最容易接觸到的入口點,也許是一個好目標,上一次出現的漏洞是在 2020 年 @nghiadt1098所找到的兩個漏洞 CVE-2020-16889CVE-2020-17045

  3. 每個 driver 都各自處理一部分的內容 在 Kernel Streaming 的部分功能中,有些 driver 會各自先處理部分的內容,可能會造成一些不一致性的問題。

我們針對上面幾個角度去對整個 Kernel Streaming 做 Code Review 後,很快的就發現了幾個比較容易發現的漏洞

  • portcls.sys
    • CVE-2024-38055 (out-of-bounds read when set dataformat for Pin)
    • CVE-2024-38056
  • ksthunk
    • CVE-2024-38054 (out-of-bounds write)
    • CVE-2024-38057

不過我們這一篇不會一一講解這些漏洞,這幾個多數都是沒有檢查長度或是 index 之類的越界存取等等明顯的洞,也許會在後續的部分慢慢來講解,@Fr0st1706也在前陣子寫出了 CVE-2024-38054 的利用,這邊就暫時留給讀者研究了。

這篇要提的是,我們在 Review 過程中發現了一些有趣的事情。

你覺得下面這段程式碼是否安全呢?

__int64 __fastcall CKSThunkDevice::CheckIrpForStackAdjustmentNative(__int64 a1, struct _IRP *irp, __int64 a3, int *a4)
{

    if ( irp->RequestorMode )
    {
        v14 = 0xC0000010;
    }
    else
    {
        UserBuffer = (unsigned int *)irp->UserBuffer;
        ...
        v14 = (*(__int64 (__fastcall **)(_QWORD, _QWORD, __int64 *))    (Type3InputBuffer + 0x38))(// call Type3InputBuffer+0x38
                *UserBuffer,
                0LL,
               v19);
    }
}

看到這段程式碼讓我想起了 CVE-2024-21338,該漏洞原先並沒有任何檢查,而在修補後則是新增了 ExGetPreviousMode,但這邊檢查則是使用了 IRP中的 RequestorMode 來做檢查,不過一般情況下從使用者呼叫的 IOCTL 的 RequestorMode 都會是 UserMode(1) 是不會有問題的。

此時我又想起來 James ForshawWindows Kernel Logic Bug Class: Access Mode Mismatch in IO Manager這篇文章。

The overlooked bug class

這邊我們必須先來提一下幾個名詞跟概念,不過如果你對 PreviousMode 及 RequestorMode 很熟悉,可以跳至 A logical bug class

PreviousMode

第一個是 PreviousMode,在 Application 中如果使用者透過 Nt* 等 System Service Call 來對設備或檔案中操作時,進入 Kernel 後就會在 _ETHREAD中的 PreviousMode 標註 UserMode(1) 表示這個 System Service Call 是來自 User mode 的 Application。如果你是從 Kernel mode 中,例如設備 driver 呼叫 Zw* System Service Call 的 API 就會標記成 KernelMode(0)。

RequestorMode

另外一個類似的則是 IRP 中的 RequestorMode 這邊就是記錄你原始的 requests 是來自 UserMode 還是 KernelMode,在 Kernel driver 中的程式碼是非常常用到的欄位,通常會來自 PreviousMode。

很常被用來決定是否要對來自使用者的 requests 做額外檢查,像是 Memory Access Check 或是 Security Access Check,例如下面這個例子中,如果 requests 來自 UserMode 就會檢查使用者提供的位置,如果是從 Kernel 來的,就不做額外檢查增加效率。

但實際上這也出現了一些問題…

A logical bug class

James ForshawWindows Kernel Logic Bug Class: Access Mode Mismatch in IO Manager中,就提到了一種 Bug Class

這邊可以先想想看,使用者呼叫 NtDeviceIoControlFile 之類的 System Service Call 之後,如果處理的 driver 又去用使用者可控的資料來作為 ZwOpenFile 的參數,會發生什麼事

在 driver 呼叫 ZwOpenFile 之後, PreviousMode 會轉換成為 KernelMode,並且在 NtOpenFile 處理時,就會因為 PreviousMode 是 KernelMode的關係少掉大部分的檢查,而後續的 Irp->RequestorMode也會因此變成 KernelMode,從而繞過 Security Access Check 及 Memory Access Check。不過這邊很看後續處理的 driver 怎麼去實作這些檢查,如果只依賴 RequestorMode 來決定要不要檢查,就可能會有問題。這邊省略了一些細節,實際上的狀況會稍微再複雜一點點,也會跟 CreateFile 的 flag 有關,細節可參考下列幾篇文章 :

這邊有這樣的概念就好,原先這些研究主要是在 Zw* 系列的 System Service Call 上面,大家可以思考一下,有沒有其他類似的情況,也可能造成這種邏輯漏洞呢?

The new bug pattern

事實上來說是有的,使用 IoBuildDeviceIoControlRequest這個方法去創建一個 DeviceIoControl 的 IRP 時,萬一沒注意到也很容易有這樣的問題。這個 API 主要是 Kernel driver 用來呼叫 IOCTL 的其中一種方法,它會幫你建好 IRP,而後續在去呼叫 IofCallDriver,就可以在 Kernel driver 中呼叫 IOCTL。在 Microsoft Learn中,有一段話特別值得注意 :

也就是預設情況下,如果你沒有特別去設置 RequestorMode 就會直接以 KernelMode 形式去呼叫 IOCTL。

按照這個思路,我們重新回頭審視一下我們的目標 Kernel Streaming,我們發現了一個吸引我們的地方。

在 Kernel Streaming 中使用這個 IoBuildDeviceIoControlRequest 地方是在 ks!KsSynchronousIoControlDevice,而主要內容明顯就是在用剛剛提到的方法,在 Kernel 中呼叫 DeviceIoControl,不過這邊看似有好好的設置 Irp->RequestorMode,且會根據 KsSynchronousIoControlDevice 參數不同而去設置不同的數值,對於開發者來說會是一個方便的函式庫。

然而…

ks!CKsPin::GetState

ks!SerializePropertySet

ks!UnserializePropertySet

我們發現到在 Kernel Streaming 中,全部有使用到 KsSynchronousIoControlDevice的地方都是固定的使用 KernelMode(0),到這邊就可以仔細的檢查看看,有用到的地方是否有安全上的問題了。因此我們將 Kernel Streaming 中的 bug pattern 轉換成下列幾點:

  1. 有使用 KsSynchronousIoControlDevice
  2. 有可控的
    • InputBuffer
    • OutputBuffer
  3. 第二次處理 IOCTL 的地方有依賴 RequestorMode 做安全檢查,並且有可以作為提權利用的地方。

按照這個 Pattern 我們很快地就找到了第一個洞

The vulnerability & exploitation

CVE-2024-35250

這個漏洞也是我們今年在 Pwn2Own Vancouver 2024 中所使用的漏洞。在 Kernel Streaming 的 IOCTL_KS_PROPERTY 功能中,為了讓效率增加,提供了 KSPROPERTY_TYPE_SERIALIZESETKSPROPERTY_TYPE_UNSERIALIZESET功能允許使用者透過單一呼叫與多個 Property 進行操作。當我們用這功能時,這些 requests 將被 KsPropertyHandler 函數分解成多個呼叫,詳情可參考這篇

該功能實作在 ks.sys 中

上圖中可以看到,在 ks 處理 Property 時,如果有給上述的 flag 就會由 UnserializePropertySet 來處理你的 request

我們這邊就先來看一下 UnserializePropertySet

unsigned__int64__fastcallUnserializePropertySet(PIRPirp,KSIDENTIFIER*UserProvideProperty,KSPROPERTY_SET*propertyset_){...New_KsProperty_req=ExAllocatePoolWithTag(NonPagedPoolNx,InSize,0x7070534Bu);...memmove(New_KsProperty_req,CurrentStackLocation->Parameters.DeviceIoControl.Type3InputBuffer,InSize);//------[1] ...status=KsSynchronousIoControlDevice(CurrentStackLocation->FileObject,0,CurrentStackLocation->Parameters.DeviceIoControl.IoControlCode,New_KsProperty_req,InSize,OutBuffer,OutSize,&BytesReturned);//-----------[2]...}

可看到在處理過程中會先將原始的 request,複製到新分配出來的 Buffer 中 [1],而後續就會使用這個 Buffer 來使用 KsSynchronousIoControlDevice 呼叫新的 IOCTL [2]。其中 New_KsProperty_reqOutBuffer都是使用者所傳入的內容。

而呼叫 UnserializePropertySet 時的流程,大概如下圖所示 :

這邊呼叫 IOCTL 時可以看到圖中第 2 步 I/O Manager 會將 Irp->RequestorMode設成 UserMode(1),直到第 6 步時,ks 會去判斷使用者 requests 的 Property 是否存在於該 KS Object 中,如果該 KS Object 的 Property 存在,並且有設置 KSPROPERTY_TYPE_UNSERIALIZESET就會用 UnserializePropertySet來處理指定的 Property

而接下來第 7 步就會呼叫 KsSynchronousIoControlDevice 重新做一次 IOCTL,而此時新的 Irp->RequestorMode就變成了 KernelMode(0) 了,而後續的處理就如一般的 IOCTL_KS_PROPERTY 相同,就不另外詳述了,總之我們到這裡已經有個可以任意做 IOCTL_KS_PROPERTY 的 primitive 了,接下來我們必須尋找看看是否有可以 EoP 的地方。

The EoP

最先看到的想必就是入口點 ksthunk,我們這邊可以直接來看 ksthunk!CKSThunkDevice::DispatchIoctl

__int64__fastcallCKSThunkDevice::DispatchIoctl(CKernelFilterDevice*a1,IRP*irp,unsignedinta3,NTSTATUS*a4){...if(IoIs32bitProcess(irp)&&irp->RequestorMode)//------[3]{//Convert 32-bit requests to 64-bit requests}elseif(CurrentStackLocation->Parameters.DeviceIoControl.IoControlCode==IOCTL_KS_PROPERTY){returnCKSThunkDevice::CheckIrpForStackAdjustmentNative((__int64)a1,irp,v11,a4)//-----[4];}}

ksthunk 會先判斷是否是 WoW64 的 Process 的 request,如果是就會將原本 32-bit 的 requests 轉換成 64-bit 的 [3],如果原本就是 64-bit 則會呼叫 CKSThunkDevice::CheckIrpForStackAdjustmentNative [4] 往下傳遞

__int64__fastcallCKSThunkDevice::CheckIrpForStackAdjustmentNative(__int64a1,struct_IRP*irp,__int64a3,int*a4){...if(*(_OWORD*)&Type3InputBuffer->Set==*(_OWORD*)&KSPROPSETID_DrmAudioStream&&!type3inputbuf.Id&&(type3inputbuf.Flags&2)!=0)//-----[5]   {if(irp->RequestorMode)//-------[6]{v14=0xC0000010;}else{UserBuffer=(unsignedint*)irp->UserBuffer;...v14=(*(__int64(__fastcall**)(_QWORD,_QWORD,__int64*))(Type3InputBuffer+0x38))(// call Type3InputBuffer+0x38*UserBuffer,0LL,v19);//------------[7]}}}

我們在 [5] 看到,如果我們給定的 Property Set 是 KSPROPSETID_DrmAudioStream,就有特別的處理。而在 [6] 時,會先去判斷 Irp->RequestorMode 是否為 KernelMode(0),如果從 UserMode(1) 呼叫的 IOCTL 就會直接返回錯誤,但如果我們使用前面所說的 KSPROPERTY_TYPE_UNSERIALIZESET來呼叫 IOCTL,並指定 KSPROPSETID_DrmAudioStream這個 Property,那麼這裡 [6] 就會是 KerenlMode(0)。接下來就會在 [7] 直接使用使用者所傳入的內容做為 function 呼叫,甚至第一個參數是可控的,實際寫 PoC 後,驗證了我們的結果。

這邊可能會有人有疑惑,什麼設備或是情況下會有 KSPROPSETID_DrmAudioStream?實際上來說音訊設備大多情況下都會有,主要是用來設置 DRM 相關內容用的。

Exploitation

在有了任意呼叫之後,要達成 EoP 就不是太大的問題,雖然會遇到 kCFG、kASLR、SMEP 等等保護,但在 Medium IL 下唯一比較需要處理的就只有 kCFG。

  • kCFG
  • kASLR
    • NtQuerySystemInformation
  • SMEP
    • Reuse Kernel Code

Bypass kCFG

那我們目標很簡單,就是從合法的 function 做出任意寫的 primitive,而之後就可以利用常見的方法用 System token 取代當前的 Process token或是 Abuse token privilege去做到 EoP。

直覺地會直接去找看看,kCFG 中合法的 function 名稱有 set 的 function,比較可能是可以寫入的。我們這裡是直接拿 ntoskrnl.exe 中 export fucntion 去尋找看看是否有合法的 function,這些大多情況下都是合法的。

而很快的我們就找到了 RtlSetAllBits

它是個非常好用的 gadget 而且是 kCFG 中合法的 function,另外也只要控制一個參數 _RTL_BITMAP

struct_RTL_BITMAP{ULONGSizeOfBitMap;ULONG*Buffer;};

我們可將 Buffer 指定到任意位置並指定大小,就可以將一段範圍的 bits 全部設置起來,到這邊就差不多結束了,只要 將 Token->Privilege全部設置起來,就可以利用 Abuse Privilege 方法來做到 EoP 了。

然而…在 Pwn2Own 比賽前,我們在 Hyper-V 上安裝一個全新 Windows 11 23H2 VM 測試 Exploit,結果失敗了。 而且是在開啟設備階段就失敗。

經過調查後發現到 Hyper-V 在預設情況下並不會有音訊設備,造成 exploit 會失敗。

在 Hyper-V 中,預設情況下只會有 MSKSSRV,然而 MSKSSRV 也沒有 KSPROPSETID_DrmAudioStream 這個 Property,使得我們無法成功利用這個漏洞達成 EoP,因此我們必須找其他方式觸發或者找新的漏洞,此時我們決定重新 Review 一遍整個流程,看看是否還有其他可能利用的地方。

CVE-2024-30084

重新審視後,發現到 IOCTL_KS_PROPERTY 是使用 Neither I/O來傳遞資料的,也就是說會直接拿使用者的 Input buffer 來做資料上的處理,一般來說不太建議使用這個方法,很常出現 Double Fetch 的問題。

我們可從上圖中 KspPropertyHandler 看到,在使用者呼叫 IOCTL 之後,會直接將 Type3InputBuffer 複製到新分配出來的 Buffer 中,其中會存有 KSPROPERTY結構,接下來會用這結構中的 GUID 來查詢 Property 是否有在該設備所支援的 Property 中,若存在才會繼續往下呼叫 UnserializePropertySet

這邊我們再回頭看一眼 UnserializePropertySet

我們可以發現到,它又再次從 Type3InputBuffer 複製使用者所提供的資料做為新的 IOCTL 的輸入,很明顯的這邊就存在了一個 Double Fetch 的漏洞,因此我們將整個利用流程改成下圖的樣子

我們一開始發送 IOCTL_KS_PROPERTY 時,就會先以 MSKSSRV 既有的 Property KSPROPSETID_Service來做後續操作,而在圖中第 6 步時,會先複製一份 Property 的 GUID 到 Kernel 中,而後再用這個 Property GUID 去查詢是否有在該 KS Object 的支援清單中,而這邊因為 MSKSSRV 有支援,就會往下呼叫 UnserializePropertySet

在呼叫 UnserializePropertySet 後,因為有 Double Fetch 的漏洞,讓我們可以在檢查後到使用之間,將 KSPROPSETID_Service換成 KSPROPSETID_DrmAudioStream,而接下來就可以讓 ks 使用 KSPROPSETID_DrmAudioStream作為 requests 來發送 IOCTL,從而觸發前述了 CVE-2024-35250 邏輯漏洞,使這個漏洞不論在甚麼環境下都可以使用。

最終我們成功在 Pwn2Own Vancouver 2024 中,成功攻下 Micorsoft Windows 11。

在 Pwn2Own 結束後,經過我們調查,發現到這個漏洞從 Windows 7 就存在了,至少存在將近 20 年,而且利用上非常穩定,有著百分之百的成功率,強烈建議大家盡速更新至最新版本 。

To be continued

這篇主要著重在我們如何找到今年在 Pwn2Own 中所使用的漏洞及 Kernel Streaming 的攻擊面分析。在找到這個洞之後,我們後續也持續朝這個方向繼續研究,也發現了另外一個也是 Exploitable 的漏洞以及其他更多有趣的漏洞,我們預計在今年十月發表,敬請期待 Part II。

Reference

DEVCORE 2024 全國資訊安全獎學金、資安教育活動贊助計畫即日起開放報名

$
0
0

繼輔仁大學及國立臺灣科技大學戴夫寇爾資訊安全獎學金頒布後,我們很高興地宣佈,2024 年度「全國資訊安全獎學金」及「資安教育活動贊助計畫」於即日起開放報名。

自 2012 年創立之初,DEVCORE 即秉持著提升台灣資安競爭力、讓世界更安全的初衷,將人才培育視為己任,透過參與教育部資安人才培育計畫、創辦 DEVCORE 實習生計畫、啟動戴夫寇爾資安獎學金、辦理資安教育活動贊助計畫等方式,協助資安人才茁壯成長。

DEVCORE 全國資訊安全獎學金

DEVCORE 於 2020 年首次舉辦「戴夫寇爾資安獎學金計畫」,原為感念過去學生時代受到的多方資源及鼓勵,獎學金頒發範圍為經營團隊母校的輔仁大學及國立臺灣科技大學,後為培育更多有志青年學子,擴大獎學金範圍,開放全國各地學生報名申請,期待能推廣「駭客思維」、強化資安技能,並幫助在學學生了解資安產業生態及現況、降低學用落差,未來成為新一代的攻擊型資安人才,為資安產業注入新活力。

「戴夫寇爾全國資訊安全獎學金」歡迎所有在資訊安全領域有出眾研究成果的學生報名申請,有意申請者須提出學習資安的動機與歷程,並繳交資安研究或比賽成果,我們將從中擇優選取 10 名,獲選者可獲最高 2 萬元的研究補助。詳細申請辦法如下:

  • 申請資格:全國各大專院校學生皆可以申請。
  • 獎學金金額/名額:每年度取 10 名,每名可獲得獎學金新台幣 20,000 元整,共計 20 萬元。如報名踴躍,我們將視申請狀況增加名額。
  • 申請時程:
    • 2024/9/6 官網公告獎學金計畫資訊
    • 2024/9/6 - 2024/10/3 開放收件
    • 2024/10/31 公布審查結果,並將於 11 至 12 月間頒發獎學金
  • 申請辦法:
    • 請依⽂件檢核表項次順序排列已附⽂件,彙整為⼀份 PDF 檔案,寄⾄ scholarship@devco.re
    • 信件主旨及 PDF 檔案名稱請符合以下格式:[全國獎學⾦申請] 學校名稱_學號_姓名(範例:[全國獎學⾦申請] 輔仁⼤學_B11100000_王⼩美)。
    • 請申請⼈⾃我檢核並於申請⼈檢核區勾選已附⽂件,若⽂件不⿑或未確實勾選恕不受理申請。
  • 須檢附文件:
    • 本獎學⾦申請表
    • 在學證明
    • 最近⼀學期成績單
    • 學習資訊安全之動機與歷程⼼得⼀篇:字數 500 - 2000 字
    • 資訊安全技術相關研究成果:至少須從以下六項目中擇一繳交,包含研討會投稿、漏洞獎勵計畫、弱點研究、資訊安全比賽、資安工具研究、技術文章發表等成果
    • 社群經營成果:至少須從以下兩項目中擇一繳交,包含校園資安社團、公開資安社群等
    • 推薦函:導師、系主任、其他教授或業界⼈⼠推薦函,⾄少須取得兩封以上推薦函

DEVCORE 資安教育活動贊助計劃

今年我們也將持續贊助資安教育活動,提供經費予資安相關之社群、社團辦理各項活動,凝聚台灣資安社群,加速培育台灣的資安新銳。

  • 申請資格:與資安議題相關之社群、社團活動,請由 1 位社團代表人填寫資料。
  • 贊助金額:依各社團活動需求及與 DEVCORE 團隊討論而定,每次最高補助金額為新台幣 20,000 元整。
  • 申請時程:如欲申請此計畫的社團或活動,請於 2024/10/31 前透過以下連結填寫初步資料,我們將於 30 日內通知符合申請資格者提供進一步資料,不符合資格者將不另行通知。
  • 申請連結:DEVCORE 2024 年資安教育活動贊助調查
  • 須提供資料:
    • 申請資格:申請人需以各資安社群或社團名義提出申請。
    • 聯絡電子郵件
    • 想要辦理的活動類型
    • 想要辦理的活動方式
    • 活動總預算
    • 預計需要贊助金額
    • 代表人姓名、連絡電話
    • 團體名稱
    • 團體單位網址
  • 注意事項:
    • 申請案審核將經過 DEVCORE 內部審核機制,並保有最終核決權。
    • 本問卷僅供初步意願蒐集用途,符合申請資格者,DEVCORE 將於 30 日內通知提供進一步資料供審核,其餘將不另行通知。
    • DEVCORE 保有修改、暫停或終止本贊助計畫之權利。

Streaming vulnerabilities from Windows Kernel - Proxying to Kernel - Part II

$
0
0

English Version, 中文版本

This is a series of research related to Kernel Streaming attack surface. It is recommended to read the following articles first.

In the previous research on Proxying to Kernel, we discovered multiple vulnerabilities in Kernel Stearming as well as an overlooked bug Class. We successfully exploited vulnerabilities CVE-2024-35250 and CVE-2024-30084 to compromise Windows 11 at Pwn2Own Vancouver 2024.

In this article, we will continue to explore this attack surface and bug Class, revealing another vulnerability and exploitation technique, which was also presented at HEXACON 2024.

After Pwn2Own Vancouver 2024, we continued to investigate the ks!KsSynchronousIoControlDevice bug pattern to see if there were any other security issues. However, after some time, we did not find any other exploitable points in the property operations of KS object. Therefore, we shifted our focus to another feature, KS Event.

KS Event

Similar to the KS Property mentioned in the previous article, the KS object not only has its own property set but also provides the functionality to set KS Event. For instance, you can set an event to trigger when the device status changes or at regular intervals, which is convenient for developers of playback software to define subsequent behaviors. Each KS Event, like a property, requires the KS object to support it to be used. We can register or disable these Events through IOCTL_KS_ENABLE_EVENT and IOCTL_KS_DISABLE_EVENT.

KSEVENTDATA

When registering a KS Event, you can register the desired event by providing KSEVENTDATA. You can include handles such as EVENT_HANDLE and SEMAPHORE_HANDLE in the registration. When KS triggers this event, it will notify you using the provided handle.

The work flow of IOCTL_KS_ENABLE_EVENT

The entire work flow is similar to IOCTL_KS_PROPERTY. When calling DeviceIoControl, as shown in the figure below, the user’s requests are sequentially passed to the corresponding driver for processing.

Similarly, in step 3, 32-bit requests will be converted into 64-bit requests. By step 6, ks.sys will determine which driver and addhandler to handle your request based on the event of your requests.

Finally, forward it to the corresponding driver. As shown in the figure above, it is finally forwarded to KsiDefaultClockAddMarkEvent in ks to set the timer.

After grasping the KS Event functionality and process, we swiftly identified another exploitable vulnerability, CVE-2024-30090, based on the previous bug pattern.

Proxying to kernel again !

This time, the issue occurs when ksthunk converts a 32-bit request into a 64-bit one.

As shown in the figure below, when ksthunk receives an IOCTL_KS_ENABLE_EVENT request and the request is from a WoW64 Process, it will perform the conversion from a 32-bit structure to a 64-bit structure.

The conversion would call ksthunk!CKSAutomationThunk::ThunkEnableEventIrp to handle it.

__int64 __fastcall CKSAutomationThunk::ThunkEnableEventIrp(__int64 ioctlcode_d, PIRP irp, __int64 a3, int *a4)
{
  ...
  if ( (v25->Parameters.DeviceIoControl.Type3InputBuffer->Flags & 0xEFFFFFFF) == KSEVENT_TYPE_ENABLE
    || (v25->Parameters.DeviceIoControl.Type3InputBuffer->Flags & 0xEFFFFFFF) == KSEVENT_TYPE_ONESHOT
    || (v25->Parameters.DeviceIoControl.Type3InputBuffer->Flags & 0xEFFFFFFF) == KSEVENT_TYPE_ENABLEBUFFERED )  
  {
    // Convert 32-bit requests and pass down directly
  }
  else if ( (v25->Parameters.DeviceIoControl.Type3InputBuffer->Flags & 0xEFFFFFFF) == KSEVENT_TYPE_QUERYBUFFER ) 
  {
    ...
    newinputbuf = (KSEVENT *)ExAllocatePoolWithTag((POOL_TYPE)0x600, (unsigned int)(inputbuflen + 8), 'bqSK');
    ...
    memcpy(newinputbuf,Type3InputBuffer,0x28);  //------------------------[1]
    ...
    v18 = KsSynchronousIoControlDevice( 
            v25->FileObject,
            0,
            IOCTL_KS_ENABLE_EVENT,
            newinputbuf,
            inputbuflen + 8,
            OutBuffer,
            outbuflen,
            &BytesReturned);  //-----------------[2]
    ...
  }
  ...
}

In CKSAutomationThunk::ThunkEnableEventIrp, a similar bug pattern is clearly visible. You can see that during the processing, the original request is first copied into a newly allocated buffer at [1]. Subsequently, this buffer is used to call the new IOCTL using KsSynchronousIoControlDevice at [2]. Both newinputbuf and OutBuffer are controlled by the user.

The flow when calling CKSAutomationThunk::ThunkEnableEventIrp is illustrated as follows:

When calling IOCTL in a WoW64 process, you can see in step 2 of the diagram that the I/O Manager sets Irp->RequestorMode to UserMode(1). In step 3, ksthunk converts the user’s request from 32-bit to 64-bit, handled by CKSAutomationThunk::ThunkEnableEventIrp.

Afterward, in step 5, KsSynchronousIoControlDevice will be called to issue the IOCTL, and at this point, the new Irp->RequestorMode has become KernelMode(0). The subsequent processing is the same as a typical IOCTL_KS_ENABLE_EVENT, so it won’t be detailed further. In summary, we now have a primitive that allows us to perform arbitrary IOCTL_KS_ENABLE_EVENT with KernelMode. Next, we need to look for places where we can achieve EoP.

The Exploitation

Following the previous approach, we first analyzed the entry point ksthunk. However, after searching for a while, we found no potential privilege escalation points. In ksthunk, most instances where Irp->RequestMode is KernelMode(0) are directly passed down without additional processing. Therefore, we shifted our eyes to the next layer, ks, to see if there are any opportunities for privilege escalation during the event handling process.

Quickly, we found a place that caught our attention.

In the KspEnableEvent handler, a code snippet first checks the NotificationType in the KSEVENTDATA you passed in to determine how to register and handle your event. In general, it usually provides an EVENT_HANDLE or a SEMAPHORE_HANDLE. However, in ks, if called from KernelMode, we can provide an Event Object or even a DPC Object to register your event, making the overall handling more efficient.

This means we can use this DeviceIoControl with KernelMode primitive to provide a kernel object for subsequent processing. If constructed well, it might achieve EoP, but it depends on how this Object is used later.

However, after trying for a while, we discovered that …

__int64 __fastcall CKSAutomationThunk::ThunkEnableEventIrp(__int64 ioctlcode_d, PIRP irp, __int64 a3, int *a4)
{
  ...
  if ( (v25->Parameters.DeviceIoControl.Type3InputBuffer->Flags & 0xEFFFFFFF) == KSEVENT_TYPE_ENABLE
    || (v25->Parameters.DeviceIoControl.Type3InputBuffer->Flags & 0xEFFFFFFF) == KSEVENT_TYPE_ONESHOT
    || (v25->Parameters.DeviceIoControl.Type3InputBuffer->Flags & 0xEFFFFFFF) == KSEVENT_TYPE_ENABLEBUFFERED )  //-------[3]
  {
    // Convert 32-bit requests and pass down directly
  }
  else if ( (v25->Parameters.DeviceIoControl.Type3InputBuffer->Flags & 0xEFFFFFFF) == KSEVENT_TYPE_QUERYBUFFER ) //-------[4]
  {
    ...
    newinputbuf = (KSEVENT *)ExAllocatePoolWithTag((POOL_TYPE)0x600, (unsigned int)(inputbuflen + 8), 'bqSK');
    ...
    memcpy(newinputbuf,Type3InputBuffer,0x28); //------[5] 
    ...
    v18 = KsSynchronousIoControlDevice( 
            v25->FileObject,
            0,
            IOCTL_KS_ENABLE_EVENT,
            newinputbuf,
            inputbuflen + 8,
            OutBuffer,
            outbuflen,
            &BytesReturned);  
    ...
  }
  ...
}

If you want to provide a kernel object to register an event, then the flag given in the IOCTL for KSEVENT must be KSEVENT_TYPE_ENABLE at [3]. However, at [4], where the vulnerability is triggered, it must be KSEVENT_TYPE_QUERYBUFFER, and it is impossible to directly provide a kernel object as we might have expected.

Fortunately, IOCTL_KS_ENABLE_EVENT also uses Neither I/O to transmit data. It also presents a Double Fetch issue again.

As shown in the figure above, we can set the flag to KSEVENT_TYPE_QUERYBUFFER before calling IOCTL. When checking, it will handle it with KSEVENT_TYPE_QUERYBUFFER. Before the second KsSynchronousIoControlDevice call, we can change the flag to KSEVENT_TYPE_ENABLE.

This way, we can successfully trigger the vulnerability and construct a specific kernel object to register the event.

Trigger the event

When would it use the kernel object that you constructed? When an event is triggered, ks will call ks!ksGenerateEvent through DPC. At this point, it will determine how to handle your event based on the NotificationType you specified.

Let’s take a look at KsGenerateEvent

NTSTATUS __stdcall KsGenerateEvent(PKSEVENT_ENTRY EventEntry)
{

  switch ( EventEntry->NotificationType )
  {
    case KSEVENTF_DPC:
      ...
      if ( !KeInsertQueueDpc(EventEntry->EventData->Dpc.Dpc, EventEntry->EventData, 0LL) )
        _InterlockedAdd(&EventEntry->EventData->EventObject.Increment, 0xFFFFFFFF); //--------[6]
      ...
    case KSEVENTF_KSWORKITEM:
      ...
      KsIncrementCountedWorker(eventdata->KsWorkItem.KsWorkerObject); //-----------[7]

  }
}

At this point, there are multiple ways to exploit this. The most straightforward method is to directly construct a DPC structure and queue a DPC to achieve arbitrary kernel code execution, which corresponds to the code snippet at [6]. However, the IRQL when calling KsGenerateEvent is DISPATCH_LEVEL, making it very difficult to construct a DPC object in User space, and the exploitation process will encounter many issues.

Therefore, we opt for an alternative route using KSEVENTF_KSWORKITEM at [7]. This method involves passing in a kernel address and manipulating it to be recognized as a pointer to KSWORKITEM.

It can achieve an arbitrary kernel address increment by one. The entire process is illustrated in the diagram below.

When calling IOCTL_KS_ENABLE_EVENT, after constructing KSEVENTDATA to point to a kernel memory address, ks will handle it as a kernel object and register the specified event.

When triggered, ks will increment the content at our provided memory address. Therefore, we have a kernel arbitrary increment primitive here.

Arbitrary increment primitive to EoP

From arbitrary increment primitive to EoP, there are many methods that can be exploited, among which the most well-known are abuse token privilege and IoRing. Initially, it seemed like this would be the end of it.

However, both of these methods have certain limitations in this situation:

Abuse token Privilege

If we use the method of abusing token privilege for EoP, the key lies of the technique in overwriting Privileges.Enable and Privileges.Present. Since our vulnerability can only be incremented by one at a time, both fields need to be written to obtain SeDebugPrivilege. The default values for these two fields are 0x602880000 and 0x800000, which need to be changed to 0x602980000 and 0x900000. This means each field needs to be written 0x10 times, totaling 0x20 writes. Each write requires a race condition, which takes times and significantly reduces stability.

IoRing

Using IoRing to achieve arbitrary writing might be a simpler method. To achieve arbitrary write, you just need to overwrite IoRing->RegBuffersCount and IoRing->RegBuffers. However, a problem arises.

When triggering the arbitrary increment, if the original value is 0, it will call KsQueueWorkItem, where some corresponding complex processing will occur, leading to BSoD. The exploitation method of IoRing happens to encounter this situation…

Is it really impossible to exploit it stably?

Let’s find a new way !

When traditional exploitation methods hit a roadblock, it might be worthwhile to dive deeper into the core mechanics of the technique. You may unexpectedly discover new approaches along the way.

After several days of contemplation, we decided to seek a new approach. However, starting from scratch might take considerable time and may not yield results. Therefore, we chose to derive new inspiration from two existing methods. First, let’s look at abusing token privilege. The key aspect here is exploiting a vulnerability to obtain SeDebugPrivilege, allowing us to open high-privilege processes such as winlogon.

The question arises: why does having SeDebugPrivilege allow you to open high-privilege processes?

We need to take a look at nt!PsOpenProcess first.

From this code snippet, we can see that when we open the process, the kernel will use SeSinglePrivilegeCheck to check if you have SeDebugPrivilege. If you have it, you will be granted PROCESS_ALL_ACCESS permission, allowing you to perform any action on any process except PPL. As the name implies, it is intended for debugging purposes. However, it is worth noting that nt!SeDebugPrivilege is a global variable in ntoskrnl.exe.

It’s a LUID structure that was initialized at system startup. The actual value is 0x14, indicating which bit in the Privileges.Enable and Privileges.Present fields represent SeDebugPrivilege. Therefore, when we use NtOpenProcess, the system reads the value in this global variable

Once the value of nt!SeDebugPrivilege is obtained, it will be used to inspect the Privileges field in the Token to see if the Enable and Present fields are set. For SeDebugPrivilege, it will check the 0x14 bit.

However, there is an interesting thing…

The global variable nt!SeDebugPrivilege is located in a writable section!

A new idea was born.

Make abusing token privilege great again !

By default, a normal user will have only a limited number of Privileges, as shown in this diagram.

We can notice that in most cases, SeChangeNotifyPrivilege is enabled. At this point, we can look at the initialization part and find that SeChangeNotifyPrivilege represents the value 0x17.

What would happen if we use the vulnerability to change nt!SeDebugPrivilege from 0x14 to 0x17?

As shown in the figure, in the NtOpenProcess flow, it will first get the value of nt!SeDebugPrivilege, and at this time the obtained value is 0x17 (SeChangeNotifyPrivilege)

The next check will verify the current process token using 0x17 to see if it has this Privilege. However, normal users generally have SeChangeNotifyPrivilege, so even if you don’t have SeDebugPrivilege, you will still pass the check and obtain PROCESS_ALL_ACCESS. In other words, anyone with SeChangeNotifyPrivilege can open a high-privilege process except PPL.

Furthermore, by using the vulnerability mentioned above, we can change nt!SeDebugPrivilege from 0x14 to 0x17. Since the original value is not 0, it will not be affected by KsQueueWorkItem, making it highly suitable for our purposes.

Once we can open a high-privilege process, the privilege escalation method is the same as the abuse token privilege approach so that we won’t elaborate on that here. Ultimately, we successfully achieved EoP on Windows 11 23H2 by again utilizing Proxying to kernel.

Remark

Actually, this technique also applies to other Privilege.

  • SeTcbPrivilege = 0x7
  • SeTakeOwnershipPrivilege = 0x9
  • SeLoadDriverPrivilege = 0xa

The Next & Summary

The focus of these two articles is primarily on how we analyze past vulnerabilities to discover new ones, how we gain new ideas from previous research, find new exploitation methods, new vulnerabilities, and new attack surfaces.

There may still be many security issues of this bug class, and they might not be limited to Kernel Streaming and IoBuildDeviceIoControlRequest. I believe this is a design flaw in Windows, and if we search carefully, we might find more vulnerabilities.

For this type of vulnerability, you need to pay attention to the timing of setting Irp->RequestorMode. If it is set to KernelMode and then user input is used, issues may arise. Moreover, this type of vulnerability is often very exploitable.

In Kernel Streaming, I believe there are quite a few potential security vulnerabilities. There are also many components like Hdaudio.sys or Usbvideo.sys that might be worth examining and are suitable places for fuzzing. If you are a kernel driver developer, it is best not to only check Irp->RequestorMode . There might still be issues within the Windows architecture. Finally, I strongly recommend everyone to update Windows to the latest version as soon as possible.

Is that the end of it ?

Apart from proxy-based vulnerabilities, we have also identified many other bug classes, allowing us to discover over 20 vulnerabilities in Kernel Streaming. Some of these vulnerabilities are quite unique, so stay tuned for Part III.

Reference

Streaming vulnerabilities from Windows Kernel - Proxying to Kernel - Part II

$
0
0

English Version, 中文版本

這是一系列有關 Kernel Streaming 的相關的漏洞研究,建議先閱讀以下文章

在先前 Proxying to Kernel 的研究中,我們在 Kernel Stearming 中找到了多個漏洞以及一個被忽視的 Bug Class,並在今年 Pwn2Own Vancouver 2024 中利用漏洞 CVE-2024-35250CVE-2024-30084成功攻下 Windows 11。

而在這篇研究中,我們將繼續延續這個攻擊面和這個 Bug Class,也將揭露另外一個漏洞和利用手法,亦發表於 HEXACON 2024中。


在 Pwn2Own Vancouver 2024 之後,我們繼續針對 ks!KsSynchronousIoControlDevice這個 bug pattern 去看看有沒有其他安全性上的問題,然而找了一段時間後,針對 KS Object 的 Property 操作中,並沒有找到其他可以利用的點,因而我們將方向轉往另外一個功能 KS Event上。

KS Event

KS Event 與前一篇提到的 KS Property 類似。KS Object 中除了有自己的 Property Set 之外,也有提供設定 KS Event 的功能,比如說你可以設定設備狀態改變或是每個一段時間就觸發 Event,方便播放軟體等開發者定義後續的行為,而每個 KS Event 就如同 Property 一樣,要使用就必須該 KS Object 有支援。我們可以透過 IOCTL_KS_ENABLE_EVENTIOCTL_KS_DISABLE_EVENT來註冊或關閉這些 Event。

KSEVENTDATA

而在註冊 KS Event 時,你可以藉由提供 KSEVENTDATA來註冊你想要的事件,其中可以提供 EVENT_HANDLE 及 SEMAPHORE_HANDLE 等 handle 來註冊,當 KS 觸發這個事件時,就會藉由這個 handle 來通知你。

The work flow of IOCTL_KS_ENABLE_EVENT

其整個運作流程也與 IOCTL_KS_PROPERTY 雷同,在呼叫 DeviceIoControl 時,就會像下圖一樣,將使用者的 requests 依序給相對應的 driver 來處理

同樣會在第 3 步時做 32-bit 的 requests 轉換成 64-bit 的 requests。到第 6 步時 ks.sys 就會根據你 requests 的 Event 來決定要交給哪個 driver 及 addhandler來處理你的 request。

最終再轉發給相對應的 Driver。如上圖中最後轉發給 ks 中的 KsiDefaultClockAddMarkEvent 來設置 Timer

在了解了 KS Event 功能及流程後,根據之前的 bug pattern很快地又找到了一個可以利用的漏洞 CVE-2024-30090

Proxying to kernel again !

這次的問題點發生在 ksthunk 將 32-bit request 轉換成 64-bit request 的過程。

如下圖,當 ksthunk 接收到來自 WoW64 Process 的 IOCTL_KS_ENABLE_EVENT 時,會進行 32-bit 結構到 64-bit 結構的轉換

轉換過程會呼叫 ksthunk!CKSAutomationThunk::ThunkEnableEventIrp來處理

__int64 __fastcall CKSAutomationThunk::ThunkEnableEventIrp(__int64 ioctlcode_d, PIRP irp, __int64 a3, int *a4)
{
  ...
  if ( (v25->Parameters.DeviceIoControl.Type3InputBuffer->Flags & 0xEFFFFFFF) == KSEVENT_TYPE_ENABLE
    || (v25->Parameters.DeviceIoControl.Type3InputBuffer->Flags & 0xEFFFFFFF) == KSEVENT_TYPE_ONESHOT
    || (v25->Parameters.DeviceIoControl.Type3InputBuffer->Flags & 0xEFFFFFFF) == KSEVENT_TYPE_ENABLEBUFFERED )  
  {
    // Convert 32-bit requests and pass down directly
  }
  else if ( (v25->Parameters.DeviceIoControl.Type3InputBuffer->Flags & 0xEFFFFFFF) == KSEVENT_TYPE_QUERYBUFFER ) 
  {
    ...
    newinputbuf = (KSEVENT *)ExAllocatePoolWithTag((POOL_TYPE)0x600, (unsigned int)(inputbuflen + 8), 'bqSK');
    ...
    memcpy(newinputbuf,Type3InputBuffer,0x28);  //------------------------[1]
    ...
    v18 = KsSynchronousIoControlDevice( 
            v25->FileObject,
            0,
            IOCTL_KS_ENABLE_EVENT,
            newinputbuf,
            inputbuflen + 8,
            OutBuffer,
            outbuflen,
            &BytesReturned);  //-----------------[2]
    ...
  }
  ...
}

而在 CKSAutomationThunk::ThunkEnableEventIrp中,明顯可以看到類似的 bug pattern,從 [1] 中可以看到,它會複製使用者的輸入到新分配出來的 Buffer 中,接著在 [2] 處,就會利用該 Buffer 來使用 KsSynchronousIoControlDevice 呼叫新的 IOCTL,。其中 newinputbuf 及 OutBuffer 都是使用者所傳入的內容。

呼叫 CKSAutomationThunk::ThunkEnableEventIrp時的流程,大概如下圖所示 :

在 WoW64 的程式中呼叫 IOCTL 時,可以看到圖中第 2 步 I/O Manager 會將 Irp->RequestorMode設成 UserMode(1),而在第 3 步時,ksthunk 會將使用者的 request 從 32-bit 轉換成 64-bit,這邊就會用 CKSAutomationThunk::ThunkEnableEventIrp來處理。

之後第 5 步,就會透過 KsSynchronousIoControlDevice重新呼叫 IOCTL ,而此時新的 Irp->RequestorMode就變成了 KernelMode(0) 了,而後續的處理就如一般的 IOCTL_KS_ENABLE_EVENT 相同,就不另外詳述了,總之我們到這裡已經有個可以任意做 IOCTL_KS_ENABLE_EVENT 的 primitive 了,接下來我們必須尋找看看是否有可以 EoP 的地方。

The Exploitation

跟先前思路一樣,一開始還是會先分析入口點 ksthunk,然而我們找尋了一陣子之後,並沒有看到可以做為提權的地方,而且在 ksthunk 中,大多數只要看到 Irp->RequestMode是 KernelMode(0) 就會直接往下傳遞而不另外做處理。因此我們將我們的目標轉向位在下一層的 ks,看看它在處理 event 的過程中,是否有可以用來提權的地方。

很快的就找到一個吸引我們目光的地方:

在 KspEnableEvent 的 Handler 中,有一處會先判斷你所傳入的 KSEVENTDATA 中的 NotificationType 來決定要怎麼註冊及處理你的事件,在一般情況下通常是給一個 EVENT_HANDLE或是 SEMAPHORE_HANDLE,然而在 ks 中,如果是從 KernelMode 呼叫的就給以提供 Event Object甚至 DPC來註冊你的事件,讓整體的處理上更有效率。

也就是說我們可以藉由這個 KernelMode 的 DeviceIoControl 的 primitive 來提供任意 Kernel Object,讓它做後續處理,構造的好就有機會達成 EoP 但要看後續怎麼使用這個 Object 就是了。

但是我們在嘗試了一段時間後發現到……

__int64 __fastcall CKSAutomationThunk::ThunkEnableEventIrp(__int64 ioctlcode_d, PIRP irp, __int64 a3, int *a4)
{
  ...
  if ( (v25->Parameters.DeviceIoControl.Type3InputBuffer->Flags & 0xEFFFFFFF) == KSEVENT_TYPE_ENABLE
    || (v25->Parameters.DeviceIoControl.Type3InputBuffer->Flags & 0xEFFFFFFF) == KSEVENT_TYPE_ONESHOT
    || (v25->Parameters.DeviceIoControl.Type3InputBuffer->Flags & 0xEFFFFFFF) == KSEVENT_TYPE_ENABLEBUFFERED )  //-------[3]
  {
    // Convert 32-bit requests and pass down directly
  }
  else if ( (v25->Parameters.DeviceIoControl.Type3InputBuffer->Flags & 0xEFFFFFFF) == KSEVENT_TYPE_QUERYBUFFER ) //-------[4]
  {
    ...
    newinputbuf = (KSEVENT *)ExAllocatePoolWithTag((POOL_TYPE)0x600, (unsigned int)(inputbuflen + 8), 'bqSK');
    ...
    memcpy(newinputbuf,Type3InputBuffer,0x28); //------[5] 
    ...
    v18 = KsSynchronousIoControlDevice( 
            v25->FileObject,
            0,
            IOCTL_KS_ENABLE_EVENT,
            newinputbuf,
            inputbuflen + 8,
            OutBuffer,
            outbuflen,
            &BytesReturned);  
    ...
  }
  ...
}

如果要提供任意 Kernel Object 去註冊事件,那麼 IOCTL 中所給定的 KSEVENT的 flag,必須要是 KSEVENT_TYPE_ENABLE,也就是上面程式碼 [3] 的部分,然而在程式碼片段 [4] 處是觸發漏洞的地方卻是要是 KSEVENT_TYPE_QUERYBUFFER,並沒辦法如我們所想的一樣直接給一個 Kernel Object。

然而幸運的是整個 IOCTL_KS_ENABLE_EVENT 也是使用 Neither I/O直接拿使用者的 Input buffer 來做資料上的處理,再次出現了 Double Fetch 的問題。

如上圖中所示,我們可以在呼叫 IOCTL 前把 flag 設置成 KSEVENT_TYPE_QUERYBUFFER,檢查時就會以 KSEVENT_TYPE_QUERYBUFFER的功能處理,而在第二次呼叫 IOCTL 前,就把 flag 換成 KSEVENT_TYPE_ENABLE,這樣就可以成功觸發漏洞並構造特定的 Kernel Object 來註冊事件了。

Trigger the event

至於甚麼時候會用到你所構造的 KS Object 呢? 當事件觸發時, ks 會透過 DPC 呼叫 ks!ksGenerateEvent,此時就會依照你所給定的 NotificationType 來決定要怎麼處理你的事件。

我們就來看一下 KsGenerateEvent

NTSTATUS __stdcall KsGenerateEvent(PKSEVENT_ENTRY EventEntry)
{

  switch ( EventEntry->NotificationType )
  {
    case KSEVENTF_DPC:
      ...
      if ( !KeInsertQueueDpc(EventEntry->EventData->Dpc.Dpc, EventEntry->EventData, 0LL) )
        _InterlockedAdd(&EventEntry->EventData->EventObject.Increment, 0xFFFFFFFF); //--------[6]
      ...
    case KSEVENTF_KSWORKITEM:
      ...
      KsIncrementCountedWorker(eventdata->KsWorkItem.KsWorkerObject); //-----------[7]

  }
}

其實到這邊就有多種利用方式可以利用,最直接的莫過於直接構造 DPC 結構註冊 DPC 來達成任意 Kernel 程式碼執行,也就是上面程式碼片段 [6] 的地方,但在呼叫 KsGenerateEvent 時的 IRQL 是 DISPATCH_LEVEL很難在 User space 下構造 DPC object 利用過程也會遇到許多問題

所以我們改用另外一條 KSEVENTF_KSWORKITEM,也就是程式碼片段 [7] 的部分,藉由傳入 Kernel 位置,讓他誤認為是 KSWORKITEM 的指標。

其中就會對該指標指向位置加上 0x5c 的地方加一,也就是可以達到任意 Kerenl Address 加一的寫入,其整個過程就如下圖:

在呼叫 IOCTL_KS_ENABLE_EVENT 時,構造 KSEVENTDATA 指向 Kernel 記憶體位置後,ks 處理時就會將它作為 Kernel Object 來操作,並註冊指定的事件

而到觸發時,ks 就會將我們給的記憶體位置內容 +1,因此我們這邊就有了一個 kernel 任意 +1 的 primitive 了。

Arbitrary increment primitive to EoP

從任意記憶體位置 +1 到提權有許多方法可以利用,其中最知名的莫過於 Abuse token privilege以及 IoRing,原本以為到這邊就差不多結束了…

然而上述兩種方法在這個情境中都有一定的侷限:

Abuse token Privilege

如果是以 Abuse token privilege 方法來做提權,其關鍵在於覆寫 Privileges.Enable 及 Privileges.Present,而我們漏洞一次只能 +1 ,如果要拿到 SeDebugPrivilege 就必須兩個欄位都要寫到,這兩格欄位的預設數值為 0x6028800000x800000必須要變成,0x602980000 及 0x900000,也就是說分別都要各寫 0x10 次,總共要 0x20 次的寫入,每次的寫入都要 race,需要花上不少時間,穩定度也大幅下降。

IoRing

透過 IoRing 來達到任意寫入,也許會是個更簡單的方法,只需覆寫 IoRing->RegBuffersCount and IoRing->RegBuffers就可達到任意寫入,然而有個問題就發生了…

在觸發任意記憶體位置 +1 這個 primitive 時,如果原先的數值是 0 時,就會進到 KsQueueWorkItem 中,其中會有一些相對應複雜的處理,就會導致 BSoD, IoRing 的利用方式剛好就會遇到這狀況…

是不是真的沒辦法穩定利用了呢?

Let’s find a new way !

當傳統的利用方法遇到瓶頸時,深入探討技術的核心機制可能會是值得的。你或許會在此過程中意外發現新的方法。

經過幾天沉思之後,我們決定找尋新方法,但從頭找新的方法可能會花不少時間也可能找不到,於是我們決定從舊有的兩個方法中找尋新的靈感,首先來看的是 Abuse token privilege,其中最關鍵的就是利用漏洞拿到 SeDebugPrivilege 使得我們可以 Open 像是 winlogon 等高權限的 Process。

問題就來了,為什麼只要有 SeDebugPrivilege 就可以開啟高權限的 Process 呢?

這邊就要來看一下 PsOpenProcess,以下是 PsOpenProcess 的程式碼片段:

由此可見,當我們在 Open Process 時, kernel 會優先使用 SeSinglePrivilegeCheck 檢查你是否有 SeDebugPrivilege,如果你具有 SeDebugPrivilege 那就會給你 PROCESS_ALL_ACCESS的權限,不會有其他 ACL 的檢查,讓你可以對任意 Process 去做任何事情,顧名思義就是讓你 Debug 用的,然而有一點值得注意的地方是 SeDebugPrivilege 是在 ntoskrnl.exe上的全域變數。

它會是個 LUID結構,會在系統啟動時初始化,實際數值為 0x14 ,表示在 Privileges.Enable 及 Privileges.Present 欄位中哪個 bit 是代表 SeDebugPrivilege。所以當我們在用 NtOpenProcess 時,系統去查看這個全域變數中的數值。

獲得要檢查的數值後,就會依照這個數值去檢查 Token 中的 Privileges 欄位是否有 Enable 及 Present 這個欄位,以 SeDebugPrivilege 來說就會檢查第 0x14 bit

然而有一件有趣的事情是…

nt!SeDebugPrivilege這個全域變數是位於可寫的區段中!

因此一個新的想法就誕生了。

Make abusing token privilege great again !

預設情況下,一般權限的使用者會像這張圖一樣,僅有少數的 Privileges

不過我們可以注意到的是,大部分情況下都會有 SeChangeNotifyPrivilege 且是 Enable 的。這時我們就可以來看看初始化的地方,就可發現 SeChangeNotifyPrivilege 所代表是數值為 0x17。

那如果我們利用漏洞把 SeDebugPrivilege 從 0x14 換成 0x17 會發生甚麼事情呢?

如上圖,在原先 OpenProcess 的流程中,依舊會先去看 nt!SeDebugPrivilege中的數值,而這時獲得的數值為 0x17(SeChangeNotifyPrivilege)

接下來的檢查就會以 0x17 對當前 Process token 做驗證,看看有沒有這個 Privilege,然而一般使用者都會有這個 Privilege,因此即使你沒有 SeDebugPrivilege 也會直接通過檢查,拿到 PROCESS_ALL_ACCESS也就是說任何擁有 SeChangeNotifyPrivilege 都可以 open 除了 PPL 之外的高權限的 Process

此外利用我們上述的漏洞來將 nt!SeDebugPrivilege從 0x14 改成 0x17,因為原本的數值不是 0 是不會受到 KsQueueWorkItem 影響的,因此非常適合我們。

在可以 open 高權限的 Process 後,提權方式就與一般的 Abuse token privilege 方法相同就不再這邊多提了,最終我們又在一次利用 Proxying to kernel 成功在 Windows 11 23H2 上達成 EoP。

Remark

實際上來說,這個方法也適用於其他高權限的 Privilege 中

  • SeTcbPrivilege = 0x7
  • SeTakeOwnershipPrivilege = 0x9
  • SeLoadDriverPrivilege = 0xa

The Next & Summary

這兩篇文章中,主要著重於我們怎麼從過往的漏洞分析到發現新漏洞的過程,如何從過去的研究之中獲得新的想法、新的利用方式,新的漏洞以及新的攻擊面。關於這種 Proxy 類型的 Bug class 可能還存在很多,也可能不只侷限於 Kernel Streaming 和 IoBuildDeviceIoControlRequest,我認為算是 Windows 設計上的一個小缺陷,如果認真找可能還會找到一些漏洞,這類型的漏洞你需要關注的地方就是 Irp->RequestorMode設置的時間點,如果設置 KernelMode 之後還有拿使用者的輸入做事情,就有機會出問題,而且這類型的漏洞往往都很好用。

在 Kernel Streaming 中,我認為應該不少潛在的安全性漏洞,他也還有很多元件像是 Hdaudio.sys或是 Usbvideo.sys可能也是個可以看的方向,也是個適合 fuzzing 的地方。如果你是個 Kernel driver 開發者最好不要只有檢查 Irp->Requestormode,Windows 架構下很有可能還是有問題。最後再次強烈建議大家盡速更新 Windows 到最新版本中。

Is that the end of it ?

實際上來說除了 Proxy 類型的漏洞之外,我們還有找到其他更多的 Bug class 使得我們在 Kernel Streaming 上找到超過 20 個漏洞,有些漏洞非常特別,敬請期待 Part III。

Reference

Viewing all 145 articles
Browse latest View live