Thursday, March 3, 2011

Duplicating User Mode Handles

Among the many new verifier checks in Win7 is a particular one about using user handles in kernel mode. I won't go into the details of why that is potentially bad and instead I'll focus on how to work around the issue. However I'd like to point to some documentation explaining the improvements in Driver Verifier in Win7 in general and this check in particular. There is a PPT "Driver Verifier Advancements In Windows 7" that is pretty good for a high-level view and there is also a more detailed document "Driver Verifier in Windows 7". The verifier bugcheck message in this case is this:

DRIVER_VERIFIER_DETECTED_VIOLATION (c4)
A device driver attempting to corrupt the system has been caught.  This is
because the driver was specified in the registry as being suspect (by the
administrator) and the kernel has enabled substantial checking of this driver.
If the driver attempts to corrupt the system, bugchecks 0xC4, 0xC1 and 0xA will
be among the most commonly seen crashes.
Arguments:
Arg1: 000000f6, Referencing user handle as KernelMode.
Arg2: xxxxxxxx, Handle value being referenced.
Arg3: xxxxxxxx, Address of the current process.
Arg4: xxxxxxxx, Address inside the driver that is performing the incorrect reference.
Before we go into more detail I'd also like to explain what I was trying to do in one case where I ran into this issue. I was writing a driver that had a user mode command line utility that was used to send commands to the driver. One of these commands required sending a user mode file to the driver so it could write logging information into that file. One approach could be to pass in the name of the file and use the driver to open the file, but this is not trivial for various reasons:

  • How to get the file name? The user might call the command line utility with a relative path (like "foo.exe -file ..\bar.txt") and so I need to figure out either the full path or to send the current directory path to the driver (yuck!).
  • Even using a full path wouldn't be enough because the drive letter might be different depending on the session the user is in. Besides, who knows what a path really points to ? Some followers of this blog might know how much I dislike file names and how I try to avoid them.
  • The user might not actually have access to write to that file but the kernel would so I would have to impersonate the user before trying to create the file.
So anyway my decision was to open the file in user mode and then call the driver and tell it the handle to the file and let the driver figure out what the object is and how to use it. However, what I wanted was to use the ZwWriteFile API to write to the file and so I needed a handle to that object. Looking at the OB APIs it's easy to see that we could simply call ObReferenceObjectByHandle followed by ObOpenObjectByPointer to create the new kernel handle. This is what my code looked like:
        status = ObReferenceObjectByHandle( ioctlBuffer->UserHandle,
                                            FILE_READ_DATA | FILE_WRITE_DATA | SYNCHRONIZE | STANDARD_RIGHTS_READ | FILE_READ_ATTRIBUTES,
                                            *IoFileObjectType,
                                            UserMode,
                                            &userFileObject,
                                            NULL );
 
        if (!NT_SUCCESS(status)) {
 
            __leave;
        }
 
        ASSERT(FlagOn( userFileObject->Flags, FO_HANDLE_CREATED ) && 
                   !FlagOn( userFileObject->Flags, FO_CLEANUP_COMPLETE ));
 
        status = ObOpenObjectByPointer( userFileObject,
                                        OBJ_KERNEL_HANDLE,
                                        NULL,
                                        0,//FILE_READ_DATA | FILE_WRITE_DATA,
                                        *IoFileObjectType,
                                        KernelMode,
                                        &kernelFileHandle);
This worked pretty well for local files but when I tried to open a file that was on a remote file system it failed to open the kernel handle with STATUS_ACCESS_DENIED . I spent some time tracing through the code and what I found was that ObOpenObjectByPointer in this case always ends up sending an IRP_MJ_QUERY_SECURITY request to the file system. Moreover, this request seemed to always ask for all the security information (which you can see if you disassemble nt!ObpGetObjectSecurity and look at how it sets the SecurityInformation; on my Win7 it's a "mov dword ptr [xxx],1Fh"):
#define OWNER_SECURITY_INFORMATION       (0x00000001L)
#define GROUP_SECURITY_INFORMATION       (0x00000002L)
#define DACL_SECURITY_INFORMATION        (0x00000004L)
#define SACL_SECURITY_INFORMATION        (0x00000008L)
#define LABEL_SECURITY_INFORMATION       (0x00000010L)
 
3: kd> dt 0xfffff9801a3d8fb8 nt!_IO_STACK_LOCATION Parameters.QuerySecurity.
   +0x008 Parameters                : 
      +0x000 QuerySecurity             : 
         +0x000 SecurityInformation       : 0x1f
         +0x008 Length                    : 0x100
However, while this works well on the local system, it almost always fails over SMB. Looking at the page "2.2.1.3 SECURITY_INFORMATION", there is this table that describes what the caller needs in order to be able to read various information types. For SACL_SECURITY_INFORMATION we see that in fact READ_CONTROL is not enough and that a certain privilege is required. This privilege is unlikely to be granted to any client of the server and so in the general case the IRP_MJ_QUERY_SECURITY issued by nt!ObpGetObjectSecurity will fail with STATUS_ACCESS_DENIED.

Security information access requested
Rights required of caller on server
Privileges required of caller on server
OWNER_SECURITY_INFORMATION
READ_CONTROL
Does not apply.
GROUP_SECURITY_INFORMATION
READ_CONTROL
Does not apply.
DACL_SECURITY_INFORMATION
READ_CONTROL
Does not apply.
SACL_SECURITY_INFORMATION
Does not apply.
Security privilege.

So now since this approach was out of the picture, I needed something else. Unfortunately, I have been unable to figure out a documented way to achieve this (pretty much anything I tried called nt!ObpGetObjectSecurity at some point). However, there is one undocumented function that actually does exactly what I wanted, ZwDuplicateObject. So now my code looks something like this:
    //
    // duplicate the handle... first get a handle to the system process
    // so we can call ZwDuplicateObject on it.
    //

    status = ObOpenObjectByPointer( PsInitialSystemProcess,
                                    OBJ_KERNEL_HANDLE,
                                    NULL,
                                    STANDARD_RIGHTS_READ,
                                    NULL,
                                    KernelMode,
                                    &systemProcessHandle );

    
    if (!NT_SUCCESS(status)) {

        return status;
    }

    status = ZwDuplicateObject( NtCurrentProcess(),
                                ioctlBuffer->UserHandle,
                                systemProcessHandle,
                                &kernelFileHandle,
                                FILE_READ_DATA | FILE_WRITE_DATA,
                                OBJ_KERNEL_HANDLE,
                                DUPLICATE_SAME_ATTRIBUTES | DUPLICATE_SAME_ACCESS );

This approach works because when using DUPLICATE_SAME_ACCESS ZwDuplicateObject() doesn't actually try to validate the access. This works fine in cases like the one I described where I didn't want any more rights than the user had. However, if the driver needs more (or maybe just different) access to the object then this function will also perform access checks.
Another possible approach would have been to open the file again in the driver and create a new handle, which would work because the security privilege isn't necessary to open a file on a remote server. However in this case I would have had to make sure that the user had the right type of access to the file. There is also the performance issue to consider, since issuing a new IRP_MJ_CREATE is not exactly cheap. It didn't really matter in my case but I'm just mentioning it here just in case.
And finally, there are some caveats to consider for this approach:
  • Because we've duplicated the handle, we are effectively using the same FILE_OBJECT as the user and so any changes we make will potentially affect them. For example, if the IO manager keeps track of the current byte offset for this FILE_OBJECT, operations the driver might perform change that and so the user mode component might get confused. So if the driver is planning on being transparent to the user mode client, then it needs to be extra careful about this sort of things. This wasn't a concern in my case since the user mode client was aware it was sending the handle to a driver and didn't use the handle afterwards, but it might be different for a minifilter.
  • Since we've duplicated the handle, it is possible that IRP_MJ_CLEANUP no longer arrives in the context of the user process (depending on whether the kernel handle gets closed first or not). This might have an impact on some minifilters as well as on any byte range locks on the file.
  • Since ZwDuplicateObject() is not documented it might not be supported in this form (or at all) in future OS releases. Though IMO Microsoft should document this API.
  • In XP and Server 2003 (SRV03) there is a bug (fixed in XP SP3 and SRV03 R2 SP1 (I'm not sure about the version)) where the handle that is returned by ZwDuplicateObject is a kernel handle (it belongs in the system process' handle table) but is not marked as such (the most significant bit is not set).
  • Finally, there is one other flag to ZwDuplicateObject that might be interesting to anyone using to duplicate user mode handles, DUPLICATE_CLOSE_SOURCE. This closes the user mode handle before the function returns. According to Gary Nebbett's book, it will close the handle regardless of the status of the operation.

No comments:

Post a Comment