Using Kernel Rootkits to Conceal Infected MBR

If you’ve look at any of the major bootkits such as TDL4 and Rovnix, you’ve probably noticed they employ certain self defense features to prevent removal; specifically, intercepting read/write requests to the boot sectors. While these defense mechanisms can fool some software, they may, in some cases, make infections even easier to spot.

Rovnix is probably the less stealthy of the two: It intercepts read/write requests to the boot disk by hooking the miniport driver, on read attempts it fills the buffer with zeros, resulting in the boot sectors appearing completely empty, on write requests it simply returns ACCESS_DENIED. Although this does prevent reading & writing the sectors, it’s usually a sure sign of an infection when the boot sector is blank but your system boots, or you can’t write the boot sector even with SYSTEM privileges.

On the other had we have TDL4, which goes a step further: Instead of filling the buffer with zeros during read attempts, it instead replaces the read data with the original, non-infected master boot record (MBR). As a result of TDL4’s clever trickery, any tools attempting to read the MBR will just see the original windows MBR and assume nothing is wrong; however, TDL4 also opted for a similar method to Rovnix by just denies writes to the boot sector, but with the slightly more inconspicuous error: STATUS_INVALID_DEVICE_REQUEST.

Obviously straight up denying write requests to the boot sector is going to raise some questions, so what if we improved upon TDL4’s method and also allowed writing to the spoofed, non-infected MBR, instead of the real one on disk.

Intercepting Disk I/O

There’s a lot of places in the kernel that disk I/O can be intercepted to trick user mode tools; however, any software using kernel drivers can bypass high level hooks. The first though would probably be to hook Disk.sys (the driver responsible for handling disk operations), but although this would work against some tools, there are trick to avoid it, I’ll explain how.

Disk.sys handles disk I/O, it doesn’t actually send any requests to the hard drive, it simply acts as a middleman translating kernel disk I/O requests into SCSI requests (the protocol used to communicate with the hard drive). Once Disk.sys has translated a request, it dispatches it to another, lower level driver (known as a Miniport driver), in the form of an SCSI_REQUEST_BLOCK, which the Miniport sends to the hardware via the Port driver.

The Miniport is generally operating system independent, whilst the port driver is specific to certain OS version and even hardware, making the Miniport the best place to hook without getting into hardware dependent territory. Finding a Miniport driver is pretty straight forward as all drivers/devices are stacked, so we simply walk down the device stack until we reach the bottom most device (The Miniport). | | |—| | 2 identical device stacks for different disks |

The device “DeviceHardDisk0DR0” is almost always the boot disk and is the NT device name for “.PhysicalDrive0”. The device directly below the disk device is the Miniport and usually belongs to atapi.sys, scsiport.sys, iastor.sys (or in the case of vmware, lsi_sas.sys), this is the driver we want to hook. We can get the device object of the Miniport by opening “DeviceHardDisk0DR0” then calling “IoGetLowerDeviceObject” with it, all we then need to do is replace the IRP_MJ_SCSI pointer in the driver’s object with a pointer to our filter routine, which will intercept all I/O for that disk device.

Filtering Miniport Requests

Srb = SCSI_REQUEST_BLOCK Cdb = CDB (SCSI Command Block)

All the information we need is in the SCSI_REQUEST_BLOCK pointed to by IoStack->Parameters.Scsi.Srb, we only need to filter WRITE(10) and READ(10) SCSI operations on disks 2 TB or smaller (“Srb.CdbLength == 10”). Next we simply check the opcode in the Cdb for SCSIOP_READ or SCSIOP_READ_DATA_BUFF for read operations and similarly SCSIOP_WRITE or SCSIOP_WRITE_DATA_BUFF for write operations.

Now we need to see if the request is attempting to read or write sectors that overlap the MBR, which is located at logical block address (LBA) 0, by checking the LogicalBlock and TransferLength field in the Cdb, (these values are big-endian so will need to be converted to little-endian before checking them).

The driver will store the clean MBR into a buffer allocated at runtime, all read/write requests will be done to/from the clean MBR buffer, instead of the actual MBR on disk.

Processing Intercepted Read Requests

  1. Set up a completion routine to be called after the hard disk has processed the read request.
  2. When the completion routine is called, map the caller’s buffer (Irp->MdlAddress) into system memory and replace the infected mbr with the clean one in it.
  3. Allow the request to complete.

Processing Intercepted Write Requests Write requests are a little different: If the caller is only trying to write 1 sector (just the MBR), we can process the request ourselves; however, if the caller is trying to write multiple sectors (including the MBR), things get a bit more tricky.

  1. Map the caller’s buffer (Srb->DataBuffer) into system memory and read the first 512 bytes (1 sector) into the clean MBR buffer.
  2. Increment the caller’s buffer (Srb->DataBuffer) by 512 bytes.
  3. Decrement the transfer length (Srb->DataTransferLength) by 512 bytes.
  4. Add 1 to the Logical Block Address (Stored in Cdb).
  5. Subtract 1 from the Number of blocks to transfer (Stored in Cdb).
  6. Pass the request to the real Miniport (this will write all the sectors the caller wanted, except the MBR).
  7. Replace the original Srb->DataBuffer (Just to be safe).
  8. If the call succeeded, add 512 to Srb->DataTransferLength (This is the number of bytes actually written to disk, because we skipped them MBR we need to make it seem like we didn’t).
  9. Allow the request to complete.

Proof of Concept

I’ve written a proof of concept driver that will make the MBR appear to contain the following text:> Is this the real code?

Is this just spoofed for me?
bots trying to hide.
Not sure of the legality.

The system will be able to read/write to this fake MBR without modifying the real one, when the driver is unloaded or the system rebooted, the original MBR will still be intact and the face one will be gone. For some reason the driver will crash the system if loaded then unloaded many times in a row without reboot, but it’s not a huge issue and I’m too lazy to debug.

GitHub of code: