Getting Original Pointers
XP is a little more complicated than newer systems due to the use of a single driver for both port and miniport; however, getting the original pointers is fairly straight forward depending on how you do it.
IRP_MJ_SCSI & DriverStartIo – Method 1 (Windows XP)
A common method is to programmatically disassemble the miniport’s DriverEntry, looking for the code which initializes the driver’s object, then you can extract and calculate the addresses from “mov [esi+30h], offset” and “mov [esi+74h], offset” for DriverStartIo and IRP_MJ_SCSI respectively.
|An example of code initializing the driver object (taken from atapi.sys)
The obvious problem with this method is the initialization code may not be in DriverEntry, but a sub function called from it (it may even be necessary to follow jumps). It’s also not guaranteed that the instruction will use esi as the pointer to the driver object or an immediate for the function address, in fact you’re probably going to have to account for quite a few different instructions.
IRP_MJ_SCSI & DriverStartIo – Method 2 (Windows XP)
In my tests, it was possible to simply call the DriverEntry of the miniport driver with the parameters from your own driver entry, thus having the miniport set up your driver’s object as if it were its own. The only issue with this method is if the driver uses GsDriverEntry (it usually does), the entry point will be invalidated after the driver is initialized, so you cannot call it. To deal with GsDriverEntry you’d first need to load the original image from disk, then search until you reach an unconditional relative jump (this is the offset to real entry point and you can use it to calculate the same address within the loaded driver).
IRP_MJ_SCSI (Windows Vista+)
On newer systems, things are wonderfully easier: There’s no DriverStartIo field and you can initialize all the major functions in your DriverObject with a call to AtaPortInitialize, ScsiPortInitialize, or StorPortInitialize which are all exported from the relevant port drivers (ataport.sys, scsiport.sys, or storport.sys).
Bypassing Inline Hooks
Although not many bootkits actually perform inline hooking on miniports, it’s worth taking care of. You’ll need to read a the original miniport or port driver’s file into memory, then do a bit of pointer math to calculate the addresses of IRP_MJ_SCSI or DriverStartIo within the clean image. I’m not too sure of the best way to call the clean functions, but here are 2 viable methods to chose from.
Usually a hook is placed within the first few bytes of a function, so you can simply read and relocate the first few bytes from the clean function into a buffer, then append it with a jump to the same offset within the real driver(this is the same way a hooking engine would call the unhooked version of a function).
A more difficult but effective method is to manually map a clean copy of the driver into memory, then relocate it so that all absolute instructions will reference the real driver, meaning you don’t have to worry about initializing any global variables or such.
Creating a Clean Call Path
Due to the fact a lot of bootkits run persistence threads for replacing any driver object hooks which get removed, you don’t want to unhook the real driver but instead create a parallel one, so you can maintain your own hook-free call path.
Step 1 (XP & Vista)
- Get the device object for the boot disk miniport, this is usually DeviceHarddisk0Dr0
- Use the size field of the device object to allocate some non paged memory and copy the entire object (this is your clean miniport).
- Set the DriverObject field to point to your own driver’s object, in which you’ve set the IRP_MJ_SCSI and DriverStartIo field appropriately (DriverStartIo can be skipped on Vista+).
Step 2 (XP Only)
- Set the DeviceExtension field of your clean miniport device object to point to directly after its device object (DeviceObject + sizeof(DEVICE_OBJECT)).
- Get the address stored at offset 0x5C into your clean miniport’s device extension and check it’s valid (this is the address of the corresponding port’s device extension).
- Read the addresses stored at offset 0x0C into the port’s device extension (this is the address of the port’s device object).
- Use the size field of the port’s device object to allocate some non paged memory and copy the entire object (this is your clean port).
- Set the DeviceExtension field of your clean port’s device object to point to directly after its device object (DeviceObject + sizeof(DEVICE_OBJECT)).
- Set the DriverObject field of your clean port’s device object to point to your own driver’s object, in which you’ve set the IRP_MJ_SCSI field appropriately.
- Change offset 0x5C into your clean miniport’s device extension to contain the address of the clean port’s device extension.
- Set offset 0x0C into the clean port’s device extension to contain the address of the clean port’s device object.
Using the Clean Path
You’re going to need to build a raw SCSI request which is pretty complicated; however, the Chinese are already a step ahead, so you can look to this example
for help (This request can be issued by passing the clean miniport device object and the IRP to IofCallDriver).
It’s important to note that miniport drivers are PnP, so if you don’t create any devices (IoCreateDevice): the driver will be unloaded as soon as DriverEntry returns, if you do: the driver can’t be unloaded at all. Although it’s not recommended, you can set the driver back to a legacy driver by setting the AddDevice pointer within the driver’s extension to 0, allowing the driver to be unloaded normally.
This concludes my 3 part series, any feedback in the comments would be greatly appreciated and will be taken into consideration when I create a whitepaper version of the series in a few weeks.
Other resources of note