Skip to content

Hooking IOCTL Communication via Hooking File Objects

Bill Demirkapi edited this page Aug 5, 2020 · 1 revision

One of the key features of the Spectre Rootkit is its ability to abuse legitimate network communications to receive instructions from a C2. Before we get into how the Spectre Rootkit uses this visibility, it's important to understand how this hooking is performed.

To begin, some background. A focus of my research was finding a "new" way to intercept network communication in a way that had not been done before. Networking relating to Winsock is handled by Afd.sys, otherwise known as the “Ancillary Function Driver for Winsock”. mswsock.dll uses NtDeviceIoControlFile in order to communicate with the Afd.sys driver.

Let's talk a little bit about what happens in the background when an application calls NtDeviceIoControlFile. Here is a very high-level overview.

IOCTL Path

For our purposes, IoGetRelatedDeviceObject retrieves the DeviceObject member of the FILE_OBJECT structure:

typedef struct _FILE_OBJECT {
  ...
  PDEVICE_OBJECT                    DeviceObject;
  ...
} FILE_OBJECT, *PFILE_OBJECT;

With the device object in hand, the kernel will attempt to dispatch the request via FastIo and otherwise with an Irp passed to IoCallDriver. For IoCallDriver, the kernel will determine what function to call by looking up the "Major Function Code" specified inside of the Irp within the MajorFunction array in the DRIVER_OBJECT structure.

typedef struct _DRIVER_OBJECT {
  ...
  PDRIVER_DISPATCH   MajorFunction[IRP_MJ_MAXIMUM_FUNCTION + 1];
} DRIVER_OBJECT, *PDRIVER_OBJECT;

One common way of hooking this form of communication is to replace an entry in the MajorFunction array with a pointer to your own function. The problem? Driver objects are very easy to find and there will only be a handful loaded at a time. It would be trivial for an anti-virus solution to enumerate these driver objects and detect tampering. To clarify, there is nothing wrong with the method of replacing the MajorFunction array entry, rather the problem lies with the fact that it is performed on a driver object.

The question I asked was, "What's stopping someone from overwriting the DeviceObject pointer of the FILE_OBJECT structure with their own device?". Well, it turns out, absolutely nothing!

What we can do is create our own fake driver/device object and then replace this DeviceObject pointer with our own. We can intercept IOCTL communication by performing the previously mentioned hooking method for DRIVER_OBJECTs, except we would apply the hook to our own driver object, instead of the driver object that can be easily found. Let's talk about how we go about doing this.

First, we need to actually obtain file objects for the Afd device. We can achieve this through the use of the ZwQuerySystemInformation function, specifically using the SystemHandleInformation information class to query every open handle on the system. This information class will return the following structure for each handle:

typedef struct _SYSTEM_HANDLE {
	ULONG ProcessId;
	BYTE ObjectTypeNumber;
	BYTE Flags;
	USHORT Handle;
	PVOID Object;
	ACCESS_MASK GrantedAccess;
} SYSTEM_HANDLE, *PSYSTEM_HANDLE;

With this information we can determine if a handle is for the Afd device with the following steps:

  1. To determine if a handle is for a file object, we can compare the ObjectTypeNumber against the known file object type index.
  2. Once we know that a handle is a file object, we can compare the DeviceObject member of the FILE_OBJECT structure against the known Afd device (the Afd device is the same across different file objects).

The Windows kernel uses the function ObCreateObject to create objects such as the device/driver objects. Fortunately, the kernel exports this function presumably to allow other Windows drivers to create objects for themselves. We can use this function to make our own fake device/driver objects. Here is the relevant snippet of code from FileObjHook.cpp.

	//
	// Generate the object attributes for the fake driver object.
	//
	InitializeObjectAttributes(&fakeDriverAttributes,
				   &BaseDeviceObject->DriverObject->DriverName,
				   OBJ_PERMANENT | OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE,
				   NULL,
				   NULL);

	fakeDriverObjectSize = sizeof(DRIVER_OBJECT) + sizeof(EXTENDED_DRIVER_EXTENSION);

	//
	// Create the fake driver object.
	//
	status = ObCreateObject(KernelMode,
				*IoDriverObjectType,
				&fakeDriverAttributes,
				KernelMode,
				NULL,
				fakeDriverObjectSize,
				0,
				0,
				RCAST<PVOID*>(&fakeDriverObject));
	
	...
	
	//
	// Generate the object attributes for the fake device object.
	//
	InitializeObjectAttributes(&fakeDeviceAttributes,
				   &realDeviceNameHeader->Name,
				   OBJ_KERNEL_HANDLE | OBJ_PERMANENT,
				   NULL,
				   BaseDeviceObject->SecurityDescriptor);

	fakeDeviceObjectSize = sizeof(DEVICE_OBJECT) + sizeof(EXTENDED_DEVOBJ_EXTENSION);

	...
	
	//
	// Create the fake device object.
	//
	status = ObCreateObject(KernelMode,
				*IoDeviceObjectType,
				&fakeDeviceAttributes,
				KernelMode,
				NULL,
				fakeDeviceObjectSize,
				0,
				0,
				RCAST<PVOID*>(&FileObjHook::FakeDeviceObject));

To hook our fake driver object, we can use the same trick I mentioned before and replace the MajorFunctions array part of the DRIVER_OBJECT structure to point to our hook functions. Keep in mind that this hooking is being performed on our copy of the driver object, not the actual driver object that can easily be found. The final step is to replace the DeviceObject member of the FILE_OBJECT with our own device.

Now the file object is hooked, any call to IoGetRelatedDeviceObject will return our fake device which IoCallDriver will use to call our patched MajorFunctions array.