Uninformed: Informative Information for the Uninformed

Vol 8» 2007.Sep

Asynchronous Read Loop

It's not always necessary to hook some portion of the kernel when attempting to implement a local kernel-mode backdoor. In some cases, it's easiest to just make use of features included in the target operating system to blend in with normal behavior. One particularly good candidate for this involves abusing some of the features offered by Window's I/O (input/output) manager.

The I/O model used by Windows has many facets to it. For the purposes of this paper, it's only necessary to have an understanding of how it operates when reading data from a file. To support this, the kernel constructs an I/O Request Packet (IRP) with its MajorFunction set to IRP_MJ_READ. The kernel then passes the populated IRP down to the device object that is related to the file that is being read from. The target device object takes the steps needed to read data from the underlying device and then stores the acquired data in a buffer associated with the IRP. Once the read operation has completed, the kernel will call the IRP's completion routine if one has been set. This gives the original caller an opportunity to make forward progress with the data that has been read.

This very basic behavior can be effectively harnessed in the context of a backdoor in a fairly covert fashion. One interesting approach involves a user-mode process hosting a named pipe server and a blob of kernel-mode code reading data from the server and then executing it in the kernel-mode context. This general behavior would make it possible to run additional code in the kernel-mode context by simply shuttling it across a named pipe. The specifics of how this can be made to work are almost as simple as the steps described in the previous paragraph.

The user-mode part is simple; create a named pipe server using CreateNamedPipe and then wait for a connection. The kernel-mode part is more interesting. One basic idea might involve having a kernel-mode routine that builds an asynchronous read IRP where the IRP's completion routine is defined as the kernel-mode routine itself. In this way, when data arrives from the user-mode process, the routine is notified and given an opportunity to execute the code that was supplied. After the code has been executed, it can simply re-use the code that was needed to pass the IRP to the underlying device associated with the named pipe that it's interacting with. The following pseudo-code illustrates how this could be accomplished:

KernelRoutine(DeviceObject, ReadIrp, Context)
  // First time called, ReadIrp == NULL
  if (ReadIrp == NULL)
    FileObject = OpenNamedPipe(...)
  // Otherwise, called during IRP completion
    FileObject = GetFileObjectFromIrp(ReadIrp)

  DeviceObject = IoGetRelatedDeviceObject(FileObject)
  ReadIrp = IoBuildAsynchronousFsdRequest(...)
  IoSetCompletionRoutine(ReadIrp, KernelRoutine)
  IoCallDriver(DeviceObject, ReadIrp)

Category: Type II

Origin: The authors believe this to be the first public description of this technique.

Capabilities: Kernel-mode code execution.

Covertness: The authors believe this technique to be fairly covert due to the fact that the kernel-mode code profile is extremely minimal. The only code that must be present at all times is the code needed to execute the read buffer and then post the next read IRP to the target device object. There are two main strategies that might be taken to detect this technique. The first could include identifying malicious instances of the target device, such as a malicious named pipe server. The second might involve attempting to perform an in-memory fingerprint of the completion routine code, though this would be far from fool proof, especially if the kernel-mode code is encoded until invoked.