Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

A step-by-step walkthrough of how to write a Client and a Driver to communicate with each other and boost the priority of a thread.

NotificationsYou must be signed in to change notification settings

whokilleddb/BoosterDriver

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

20 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

A Proof-of-Code and code walkthrough to demonstrate how to facilitate communication between userland applications and Windows kernel driver. This is a follow-up tomy last Windows Kernel development repository where I document my journey into Windows Kernel land - while giving extensive code walkthroughs.

In this repository, we write a Client and a Driver which work together to boost a thread's Base Priority.

Usage

To send a signal to the Driver to increase the base priority of a thread, use the following command:

BoosterClient.exe <Thread ID> <Target Priority>

Walkthrough

This part of the guide walks you through the Driver and Client code to explain the underlying concepts. First, we take a look into the driver itself which explores concepts like Handling Dispatch routines, Major Functions, etc, while the Client covers topics like how to useCreateFile() andWriteFile() to communicate with a driver.

Also, we briefly touch upon IRQs but more upon that in future articles.

References

This article is directly influenced by@zodicon's Windows Internal training and I recommend everyone to check it out.

The Driver

We will be breaking down this section by the different functions which constitute our driver, namely:

  • DriverEntry() - This serves as an entry point when the driver is loaded by the system
  • BoosterCreateClose() - This function handles Create/Close dispatch routines issued by the Client
  • BoosterWrite() - This function handles the Write dispatch routine
  • BoosterUnload() - This function is called when the system unloads our driver

DriverEntry

Looking at theDriverEntry() function, it has the following code:

NTSTATUSDriverEntry(PDRIVER_OBJECTDriverObject,PUNICODE_STRING_RegistryPath) {// Set major functions to indicate supported functionsDriverObject->DriverUnload=BoosterUnload;DriverObject->MajorFunction[IRP_MJ_WRITE]=BoosterWrite;DriverObject->MajorFunction[IRP_MJ_CREATE]=DriverObject->MajorFunction[IRP_MJ_CLOSE]=BoosterCreateClose;// Create a device object for the client to talk toPDEVICE_OBJECTdevice_obj=NULL;UNICODE_STRINGdevice_name=RTL_CONSTANT_STRING(L"\\Device\\Booster");NTSTATUSstatus=IoCreateDevice(DriverObject,0,&device_name,FILE_DEVICE_UNKNOWN,0, FALSE,&device_obj);if (!NT_STATUS(status))returnstatus;device_obj->Flags |=DO_BUFFERED_IO;// Create symbolic linkUNICODE_STRINGsymlink_name=RTL_CONSTANT_STRING(L"\\??\\Booster");status=IoCreateSymbolicLink(&symlink_name,&device_name);if (!NT_SUCCESS(status)) {IoDeleteDevice(device_obj);returnstatus;  }returnstatus;}

There are three major parts to the function - Setting the major functions to indicate the functions our driver supports, creating a device object for the client to interact with, and finally creating a symbolic link for the client to callCreateFile() on.

The first part of the code sets the necessary function pointers:

  • First, set theDriverUnload member of theDriverObject which points to the unload routine.
  • Then we set theMajorFunction members. TheMajorFunction array contains a list of function pointers that serve as entry points for the Driver's dispatch routines. These indicate the functionalities supported by the driver. In our case, we support three routines:
    • IRP_MJ_CREATE: A routine to deal with requests sent by the client when it tries to open a handle to the Device object
    • IRP_MJ_CLOSE: A routine to deal with requests sent by the client when it tries to close the handle to the Device object
    • IRP_MJ_WRITE: A routine to deal with requests sent by the client when it tries to transfer data to the driver using operations likeWriteFile() orNtWriteFile()

For the sake of simplicity, we will point the major functions indicated byIRP_MJ_CREATE andIRP_MJ_WRITE to the same dispatch routine. But, why do we need to specify these functions in the first place?Microsoft Documentation specifies that we need to specify these functions to handle the Create/Close Dispatch routines so that the clients can have a handle for it, and, in turn, uses functions likeWriteFile() which need a handle to be passed in as one of the parameters.

Next up, we create a Device for the Client to interact with. We use theRTL_CONSTANT_STRING macro to initialize aUNICODE_STRING with the full path name of the device. We create a device calledBooster in the\Deviceobject directory, which is where devices are usually created.

Following that, we use theIoCreateDevice() to go ahead and actually create the device. The parameters passed to this function are as follows:

ParameterValueDescription
PDRIVER_OBJECT DriverObjectDriverObjectPointer to the driver object for the caller. In our case, we get the pointer as a parameter for theDriverEntry() function.
ULONG DeviceExtensionSize0Specifies the driver-determined number of bytes to be allocated for the device extension of the device object. This allows us to attach extra information to the devices, in case we need to. In our case, we dont have any such special requirements.
PUNICODE_STRING DeviceName&device_namePointer to the null-terminated device name Unicode string.
DEVICE_TYPE DeviceTypeFILE_DEVICE_UNKNOWNIndicates the type of device - since we do not confront to the usual predefined driver types, we specifyFILE_DEVICE_UNKNOWN.
ULONG DeviceCharacteristics0Specifies additional information about the driver's device - since we have no special permissions, we set it to 0.
BOOLEAN ExclusiveFALSESpecifies if the device object represents an exclusive device. Most drivers set this value to FALSE.
PDEVICE_OBJECT *DeviceObject&device_objPointer to a variable that receives a pointer to the newly created DEVICE_OBJECT structure.

If the function runs successfully - we should have a valid Device object. This address to this device can also be found at the first index of the linked list pointed by theDeviceObject field ofDriverObject.

Next up, we also create a symbolic link for the device so that the client can easily access it. We use theIoCreateSymbolicLink() function to create a symbolic link to our device called\??\Booster where\?? is a "fake" prefix that refers to per-user Dos devices.

However, if theIoCreateSymbolicLink() fails, we need to delete the previously created device object as if theDriverEntry``()`` function returns something other thanSTATUS_SUCCESS`, the unload routine is never called - so we don't have any opportunities to clean up after ourselves. If we do not delete the device object - we will leak the device object.

Finally, we return the validNTSTATUS from the function signifying that theDriverEntry routine was complete.


BoosterCreateClose

This function is responsible for handling Create/Close dispatch routines - and has the following code:

NTSTATUSBoosterCreateClose(PDEVICE_OBJECT_DriverObject,PIRPIrp) {  ...        ...Irp->IoStatus.Status=STATUS_SUCCESS;Irp->IoStatus.Information=0;IoCompleteRequest(Irp,0);returnSTATUS_SUCCESS;}

The function takes in two parameters - the pointer to theDriverObject, and a pointer to anIRP structure that represents an I/O request packet. For our driver, we won't need anything fancy - so we would just let the operation complete successfully. To do it, we need to do a couple of things:

  • First, we set the final status of the request asSTATUS_SUCCESS by assigning that value to theStatus component of theIO_STATUS_BLOCK. TheIO_STATUS_BLOCK structure stores status and information before callingIoCompleteRequest()[more on that in a moment].
  • Next up, we set theInformation field ofIoStatus to indicate that we do not pass any additional information to the Client. For example, for Write/Read, this field can define the number of bytes that were written/read and return that information to the caller. Since for Create/Close we don't have any such requirements, we set it to 0.
  • Finally, we complete the request withIoCompleteRequest() indicating that we have completed the I/O request and returned the IRP to the I/O manager. We pass two parameters to the function - theIrp structure pointer as well as the value for the priority boost for the original thread that requested the operation. Since we complete the IRP synchronously, we set it to 0.
  • Finally, we return the same status as the one we put inIrp->IoStatus.Status. However, we cannot just something likereturn Irp->IoStatus.Status because after theIoCompleteRequest() function is called - the value stored in the address might change.

With this, we complete the function allowing us to open and close handles to the driver. Onto the next 🚀


BoosterWrite

The code we have written so far can more or less be considered a boiler template - something which we would find unchanged across a lot of future work, but this function is the crux of the whole driver.

NTSTATUSBoosterWrite(PDEVICE_OBJECT_DriverObject,PIRPIrp) {UNREFERENCED_PARAMETER(_DriverObject);ULONGinfo=0;PETHREADthread=NULL;NTSTATUSstatus=STATUS_SUCCESS;PIO_STACK_LOCATIONirp_sp=IoGetCurrentIrpStackLocation(Irp);if (irp_sp->Parameters.Write.Length!=sizeof(ThreadData)) {status=STATUS_BUFFER_TOO_SMALL;gotoio;}ThreadData*p_data= (ThreadData*)(Irp->UserBuffer);if (p_data==NULL||p_data->TargetPriority<1||p_data->TargetPriority>31) {status=STATUS_INVALID_PARAMETER;gotoio;}HANDLEh_tid=ULongToHandle(p_data->ThreadId);status=PsLookupThreadByThreadId(h_tid,&thread);if (!NT_SUCCESS(status)) gotoio;KPRIORITY_old_priority=KeSetPriorityThread(thread,p_data->TargetPriority);KdPrint((DRIVER_PREFIX"Changed priority from %ld to %d",_old_priority,p_data->TargetPriority));ObDereferenceObject(thread);info=sizeof(ThreadData);io:Irp->IoStatus.Status=status;Irp->IoStatus.Information=info;IoCompleteRequest(Irp,0);returnstatus;}

The first thing we do is get a pointer to the IRP's stack withIoGetCurrentIrpStackLocation() - essentially returning a pointer to anIO_STACK_LOCATION structure which contains information associated with each IRP (more about these in some future article). Another important point to note before we proceed is to look into theBoosterCommon.h header file which defines theThreadData structure as follows:

typedefstruct_ThreadData {_In_ULONGThreadId;_In_intTargetPriority;}ThreadData,*PThreadData;

This structure will be shared by the Driver and the Client to pass information back and forth.

One important thing to do is to make sure that we got the right data from the Client and enforce the necessary checks on the Driver side of the code regardless of the restrictions imposed by the client code, just to make sure we don't get a BSOD.

The first thing we do is check the length of the buffer received from the Client - to ensure that we have received the complete structure. For this, we check theLength parameter of theWrite struct in theParameter union (phew - that was long). TheParameter union is an important component ofIO_STACK_LOCATION which contains many different structures corresponding to different IRPs, which, in our case, is theWrite structure. Coming back, we check theLength value ofWrite and in case we find that it is not equal to the size of theThreadData structure, we set the appropriateNTSTATUS and jump to complete the I/O Request.

Next, we need a pointer to the data sent through by the client and we get it from theUserBuffer component of theIRP structure. This is the user-mode address provided by the client but since we are operating from Kernel Land, we have access to it without any trouble. This isn't the best way(or the safest) way to go about things but this is what we are working with for now. Note that we can do this because the thread that makes the Write dispatch call, is the same one that jumps into the Kernel viaNtWriteFile syscall - hence we have the correct process context.

We map the buffer to a pointer of the typeThreadData and then proceed to check it the pointer is valid, followed by checks to ensure that the requested thread priority lies in the valid range of 1 to 31 (we cannot have a thread priority of 0 asonly the zero-page thread can have a priority of zero). If these checks fail, we again set the appropriateNTSTATUS and skip to completing the I/O Request.

Here comes the part where we actually change the thread's priority. I will work my way in the reverse order for the following pseudo-codeL

HANDLEh_tid=ULongToHandle(p_data->ThreadId);status=PsLookupThreadByThreadId(h_tid,&thread);if (!NT_SUCCESS(status))// return bad statusKeSetPriorityThread(thread,p_data->TargetPriority);

TheKeSetPriorityThread() function is responsible for setting the run-time priority of the thread. It takes in two parameters - a thread object and the target priority. We get the latter from theThreadData structure passed to us by the client. However, we do not have a thread object - we only have a Thread ID which the Client passes to us. To get the corresponding Thread object, we need thePsLookupThreadByThreadId() function which again, takes in two parameters - a Thread ID in the form of aHANDLE, and a pointerPETHREAD variable which receives the pointer to the Thread object. If this function fails, we again do the same old drill of completing the IRP with the error status. We are almost there - just one minor issue remains and it's the fact that thePsLookupThreadByThreadId() takes in aHANDLE as the first parameter, so we need to convert theThreadId member of theThreadData structure into the right format. Luckily, we have a macro for that calledULongToHandle() which does just that. Note that we cannot simply cast aULONG into aHANDLEas they differ in sizes, but a lazier workaround can be something like:

PsLookupThreadByThreadId((HANDLE)(ULONG_PTR)p_data->ThreadId,&thread);

One more thing to remember is that thePsLookupThreadByThreadId() function increases the thread's reference count - so the thread object won't be destroyed even after the thread dies, because it would still have an outstanding reference - at least one. So,KeSetPriorityThread() would never fail - because the thread object still has a valid reference count, and even though it might not make sense to change the priority of a thread that has died - it is still not an error.

However, onceKeSetPriorityThread() runs, we need to decrease the reference count or else the thread object lives forever till the system restarts. To do this, we use theObDereferenceObject() macro on the thread object to decrease the reference count. Finally, we set theinfo variable to the size of theThreadData structure and pass it toIrp->IoStatus.Information to indicate to the Client that the structure was processed(this is the value you get back in thelpNumberOfBytesWritten variable ofWriteFile() on the client's end).

We set the final bit of theIO_STATUS_BLOCK and complete the IRP request, as well as return the corresponding status from the dispatch routine.


That concludes everything on the driver's end for this project - time to write our client.


The Client

With the driver done, we can take a look at the client which we would use to send messages to the driver itself. The pseudo-code for the client looks as such:

#include<stdio.h>#include<stdlib.h>#include<Windows.h>#include"..\BoosterDriver\BoosterCommon.h"intmain(intargc,char*argv[]) {if (argc!=3) {usage();return-1;}ULONGthread_id= (ULONG)atoi(argv[1]);intt_priority=atoi(argv[2]);// Check for thread id    ...// Check for thread priority    ...// Try to print current base thread priority...// Make a call to driverThreadDatat_data= {0};t_data.ThreadId=thread_id;t_data.TargetPriority=t_priority;HANDLEhDevice=CreateFile(L"\\\\.\\Booster",GENERIC_WRITE,0,NULL,OPEN_EXISTING,0,NULL);BOOLbRes=WriteFile(hDevice,&t_data,sizeof(t_data),&ret,NULL);printf("[i] Priority changed to:\t\t%d",t_data.TargetPriority);CloseHandle(hDevice);return0;}

Before we begin diving intomain(), notice we include theBoosterCommon.h header which we had previously used in the Driver. We include this header file so we can have access to theThreadData structure used to pass information to the Driver.

Looking intomain(), we take in the target Thread ID and the priority we want to set it to from the command line. Then, after performing some checks to make sure the inputs are reasonable, we print the current thread's base priority.

We initialize aThreadData with the provided Thread ID and the target priority - we will use this to tell the driver what we want. Next up, we open a handle to theBooster Device with:

HANDLE hDevice = CreateFile(L"\\\\.\\Booster", GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);

Dissecting the parameters passed toCreateFile() we have as follows:

ParameterValueDescription
LPCSTR lpFileNameL"\\\\.\\Booster"The device path which points to the Booster device in theDevice Namespace
DWORD dwDesiredAccessGENERIC_WRITERequestWrite access
DWORD dwShareMode0Do not share Device
LPSECURITY_ATTRIBUTES lpSecurityAttributesNULLNo specific security attributes are required
DWORD dwCreationDispositionOPEN_EXISTINGThis is the only acceptable value for Devices as we can only open a handle to a device which already exists
DWORD dwFlagsAndAttributes0No special flags and attributes are necessary
HANDLE hTemplateFileNULLNo template file is necessary

This should get us a handle to the Device. We can then useWriteFile() to communicate with the Device and send the previously filledThreadData structure. If theWriteFile() succeeds, we should see that the Dynamic priority for the thread should be set to target priority.

Finally, we close the open handle to the device and exit out of the client program.


With that, we have completed the Driver and the Client programs. Once compiled, it is time to see them in action.


Driver-Client in Action

First, wecreate a kernel-mode service using theService Controller and use it to launch our driver while capturing events usingDebugView. As soon as we start the service, we should get an output inDebugView as such:

Along with this, we should also see two more things:

  • ABooster device was created under theDevice Object Namespace.

  • A symbolic link to theBooster Device in theGLOBAL?? namespace.

Okay, so our device and the symbolic link were created - so far so good. Time to see the client and driver in action. First, we pick a thread from a process to boost. We are going with this one frommsedge.exe with the Thread ID being8116:

We would try to set theDynamic Priority of this thread to, let's say, 20 using our client as:

BoosterClient.exe 8116 20

As soon as we do it, we should see more output onDebugView where it indicates that the priority change happened:

We see that the Client issued theCreate dispatch call to open a handle to the device, aWrite dispatch call to request a change in Thread Priority, and aClose dispatch call the close the previously acquired handle.

Further confirming that the priority was successfully boosted, we have,

Finally, we stop the Kernel service, which should cause the driver to be unloaded and the unload routine to be called - thereby deleting our device and symbolic link:

Therefore, our Client-Driver system seems to work as intended! Hurrah!


Conclusion

This concludes the code walkthrough of this project. This blog post is a part of my efforts to document my journey into Windows Kernel Land while going through@zodicon's Windows Internal training. Feel free to reach out to me with any and all feedback - and follow myGithub/LinkedIn for more updates in the future. Till next time - Happy Hacking 🎉

About

A step-by-step walkthrough of how to write a Client and a Driver to communicate with each other and boost the priority of a thread.

Topics

Resources

Stars

Watchers

Forks

Languages


[8]ページ先頭

©2009-2025 Movatter.jp