Geoff Chappell, Software Analyst
This function starts the profiling that has been configured for a given profile object.
NTSTATUS NtStartProfile (HANDLE ProfileHandle);
The ProfileHandle argument is a handle to an executive profile object, such as created by NtCreateProfile or NtCreateProfileEx.
The function returns STATUS_SUCCESS if successful, else a negative error code.
The NtStartProfile function and its alias ZwStartProfile are exported by name from NTDLL in version 3.10 and higher. In kernel mode, where ZwStartProfile is a stub and NtStartProfile is the implementation, neither is exported until the 1607 release of Windows 10 exports the stub.
Neither NtStartProfile nor its alias is documented. As ZwStartProfile, it is declared in the ZWAPI.H file from an Enterprise edition of the Windows Driver Kit (WDK) for Windows 10.
Unusually for native API functions, no repackaging of NtStartProfile, documented or not, is known in any higher-level user-mode module that is distributed as standard with Windows.
The following implementation notes come mainly from inspection of the kernel from the original release of Windows 10. They may some day get revised to account for other versions. Where anything is written about earlier versions, take it not as an attempt at presenting a comprehensive history but as a bonus from my being unable to resist a quick trip down memory lane. I have no programme of revisiting this analysis for later releases. Updates mean just that I happen to have noticed something and to have found time to add it.
The function fails unless the given handle references an executive profile object (rather than any other type of object) and has whatever permission is represented by the access mask 0x00000001. Microsoft’s name for this one permission that is defined for profile objects is not known, but there would be no surprise if it turns out to be EPROFILE.
The profile object retains parameters that were supplied on some earlier call to NtCreateProfile or NtCreateProfileEx. Among these is the address of a buffer that is to receive the ULONG counters of times that execution is discovered in successive buckets that span a profiled region of address space. Because the counters will be incremented while handling a hardware interrupt, that part of the buffer that will be needed for the counters must be locked into physical memory and mapped into system address space at all times that profiling is started but not yet stopped. This mapped address for the buffer is also kept with the profile object. If it’s present, then profiling is deemed to have started already, and the function returns STATUS_PROFILING_NOT_STOPPED.
There is a system-wide limit to the number of profile objects that can at any one time be started but not yet stopped. If this limit is reached, the function returns STATUS_PROFILING_AT_LIMIT. Up to and including version 5.0, this limit seems intended to have been 8 but is ineffective since the started profiles are not counted. The limit was then dropped, but since its restoration for version 6.1 it is 8,192 times the number of processors (which seems implausibly large).
Given that the executive profile object is in order, the function links it to a kernel profile object. Microsoft’s name for this as a structure is not known, though there would be no surprise if it turns out to be KPROFILE. It is a kernel object in the sense of beginning with a type and size. The type, from the KOBJECTS enumeration, is specifically ProfileObject (0x17 in most versions). The structure itself is followed immediately by an MDL to describe the buffer that is to receive the execution counts. If the function cannot get memory for this profile object, it returns STATUS_INSUFFICIENT_RESOURCES.
This MDL is not just a re-description. The executions counts that this buffer exists to receive will be updated by the KeProfileInterruptWithSource function when it is called from the Hardware Abstraction Layer (HAL) once profiling is started. Because this will happen while handling hardware interrupts, paging I/O is not even close to being possible and the buffer must therefore be locked into physical memory. Failure at this is failure for the function. Moreover, the interruption can be of arbitrary threads, including for other processes and so the buffer must be mapped into system address space. If the function fails at this, it returns STATUS_INSUFFICIENT_RESOURCES.
Note that the function locks and maps the whole of the buffer, not just the possibly very small part that actually is needed. It’s surely not an important vulnerability, but it does allow that an under-privileged user-mode program that would otherwise be unable to lock arbitrary amounts of memory can do so by abusing NtStartProfile.
The locking and mapping is the last work that may fail. Parameters that the function received in the executive profile object are transferred to the kernel profile object, which the function then inserts into a double-linked list so that its parameters can be acted on when interrupts occur. The function has a choice of two lists. If a process was specified for the profiling, the kernel profile object goes into a list for that process, else into a global list. The function ends by directing the HAL, via its exported HalStartProfileInterrupt function, to start a profile interrupt on each processor that was specified for the profiling. This continues until profiling is stopped by the NtStopProfile function or by deleting the executive profile object.