BACK

Bypassing Driver Security With Race Conditions

Background

NetFilterSDK is a software development kit (SDK) that enables developers to use a kernel-mode driver and user-mode API (library) to interact with network devices/events on a host device. It is sold to companies as a tool that can be embedded within their own programs and as such, the SDK seems to be pretty widely deployed in a number of environments.

It's probably also worth noting that the binary name 'NetFilterSDK' was used by malware that ended up being signed by Microsoft , leading to a substantial amount of name-confusion between the virus and legitimate product online.

There are two main vulnerabilities that are outlined in this writeup: a lack of security level properties applied to a repackaged NetFilterSDK driver, and the NetFilterSDK's race condition in enforcing security parameters. The former provides access to a machine's live networking traffic (including possible SSL decryption) as signed by an unnamed vendor, and the latter provides similar access to any driver based on the SDK regardless of security parameters.

EDIT: Six months after initially attempting to contact the product owner, CVE-2023-35863 has been published which names the vendor as MADEFORNET. The vulnerability remains unpatched and no response was ever received to my initial email to the company.

Finding an Interesting Driver

Let's take a look at what PE-Imports would output if it was used to scan the driver in question (reordered listing for readability):

IMPORTS LISTING link
driver_name.sys
    ntoskrnl.exe!ZwSetSecurityObject/null
    ntoskrnl.exe!MmBuildMdlForNonPagedPool/null
    ntoskrnl.exe!MmMapLockedPagesSpecifyCache/null
    ntoskrnl.exe!RtlCreateSecurityDescriptor/null
    ntoskrnl.exe!RtlSetDaclSecurityDescriptor/null
    ntoskrnl.exe!MmUnmapLockedPages/null
    ntoskrnl.exe!MmAllocatePagesForMdl/null
    ntoskrnl.exe!RtlLengthSid/null
    ntoskrnl.exe!RtlCreateAcl/null
    ntoskrnl.exe!RtlAddAccessAllowedAce/null
    ntoskrnl.exe!IofCompleteRequest/null
    ntoskrnl.exe!IoCreateDevice/null
    ntoskrnl.exe!IoCreateSymbolicLink/null
    ntoskrnl.exe!MmFreePagesFromMdl/null
    ntoskrnl.exe!PsCreateSystemThread/null
    ntoskrnl.exe!PsTerminateSystemThread/null
    ntoskrnl.exe!IoAllocateMdl/null
    ntoskrnl.exe!IoDeleteDevice/null
    ntoskrnl.exe!IoDeleteSymbolicLink/null
    ntoskrnl.exe!IoFreeMdl/null
    ntoskrnl.exe!IoReleaseCancelSpinLock/null
    ntoskrnl.exe!ExFreePoolWithTag/null
    ntoskrnl.exe!ExQueryDepthSList/null
    ntoskrnl.exe!ExpInterlockedPopEntrySList/null
    ntoskrnl.exe!ExpInterlockedPushEntrySList/null
    ntoskrnl.exe!ExInitializeNPagedLookasideList/null
    ntoskrnl.exe!ExDeleteNPagedLookasideList/null
    ntoskrnl.exe!ObReferenceObjectByHandle/null
    ntoskrnl.exe!ObfDereferenceObject/null
    ntoskrnl.exe!ZwClose/null
    ntoskrnl.exe!ZwOpenKey/null
    ntoskrnl.exe!ZwQueryValueKey/null
    ntoskrnl.exe!PsGetCurrentProcessId/null
    ntoskrnl.exe!ZwSetInformationThread/null
    ntoskrnl.exe!PsLookupProcessByProcessId/null
    ntoskrnl.exe!ObOpenObjectByPointer/null
    ntoskrnl.exe!__C_specific_handler/null
    ntoskrnl.exe!SeExports/null
    fwpkclnt.sys!FwpmTransactionBegin0/null
    fwpkclnt.sys!FwpmTransactionCommit0/null
    fwpkclnt.sys!FwpmTransactionAbort0/null
    fwpkclnt.sys!FwpmProviderAdd0/null
    fwpkclnt.sys!FwpmProviderContextDeleteByKey0/null
    fwpkclnt.sys!FwpmSubLayerAdd0/null
    fwpkclnt.sys!FwpmSubLayerCreateEnumHandle0/null
    ...

First of all, there are some references to functions with 'Mdl' or references to 'mapping' in the name, these imports refer to interactions with a 'memory descriptor listing' where Windows can map physical memory pages to virtual memory buffers - this is a pretty important function as, if misused, it is capable of acting as a privilege escalation vector (this is how most instances of such user-to-kernel escalations happen).

Being able to load data into arbitrary kernel regions isn't useful if the attacker already needs to be running as an administrator, and given the driver's nature (intercepting and possibly even decrypting host machine traffic) there isn't a huge amount of capability that a kernel implant could deliver beyond what is already included in the NetFilterSDK driver. In order to interact with the driver and intercept traffic, the attacker will need to acquire a handle to the 'device object' - this is where the next set of 'good' imports comes in; IoCreateDevice creates a device object which the user-mode API will later interact with (it should be noted that most drivers use the DeviceCharacteristics argument to pass in FILE_DEVICE_SECURE_OPEN to limit security access), IoCreateSymbolicLink establishes a symbolic link that the user-mode application can use to communicate with the driver (another great sign if we hope to access the driver), and finally IofCompleteRequest which implies that the driver - to some degree - can handle IRPs (the main way that handle-owners can communicate with drivers, often passing buffers that require delicate translation between kernel and user-mode memory).

On the not-so-great side, the driver appears to make use of a number of functions that setup/verify/assign security descriptors to events/objects, possibly meaning that any access to the driver could be subject to a security routine.

Reverse Engineering

This section covers reversing the driver, the environment used was primarily comprised of a VMware Windows 11 instance with a WinDbg kernel pipe and some other tools. In addition to dynamic analysis programs, I used Ghidra to actually take a look into the binary of the '.sys' file and once I'd figured out that the driver was from NetFilterSDK, the API's '.dll' file (choosing Ghidra over the Rizin Cutter project was due to the inherent KMDF support that could be imported through 'gdt' files, and chosing it over IDA Pro came down to my need to work across multiple devices which a self-hosted server could support).

Whenever any binary image is loaded to be run on Windows, an entrypoint function is called (unless compiled not to export a default entrypoint) - this can be WinMain, DllMain, or in the instance of kernel-mode drivers, DriverEntry.

DriverEntry has two parameters: a DRIVER_OBJECT*, and a UNICODE_STRING* with the former referring to the driver whose entrypoint is being called, and the latter containing its 'registry path'. When this entrypoint is called in the driver being analysed, two functions are called: one checks the state of some global variables and the other function (FUN_000208e0/'DriverInitFn') is called with the driver object and registry path being passed in through RCX and RDX, respectively:


Dispatch Setup

ASSEMBLYDriverInitFn (FUN_000208e0)

0001133d         LEA        RDI,[RCX + 0x70]
00011341         LEA        RAX,[FUN_0001813c]
00011348         MOV        ECX,0x1c
0001134d         STOSQ.REP  RDI=>FUN_0001813c
00011350         LEA        RAX,[FUN_000112e4]
00011357         MOV        qword ptr [RCX + 0x68],RAX=>FUN_000112e4

This segment of code takes the structure at DRIVER_OBJECT + 0x70 -- the DRIVER_DISPATCH list -- and loads its address to RDI and then loads the pointer to another function (FUN_0001813c/MajorIRPHandler) into RAX before setting ECX to 0x1C. All of these assignments are essentially setting up a call to STOSQ.REP. The STOSQ instruction on its own can be thought of as your CPU's interpretation of memcpy(RDI, RAX, sizeof(void*)) but when the 'REP'-suffixed version is used, it essentially loops the aforementioned operation ECX times.

After getting that far into the assembly, it becomes apparent that the above code loops over the local driver's dispatch table and sets all of the major IRP functions to a single handler, MajorIRPHandler, at FUN_0001813c. More on that later!

In addition, the final two instructions just load a pointer to the function as FUN_000112e4 into the member at 0x68 of the DRIVER_OBJECT object; thus setting the DRIVER_UNLOAD dispatch to call FUN_000112e4.



Device Initialization

As previously mentioned, in order to interact with this kernel-mode driver via IRPs, we need a handle to an exposed device object where a handle can be opened from user-mode - preferably from an unprivileged perspective.
Immediately following the setup of the dispatch functions, FUN_000112e4 is called. This function is where the interactivity capability is established by calling IoCreateDevice with a semi-hardcoded name (\Device\CtrlSMVARIABLE) and unfortunately for a would-be attacker, this function is called with the flag FILE_DEVICE_SECURE_OPEN.

By default, Windows will let any user try to open a handle to any device (there are still a couple of ways that handle creation can be selectively blocked by hooking handle creation or by blocking IRP_MJ_CREATE but that isn't what this driver does). The operating system allows drivers the ability to limit access to its devices using functions like IoCreateDeviceSecure and the FILE_DEVICE_SECURE_OPEN device characteristics which essentially mirrors access control restrictions from the driver object to any attempts to access the device from user-mode. This means that we now need to confirm that those security descriptor functions aren't appropriately used in the environment that this driver is running (otherwise, interaction with the targeted driver is confined to other functions, mostly in the form of system-wide callbacks which are heavily sanitized by the OS).

Next up, a symbolic link is created between the device and \DosDevices\CtrlSMVARIABLE, pretty standard for any driver that wants to handle inter-process communication. We drop into FUN_000138dc which checks the seclevel member at HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\VARIABLE. If the seclevel value isn't set to allow universal access, another function is called.

In conclusion, as long as the driver doesn't set the seclevel registry key to 1 or 2, opening a handle is fine from Windows' verification perspective. In case you were wondering, here's the flow of the IRP_MJ_CREATE handler, which could have been used to reject an unauthorised handle request but simply prevents multiple handles being issued using a spinlock (a.k.a 'fancy kernel mutex'):

Exploitation

The end goal for this writeup is gaining a handle, from an unprivileged user-mode application, to the vendor-compiled NetFilterSDK driver. From that point, a malicious application would be able to make legitimate API calls that deliver access to raw TCP/UDP streams as well as intercepting system-wide network traffic (including HTTPS) without on-system security products flagging anything suspicious. In the third-party application that I discovered this issue on, an administrative service application launched the driver using CreateServiceA and subsequently StartServiceA to launch the driver after installing it, I didn't identify any modification of the seclevel value however there seemed to be some form of attempt to prevent unprivileged access through the usage of SetSecurityDescriptorSacl, SetSecurityDescriptorDacl, InitializeSecurityDescriptor, ConvertStringSecurityDescriptorToSecurityDescriptorA, and LookupAccountSidA to apply the SDDL S:(ML;;NW;;;LW).

In either case, this application has a gap between starting the driver's service and actually getting the single handle to that driver. This timing gap is what we'll exploit by rapidly making attempts at claiming the driver's handle in order to hope that our 'malicious' user-mode application is able to complete a request between the time it takes for the official service to create the driver server and send its own request.

This proof-of-concept allows the process to get a handle to the repackaged NetFilterSDK driver installed by the third party vendor, from there - an attacker can intercept almost any networking traffic on the system but in the PoC script just calls the IOCTL 0x12C800 to allocate and map some kernel-mode memory into the calling process' address space.

A (slightly redacted) demonstration of the exploit with 'seclevel' set to only allow admins.

Conclusion

There seems to be a lack of awareness about how device interaction could be limited throughout this application, ranging from the vendor failing to use seclevel to the fact that the key probably wouldn't have changed much as the security descriptors are applied after the device is setup, a symbolic link is applied, and an IRP dispatch handler is assigned.

When I first discovered this vulnerability, I was unaware that the vendor had repackaged the NetFilterSDK binary and so I emailed them a couple of times a few months ago to no reply. After finding out that the driver was from NetFilterSDK I reached out to the owner of NetFilterSDK who confirmed that the behaviour was expected and that the third-party could patch the issue by using the aforementioned registry key to limit access (again, something that seems unlikely due to the order of applying the security policy).

No patch is currently available for this as the third-party didn't acknowledge any emails. Additionally, this vendor seems to be a primarily B2B company that lists a number of fortune 500 companies as its customers, making it unlikely that an individual subscriber such as myself will get a response from their support team any time soon.