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)
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.