본문 바로가기
Pwn + Research (EN)/Research (EN)

[Research] Finding Undocumented Properties: Mining KS Driver's Hidden Features (EN)

by Libera 2026. 7. 10.

[Research] Finding Undocumented Properties: Mining KS Driver's Hidden Features (EN)

Source : PIKOTAROピコ太郎OFFICIAL CHANNEL

Hello, I'm Libera

I've recently become interested in Kernel Streaming and have been looking into it. Seeing vulnerabilities emerge in KS-related drivers at Pwn2Own Vancouver 2024 and new vulnerabilities being announced at Code Blue 2025 made me think this area is a worthwhile attack vector to analyze.

While analyzing, one question arose. Manufacturers often create features not documented in standards for debugging or internal testing when developing drivers. I thought that if we could find these undocumented properties and focus our analysis on them, we might find vulnerabilities more efficiently.

Therefore, this time, I'd like to introduce how these undocumented properties were added and developed from V0 through V4.

1. What is Kernel Streaming

KS is a framework for processing large streaming data such as audio and video with low latency in Windows kernel mode. Here, data flows between objects called Filters (devices), Pins (data channels), and Nodes (internal functions).

From an attacker's perspective, the most critical element is the DeviceIoControl function, which communicates with kernel drivers from user mode. Among its operations, IOCTL_KS_PROPERTY is particularly significant. This IOCTL is used to modify or query specific driver settings, requiring a KSPROPERTY structure as input.

typedef struct {
    GUID Set;       // Property Set GUID (Function Categories)
    ULONG Id;       // Property ID (Detailed Function Number)
    ULONG Flags;    // Action flags such as GET, SET, etc.
} KSPROPERTY, *PKSPROPERTY;

The structure consists of a GUID, Property ID, and Flags. This combination enables the driver to trigger specific functions and access the structure.

2. KS Flow

How do user-mode applications in the KS architecture invoke specific functions within a driver using GUIDs and Property IDs? This process is broadly divided into user mode (request preparation and transmission) and kernel mode (request reception and processing).

This entire flow is broadly divided into four stages: Request Preparation (User) → Transmission (I/O Manager) → Dispatch → Execution. Shall we examine the details?

2-1. User Mode: Request Preparation (Steps 1–2)

Everything begins in user mode. The attacker or regular user must specify the task the driver must perform.

At this point, the KSPROPERTY structure is used to specify the task.

typedef struct {
    GUID Set;       // Property Set GUID (Function Categories)
    ULONG Id;       // Property ID (Detailed Function Number)
    ULONG Flags;    // Action flags such as GET, SET, etc.
} KSPROPERTY, *PKSPROPERTY;

A Set is a category of functions identified by a GUID. Examples include KSPROPSETED_Audio and others. The ID is a Property ID, an index within the category representing a specific function. For example, it signifies functions like volume control or mute. Flags determine whether to read (Get) or write (Set) the value.

Then, DeviceIoControl is called. The most critical element here is the Control Code. The KS framework uses a dedicated IOCTL called IOCTL_KS_PROPERTY for property control. The moment this code is used to call DeviceIoControl, the OS recognizes this request as a KS Property request, not a simple I/O operation.

DeviceIoControl(
    hDevice,
    IOCTL_KS_PROPERTY, // Key: Notification that this is a KS property request
    &Property,         // The previously set GUID and ID
    sizeof(Property),
    ...
);

2-2. Crossing the Boundary(Step 3)

When entering kernel mode via a syscall, the I/O Manager appears. The I/O Manager packages the user's request into a box called an IRP (I/O Request Packet). At this point, the IRP's Major Function code becomes IRP_MJ_DEVICE_CONTROL. The user-requested IOCTL_KS_PROPERTYand data buffer are stored in the IRP's CurrentStackLocation and passed to the drive r.

2-3. Kernel Mode: Dispatch & Parsing (Steps 4–5)

The DispatchDeviceControl routine registered in the driver object's DriverEntry receives the IRP. The driver examines the IRP and determines that it is an IOCTL_KS_PROPERTY request.

Most KS drivers do not parse this complex IRP directly but instead pass the IRP to the KsPropertyHandler function in the ks.sys library. This function acts as a kind of router. It extracts the KSPROPERTY structure (GUID, ID) sent by the user from the input buffer within the IRP.

2-4. Automation Table & Execution(Step 6~8)

Examining the structure reveals a hierarchical chain: KSDEVICE_DESCRIPTOR -> KSFILTER_DESCRIPTOR -> KSAUTOMATION_TABLE -> KSPROPERTY_SET. This makes the Automation Table one of the critical structures.

Additionally, driver developers define which properties they support within an array of structures called the Automation Table. KsPropertyHandler compares (matches) the GUID and ID pair sent by the user against the internal table within the driver.

If a match is found, it retrieves the callback function pointer mapped to that ID. If no match is found, it returns an error and terminates.

Upon successful matching, the actual handler function executes. Within this function, logic that manipulates hardware registers or modifies internal structures is performed.

After the handler function completes its task, it returns a success or failure status.

2-5. Back to User(Step 9)

The result value is returned via the IRP through the I/O Manager as the return value of the user-mode DeviceIoControl. We can verify how much of the requested data arrived using BytesReturned and similar fields.

Thus, KS calls required functions using combinations of GUIDs and IDs, and attackers can exploit these combinations to trigger specific functions. This led me to think: wouldn't analyzing these GUID-ID combinations—including undisclosed ones—enable more efficient and effective vulnerability analysis? Now, let's create a script to find undisclosed GUIDs and IDs.

3. V1 - Feasibility Verification

First, we verified whether it was possible to find a non-public Property ID when given a specific GUID. We selected KSPROPSETID_Service as the test target, as it has recently been the source of several privilege escalation vulnerabilities.

Having identified the GUID, we send a request with the target GUID and a random ID(0~N) in the KSPROPERTY structure. We then analyze the returned NTSTATUS code to determine the result.

Three possible returns occur: an invalid ID returns STATUS_NOT_FOUND. If the functionality exists but the requested buffer is too small, it returns STATUS_BUFFER_TOO_SMALL. If the passed parameter is incorrect, it returns STATUS_INVALID_PARAMETER.

Here, if STATUS_BUFFER_TOO_SMALL or STATUS_INVALID_PARAMETER is returned, it is interpreted as a valid ID, and the corresponding ID is output.

4. V2 - Extraction of undisclosed GUIDs and Property IDs

KS drivers must inform the OS about the features they support so applications can use them. Therefore, somewhere within the driver binary, there must be a section stating “I support this.” So, we'll statically extract this section to identify scan targets and create code to scan what IDs these targets possess.

4-1. Finding the Entry Point

First, you must find the entry point. Most AVStream drivers call KsInitializeDriver within DriverEntry.

NTSTATUS KsInitializeDriver(
  [in]           PDRIVER_OBJECT      DriverObject,
  [in]           PUNICODE_STRING     RegistryPathName,
  [in, optional] const KSDEVICE_DESCRIPTOR *Descriptor // <-- This guy!
);

The third argument, Descriptor, is KSDEVICE_DESCRIPTOR. According to the x64 calling convention, the first argument goes into the RCX register, the second into the RDX register, and the third into the R8 register. Therefore, you need to find what address is loaded into the R8 register immediately before calling KsInitializeDriver.

import idautils
import idc
import idaapi

def find_ks_descriptors():
    # 1. KsInitializeDriver Address acquisition
    ks_init_ea = idc.get_name_ea_simple("KsInitializeDriver")
    if ks_init_ea == idc.BADADDR:
        print("[-] KsInitializeDriver not found (Not an AVStream driver?)")
        return

    # 2. Xref Tracking
    for xref in idautils.XrefsTo(ks_init_ea):
        call_addr = xref.frm
        print(f"[+] Found KsInitializeDriver call at {hex(call_addr)}")

        # 3. Trace back the source (Backward Slicing)
        # Trace back through the commands preceding the CALL instruction to find the LEA instruction that sets R8 (the third operand).
        descriptor_addr = trace_argument(call_addr, 2) # arg index 2 corresponds to R8

        if descriptor_addr:
            print(f"[!] Found KSDEVICE_DESCRIPTOR at {hex(descriptor_addr)}")
            parse_device_descriptor(descriptor_addr)

This logic worked quite well, but not all drivers used KsInitializeDriver. Some directly called KsCreateFilterFactory instead. So I created a target API list (KsInitializeDriver, KsCreateFilterFactory, KsFilterCreateNode, etc.) and modified the script to track all Xrefs for each one.

4-2. Structure Parsing and GUID Extraction

Once you've located the Descriptor address, it's time to calculate the offset. Find the FilterDescriptors array within KSDEVICE_DESCRIPTOR, locate the AutomationTable within each filter, and then navigate into the PropertySets.

The biggest hurdle here was interpreting the GUID. The 16-byte data visible as a byte array in IDA must be converted into a GUID string ({XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}) for matching to be possible.

import struct

def get_guid_str(ea):
    # Read 16 bytes from memory
    data = idc.get_bytes(ea, 16)
    if not data or len(data) < 16:
        return None

    # Windows GUIDs are stored in Little-endian format (Data1, Data2, Data3)
    # Data4 (8 bytes) is treated as Big-endian (array)
    g = struct.unpack("<IHH8B", data)

    return "{%08X-%04X-%04X-%02X%02X-%02X%02X%02X%02X%02X%02X}" % (
        g, g, g, 
        g, g, g, g, g, g, g, g
    )

Using this script, you can now extract which Property IDs (e.g., ID 1, 5, 7) a given driver officially supports based solely on its binary, provided you have the driver binary.

4-3. Undocumented Property Detection

Source : BODA Youtube

Let's assume the static analysis results are {ID: 1, 2, 5, 10}. What exactly is an undocumented property here?

When developers define property IDs using enum, they typically increment sequentially starting from 0. However, looking at the list above, 3, 4 and 6, 7, 8, 9 are empty. Doesn't this gap seem suspicious? These gaps could be IDs that the developer, either accidentally or intentionally (for debugging or backdoor purposes), did not register in KSAUTOMATION_TABLE but instead handles directly within the switch-case statement inside KsPropertyHandler. These IDs are highly likely to correspond to undocumented features.

5. V3 - Anckor Algorithm Implementation

The connection relationships between KS structures in memory

When examining a driver with static analysis tools, numerous data sections and functions are often intermingled, making it difficult to identify which structure corresponds to the KSPROPERTY_SET table. Therefore, we introduced the concept of an anchor to address this issue. An anchor refers to a reliable, unchanging reference point within the binary. We designated the kernel APIs that the driver must call to announce its presence to the OS as these anchors.

Earlier, we discussed entry points. Recalling that, most AVStream drivers call the KsInitializeDriver function during initialization. The crucial point here is that the third argument of KsInitializeDriver, the Descriptor, points to the KSDEVICE_DESCRIPTOR structure I was seeking. According to the calling convention, it is passed via the R8 register. Therefore, by locating the point where KsInitializeDriver is called, marking it as an anchor, and then analyzing the instructions immediately preceding it to trace back which address is loaded into the R8 register, we can find the location of the structure.

The script I developed performs a total of three steps.

First, it identifies anchors.

The script first scans the Import Table to obtain the addresses of key KS-related APIs such as KsInitializeDriver, KsCreateFilterFactory, and KsFilterCreateNode. These addresses serve as the anchors that mark the starting point for analysis.

def find_ks_anchors():
    anchors =
    found_anchors =
    for name in anchors:
        ea = idc.get_name_ea_simple(name)
        if ea!= idc.BADADDR:
            found_anchors.append(ea)
    return found_anchors

Second, we trace the data in reverse.

Using the captured anchor function, we locate all points calling this anchor function via Xrefs. At each call point, we trace back through the instructions, searching for LEA instructions that set values in the argument registers.

This approach enabled us to achieve higher accuracy than simple byte scanning.

def trace_argument(call_addr, arg_index):
    # Simplified backtracking logic: Checking register settings while moving upward from the call point
    current_addr = call_addr
    target_reg = get_arg_register(arg_index) # e.g., arg 2 -> R8

    for _ in range(20): # Search up to 20 commands prior
        current_addr = idc.prev_head(current_addr)
        if is_lea_instruction(current_addr) and get_dest_reg(current_addr) == target_reg:
            return get_operand_value(current_addr, 1) # Return the address of the structure
    return None

Third, restore the structure and extract the GUID and Property ID.

Once the address of KSDEVICE_DESCRIPTOR is found via the anchor algorithm, parse the tree structure following the structure definition.

We delve into the structure in the following order: Device DescriptorFilter DescriptorsAutomation TableProperty Sets, ultimately extracting the list of GUIDs and Property IDs.
At this point, since the GUID exists in memory as a byte array, we also need functionality to convert the GUID into a readable string format.

def get_guid_tuple(ea):
    data = ida_bytes.get_bytes(ea, 16)
    if not data or len(data) != 16: return None
    return struct.unpack('<IHHBBBBBBBB', data)

def guid_to_string(g):
    if not g: return "{INVALID}"
    d4_str = "".join(["{:02X}".format(b) for b in g[3:]])
    return "{{{:08X}-{:04X}-{:04X}-{}-{}}}".format(g[0], g[1], g[2], d4_str[:4], d4_str[4:])

Finally, I wrapped up by returning the GUID and Property ID found this way, along with the conditions for accessing this feature, not only as output within IDA but also as a TXT file.

The code I created is available on Github, so if you're interested, feel free to check it out and give it a try!!

In closing

So far, we've built a tool to explore undocumented GUIDs and Property IDs in Kernel Streaming.

Looking at recent trends in KS-related vulnerabilities, issues often arise from undocumented hidden properties. Exploring attack surfaces others rarely look at is precisely the shortcut to boosting vulnerability analysis efficiency!

Thank you for reading this long post. See you next time~!