When Does “Output” Mean “Input”?

After more philosophization on the meaning of direct IOCTL codes, I came to the conclusion that I’ve never used METHOD_IN_DIRECT in a driver. Naturally, I wondered if it was any different than METHOD_OUT_DIRECT. Boy, was that an interesting investigation.

To start off with, you have to know a thing or two about how an IRP works. An IRP is the basic data structure passed into all driver dispatch routines. It contains all of the caller’s parameters, as well as an associated data structure that replaces the traditional stack used during function calls. In particular, IRPs have a member called MdlAddress. Note that it doesn’t say “InMdlAddress” and “OutMdlAddress” – it’s just MdlAddress.

After some consideration, I determined that when a usermode app calls DeviceIoControl() or NtDeviceIoControlFile() on a METHOD_IN_DIRECT code, it must just pass its data in the InputBuffer into the driver at MdlAddress. I put together a quick test driver to verify this fact. Nope, wrong.

The next step was to look around for any sample code that calls DeviceIoControl() with METHOD_IN_DIRECT. I searched my DDKs for about 5 minutes and finally gave up – the only samples I found were calling from the kernel, and not calling NtDeviceIoControlFile().

After fiddling with the code for long enough to convince myself that I wasn’t crazy (riiiiight), I decided to do what any sane developer would do in a similar situation: I broke out WinDbg. Knowing that all IOCTL requests from user mode end up calling NtDeviceIoControlFile, I disassembled that function:

kd> ln nt!NtDeviceIoControlFile
(8052af7e)   nt!NtDeviceIoControlFile   |  (8052afaa)   nt!NtFsControlFile
Exact matches:
    nt!NtDeviceIoControlFile = 
kd> u 8052af7e 8052afaa
nt!NtDeviceIoControlFile:
8052af7e 55               push    ebp
8052af7f 8bec             mov     ebp,esp
8052af81 6a01             push    0x1
8052af83 ff752c           push    dword ptr [ebp+0x2c]
8052af86 ff7528           push    dword ptr [ebp+0x28]
8052af89 ff7524           push    dword ptr [ebp+0x24]
8052af8c ff7520           push    dword ptr [ebp+0x20]
8052af8f ff751c           push    dword ptr [ebp+0x1c]
8052af92 ff7518           push    dword ptr [ebp+0x18]
8052af95 ff7514           push    dword ptr [ebp+0x14]
8052af98 ff7510           push    dword ptr [ebp+0x10]
8052af9b ff750c           push    dword ptr [ebp+0xc]
8052af9e ff7508           push    dword ptr [ebp+0x8]
8052afa1 e84ea70000       call    nt!IopXxxControlFile (805356f4)
8052afa6 5d               pop     ebp
8052afa7 c22800           ret     0x28

It looks like NtDeviceIoControlFile just hops directly to IopXxxControlFile(), which is not exported. Disassembling that function in WinDbg shows that this is where the real magic happens. Some selected lines:

kd> ln nt!IopXxxControlFile
(805356f4)   nt!IopXxxControlFile   |  (80535dac)   nt!IopInitializeBootLogging
Exact matches:
    nt!IopXxxControlFile = 
kd> u 805356f4 80535dac
8053579f e846befdff       call    nt!ProbeForWrite (805115ea)
805357ed e8409ff6ff       call    nt!ObReferenceObjectByHandle (8049f732)
805358da e85408efff       call    nt!IoGetRelatedDeviceObject (80426133)
805358e4 e86f06efff       call    nt!IoGetAttachedDevice (80425f58)
80535aea e84deaeeff       call    nt!IoAllocateIrp (8042453c)
80535c16 e84cebeeff       call    nt!IoAllocateMdl (80424767)

etc…

OK, so now I know we’re in the right function. Now I look for what happens to METHOD_IN_DIRECT, which (according to the DDK) is type 1. That IoAllocateMdl call looks promising, too, as we know that the function should only be allocating a MDL for DIRECT I/O. Some exploration yields:

80535c0c 53               push    ebx
80535c0d 6a01             push    0x1
80535c0f 56               push    esi
80535c10 ff752c           push    dword ptr [ebp+0x2c]
80535c13 ff7528           push    dword ptr [ebp+0x28]
80535c16 e84cebeeff       call    nt!IoAllocateMdl (80424767)

Now, remember that arguments are pushed on the stack backwards, so ebp+0x28 will be VirtualAddress, ebp+0x2c will be Length, esi (which is xor’d to 0) represents a FALSE for SecondaryBuffer, 0x1 is TRUE for ChargeQuota, and ebx holds the address of the IRP (which I know is correct, because it was set to the return value of IoAllocateIrp()).

The interesting point is that this is the *only* call to IlAllocateMdl in the entire function. In fact, it’s the only call to any MDL-related function, so that must be what’s used to set MdlAddress. A little exploration confirms that:

kd> dt nt!_IRP
   +0x000 Type             : Int2B
   +0x002 Size             : Uint2B
   +0x004 MdlAddress       : Ptr32 _MDL
...

80535c16 e84cebeeff       call    nt!IoAllocateMdl (80424767)
80535c1b 894304           mov     [ebx+0x4],eax

Here, I used the dt command to tell me the offset of the MdlAddress member of the IRP struct. Then, I looked at what happened to the return value (eax), and sure enough, it’s a match. Remember that we determined above that ebx is our IRP.

So, only one question remains: what data is mapped into that MDL? Here’s the interesting part: those arguments provided to IoAllocateMdl are statically defined. They’re not dependant on the transfer method. In other words: no matter what transfer method you choose, if you get to the IoAllocateMdl() call, you’re getting the same buffer mapped into the MDL. Which buffer is it?

To find that out, we have to identify ebp-28 and ebp-2c. Looking back at the way this function was called, we should be able to figure out what happens. The good news here is that this function uses the standard stack frame pointer, which is set up at the top of the function:

kd> u 805356f4 80535dac
nt!IopXxxControlFile:
805356f4 55               push    ebp
805356f5 8bec             mov     ebp,esp

This means we only have to look at whatever is +28 in the caller’s frame. Remember that the push we just did above is the first thing on the stack, and the return address will be next. So, we just go back to the caller’s string o pushes and look for the one at +20, which will be the 9th argument. That turns out to be ebp+0x28 as well. Using the same logic, we see that our argument is the 9th argument to NtDeviceIoControlFile. Now, we just crack open our copy of Nebbett’s Native API book, and find that the 9th argument to NtDeviceIoControlFile() is OutputBuffer!

Well, that certainly explains a lot. No matter whether you specify METHOD_IN_DIRECT or METHOD_OUT_DIRECT, it looks like Windows will just build a MDL on OutputBuffer. After this little revelation, I went back and tried to figure out what happened to InputBuffer, which is the 7th argument, at offset ebp+0x20. I didn’t have to look far – immediately above the IoAllocateMdl() stuff is this:

80535bca 397520           cmp     [ebp+0x20],esi
80535bcd 7435             jz      nt!IopXxxControlFile+0x510 (80535c04)
80535bcf 68496f2020       push    0x20206f49
80535bd4 ff7524           push    dword ptr [ebp+0x24]
80535bd7 ff75d8           push    dword ptr [ebp-0x28]
80535bda e8075ceeff       call    nt!ExAllocatePoolWithQuotaTag (8041b7e6)
80535bdf 89430c           mov     [ebx+0xc],eax
80535be2 8b4d24           mov     ecx,[ebp+0x24]
80535be5 8b7520           mov     esi,[ebp+0x20]
80535be8 8bf8             mov     edi,eax
80535bea 8bc1             mov     eax,ecx
80535bec c1e902           shr     ecx,0x2
80535bef f3a5             rep     movsd
80535bf1 8bc8             mov     ecx,eax
80535bf3 83e103           and     ecx,0x3
80535bf6 f3a4             rep     movsb
80535bf8 c7430830000000   mov     dword ptr [ebx+0x8],0x30
80535bff 33f6             xor     esi,esi
80535c01 8b4d2c           mov     ecx,[ebp+0x2c]

Remember that esi is still 0. This code allocates a buffer of ebp+0x24 (i.e. InputLength) bytes and sets it to Irp->AssociatedIrp.SystemBuffer (also found with the dt command). It then does what boils down to RtlCopyMemory(), x86-style, from source ebp+0x20 (InputBuffer) to dest SystemBuffer, length ebp+0x24 (InputLength). In other words, the system always double-buffers InputBuffer on NtDeviceIoControlFile().

OK, so I know you really have to be a geek to find this fascinating, but I really didn’t gather that this was the case just from reading the documentation, although it’s certainly possible that I missed it. The lack of samples seems to indicate that this isn’t a commonly-used code path, either.

The bad news is that this post has taken over 2 hours to write, and now it’s likely that I’m going to be late to work. See you on the flip side.

7 Replies to “When Does “Output” Mean “Input”?”

  1. LOL. I just wanted to post on this topic in the comments of the other IOCTL article, but then I realized the DDK documentation was updated and describes this correctly (as opposed to W2K DDK documentation which haven’t said the important information about METHOD_IN_DIRECT and buffer) so I scratched the few paragraphs after twenty minutes of writing. Now I see I should have posted it… well, too late 🙁

  2. Well well well, it turns out that I was looking at an older DDK, as Filip had indicated. The current DDK says:

    METHOD_IN_DIRECT or METHOD_OUT_DIRECT

    For these transfer types, IRPs supply a pointer to a buffer at Irp->AssociatedIrp.SystemBuffer. This represents the input buffer that is specified in calls to DeviceIoControl and IoBuildDeviceIoControlRequest. The buffer size is specified by Parameters.DeviceIoControl.InputBufferLength in the driver’s IO_STACK_LOCATION structure.

    For these transfer types, IRPs also supply a pointer to an MDL at Irp->MdlAddress. This represents the output buffer that is specified in calls to DeviceIoControl and IoBuildDeviceIoControlRequest. However, this buffer can actually be used as either an input buffer or an output buffer, as follows:

    METHOD_IN_DIRECT is specified if the driver that handles the IRP receives data in the buffer when it is called. The MDL describes an input buffer, and specifying METHOD_IN_DIRECT ensures that the executing thread has read-access to the buffer.

    METHOD_OUT_DIRECT is specified if the driver that handles the IRP will write data into the buffer before completing the IRP. The MDL describes an output buffer, and specifying METHOD_OUT_DIRECT ensures that the executing thread has write-access to the buffer.

    For both of these transfer types, Parameters.DeviceIoControl.OutputBufferLength specifies the size of the buffer that is described by the MDL.

    Thanks for the pointer, Filip. I would have liked to have seen your comment, though! 🙂

  3. Rob Green – It was commonly used by the NT4 sound drivers to transfer data from the user-mode WinMM driver to the kernel mode driver which did the sound playback, but I don’t remember seeing it in new drivers either (with the exception of the predefined DDK TDI IOCTLs).

  4. Like Filip, the only examples I found were the TDI IOCTLs, but TDI is only accessed from kernel-mode components, using IRP_MJ_INTERNAL_DEVICE_CONTROL.

    On further review, it looks like the .NET DDK also has DOT4 IOCTLs defined this way. I have never done anything with DOT4 either.

Leave a Reply

Your email address will not be published. Required fields are marked *