6

I'm writing a keyboard filter driver for Windows and I need to insert my custom keystrokes data into Windows message queue. I've managed to capture all the keys that are pressed setting OnReadCompletion() callback to IoSetCompletionRoutine() in my driver's Read() function like so:

NTSTATUS Read(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)
{
    PDEVICE_EXTENSION deviceExtension = DeviceObject->DeviceExtension;

    IoCopyCurrentIrpStackLocationToNext(Irp);
    IoSetCompletionRoutine(Irp, OnReadCompletion, DeviceObject, TRUE, TRUE, TRUE);
    return IoCallDriver (deviceExtension->pKeyboardDevice, Irp);
}

NTSTATUS OnReadCompletion(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp, IN PVOID Context)
{
// ...
}

This filter driver is attached to the kbdclass driver like so:

NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath)
{
// ...
CCHAR ntNameBuffer[64] = "\\Device\\KeyboardClass0";
status = IoAttachDevice(deviceObject, &uKeyboardDeviceName, &DeviceExtension->pKeyboardDevice);
// ...

}

So, I can catch all keys pressed in OnReadCompletion(). But I need to insert my own information into keyboard message flow. Here are 2 problems with that:

  1. OnReadCompletion() is only invoked when a key pressed. Ideally I'd like somehow it to be called when nothing is pressed. Can I do that somehow? I need to trigger a keyboard interrupt? I tried to write commands to a keyboard ports (0x60 and 0x64) with WRITE_PORT_UCHAR() but that didn't work out.

  2. I tried to insert my data into IRP in OnReadCompletion() to make it look like for example a key was pressed twice while actually it was pressed only once. Can someone help me out on that one too, because the following didn't work out?

    NTSTATUS OnReadCompletion(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp, IN PVOID Context)
    {
        PIO_STACK_LOCATION IrpStackLocation = NULL;
        INT BufferLength;
        INT numKeys = 0, i = 0;
        PDEVICE_EXTENSION deviceExtension = DeviceObject->DeviceExtension;
    
        IrpStackLocation = IoGetCurrentIrpStackLocation(Irp);
        BufferLength = IrpStackLocation->Parameters.Read.Length;
    
        if(Irp->IoStatus.Status == STATUS_SUCCESS)
        {
            PCHAR newSystemBuffer, oldSystemBuffer;
            PKEYBOARD_INPUT_DATA keys = (PKEYBOARD_INPUT_DATA)Irp->AssociatedIrp.SystemBuffer;
            numKeys = Irp->IoStatus.Information / sizeof(KEYBOARD_INPUT_DATA);
            for(i = 0; i < numKeys; i++)
            {
                     // here we print whatever was pressed
                     DbgPrint("%s -- ScanCode: %x\n", __FUNCTION__, keys[i].MakeCode);
            }
            // allocate new buffer twice as big as original
            newSystemBuffer = ExAllocatePool(NonPagedPool, Irp->IoStatus.Information * 2);
            // copy existing buffer twice into new buffer
                RtlCopyMemory(newSystemBuffer, keys, Irp->IoStatus.Information);
            RtlCopyMemory(newSystemBuffer + Irp->IoStatus.Information, keys, Irp->IoStatus.Information);
                // assign new buffer to Irp->AssociatedIrp.SystemBuffer
            oldSystemBuffer = Irp->AssociatedIrp.SystemBuffer;
            Irp->AssociatedIrp.SystemBuffer = newSystemBuffer;
                // tell IRP that we now have twice as much data
            Irp->IoStatus.Information *= 2;
            // free the old buffer
                ExFreePool(oldSystemBuffer);
        }
    
        if(Irp->PendingReturned)
            IoMarkIrpPending(Irp);
    
        return Irp->IoStatus.Status;
    }
    

And when I test it for example in Notepad, all I get is just one letter per keystroke. I'm really desperate. Please help!

Jonathan Leffler
  • 730,956
  • 141
  • 904
  • 1,278
Nick Borodulin
  • 3,065
  • 4
  • 21
  • 21
  • I've managed to add new data to the IRP System buffer. The trick was to use different key stroke code, not the same one like in the code above. So if you want to insert a MakeCode = 2 (equal to pressing button "1"), do this in the OnReadCompletion(): RtlCopyMemory(newSystemBuffer, keys, Irp->IoStatus.Information); keys->MakeCode = 2; RtlCopyMemory(newSystemBuffer + Irp->IoStatus.Information, keys, Irp->IoStatus.Information); – Nick Borodulin Sep 01 '11 at 05:29
  • The difference is in "keys->MakeCode = 2;" which changes MakeCode in the second KEYBOARD_INPUT_DATA message. And you get a "1" added after each keystroke, for example in the notepad. – Nick Borodulin Sep 01 '11 at 05:32
  • So the only question remains is how do I trigger an interrupt? – Nick Borodulin Sep 01 '11 at 05:33
  • As far as I know you can't. An interrupt is a very special event that only outside input devices like Mouse and Keyboards connected via PS/2 ports can provide. – Mark Tomlin Sep 01 '11 at 05:58

1 Answers1

1

Four options which I think should work:

1) You could create a new IRP to call the kbdclass driver with, as opposed to passing the IRP that you received down. You would complete the original IRP whenever you wanted to insert data as well as whenever you had real keystrokes to pass on.

2) You could have two devices, the second being a keyboard device. You would then use the kbdclass filter for removing keystrokes and the keyboard device for adding them.

3) You could redesign your driver to be an upper filter for the keyboard devices, similar to the MSDN sample driver kbfiltr.

4) You could have two devices, the second being an upper filter for one or more of the keyboard devices. You would use the kbdclass filter for removing keystrokes and the keyboard device filter for adding them.

I think the first option would be best, but I'm no expert.

Harry Johnston
  • 35,639
  • 6
  • 68
  • 158
  • Thanks for the reply, mate! But my driver is a filter driver, if I understand the terminology. It does exactly what Kbdfiltr does. I found out that you have two option to modify an IRP in a filter driver. First is when the OS make a read to the keyboard driver stack. This read is captured by your driver. An you can reply immediately, thus sending keystrokes to the OS that have never been pressed. If you don't do that this read gets to the bottom of the stack and blocked there until user presses a key. Then the IRP moves up the stack and can be captured by your driver again and modified. – Nick Borodulin Sep 02 '11 at 11:27
  • So you can modify the IRP on two occasions. But when when it gets blocked by a driver below the stack you can do nothing and wait till it gets back. That's the problem. Because if I have data arrived through ioclt that I need to insert I need to wait for a keystroke to get an opportunity to insert it in the IRP. – Nick Borodulin Sep 02 '11 at 11:34
  • As to your second idea regarding another keyboard - do you mean actually connecting a second keyboard to the computer or just making a filter driver to an non-existing device, like \\Device\\KeyboardClass1 ? – Nick Borodulin Sep 02 '11 at 11:37
  • 2
    No, your driver isn't the same as Kbdfiltr - your driver is a filter on top of the kbdclass driver, whereas kbdfiltr is a filter on top of the keyboard drivers themselves. There's a big difference. A filter on the keyboard drivers can capture the callback function which allows keyboard drivers to send keystrokes to kbdclass, so you can send keystrokes at any time. – Harry Johnston Sep 03 '11 at 03:59
  • Idea (2) is not an actual second keyboard, but a device driver (*not* a filter driver) which pretends to be a driver for a keyboard. Keyboard drivers are provided with a callback function which they can use to report keystrokes at any time. I still recommend either (1) or (3), let me know if you need more clarification on any of these. – Harry Johnston Sep 03 '11 at 04:03