Thursday, October 4, 2012

Suggestions

Hi everyone. It seems lately I'm having a hard time coming up with good topics, so I wanted to ask you to send me emails with topics you might be interested in. These could be things you already know but you think might be beneficial to have posted somewhere to the rest of the community or things you're not sure about. However, please keep the requests specific. Things like "minifilter interaction with the memory manager" are simply too vague to be helful. You could ask about specific functions or behaviors or structure members (but again, please keep it specific). You can find my email address on my contact information on this blog.

Thanks,
Alex.

Thursday, September 27, 2012

A Helpful Debugging Technique: Debug Stream Contexts

In this post I'd like to discuss a debugging technique that I found pretty useful. I'm a big fan of keeping lots of information around in debug builds and ASSERTing all over the place. I try to be very careful about what information I keep so that I don't change the actual flow of execution for the code. For example, I tend to try to not add references when I copy some pointers because I don't want to alter when freeing the memory happens for the objects I'm tracking (but this approach requires extra care when asserting since I don't want to use pointers to free memory).
One particularly useful thing I like is to keep track of all the files I've processed in my filter and remember various decisions I made about them and keep back-pointers to things that would otherwise be impossible to find (for example when I base my decision on some parameters in the FLT_CALLBACK_DATA I might take a snapshot of those parameters; or I might keep a stack trace for certain operations and so on). The problem that arises with this approach is, of course, where to store this information. In some cases it's feasible to use some of the structures I'm already using (StreamContext or StreamHandleContext) and just add extra members in debug builds. However, there are other times where this becomes a significant pain and it would alter the filter quite a bit (for example, if the logic of the filter is to check for presence of a context to decide on what to do with a file then adding a context in which I only populate the debugging information for files I don't intend to process would change that logic). Also, certain contexts might store data that isn't fixed in size (like a file name or path) which I typically set to be the last thing in the context so adding more debugging information (which might itself be dynamic in size) would make things very complicated.
The ideal approach would be to set more than one context on an object, but FltMgr's functions don’t really allow that. Well, here is where the some FsRtl functions really come in handy. There is support built in the file system library (FsRtl code) for contexts (which I've discussed in more detail in my post on Contexts in legacy filters and FSRTL_ADVANCED_FCB_HEADER) and the library supports setting multiple contexts for the same stream, with different keys. Moreover, the contexts have two keys and there are pretty cool searching semantics that allow searching on only one key. The functions that deal with this are FsRtlInitPerStreamContext, FsRtlInsertPerStreamContext and FsRtlLookupPerStreamContext. They’re pretty easy to use and there is even a page on MSDN on Creating and Using Per-Stream Context Structures.
Here is some code that shows how to use these functions:
typedef struct _MY_DEBUG_CONTEXT {

    //
    // this is the header for the perStream context structure. 
    //

    FSRTL_PER_STREAM_CONTEXT PerStreamContext;

    // 
    // this is the information we want to track.
    //
    
    BOOLEAN Flag;

} MY_DEBUG_CONTEXT, *PMY_DEBUG_CONTEXT;



VOID
FreeDebugContext(
    __in PVOID ContextToFree
    )
{

    //
    // here we would release and uninitialize any other data.. 
    //

    //
    // and then we free the actual context.
    //
    
    ExFreePoolWithTag( ContextToFree, 'CgbD' );
}


NTSTATUS
SetDebugContext(
    __in PFILE_OBJECT FileObject,
    __in PVOID Key1,
    __in_opt PVOID Key2,
    __in BOOLEAN Flag
    )
{
    PMY_DEBUG_CONTEXT context = NULL;
    NTSTATUS status = STATUS_SUCCESS;

    __try {

        //
        // allocate and initialize the context.
        //

        context = ExAllocatePoolWithTag( PagedPool, 
                                         sizeof(MY_DEBUG_CONTEXT),
                                         'CgbD' );

        if (context == NULL) {

            status = STATUS_INSUFFICIENT_RESOURCES;
            __leave;    
        }

        FsRtlInitPerStreamContext( &context->PerStreamContext,
                                   Key1,
                                   Key2,
                                   FreeDebugContext );

        //
        // initialize all the members that we want to track.
        //

        context->Flag = Flag;

        //
        // try to set the context.
        //

        status = FsRtlInsertPerStreamContext( FsRtlGetPerStreamContextPointer(FileObject),
                                              &context->PerStreamContext );

        NT_ASSERT(status == STATUS_SUCCESS);

    } __finally {

        if (!NT_SUCCESS(status)) {

            if (context != NULL) {
            
                ExFreePoolWithTag( context, 'CgbD' );
            }
        }
    }

    return status;
}


BOOLEAN
GetDebugContextFlag(
    __in PFILE_OBJECT FileObject,
    __in PVOID Key1,
    __in_opt PVOID Key2
    )
{
    PMY_DEBUG_CONTEXT context = NULL;
    
    context = (PMY_DEBUG_CONTEXT)FsRtlLookupPerStreamContext( FsRtlGetPerStreamContextPointer(FileObject),
                                                              Key1,
                                                              Key2 );

    if ((context != NULL) &&
        (context->Flag)) {

        return TRUE;
    }

    return FALSE;
}
Finally, one more thing to show is how easy it is to find this structure in the debugger:
0: kd> dt @@(tempFileObject) nt!_FILE_OBJECT FsContext  <- get the FsContext
   +0x00c FsContext : 0xa24d05f8 Void
0: kd> dt 0xa24d05f8 nt!_FSRTL_ADVANCED_FCB_HEADER FilterContexts. <- look at the FilterContexts  in the FsContext
   +0x02c FilterContexts  :  [ 0xa25a1108 - 0x936e93e4 ]
      +0x000 Flink           : 0xa25a1108 _LIST_ENTRY [ 0x936e93e4 - 0xa24d0624 ]
      +0x004 Blink           : 0x936e93e4 _LIST_ENTRY [ 0xa24d0624 - 0xa25a1108 ]
0: kd> dl 0xa25a1108 <- look at all the entries in the list
a25a1108  936e93e4 a24d0624 9441eb40 92e3cb40
936e93e4  a24d0624 a25a1108 93437438 a24d05f8
a24d0624  a25a1108 936e93e4 00000000 a24d05f4
0: kd> !pool a25a1108 2  <- check the pool type for the first one
Pool page a25a1108 region is Paged pool
*a25a1100 size:   20 previous size:  100  (Allocated) *DbgC  <- this is our structure
  Owning component : Unknown (update pooltag.txt)
0: kd> !pool 936e93e4  2   <- for fun, let's see what the other context is 
Pool page 936e93e4 region is Nonpaged pool
*936e93d8 size:   68 previous size:    8  (Allocated) *FMsl   
  Pooltag FMsl : STREAM_LIST_CTRL structure, Binary : fltmgr.sys  <- obviously FltMgr's context
0: kd> dt a25a1108 _MY_DEBUG_CONTEXT  <- display the debugging context
PassThrough!_MY_DEBUG_CONTEXT
   +0x000 PerStreamContext : _FSRTL_PER_STREAM_CONTEXT
   +0x014 Flag             : 0x1 ''

Thursday, September 20, 2012

Volume Mount Points, Directory Junctions and STATUS_ACCESS_VIOLATION

Hi everyone. I've just spent a couple of days debugging an issue that I think is pretty interesting and I hope publishing this post might save someone a lot of pain.
Before I go into the specifics, let me explain a bit about my setup. My filter opens files for its own use but on behalf of the user (in other words it opens files in response to certain user actions, but the user must have access to the files as well (which is different from certain kinds of filters where the filter uses files but there is no connection between the user and the files; in those cases the files are similar to file system metadata)). It does this by calling FltCreateFile and it uses OBJ_FORCE_ACCESS_CHECK to make sure that all the access checks are performed. The files that the filter opens depend on the file that the user opens and so it's possible that the filter ends up opening symlinks or volume mount points and such. In these cases the file system will return a STATUS_REPARSE and the create will be retried on a different path; the same can happen if a file system filter returns STATUS_REPARSE. If the new path happens to be located on a different volume (which is usually the case with volume mount points, though not always since one can in fact create a mountpoint for C:\ at C:\mnt for example…) then the FltCreateFile call will generally fail with STATUS_MOUNT_POINT_NOT_RESOLVED. I've discussed this at great length in my post on Reparsing to a Different Volume in Win7 and Win8.
The problem I was running into was that when my filter tried to open a path containing a volume mount point it would fail with STATUS_ACCESS_DENIED. This behavior was different from symlinks where the call to FltCreateFile still failed with STATUS_MOUNT_POINT_NOT_RESOLVED. This was something I wanted to investigate since the STATUS_ACCESS_VIOLATION status is one I've learned to pay attention to because it generaly indicates a bug in my code. I thought that maybe I was calling FltCreateFile with some parameter that wasn't set up correctly which led to NTFS or the IO manager or the Object manager to try an invalid access, which was probably trapped in some exception handler and returned to the caller. This was definitely worth fixing since such problems can be exploited.
So I started the looong process of debugging this issue. Being an access violation I figured it comes from some improper memory access, which is different from calling some function that returns an error, because most instructions perform memory accesses so my usual approach of stepping to the next call and walking over it to see the return status would not work. Moreover, I had no guarantee that if I set a breakpoint later on I will actually hit it since any of the memory accesses up to that breakpoint could in fact be the culprit. And, to make things worse, this was in the create path for the file system, which is heavily exercised on a running system, which means that any breakpoint that I set in the debugger that's not threaded will often trigger when another thread happens to try to do a create. Anyway, to spare you the gory details, I eventually found the problem and this is what the stack looked like when the instruction I was about to execute was the one that triggered the access violation:
0: kd> kbn L0xa
 # ChildEBP RetAddr  Args to Child              
00 b0583190 82a8f20f 92637001 92637001 b0583734 nt!ObpCaptureObjectCreateInformation+0x61
01 b05831dc 82ae2587 b0583734 924d6f78 92637001 nt!ObOpenObjectByName+0x9b
02 b0583338 82ae17f6 b0583734 00120089 92637001 nt!IopFastQueryNetworkAttributes+0x127
03 b05833a4 82a88936 92637001 dc6a18a4 b0583550 nt!IopQueryNetworkAttributes+0x40
04 b0583480 82a6926b 938e34d8 974d6f78 92637008 nt!IopParseDevice+0x115e
05 b05834fc 82a8f2d9 00000000 b0583550 00000640 nt!ObpLookupObjectName+0x4fa
06 b0583558 82a8762b b0583734 924d6f78 00010000 nt!ObOpenObjectByName+0x165
07 b05835d4 82abee29 b058371c 00120089 b0583734 nt!IopCreateFile+0x673
08 b0583630 96297b62 b058371c 00120089 b0583734 nt!IoCreateFileEx+0x9e
09 b05836bc 9631f8f1 92ed7008 938fd008 b058371c fltmgr!FltCreateFileEx2+0xba
0: kd> u
nt!ObpCaptureObjectCreateInformation+0x61:
82a78339 8a01            mov     al,byte ptr [ecx]
82a7833b 833b18          cmp     dword ptr [ebx],18h
0: kd> r
eax=7fff0000 ebx=b0583734 ecx=7fff0000 edx=00000000 esi=00000000 edi=9260dd94
eip=82a78339 esp=b0583154 ebp=b0583190 iopl=0         ov up ei pl nz na po nc
cs=0008  ss=0010  ds=0023  es=0023  fs=0030  gs=0000             efl=00000a02
nt!ObpCaptureObjectCreateInformation+0x61:
82a78339 8a01            mov     al,byte ptr [ecx]          ds:0023:7fff0000=??
So as you can see we 're trying to read one byte from ECX, which is set to 0x7fff0000. Since we're in kernel mode this will fail with access violation. Let's look a bit up the stack to see where that weird ECX value came from (and I added a couple instructions on the bottom just to get the full picture):
0: kd> ub . L0xE
nt!ObpCaptureObjectCreateInformation+0x35:
82a7830c 807d0800        cmp     byte ptr [ebp+8],0    <- compare a parameter to the function with 0. 
82a78310 7429            je      nt!ObpCaptureObjectCreateInformation+0x63 (82a7833b) <- and jump if it's 0
82a78312 64a124010000    mov     eax,dword ptr fs:[00000124h] <- look at some address 
82a78318 80b83a01000000  cmp     byte ptr [eax+13Ah],0 <- and see if another byte is 0
82a7831f 741a            je      nt!ObpCaptureObjectCreateInformation+0x63 (82a7833b) <- and if that is 0 jump to the same place as above (so we're probably exiting an IF statement)
82a78321 8bcb            mov     ecx,ebx
82a78323 f6c303          test    bl,3 <- check if any of the least significant two bits in ebx are set 
82a78326 7406            je      nt!ObpCaptureObjectCreateInformation+0x56 (82a7832e)
82a78328 e8281f0d00      call    nt!ExRaiseDatatypeMisalignment (82b4a255) <- and if they're set then raise an exception for data misalignment
82a7832d cc              int     3 <- and look, there's a breakpoint
82a7832e a11c079b82      mov     eax,dword ptr [nt!MmUserProbeAddress (829b071c)] <- load MmUserProbeAddress into EAX
82a78333 3bd8            cmp     ebx,eax <- and compare it with EBX
82a78335 7202            jb      nt!ObpCaptureObjectCreateInformation+0x61 (82a78339) <- and exit the IF statement if EBX is smaller than MmUserProbeAddress
82a78337 8bc8            mov     ecx,eax <-move MmUserProbeAddress into ECX
82a78339 8a01            mov     al,byte ptr [ecx] <- and try to read a byte from it.
82a7833b 833b18          cmp     dword ptr [ebx],18h
0: kd> dp nt!MmUserProbeAddress L1
829b071c  7fff0000
Before I explain what's going on let's see what the IF is actually checking:
0: kd> dp fs:[00000124h] L1
0030:00000124  926bfd48
0: kd> !pool 926bfd48 2
Pool page 926bfd48 region is Nonpaged pool
*926bfd18 size:  2e8 previous size:   10  (Allocated) *Thre (Protected)   <- this is a thread!
  Pooltag Thre : Thread objects, Binary : nt!ps
0: kd> dt nt!_KTHREAD
   +0x000 Header           : _DISPATCHER_HEADER
   +0x010 CycleTime        : Uint8B
...
   +0x13a PreviousMode     : Char   <-and at offset 0x13A we have the PreviousMode !
...
Let's piece it all together. This piece of code is trying to figure out if a buffer that it receives as a parameter (I didn't show that code but that's what's in EBX) is accessible. It first looks at some boolean parameter and if that's TRUE then it looks at the PreviousMode and if that's UserMode then it checks the buffer for alignment (and it raises an exception if it's not aligned) and then it checks it's in user mode memory range ( smaller than MmUserProbeAddress ). If it's not smaller than MmUserProbeAddress then it just does a probe at MmUserProbeAddress. The buffer in question is the _OBJECT_ATTRIBUTES structure, which is actually the same one as the one I pass in for the original create. This is interesting because ObOpenObjectByName can be seen twice on the stack, with the same _OBJECT_ATTRIBUTES parameter, but the first time the call to nt!ObpCaptureObjectCreateInformation works when checking the buffer and the second time it doesn't. Since the PreviousMode is UserMode in both cases, the only difference is that the boolean parameter that ObpCaptureObjectCreateInformation is called with is 1 in the first case and 0 in the second case. This parameter seems to be related to OBJ_FORCE_ACCESS_CHECK since if I don't use OBJ_FORCE_ACCESS_CHECK then the parameter is 1 in both cases and no access violation happens.
One additional thing I'd like to explain is why this check happens only for volume mount points and not symlinks. Volume mount points are different from symlinks in that there before a new IRP_MJ_CREATE is sent to the new path, for volume mount points the IO manager checks whether the user has access to the target volume. The same thing happens for directory junctions, which are similar in implementation to volume mount points. Symlinks don't perform this check and instead the new IRP_MJ_CREATE is simply issued to the new path and access checks will be performed there. Now, for volume mount points this access check takes the form of a IopQueryNetworkAttributes, which in turn is implemented as a special kind of OPEN operation (hence the call to ObOpenObjectByName). It's this second open where ObpCaptureObjectCreateInformation fails and the access violation happens.
This problem was fixed in Win8 and it was very likely a windows bug.
Before I end this post I'd like to summarize some of the things I've discussed:
  • There is a windows 7 bug where if FltCreateFile tries to open a path that traverses a volume mount point or directory junction the call will fail with STATUS_ACCESS_VIOLATION. Fixed in Win8.
  • This happens because of the extra access checking for volume mount points and directory junctions.
  • This doesn't happen for symlinks.
  • Workaround: I can't think of anything a filter could do but one posibility to address this problem is to ask customers to not use volume mount points and instead use directory symlinks.
Finally, in closing I'd like to show some code (a modification of the PassThrough sample) that calls FltCreateFile whenever the user tries to open anything that has the name "mnt" (file or folder). After the FltCreateFile call the minifilter breaks so that I get a chance to look at the status returned by the FltCreateFile. Here is the code:

    NTSTATUS status;

    UNREFERENCED_PARAMETER( FltObjects );
    UNREFERENCED_PARAMETER( CompletionContext );

    PT_DBG_PRINT( PTDBG_TRACE_ROUTINES,
                  ("PassThrough!PtPreOperationPassThrough: Entered\n") );




    if (Data->Iopb->MajorFunction == IRP_MJ_CREATE) {

        UNICODE_STRING myFile = RTL_CONSTANT_STRING( L"mnt" );
        OBJECT_ATTRIBUTES fileAttributes;
        HANDLE fileHandle = NULL;
        IO_STATUS_BLOCK ioStatus;
        PFLT_FILE_NAME_INFORMATION fileName = NULL;  

        NTSTATUS ecpFindStatus;
        ULONG targetEcpSize = 0;

        __try {

            //  
            // this is a preCreate, get the name.  
            //  

            status = FltGetFileNameInformation( Data,   
                                                FLT_FILE_NAME_OPENED | FLT_FILE_NAME_QUERY_DEFAULT,   
                                                &fileName );  
            if (!NT_SUCCESS(status)) {

                DbgBreakPoint();
                __leave;
            }
    
            //
            // we need to parse the name to get the final component
            //

            status = FltParseFileNameInformation( fileName );

            if (!NT_SUCCESS(status)) {

                DbgBreakPoint();
                __leave;
            }

            //
            //  Compare to see if this is a file we care about.
            //

            if (!RtlEqualUnicodeString( &fileName->FinalComponent,
                                        &myFile,
                                        TRUE )) {

                __leave;
            }

            //
            // intialize the attributes and issue the create.
            //

            InitializeObjectAttributes( &fileAttributes, 
                                        &fileName->Name,
                                        OBJ_KERNEL_HANDLE | OBJ_FORCE_ACCESS_CHECK,
                                        NULL,
                                        NULL );

            status = FltCreateFileEx2( FltObjects->Filter,
                                       FltObjects->Instance,
                                       &fileHandle,
                                       NULL,
                                       FILE_READ_DATA | SYNCHRONIZE | FILE_READ_ATTRIBUTES,
                                       &fileAttributes,
                                       &ioStatus,
                                       NULL,
                                       FILE_ATTRIBUTE_NORMAL,
                                       FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
                                       FILE_OPEN,
                                       FILE_SYNCHRONOUS_IO_ALERT,
                                       NULL,
                                       0,
                                       0,
                                       NULL );


            DbgBreakPoint();
        } __finally {

            if (fileName != NULL) {

                FltReleaseFileNameInformation( fileName );
            }

            if (fileHandle != NULL) {

                ZwClose(fileHandle);
            }
        }
    }



    //
    //  See if this is an operation we would like the operation status
    //  for.  If so request it.
    //
    //  NOTE: most filters do NOT need to do this.  You only need to make
    //        this call if, for example, you need to know if the oplock was
    //        actually granted.
    //

    if (PtDoRequestOperationStatus( Data )) {

        status = FltRequestOperationStatusCallback( Data,
                                                    PtOperationStatusCallback,
                                                    (PVOID)(++OperationStatusCtx) );
        if (!NT_SUCCESS(status)) {

            PT_DBG_PRINT( PTDBG_TRACE_OPERATION_STATUS,
                          ("PassThrough!PtPreOperationPassThrough: FltRequestOperationStatusCallback Failed, status=%08x\n",
                           status) );
        }
    }

Thursday, September 13, 2012

Interesting article

I'm pretty swamped at work so I won't post anything this week, but I'd like to share an article that I found interesting. It's about some malware, Backdoor.Proxybox, that hooks NTFS directly. This is the page on Symantec's blog: Backdoor.Proxybox: Kernel File System Hooking.

Thursday, September 6, 2012

Completing Information Queries in File System Filters

Completing IRP_MJ_QUERY_INFORMATION requests isn't a terribly difficult task but I wanted to discuss some of the basics as well as share some code to show how a minifilter might go about doing this. I've already discussed some of the more advanced topics related to this in my previous posts on Filters And IRP_MJ_QUERY_INFORMATION and on Setting IoStatus.Information.

There are many information classes and many of them can be found on the documentation page for the ZwQueryInformationFile function, though the complete list is documented on the 2.4 File Information Classes page in the MS_FSCC document. Please note that not all of them can be used with IRP_MJ_QUERY_INFORMATION. However, for this post I'll only be focusing on those that can.

Pretty much each information class has an associated structure. For example, the information class of FileAlignmentInformation has the associated FILE_ALIGNMENT_INFORMATION structure. Some of these structures have a fixed size so in that case all a filter has to do is to populate the information and complete the request:

        switch ( Data->Iopb->Parameters.QueryFileInformation.FileInformationClass ) {

            case FilePositionInformation:
                {
                    PFILE_POSITION_INFORMATION positionInfo = Data->Iopb->Parameters.QueryFileInformation.InfoBuffer;

                    // retrieve the current position from the file object

                    positionInfo->CurrentByteOffset = FltObjects->FileObject->CurrentByteOffset;

                    // then complete the operation

                    Data->IoStatus.Status = STATUS_SUCCESS;
                    Data->IoStatus.Information = sizeof(positionInfo->CurrentByteOffset);
                    callbackStatus = FLT_PREOP_COMPLETE;
                }
                break;
 ....
 }
 ...
 return callbackStatus;

The filter can expect that a buffer of the appropriate size is received, otherwise the IO manager will reject the operation with STATUS_INFO_LENGTH_MISMATCH. This is done inside the IO manager, where the IO manager has an array of minimum sizes for each information class (can be seen in the debugger as nt!IopQueryOperationLength) and if the size is smaller than that then the request will fail with the error code I mentioned:

0: kd> u nt!NtQueryInformationFile+0x36 L0xB
nt!NtQueryInformationFile+0x36:
82aa87dc 8b4518          mov     eax,dword ptr [ebp+18h]
82aa87df 83f838          cmp     eax,38h
82aa87e2 7377            jae     nt!NtQueryInformationFile+0xb5 (82aa885b)
82aa87e4 8a80d4a0a782    mov     al,byte ptr nt!IopQueryOperationLength (82a7a0d4)[eax]
82aa87ea 84c0            test    al,al
82aa87ec 746d            je      nt!NtQueryInformationFile+0xb5 (82aa885b)
82aa87ee 0fb6c0          movzx   eax,al
82aa87f1 394514          cmp     dword ptr [ebp+14h],eax
82aa87f4 730a            jae     nt!NtQueryInformationFile+0x5a (82aa8800)
82aa87f6 b8040000c0      mov     eax,0C0000004h
82aa87fb e98e080000      jmp     nt!NtQueryInformationFile+0x8e3 (82aa908e)

Now, there are certain information classes that have variable length. In general this happens with classes that contain names (for example, FileNameInformation) or that return certain lists (like FileStreamInformation that returns all the alternate data streams for the file). In these cases it's impossible for the IO manager to know what the required size might be without querying the file system and so file systems and file system filters will receive requests with what is possibly an insufficient buffer size (there are apps that allocate huge buffer just to avoid reissuing a request, but there are many callers that don't do that; I generally prefer to issue a request with enough buffer in cases where that's possible, for example most files don't have Alternate Data Streams so it's possible to calculate the size of the buffer). Those requests will be failed by the file system (or filter) with the STATUS_BUFFER_OVERFLOW status (which is actually a warning, not an error!). However, different information classes have different requirements about how the structure has to be populated in such cases. There are some structures (like FILE_NAME_INFORMATION) that have a field where the caller can fill in the required size so that the caller can just allocate a buffer of the appropriate size and resend the request. Other structures don't have any such field (like FILE_STREAM_INFORMATION) and so any caller must simply increase the size of the buffer without knowing in advance if it will be sufficient (I personally double the size of the buffer for each iteration but there are other ways of doing it) and then they must issue the request and see whether it was enough.

Finally, there are certain requirements for each information class about how to fill in the buffer even in the event when the request is going to be failed with STATUS_BUFFER_OVERFLOW. For example, for FileNameInformation the buffer must be filled with as many characters as will fit the buffer whereas the FILE_STREAM_INFORMATION must be filled only with complete records. These type of requirements make it necessary for whoever completes the request to be able to specify how much of the buffer actually contains real information. This can be achieved by setting the IoStatus.Information member to the number of bytes to return the caller. The IO manager will then copy the specified number of bytes to the caller's buffer. It is very important to get this right because returning a larger number here will result in the IO manager copying more bytes than were actually written by the file system or filter and is a security problem. For more details on this see Doron's post on How to return the number of bytes required for a subsequent operation. Of course, the specifics of how the buffer must be set up for each class are documented in the pages for each information class or associated structure, so whenever dealing with this subject make sure to re-read those pages and pay great attention to the details.

I wanted to show some more code on how to do this, but there is plenty of code that shows how this is done in the file system sources that come with the WDK. I found the CDFS sources to be a bit more clear for this specific topic, where CdCommonQueryInfo pretty much does it all and shows exactly how to set things for most information classes. FastFat can be a bit more convoluted at times.

Finally, I want to say that whenever I have to deal with this I find it extremely useful to just use FileTest and then query the information class I want while supplying different sized buffers (starting with 1 byte, then 2 bytes and so on) and make sure that I understand exactly what the file system returns without my filter present and how things look with the filter installed.

Thursday, August 30, 2012

Mount Point Resolution with FltGetFileNameInformation - Part II

In the previous post I showed a couple of examples of how FltMgr handles mount point resolution. I also ran into a problem where querying for the normalized name during preCreate for a path that would traverse a mountpoint failed, and I promised I would investigate and post my findings. In this post I will explain what I found and how I went about it (which might be interesting for someone that doesn't have a lot of experience debugging the workings of fltmgr or other kernel components).
So first, let me explain my findings. As a quick refresher, FltMgr performs name normalization by taking an opened name for a file (like C:\foo~1\bar~2.txt) and opening the parent directory (C:\foo~1) and then querying the directory (via the directory enumeration call) for the file name (bar~2.txt). FltMgr uses one of the directory structures that returns the long name for the file (like FILE_BOTH_DIR_INFORMATION or FILE_NAMES_INFORMATION) and then copies the long name for the file (let's say it is BarBarBar.txt). It then does the same for current directory name (for C:\foo~1 it would open C:\ and query for foo~1 and get back FooFooFoo) and then assembles everything back together into C:\FooFooFoo\BarBarBar.txt. Now, this process will fail with STATUS_NOT_SAME_DEVICE if when trying to open a parent directory (C:\foo~1) FltMgr encounters a reparse point that reparses to a different volume. The relevant code is this:
0: kd> u fltmgr!FltpExpandFilePathWorker+0x29e
fltmgr!FltpExpandFilePathWorker+0x29e:
96034a1c ff154c030296    call    dword ptr [fltmgr!_imp__IoGetAttachedDevice (9602034c)]
96034a22 ff765c          push    dword ptr [esi+5Ch]
96034a25 8bf8            mov     edi,eax
96034a27 ff1548010296    call    dword ptr [fltmgr!_imp__IoGetRelatedDeviceObject (96020148)]
96034a2d 3bc7            cmp     eax,edi
96034a2f 740c            je      fltmgr!FltpExpandFilePathWorker+0x2bf (96034a3d)
96034a31 c74508d40000c0  mov     dword ptr [ebp+8],0C00000D4h
96034a38 e9e8feffff      jmp     fltmgr!FltpExpandFilePathWorker+0x1a7 (96034925)
As you can see, FltMgr compares two devices, the one on which the IRP_MJ_CREATE to open the parent directory was issued and the one where it was actually opened on, and if they don't match it will fail with STATUS_NOT_SAME_DEVICE (which is an interesting and confusing choice since the actual error message (from ntstatus.h) is "The target file of a rename request is located on a different device than the source of the rename request." and there is no rename involved). There are a couple of interesting points to make:
  • This should only impact preCreate queries since after the IRP_MJ_CREATE completes successfully we are on the final volume and so the opened file name can't traverse any reparse points (as discussed in my previous post on this subject). So in postCreate (and this implies a successful create) there will be no reparse points to traverse.
  • The other interesting thing to note is that FltMgr actually issues its IRP_MJ_CREATE violating the file system filter layering rules, in that it sends the request to the top of the IO stack. It does this with the explicit goal of being able to survive reparses to other volumes (like I've discussed elsewhere on this blog). However, it then checks to figure out if it actually ended up on a different volume and fails the operation, which would have happened anyway if they targeted the IRP_MJ_CREATE properly. The only case where this might actually work and the proper layering of the IRP_MJ_CREATE would have failed is where there multiple reparses that eventually end up on the original volume where the IRP_MJ_CREATE was issued on (so you'd have C:\foo~1 reparse to D:\bar~1 which would in turn reparse back to C:\baz~1). This would fail with proper layering of the IRP_MJ_CREATE because the first reparse (from C:\ to D:\) would fail, but it would work with the current implementation because the request is targeted at C:\ and the directory that is opened eventually is on C:\ as well). However, I doubt that this behavior is what FltMgr's designers had in mind and instead I tend to believe that violating the layering in this case was unnecessary.  
Anyway, let me quickly show how I've debugged this issue to find where the error comes from in a couple of minutes. I'll first show you the code that I added to the passthrough sample to detect when this case happens:
    if (PtDoRequestOperationStatus( Data )) {

        status = FltRequestOperationStatusCallback( Data,
                                                    PtOperationStatusCallback,
                                                    (PVOID)(++OperationStatusCtx) );
        if (!NT_SUCCESS(status)) {

            PT_DBG_PRINT( PTDBG_TRACE_OPERATION_STATUS,
                          ("PassThrough!PtPreOperationPassThrough: FltRequestOperationStatusCallback Failed, status=%08x\n",
                           status) );
        }
    }

    if (Data->Iopb->MajorFunction == IRP_MJ_CREATE) {

        status = FltGetFileNameInformation( Data, 
                                            FLT_FILE_NAME_NORMALIZED,
                                            &fileName );

        if (NT_SUCCESS(status)) {

            DbgPrint("preCreate|normalized -> \"%wZ\"\n", &fileName->Name); 

            FltReleaseFileNameInformation( fileName );

        } else {

            DbgBreakPoint();

            status = FltGetFileNameInformation( Data, 
                                                FLT_FILE_NAME_NORMALIZED,
                                                &fileName );

        
        }
As you can see, when the call to FltGetFileNameInformation fails I've added a breakpoint and then I retry the operation with the exact same parameters, which allows me to investigate the call. After that, when the breakpoint triggers, I do this:
Break instruction exception - code 80000003 (first chance)    <- here I've hit the breakpoint
PassThrough!PtPreOperationPassThrough+0xd2:
a414c0f2 cc              int     3
0: kd> p <- step over the breakpoint
PassThrough!PtPreOperationPassThrough+0xd3:
a414c0f3 8d45f8          lea     eax,[ebp-8]
0: kd> t <- trace into the function
fltmgr!FltGetFileNameInformation:
9601ce78 8bff            mov     edi,edi
0: kd> pc <- find the next call
fltmgr!FltGetFileNameInformation+0x118:
9601cf90 e833010000      call    fltmgr!FltpAllocateInitializeNameGenerationContext (9601d0c8)
0: kd> p <- and step over it
fltmgr!FltGetFileNameInformation+0x11d:
9601cf95 8bf0            mov     esi,eax
0: kd> r <- and now inspect the registers. In this calling convention eax will be the NTSTATUS , so eax = 00000000 means STATUS_SUCCESS
eax=00000000 ebx=92639008 ecx=944f6ae0 edx=944f6ae0 esi=92639068 edi=00000000
eip=9601cf95 esp=a194d96c ebp=a194d988 iopl=0         nv up ei pl zr na pe nc
cs=0008  ss=0010  ds=0023  es=0023  fs=0030  gs=0000             efl=00000246
fltmgr!FltGetFileNameInformation+0x11d:
9601cf95 8bf0            mov     esi,eax
0: kd> pc <- ok, find the next call
fltmgr!FltGetFileNameInformation+0x126:
9601cf9e e85df8ffff      call    fltmgr!FltpGetFileNameInformation (9601c800)
0: kd> p <- step over it 
fltmgr!FltGetFileNameInformation+0x12b:
9601cfa3 8bf0            mov     esi,eax
0: kd> r <- inspect the result again and note that eax=c00000d4, which happens to be STATUS_NOT_SAME_DEVICE .
eax=c00000d4 ebx=92639008 ecx=00000000 edx=00000003 esi=00000000 edi=00000000
eip=9601cfa3 esp=a194d96c ebp=a194d988 iopl=0         nv up ei pl zr na pe nc
cs=0008  ss=0010  ds=0023  es=0023  fs=0030  gs=0000             efl=00000246
fltmgr!FltGetFileNameInformation+0x12b:
9601cfa3 8bf0            mov     esi,eax
So now I know that the STATUS_NOT_SAME_DEVICE error came from the call to fltmgr!FltpGetFileNameInformation. Time to continue this investigation, but this time looking into that function:
0: kd> g <- hit go and trigger the condition (which in my case was trying to traverse a mountpoint so I just did a notepad C:\mnt\foo.txt, where C:\mnt was a mount point)
Break instruction exception - code 80000003 (first chance)
PassThrough!PtPreOperationPassThrough+0xd2:
a414c0f2 cc              int     3 <- so I hit the breakpoint again, as expected. 
0: kd> bp /t @$thread fltmgr!FltpGetFileNameInformation  <- set a threaded breakpoint on the function where I know the failure should happen. The thread is useful when a function is called from multiple threads and I don't want the breakpoint to trigger for those.
0: kd> g <- hit go and we should hit the breakpoint when the current thread enters that function
Breakpoint 0 hit  <- and so it is...
fltmgr!FltpGetFileNameInformation:
9601c800 8bff            mov     edi,edi
0: kd> bc * <- clear the breakpoint . This is sometimes necessary because if there is recursion it will hit again on the same thread and might confuse things
0: kd> pc <- find the first call
fltmgr!FltpGetFileNameInformation+0x48:
9601c848 e8db9bffff      call    fltmgr!FltpGetNextCallbackNodeForInstance (96016428)
0: kd> p <- and step over to see the status
fltmgr!FltpGetFileNameInformation+0x4d:
9601c84d 894614          mov     dword ptr [esi+14h],eax
0: kd> r <- check the status, which is STATUS_SUCCESS
eax=00000000 ebx=92639008 ecx=9444cd48 edx=00000000 esi=94501858 edi=00000000
eip=9601c84d esp=a194d864 ebp=a194d888 iopl=0         nv up ei pl zr na pe nc
cs=0008  ss=0010  ds=0023  es=0023  fs=0030  gs=0000             efl=00000246
fltmgr!FltpGetFileNameInformation+0x4d:
9601c84d 894614          mov     dword ptr [esi+14h],eax ds:0023:9450186c=00000000
0: kd> pc <- find the next call
fltmgr!FltpGetFileNameInformation+0x5a:
9601c85a e8c99bffff      call    fltmgr!FltpGetNextCallbackNodeForInstance (96016428)
0: kd> p <- step again
fltmgr!FltpGetFileNameInformation+0x5f:
9601c85f 894618          mov     dword ptr [esi+18h],eax
0: kd> r <- STATUS_SUCCESS again
eax=00000000 ebx=92639008 ecx=9444cd48 edx=00000000 esi=94501858 edi=00000000
eip=9601c85f esp=a194d864 ebp=a194d888 iopl=0         nv up ei pl zr na pe nc
cs=0008  ss=0010  ds=0023  es=0023  fs=0030  gs=0000             efl=00000246
fltmgr!FltpGetFileNameInformation+0x5f:
9601c85f 894618          mov     dword ptr [esi+18h],eax ds:0023:94501870=00000000
0: kd> pc <- and then the next call
fltmgr!FltpGetFileNameInformation+0xb2:
9601c8b2 e829160000      call    fltmgr!FltpGetStreamListCtrl (9601dee0)
0: kd> p 
fltmgr!FltpGetFileNameInformation+0xb7:
9601c8b7 894508          mov     dword ptr [ebp+8],eax
0: kd> r <- this fails with STATUS_NOT_SUPPORTED. This might be a failure that gets converted into the one we're looking for. I investigated it (not shown here) but it doesn't result in a failure, it just changes the code path through the function
eax=c00000bb ebx=92639008 ecx=9601e0a8 edx=00000000 esi=94501858 edi=00000000
eip=9601c8b7 esp=a194d864 ebp=a194d888 iopl=0         nv up ei pl zr na pe nc
cs=0008  ss=0010  ds=0023  es=0023  fs=0030  gs=0000             efl=00000246
fltmgr!FltpGetFileNameInformation+0xb7:
9601c8b7 894508          mov     dword ptr [ebp+8],eax ss:0010:a194d890=94501858
0: kd> pc <-find the next call
fltmgr!FltpGetFileNameInformation+0xc2:
9601c8c2 e887fdffff      call    fltmgr!HandleStreamListNotSupported (9601c64e)
0: kd> p <- step over
fltmgr!FltpGetFileNameInformation+0xc7:
9601c8c7 894508          mov     dword ptr [ebp+8],eax
0: kd> r <- and we can see that we've hit the status we wanted, eax=c00000d4. so now we know the failure comes from fltmgr!HandleStreamListNotSupported, so set a breakpoint on it.
eax=c00000d4 ebx=92639008 ecx=00000000 edx=00000003 esi=94501858 edi=00000000
eip=9601c8c7 esp=a194d864 ebp=a194d888 iopl=0         nv up ei pl nz na po nc
cs=0008  ss=0010  ds=0023  es=0023  fs=0030  gs=0000             efl=00000202
fltmgr!FltpGetFileNameInformation+0xc7:
9601c8c7 894508          mov     dword ptr [ebp+8],eax ss:0010:a194d890=c00000bb
0: kd> g <- so now we need to go back to trigger the breakpoint
Break instruction exception - code 80000003 (first chance)
PassThrough!PtPreOperationPassThrough+0xd2:
a414c0f2 cc              int     3
0: kd> bp /t@$thread fltmgr!HandleStreamListNotSupported  <- set our threaded breakpoint on that function
0: kd> g <- and run
Breakpoint 0 hit
fltmgr!HandleStreamListNotSupported:
9601c64e 8bff            mov     edi,edi
After a couple of iterations (in about 5 minutes of debugging) we end up with this stack, where the status is actually generated.:
0: kd> k
ChildEBP RetAddr  
a194d8ac 96034c59 fltmgr!FltpExpandFilePathWorker+0x2b3
a194d8c4 96034dc3 fltmgr!FltpExpandFilePath+0x19
a194d8e0 96035505 fltmgr!FltpGetNormalizedFileNameWorker+0x7d
a194d8f8 96032765 fltmgr!FltpGetNormalizedFileName+0x19
a194d910 9601c773 fltmgr!FltpCreateFileNameInformation+0x81
a194d930 9601c8c7 fltmgr!HandleStreamListNotSupported+0x125
a194d960 9601cfa3 fltmgr!FltpGetFileNameInformation+0xc7
a194d988 a414c102 fltmgr!FltGetFileNameInformation+0x12b
a194d9ac 96016aeb PassThrough!PtPreOperationPassThrough+0xe2 [c:\temp11\passthrough\passthrough.c @ 705]
a194da18 960199f0 fltmgr!FltpPerformPreCallbacks+0x34d
a194da30 9602d1fe fltmgr!FltpPassThroughInternal+0x40
a194da44 9602d8b7 fltmgr!FltpCreateInternal+0x24
a194da88 828884bc fltmgr!FltpCreate+0x2c9
a194daa0 82a8c6ad nt!IofCallDriver+0x63
a194db78 82a6d26b nt!IopParseDevice+0xed7
a194dbf4 82a932d9 nt!ObpLookupObjectName+0x4fa
a194dc50 82a8b62b nt!ObOpenObjectByName+0x165
a194dccc 82ac667e nt!IopCreateFile+0x673
a194dd14 8288f44a nt!NtOpenFile+0x2a
a194dd14 776064f4 nt!KiFastCallEntry+0x12a
WARNING: Stack unwind information not available. Following frames may be wrong.






Thursday, August 23, 2012

Mount Point Resolution in FltGetFileNameInformation

In this post I'd like to discuss one particular aspect of using FltGetFileNameInformation and how mount point resolution is handled. The issue I want to talk about is mentioned in the documentation for the FLT_FILE_NAME_INFORMATION structure, where in the Remarks section it says that a name is normalized if all the mount points are resolved. This was mentioned in the context of the OSR thread http://www.osronline.com/showThread.CFM?link=230176, where it was suggested that the definition means that opened names would not resolve mount points (see the discussion starting at message 21).
So the plan is to discuss how mount points work and how name normalization works in this context and what's the actual behavior for mount point resolution for normalized and opened names.
A mount point is a special type of reparse point, where it reparses from a directory to the root of a volume. A typical scenario would be mounting a volume like \Device\HarddiskVolume4\ as a folder under \Device\HarddiskVolume3\mnt\ (the volume names are actual names that I'm going to use in my examples later). This can be easily done using mountvol.exe, like so:
C:\>mountvol.exe

Creates, deletes, or lists a volume mount point.
....
....

Possible values for VolumeName along with current mount points are:

    \\?\Volume{a681bf9c-c97b-11de-90d7-806e6f6e6963}\
        *** NO MOUNT POINTS ***

    \\?\Volume{f4810a4e-cfbb-11de-86cd-000c291f01a1}\
        D:\

    \\?\Volume{f4810a5a-cfbb-11de-86cd-000c291f01a1}\
        E:\

    \\?\Volume{a681bf9d-c97b-11de-90d7-806e6f6e6963}\
        C:\

    \\?\Volume{a681bfa1-c97b-11de-90d7-806e6f6e6963}\
        A:\

    \\?\Volume{a681bfa0-c97b-11de-90d7-806e6f6e6963}\
        F:\

C:\>mountvol.exe D:\mnt \\?\Volume{f4810a5a-cfbb-11de-86cd-000c291f01a1}\

C:\>mountvol.exe

Creates, deletes, or lists a volume mount point.
....
....

Possible values for VolumeName along with current mount points are:

    \\?\Volume{a681bf9c-c97b-11de-90d7-806e6f6e6963}\
        *** NO MOUNT POINTS ***

    \\?\Volume{f4810a4e-cfbb-11de-86cd-000c291f01a1}\
        D:\

    \\?\Volume{f4810a5a-cfbb-11de-86cd-000c291f01a1}\
        E:\
        D:\mnt\

    \\?\Volume{a681bf9d-c97b-11de-90d7-806e6f6e6963}\
        C:\

    \\?\Volume{a681bfa1-c97b-11de-90d7-806e6f6e6963}\
        A:\

    \\?\Volume{a681bfa0-c97b-11de-90d7-806e6f6e6963}\
        F:\
Please note that I can still use both E:\ and D:\mnt\ to access the volume. One could also remove the E:\ mount point and rely only on D:\mnt\.
So what happens when someone tries to open D:\mnt\foo.txt ? Here are the steps, and pretty much most of this has been explained in other posts on this site, mainly around STATUS_REPARSE and IRP_MJ_CREATE processing:
  1. the IO manager issues an IRP_MJ_CREATE for the volume that is D: (\Device\HarddiskVolume3) with the FILE_OBJECT->FileName set to "\mnt\foo.txt"
  2. NTFS sees there is a reparse point on "\mnt" and returns the new path for the file, concatenating the name of the volume in the reparse point with the rest of the path, as "\Device\HarddiskVolume4\FOO.TXT"
  3. the IO manager issues a new IRP_MJ_CREATE for the volume that is E: (\Device\HarddiskVolume4) with the FILE_OBJECT->FileName set to "\foo.txt", which succeeds.
Before we start looking at what the names look like, let's quickly discuss what FltMgr does when generating names. There are two steps here, first FltMgr will generate a name for the file (the Generate step) and then, if the caller requested it, it will normalize it (the Normalize step).
The Normalize step is pretty simple so we'll discuss it first. FltMgr opens the parent directory for the name it has from the Generate step and does a directory query to get the long name associated with the file (or directory). It doesn't know if the name it has for the file is short or long and it doesn't really matter, it will figure it out from the directory query anyway. It then concatenates this with the normalized name for the parent directory. If it doesn't have the normalized name for the parent directory cached it will build it using the same algorithm. Please note that this is different in Win8, where NTFS can directly return the normalized name for a file. Also, this can explain why it can be pretty expensive to generate a normalized name, though FltMgr's name cache is pretty effective and so in most cases there is no need to perform the normalization of the parent directory name.
The Generate step differs between the time when the file isn't opened (preCreate) and the time after the file is opened (postCreate and all operations after that). If the file isn't opened FltMgr will take the name from the FILE_OBJECT->FileName and the RelatedFileObject (if there is one). If the file is opened then FltMgr will simply query NTFS for the file name using an IRP_MJ_QUERY_INFORMATION request. Please note that the OPENED name is actually the name FltMgr gets from the Generate step.
So now that all the pieces are in place, let's discuss the differences between the opened and the normalized name:
  • If the file is opened (so after a successful postCreate and for any operation where the FILE_OBJECT is opened) the name will always have the mount point resolved because during the Generate step FltMgr will simply concatenate the volume name with the path to the file that NTFS returns.
  • If this is a postCreate but the status is STATUS_REPARSE FltGetFileNameInformation will fail with STATUS_FLT_INVALID_NAME_REQUEST, so the issue of the mount point resolution doesn't apply.
  • The one case that remains is that of a preCreate, then whether mount points are resolved depends on whether this is the final IRP_MJ_CREATE or not. If it is the final IRP_MJ_CREATE then IO manager has already resolved the mount points so the opened name and the normalized name will have the same volume name. If this is not the final IRP_MJ_CREATE then the opened name will always have the volume name on which the IRP_MJ_CREATE was issued, and the normalized name will always use the name it has when opening the parent folder for the final component. This last bit is interesting because it's not clear what happens when opening the actual mount point. Will it be resolved or not ? It depends on how the query for the normalization is done, if necessary.
So let's go over some examples. First I'm going to show what happens when I open D:\mnt\folder_under_mount_point\foo.txt:
preCreate|opened -> "\Device\HarddiskVolume3\mnt\folder_under_mount_point\foo.txt" -> as expected, mount point not resolved.
preCreate|normalized -> "\Device\HarddiskVolume4\folder_under_mount_point\foo.txt" -> this is after the reparse, so mount point is resolved.
preCreate|opened -> "\Device\HarddiskVolume4\FOLDER_UNDER_MOUNT_POINT\FOO.TXT"  -> after reparse, so no mount point. Please note the case.
postCreate|normalized -> "\Device\HarddiskVolume4\folder_under_mount_point\foo.txt" -> normalize gets the actual proper case from NTFS
postCreate|opened -> "\Device\HarddiskVolume4\FOLDER_UNDER_MOUNT_POINT\FOO.TXT" -> but the opened name has the original case from the user's request.
Things to note here are the fact that there are no names from the postCreate for the first creates since they fail with STATUS_FLT_INVALID_NAME_REQUEST because those creates aren't successful. Also, interesting to note that for the first create (for which we get the opened name) trying to get the normalized name fails with STATUS_NOT_SAME_DEVICE on my system. Not sure why but I'll investigate it for next week's blog :).
Now let's look at what happens when we open something directly under the mount point (pretty much the same as above):
preCreate|opened -> "\Device\HarddiskVolume3\mnt\foo.txt" -> as expected, mount point not resolved.
preCreate|normalized -> "\Device\HarddiskVolume4\foo.txt" -> and again, after reparse, mount point resolved.
preCreate|opened -> "\Device\HarddiskVolume4\FOO.TXT" 
postCreate|normalized -> "\Device\HarddiskVolume4\foo.txt"
postCreate|opened -> "\Device\HarddiskVolume4\FOO.TXT"
And finally I want to show you what happens if I try to open the actual mount point itself, D:\mnt\.
preCreate|normalized -> "\Device\HarddiskVolume3\mnt" -> please note how the normalized name in this case is actually the mount point, so the mount point is NOT resolved.
preCreate|opened -> "\Device\HarddiskVolume3\mnt\" -> naturally, mount point not resolved since this is the opened name.
preCreate|normalized -> "\Device\HarddiskVolume4\"
preCreate|opened -> "\Device\HarddiskVolume4\"
postCreate|normalized -> "\Device\HarddiskVolume4\"
postCreate|opened -> "\Device\HarddiskVolume4\"
Hopefully this shows what's going on with mount point resolution and opened and normalized names, in more detail that anyone ever needed :).