Required Driver and Ancillary Functions
Contents:
Introduction
Process Explorer Functions
Read Memory
Required Driver Functions
DriverEntry
Unload
Device Ioctl
Building the driver
Source Code
The Read Memory Routine
After the context, the next directly related function we need is one that’ll copy kernel memory on behalf of StackWalk64 so it can continue it’s analysis of the stack area and walk down the call chain. The routine itself isn’t complicated, in essence it’s nothing more than wrapper around memcpy. The problem which arises is how to determine whether the memory requested is valid and can be read without causing an access violation. In user mode, the worst that can happen is that the app will crash, however in a driver this will cause a BSOD. Before we tackle this, let’s see how Process Explorer handles and overcomes the problem.
; starting from procexp100.sys!+0xce6 mov edi,dword ptr [ebp+10h] ; ebp+10 = start address push dword ptr [edi] ; start address call dword ptr [PROCEXP100+0x290 (fb55a290)] ; MmIsAddressValid function pointer test al,al ; test for success je PROCEXP100+0xd40 (fb55ad40) ; error handler on fail mov eax,dword ptr [edi] ; copy start address to eax mov ecx,dword ptr [ebp+1Ch] ; move size to read to ecx lea eax,[eax+ecx-1] ; calculate end of range into eax push eax ; push end of range call dword ptr [PROCEXP100+0x290 (fb55a290)] ; MmIsAddressValid test al,al ; same again je PROCEXP100+0xd40 (fb55ad40) ; error handler
The start and end addresses of the range to copy are passed to the MmIsAddressValid function which is called via a function pointer. This checks the passed in addresses to see if accessing them will cause a page fault or not. If both return TRUE, the copy is performed as below otherwise the function returns STATUS_ACCESS_VIOLATION. Now, MmIsAddressValid isn’t a silver bullet. As explained is several places including the functions doc page, an MSDN document on buffer handling and an MS kernel developer’s blog, the return value is all but useless as soon as it’s returned because the memory could’ve been freed/paged out or otherwise made invalid by a different thread. For this specific application however, its use should be ok because the memory being probed is part of a thread stack for a suspended or otherwise inert thread, so we’ll continue using it in lieu of a better solution. The rest of the function just copies the bytes into the output buffer using an intrinsic form of memcpy:
mov ecx,dword ptr [ebp+1Ch] ; size read to ecx mov dword ptr [esi],ecx ; and into dereferenced esi mov esi,dword ptr [edi] ; start address into esi mov edi,dword ptr [ebp+18h] ; output buffer to edi mov eax,ecx ; copy size to eax shr ecx,2 ; divide ecx by 4 so it contains the number of dwords to copy rep movs dword ptr es:[edi],dword ptr [esi] ; copy ecx dwords from source to dest mov ecx,eax ; copy back count to ecx and ecx,3 ; get the number of odd bytes left rep movs byte ptr es:[edi],byte ptr [esi] ; copy any odd bytes left over
and that’s as hard as it gets again. A one-to-one transform of this assembly to C is almost enough, all that needs adding is parameter validation and a way to tell DeviceIoControl how many bytes we copied and we have our memcpy wrapper.
NTSTATUS ReadMemory(ReadRequest* request, ULONG inputLen, PVOID outBuffer, ULONG outLen, ULONG* bytesCopied) { NTSTATUS stat = STATUS_SUCCESS; *bytesCopied = 0; /* initialize */ /* Validate parameters */ if(request && outBuffer && (inputLen >= sizeof(*request)) { /* ensure we don't read more than we can or more then we have to */ const ULONG bytesToRead = min(request->bytes, outLen); /* Get the delimiting pointers */ UCHAR* startOfRange = (UCHAR*)request->address; UCHAR* endOfRange = startOfRange + (bytesToRead - 1); /* Check the range is valid */ if(MmIsAddressValid(startOfRange) && MmIsAddressValid(endOfRange)) { RtlCopyMemory(outBuffer, request->address, bytesToRead); *bytesCopied = bytesToRead; } else { /* assume pointers were invalid */ stat = STATUS_ACCESS_VIOLATION; } } else { /* we were passed one or more invalid parameters */ stat = STATUS_INVALID_PARAMETER; } return stat; }
That, along with our GetThreadContext function is all the custom code we need for our driver. The rest is standard boilerplate for creating a device we can connect to with CreateFile and implementing the io controls. If you’ve done any driver programming before, there’s not likely to be anything new here. Anyhow with that said, on with the show…
Required Functions
DriverEntry
The first function called when a driver is loaded is DriverEntry(). It passes 2 arguments to the driver in the form of an object that represents the driver in the system, and a string containing the path to the registry key where the drivers’ data is kept. Even though there’s no provision user provided arguments its purpose is analogous to that provided by main() and WinMain. In a slight twist from those functions though, it should only perform initialization tasks and then return to the OS which will call the function pointers specified when an event occurs that the driver is interested in. For our driver, we’re only interested in Open (CreateFile), Close (CloseHandle), Device Control(DeviceIoControl) and Unload (StopService / computer shutdown) so those are the only events we hook up.
/* * Fill in the handlers for those requests we're interested in. * Set create and close to a simple success function * And the others to specialized functions */ pDrvObj->MajorFunction[IRP_MJ_CREATE] = pDrvObj->MajorFunction[IRP_MJ_CLOSE] = KStack_SimpleOK; pDrvObj->MajorFunction[IRP_MJ_DEVICE_CONTROL] = KStack_DevIOCTL; pDrvObj->DriverUnload = KStack_Unload;
Of course, to be able to receive the first three events we need to create something so that our driver is visible to user mode. To this end, the first step is to create a representation of our driver as an openable device using the aptly named IoCreateDevice.
/* creating a device called KStack */ UNICODE_STRING deviceName = RTL_CONSTANT_STRING(L"\Device\KStack"); PDEVICE_OBJECT interfaceDevice = NULL; */ NTSTATUS status = IoCreateDevice( pDriverObj, /* IoCreateDevice associates the device with our driver so we pass on the the object we got from DriverEntry. */ 0, /* Don't require an extension */ &deviceName, /* name of the device */ FILE_DEVICE_UNKNOWN, /* FILE_DEVICE_UNKNOWN is passed as our device type because none of the other defined types cover what our driver is */ FILE_READ_ONLY_DEVICE | FILE_DEVICE_SECURE_OPEN, /* flag the device as read-only and enforce the security descriptor */ FALSE, /* Not an exclusive device */ &interfaceDevice); /* Resulting device */
Unfortunately, this device by itself isn’t visible to user mode, so we need an extra bit of code to do that. Fortunately, this extra code is just one function called IoCreateSymbolicLink that puts a link to our driver in the global namespace where the user-mode accessible devices live[1]. After implementing this, that’s it for our DriverEntry, minus error checking. Putting it all together gives us something like this
/* these values never change and are required in both Entry and Unload funcs * unfortunately they can't be made const */ static UNICODE_STRING deviceName = RTL_CONSTANT_STRING(L"\Device\KStack"); static UNICODE_STRING deviceLink = RTL_CONSTANT_STRING(L"\DosDevices\KStack"); NTSTATUS DriverEntry(PDRIVER_OBJECT pDrvObj, PUNICODE_STRING pRegPath) { /* first create the device */ PDEVICE_OBJECT interfaceDevice = NULL; NTSTATUS status = IoCreateDevice(pDrvObj, 0, &deviceName, FILE_DEVICE_UNKNOWN, FILE_READ_ONLY_DEVICE | FILE_DEVICE_SECURE_OPEN, FALSE, &interfaceDevice); if(NT_SUCCESS(status)) { status = IoCreateSymbolicLink (&deviceLink, &deviceName); /* KdPrint is a macro that is a equivalent to a debug mode printf() * It evaluates to nothing in optimized builds * Use DbgPrint to print regardless of debug/release mode */ KdPrint(("KStack: IoCreateSymbolicLink returned 0x%xn", status)); /* * Fill in the handlers for those requests we're interested in. * Set create and close to a simple success function * And the others to specialized functions */ pDrvObj->MajorFunction[IRP_MJ_CREATE] = pDrvObj->MajorFunction[IRP_MJ_CLOSE] = KStack_SimpleOK; pDrvObj->MajorFunction[IRP_MJ_DEVICE_CONTROL] = KStack_DevIOCTL; pDrvObj->DriverUnload = KStack_Unload; } /* If something went wrong, delete what we created */ if (!NT_SUCCESS(status)) { IoDeleteSymbolicLink(&deviceLink); if(interfaceDevice ) { IoDeleteDevice( interfaceDevice ); } } KdPrint(("KStack: DriverEntry returning 0x%xn", status)); return status; }
Unload
The unload function is also nothing special. It reverses the initialization done in DriverEntry and exits. As seen in the failure case for Entry, this entails deleting the symbolic link that makes us visible to user mode and deleting the device which we created. The only argument to the unload function is the driver object that was passed to entry. This time, we use it to access the device that was associated with it by IoCreateDevice.
void KStack_Unload(PDRIVER_OBJECT DriverObject) { KdPrint(("KStack: Unloadingn")); /* Delete the symbolic link */ IoDeleteSymbolicLink(&deviceName); KdPrint(("KStack: Deleted the symbolic linkn")); /* Delete the device object */ IoDeleteDevice(DriverObject->DeviceObject); KdPrint(("KStack: Deleted the devicen")); }
IOCTL Handler
An IOCTL Handler acts as a broker between the various control codes defined by the driver writer or the system. Typically, it uses a switch on the accepted control codes to route the call to the relevant driver provided functionality. The handler takes two parameters, the device created by IoCreateDevice and an IO Request Packet. This parameter contains all the pertinent information regarding the request made. The main members we’re interested in are the system buffer, where the input is kept and output data will be stored, and the IoStatus so we can signal how many bytes we’re returning and the return values from our functions. There are a multitude of other parameters contained in the IRP but we don’t need them.
NTSTATUS KStack_DevIOCTL(PDEVICE_OBJECT pDevice, PIRP pIrp) { PIO_STACK_LOCATION iosp = IoGetCurrentIrpStackLocation (pIrp); /* Parse the request details out of the relevant buffers. * The below isn't a typo. With the ioctl mode we're using, the input buffer * is also the output buffer. */ PVOID inBuffer = pIrp->AssociatedIrp.SystemBuffer; PVOID outBuffer = pIrp->AssociatedIrp.SystemBuffer; ULONG inLength = iosp->Parameters.DeviceIoControl.InputBufferLength; ULONG outLength = iosp->Parameters.DeviceIoControl.OutputBufferLength; ULONG ioctl = iosp->Parameters.DeviceIoControl.IoControlCode; PIO_STATUS_BLOCK ioStatus = &(pIrp->IoStatus); /* call the relevant function if any */ switch(ioctl) { case IOCTL_READ_MEMORY: { ioStatus->Status = ReadMemory((ReadRequest*)inBuffer, inLength, outBuffer, outLength, &(ioStatus->Information)); } break; case IOCTL_THREAD_CONTEXT: { ioStatus->Status = GetThreadContext((ULONG*)inBuffer, inLength, (ThreadCtx*)outBuffer, outLength, &(ioStatus->Information)); } break; default: { ioStatus->Status = STATUS_NOT_SUPPORTED; ioStatus->Information = 0; } } /* Tell the IO Manager that we're finished with the packet * and not to boost the thread that made the request */ IoCompleteRequest(pIrp, IO_NO_INCREMENT); return ioStatus->Status; }
The last function we use is SimpleOK. It’s called when either CreateFile or CloseHandle are called for our driver. All it does is signal that the processing of the I/O Request Packet was successful, tells the IO manager we’re finished with the packet and finally returns success.
NTSTATUS KStack_SimpleOK(PDEVICE_OBJECT pDevice, PIRP pIrp) { /* set IRP success */ pIrp->IoStatus.Status = STATUS_SUCCESS; pIrp->IoStatus.Information = 0; /* tell the manager we've finished with it * and not to boost priority of the thread that created the request */ IoCompleteRequest(pIrp, IO_NO_INCREMENT ); return STATUS_SUCCESS; }
Building the driver
In order to build this, or any other driver you’ll need a version of the Windows Driver Kit or its predecessor the Driver Development Kit. If you don’t have one installed, the latest version is available for free on Microsoft Connect. You’ll be required to sign in with a Live ID, once logged in go to the connection directory and find the “Windows Driver Kit (WDK), Windows Logo Kit (WLK) and Windows Driver Framework (WDF)” and click the Apply Now link. At time of writing, this is a copy of said link, log in is still required.
To build the driver, an extra file called sources (with no extension) is required to give the build tools information about what type of program is being built, which source files to compile, which import libraries are required, compiler flags etc. As ours is a simple single file build, sources is as succinct as:
TARGETNAME = KStack
TARGETTYPE = DRIVER
TARGETLIBS = $(DDK_LIB_PATH)ntoskrnl.lib # $(DDK_LIB_PATH) is a macro that expands to the WDK lib directory
SOURCES = KStack.c
Actually building is then a matter of selecting the desired build-environment from the Windows Driver Kit entry on the Start menu, navigating the command prompt to the directory containing the sources file and executing “build -gz”.
Producing the KStack driver concludes this part of series. We covered how Process Explorer checks whether memory accesses are valid and coded an equivalent function, a brief overview of the required driver functions and their purposes, and how to setup and build the driver. In the final part we’ll cover interaction with the driver and putting everything together in a working application.
[1] For more information on this see Introduction to MS-DOS Device Names.