Just Let It Flow

February 14, 2009

Grabbing Kernel Thread Call Stacks the Process Explorer Way – Part 2

Filed under: Code,Windows — adeyblue @ 11:20 pm

Required Driver and Ancillary Functions

Contents:
  Introduction
  Process Explorer Functions
    Read Memory
  Required Driver Functions
    DriverEntry
    Unload
    Device Ioctl
  Building the driver
    Source Code

Last time, we discovered how Process Explorer gets a partial context for the kernel portions of a thread and wrote our own function that mimics it. By itself though, our code is useless; we need the rest of the driver in order to be able to use it, and that’s what we’ll be covering in this article.

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.

KStack driver source code.

[1] For more information on this see Introduction to MS-DOS Device Names.

No Comments

No comments yet.

RSS feed for comments on this post.

Sorry, the comment form is closed at this time.

Powered by WordPress