User-mode Callbacks in Windows
Summary of win32k.sys
The modern Windows graphics sub-system is a core part of the Windows GUI in implemented in two places:
win32k.sys, as its extension suggests, is a driver, implemented by Microsoft. It’s large and it implements more than 600 functions. It’s main purpose is to handle the graphical components of Windows. Obviously, it runs in kernel mode.
In any case, the key point of this post is to discuss a particular mechanism that
win32k.sys implements — User-mode callbacks. Since
win32k.sys is a driver, it commonly needs to execute code not in kernel-mode, but in user-mode. This is to faciliate window creation and management, and to perform common tasks that are best handled in user-mode.
Consider for instance, that code running in
win32k.sys needs to leverage code that is implemented in, say,
kernel32.dll runs in user-mode, and hence switching to user-mode is necessary. Consider yet another case, where the
win32k.sys driver wishes to load a DLL. Once again, it needs to execute that in user-mode.
In short, the
win32k.sys driver needs to get to user-mode.
Who Uses It?
User-mode callbacks are implemented in a way that is really meant for Windows use only, and not a facility for third-parties. The only “legitimate” user is actually
User-mode callbacks can be found aplenty in
user32.dll. They tend to have the word “callback” in their name.
How They Are Called
User-mode callbacks function basically as a type of reverse system calls. We recall that system calls are essentially made by choosing a particular system call (index), and making an interrupt (int 0x2E) or syscall/sysenter instruction. In a similar vein, a user-mode callback is made by specifying an
ApiNumber (index), and making a call to
We note that callbacks to user-mode are triggered from kernel-mode (obvious). We further note that the kernel-mode code that triggers the user-mode callback was first, triggered from user-mode via a system call. Hence, to see how kernel makes user-mode callbacks, we need to start from user-mode that makes a system call first.
This is a well-documented topic, so we’ll be real skimpy. Basically, from user-mode, to make a system call, we ultimately get to
KiFastCallEntry, which does the following (more or less):
KTRAP_FRAME(on kernel thread stack)
- Save the thread context and return address
- Switch to kernel mode
KeUserModeCallback is a kernel-level function implemented in the NT kernel (
ntoskrnl). It is an undocumented function. However, it is also a public function.
ApiNumber is an index into a table that can be referenced from a the Process Environment Block (PEB) of a process. The table is a table of function pointers (a function table), and is also undocumented.
To be precise, the table is found at the following address:
mov eax, large fs:18h mov eax, [eax+30h] mov eax, [eax+2Ch] call dword ptr [eax+edx*4] ; edx is ApiNumber
Once in kernel mode, when calling (back) into user-mode, what
KeUserModeCallback does is to get hold of the user-mode stack address of the process it is going to return to. This can be done because in the first place, before the kernel code runs, the user-mode context is stored on the stack in the form of a trap frame (
KTRAP_FRAME) structure, as shown above.
At the kernel side, the kernel first saves the current state on the kernel stack. Next, it sets a particular field in the current thread to point to that state on the stack. It also adjusts the kernel stack to point to the location after that state (which was how the kernel stack looked like before saving the state) so that the kernel does not need to care about the user-mode callback. The return to user-mode is then executed.
The Switch To User-Mode
At the user side,
ProbeForWrite() to ensure that there is enough room to contain the input buffer on the user-mode process’ stack.
InputBuffer is then put onto the user-mode process’ stack. The switch to user-mode is made, and it goes straight to
KiUserCallbackDispatcher (the parallel to
KiFastCallEntry in system calls).
In summary, to get back to user-mode,
KeUserModeCallback does the following:
- Copy input buffer (argument to
KeUserModeCallback) to user-mode stack
- User-mode stack is part of the
- Create new
- Set new return address in trap frame to
- Replace the user thread’s
KiServiceExit(essentially “terminating” this system call)
- Switch to user mode, jumping to this return address
The Dispatcher: KeUserCallbackDispatcher
We pass only the
InputLength to the dispatcher.
KiUserCallbackDispatcher is a user-mode function, and it grabs the same
InputBuffer that we were discussing above. It is responsible to call the correct user-mode function (that the kernel really wants to call) by indexing into the function table stored in the PEB. It then transfers control to that user-mode function.
The User-mode Function
We pass only the
InputBuffer to the user-mode function. Usually what happens is that there is structure within the buffer. It may be a struct, or arguments that are simply packaged/coded into a contiguous region of memory. Free choice of implementation.
Returning Back To Kernel Mode
Now, the user-mode function wishes to signal that it is done. Essentially, it needs to inform the kernel so that the kernel can do the necessary to move back to kernel-mode. A few things can happen here.
Remember that the user-mode callback first started executing through the call made to
KiUserCallbackDispatcher. One possible way is for the user-mode callback to simply return to
KiUserCallbackDispatcher. If that happens, it is assumed that the user-mode callback does not wish to return anything (no buffer).
KiUserCallbackDispatcher then returns a bunch of default values to
Alternatively, the user-mode callback directly calls
NtCallbackReturn. If this happens, it never returns (naturally) to
KiUserCallbackDispatcher. It passes the buffer and size to
NtCallbackReturn, which shuffles it along back to kernel mode.
The actual switch back to kernel mode is essentially a system call.
NtCallbackReturn itself makes the system call, which goes to
KiCallbackReturn (see above flow). Yet another way is for the user-mode callback to call
XyCallbackReturn, which raises
INT 0x2B. This translates also to the same
NtCallbackReturn does the following:
- Copies the return buffer and size to the kernel stack
- Restores original trap frame (PreviousTrapFrame)
- Restores kernel stack and callback stack(s)
- Returns to the return address in the kernel
Once back in kernel-mode, the function which immediately takes over is
KiCallbackReturn. It cleans up everything and transfers control back to
KeUsermodeCallback (our originating function in the kernel). Back in
KeUserModeCallback, the function grabs the data at
ecx, and stores it into the
OutputBuffer location. The user-mode callback is complete is control is transferred back to whoever called KeUserModeCallback in the first place.
The following is the complete flow of what happens during a user-mode callback, and then followed by the walk-through.
KeUserModeCallback (kernel-mode) |-> KiCallUserMode (kernel-mode) |-> KiServiceExit (kernel-mode) |-> KiUserCallbackDispatcher (user-mode) | |-> Actual user-mode callback (user-mode) | Callback code executes... | And finally calls NtCallbackReturn() (which triggers system call or INT 0x2B) | |-> KiCallbackReturn (kernel-mode) | KeUserModeCallback (kernel-mode)
NTSTATUS KeUserModeCallback (ApiNumber, *InputBuffer, InputLength, **OutputBuffer, *OutputLength); ApiNumber : Index of the KernelCallbackTable to find the user-mode callback InputBuffer : Buffer to pass to the user-mode callback InputLength : Length of the above buffer in bytes OutputBuffer : Buffer to receive the result from the user-mode callback OutputLength : Length of the above buffer in bytes
This function is the main (and direct) function that kernel code calls to initiate a user-mode callback. It takes all the necessary information, including which function to call, the input buffer, and the output buffer (to receive the result of the callback).
The main thing that this function does, is to set some information regarding the current execution state, and store in on the kernel stack. It then passes control to
KiUserCallbackDispatcher, through an intermediary call to
KiCallUserMode(**OutputBuffer, *OutputLength, *Address) OutputBuffer : Buffer to receive the result from the user-mode callback OutputLength : Length of the above buffer in bytes Address : Beginning of user-mode buffer (user stack that input buffer is copied to)
This function is responsible for setting up the TrapFrame and other management tasks to prepare to “exit” the system call and enter back into user-mode.
VOID KiServiceExit(TrapFrame, Status) TrapFrame : The trapframe to use to know where to return to in user-land Status : The exit status code of the "system call"
The purpose is to “exit” the system call so that we can get back to user-mode. At this point, the system has already manipulated the TrapFrame so that
KiServiceExit will be “tricked” into returning to where we want it to go to so as to make the user-mode callback. We want it to go to
VOID KiUserCallbackDispatcher(ApiIndex, *InputBuffer, InputLength) ApiIndex : Index of the KernelCallbackTable to user-mode callback InputBuffer : Buffer to pass to the user-mode callback InputLength : Length of the above buffer in bytes
This function is responsible for calling the correct user-mode callback. It takes the argument to the callback, and the index of the callback (in the
KernelCallbackTable is obtained through the user-mode process’ PEB.
This function also implements a default “return” call to
NtCallbackReturn(), which will be called if the user-mode callback does not explicitly make a request to return to kernel-mode. If it does so, this return never happens.
The default arguments to
NtCallbackReturn() are basically NULL everything.
NTSTATUS NtCallbackReturn(*Result, ResultLength, Status) Result : Pointer to user's allocated buffer with custom data. ResultLength : Length of the above buffer in bytes Status : Callback execution return status code Registers used: ecx : User-mode pointer for OutputBuffer (i.e. Result) edx : Length of the above buffer (i.e. ResultLength) eax : Callback execution return status code (i.e Status)
This function receives the return result of the user-mode callback, in the form of the result buffer, the result length, and the return status code. It then passes this information along in returning to
KeUserModeCallback. Information is passed back via registers.
Comments: Use and Abuse
Note that in the entire journey, there is absolutely no checking of data. There is especially no checking after the user-mode callback returns to kernel-mode. Hence, the onus is on the calling kernel function to ensure that whatever data it gets back, if used, is used and handled correctly. It needs to do its own verification.
Note also that while Microsoft does not provide any legitimate means for “registering” a user-mode callback, any user-mode process can seize control of its own or any other process’ function table, hence redirecting the user-mode callback that was supposed to be called to her own.
Just Use It
Also, although the user-mode callback mechanism is undocumented, much is known about it. Hence, there is nothing, in theory, stopping an application/driver developer from using the user-mode callback mechanism. Hence, third-party drivers may choose to implement user-mode callbacks using such functionality, so long as it is willing to live with the risks and consequences of using undocumented features of Windows.
Not Designed For Use By Third-Parties
However, in saying that, we must note that with the way user-mode callbacks are implemented now, they are not designed for use by third-parties. The only “legitimate” user is actually win32k.sys/user32.dll. First of all, the entire system is undocumented, as mentioned. Secondly, each user-mode process has exactly one function table of callback routines, and this table is not designed to be modified (no APIs, etc). Hence, there is no support for chaining and/or inserting third-party functions. Such a design is reasonable, since user-mode callbacks, by their very nature, could easily cause serious problems if not implemented correctly. In fact, as we see things today, even Microsoft did not get it quite right, given the number of user-mode callback vulnerabilities discovered.
Kernel Attacks Through User-Mode Callbacks by Tarjei Mandt (@kernelpool).