Working With Nt Query Calls: What Actually Happens Under The Hood

If you are reversing malware, writing a rootkit, or just trying to understand why your hook isn't catching something it should, you eventually hit NtQueryInformationFile and its cousins. The documentation is sparse. The behavior is inconsistent across versions. And a lot of people get tripped up because they treat these as simple API calls when they are really gateways into kernel mode transitions. NtQueryInformationFile sits in ntdll.dll as a user-mode wrapper. The actual work happens in the kernel through the IO Manager. You pass it a handle, a file information class, a buffer, and a length. The kernel resolves the handle to an ERESOURCE or ETHREAD, walks the VCB (Volume Control Block) if it is a file object, checks access rights, and then copies data back into your buffer. That is the basic path. The complications start immediately after that. The file information classes are where things get messy. FILE_BASIC_INFORMATION, FILE_STANDARD_INFORMATION, FILE_NAME_INFORMATION, FILE_ALL_INFORMATION — there are dozens. Each one has different alignment requirements, different buffer sizes, and some of them silently truncate or return partial data if you request more than the object actually has. I spent two days debugging a tool that kept returning STATUS_BUFFER_OVERFLOW on FILE_NAME_INFORMATION for a particular network share path. The issue was that the name was longer than the default 520-character Unicode buffer I had allocated. The fix was allocating a dynamic buffer based on the first call's required length, which the function returns in OutputLength even when it fails with that status code. Most people miss that OutputLength is meaningful on failure too.

STATUS_INFO_LENGTH_MISMATCH is another one that trips people up constantly. It does not mean your buffer was too small. It means the information class you passed does not match the size of the structure you gave it. FILE_NETWORK_OPEN_INFORMATION expects a different struct than FILE_INTERNAL_INFORMATION. If you misalign the struct even by a single field, you get this error and no data. I had a case where a struct I copied from an old SDK header was 4 bytes shorter than the current one due to a padding change between Windows 7 and Windows 10. The query returned STATUS_INFO_LENGTH_MISMATCH every time. Updating the struct fixed it immediately.

The Transition Layer

When you call NtQueryInformationFile from user mode, it goes through the SSDT (System Service Dispatch Table). In older builds this was a straightforward jump through KiSystemService. In modern Windows with KSS (Kernel-Mode Shadow Stack) and CFG (Control Flow Guard), the transition path has additional validation layers. The syscall number for NtQueryInformationFile is 0x0A on x64. You can verify this with a simple disassembly of ntdll!NtQueryInformationFile — the inline assembly issues a syscall instruction and the register values at that point tell you the function index. If you are hooking these calls, keep in mind that modern Windows has multiple hook evasion techniques built in. Direct SSDT hooks get detected by checking for non-pageable memory modifications in the table. Inline hooks in ntdll get caught by hashing the first bytes. The more reliable approach for research purposes is to use ETW (Event Tracing for Windows) or Kernel-Mode Callbacks instead. The callback approach requires a signed driver and gives you the file object pointer directly before the query completes, but it is the only method that survives most anti-hook mechanisms.

Get the Full Details

Process for a manual query of the Immunization Information System from ...
Process for a manual query of the Immunization Information System from ...

Common Pitfalls

One thing nobody emphasizes enough is that NtQueryInformationFile does not always return the full information even when you ask for it. FILE_DISPOSITION_INFORMATION, for example, will return STATUS_INVALID_INFO_CLASS if you pass it to a directory handle on some systems. The same query works fine on a file handle. This is because directories do not support all the same operations as files, and the IO Manager's validation path is not uniform across object types. Another issue is handle inheritance and duplication. If you are querying a handle that was duplicated from another process using DuplicateHandle, the kernel still validates the original process's access token. This means a handle you duplicated from a high-integrity process can still return STATUS_ACCESS_DENIED if your current token lacks the right privileges. I ran into this when building a cross-process monitoring tool. The solution was to open the handle directly in the target process context using NtOpenProcess and NtDuplicateObject rather than relying on inherited handles. Buffer alignment matters more than you would expect. On x64, the kernel expects structures to be naturally aligned. If you pass an unaligned buffer pointer, you get STATUS.alignment_FAULT. This is especially relevant when you are doing manual memory allocation with malloc or VirtualAlloc and casting pointers to struct types without aligning them properly. Using aligned_alloc or __declspec(align(16)) on your structs prevents this entirely.

What The Docs Leave Out

The Windows docs say NtQueryInformationFile returns information about a file object. They do not mention that the behavior changes depending on whether the file is cached, whether it is a compressed file, whether it is on a sparse volume, or whether the file is locked by another process. For compressed files on NTFS, the query returns the uncompressed size in FILE_STANDARD_INFORMATION but the on-disk size in other fields. If you are building a forensic tool that calculates file sizes, you need to check FILE_ALLOCATION_INFORMATION separately to get the actual disk usage. These discrepancies are not bugs. They are by design, but they are not documented anywhere in a single place. There is also the matter of asynchronous I/O. NtQueryInformationFile can be issued asynchronously using an OVERLAPPED structure. In that case, the function returns immediately with STATUS_PENDING and completes the operation later. Most tools ignore this path. If you are writing a high-performance monitor, you need to handle the completion callback correctly or you will leak handles and block the I/O manager's thread pool.

Practical Example

Here is what a minimal query looks like in practice. You open a handle with NtCreateFile, then call NtQueryInformationFile with FILE_BASIC_INFORMATION. The struct is 40 bytes on x64. You pass a buffer, get back the creation time, last access, last write, and change time. That is the easy part. The hard part is handling the cases where the handle is invalid, the information class is wrong, the buffer is too small, the file is deleted but the handle is still open, or the query times out because the I/O stack is backed up. I wrote a tool once that enumerated all open handles in a process and queried each one for its name. On a busy system with thousands of handles, this took several minutes because each NtQueryInformationFile call could block on I/O. The workaround was to batch the queries using asynchronous I/O with a completion port, which reduced the total time to under 30 seconds. The trade-off is that the code is significantly more complex and harder to debug when something goes wrong.

TYPES OF QUERIES IN INFORMATION RETRIEVAL: A COMPREHENSIVE GUIDE - Studocu
TYPES OF QUERIES IN INFORMATION RETRIEVAL: A COMPREHENSIVE GUIDE - Studocu

When This Approach Fails

NtQueryInformationFile cannot tell you everything. It cannot enumerate child files in a directory — you need NtQueryDirectoryFile for that. It cannot read file contents — that is NtReadFile. It cannot modify metadata directly — that is NtSetInformationFile. Some information classes require specific access rights. FILE_FULL_EA_INFORMATION needs FILE_READ_DATA access. If your handle was opened with only GENERIC_READ, certain queries will fail with STATUS_ACCESS_DENIED even though the handle is valid. For reverse engineering purposes, you might also find that certain anti-debug or anti-tamper mechanisms in malware intercept these calls at the driver level and return spoofed data. I encountered a kernel-mode rootkit that hooked the QueryFile routine in the IO Manager and returned stale timestamps for specific process handles. The only way to detect this was to compare the results from NtQueryInformationFile against what ReadDirectoryChangesW reported. When they diverged, I knew the hook was active. This kind of comparison is something you will not find in any tutorial, but it is the kind of thing that separates working detection tools from things that just look good on paper. If you are starting out with this, begin with FILE_BASIC_INFORMATION and FILE_STANDARD_INFORMATION. They are the most stable across Windows versions and the least likely to surprise you. From there you can graduate to the more obscure classes. The learning curve is steep but the payoff is understanding exactly how Windows exposes filesystem state to user-mode applications, which is useful whether you are building security tools, reverse engineering software, or just trying to figure out why your application behaves differently on Windows 10 versus Windows 11.

The key insight most people miss is that NtQueryInformationFile is not a single function. It is a family of calls that share a common interface but behave very differently depending on the information class, the handle type, the filesystem, the Windows build, and the current system state. Treat it as a protocol, not a simple API call, and you will save yourself a lot of headaches.