Summary of win32k.sys

The modern Windows graphics sub-system is a core part of the Windows GUI in implemented in two places: user32.dll and win32k.sys.

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.

User-mode Callbacks

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. 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 win32k.sys/user32.dll.

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

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 KiSystemService or KiFastCallEntry, which does the following (more or less):

  1. Create KTRAP_FRAME (on kernel thread stack)
  2. Save the thread context and return address
  3. Switch to kernel mode

Also, KeUserModeCallback is a kernel-level function implemented in the NT kernel (ntoskrnl). It is an undocumented function. However, it is also a public function.

The 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, KeUserModeCallback uses [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 KiSystemService/KiFastCallEntry in system calls).

In summary, to get back to user-mode, KeUserModeCallback does the following:

  1. Copy input buffer (argument to KeUserModeCallback) to user-mode stack
  2. User-mode stack is part of the KTRAP_FRAME information
  3. Create new KTRAP_FRAME
  4. Set new return address in trap frame to KiUserCallbackDispatcher
  5. Replace the user thread's KTRAP_FRAME pointer
  6. Call KiServiceExit (essentially "terminating" this system call)
  7. Switch to user mode, jumping to this return address

The Dispatcher: KeUserCallbackDispatcher

We pass only the ApiNumber, InputBuffer and InputLength to the dispatcher. KiUserCallbackDispatcher is a user-mode function, and it grabs the same ApiNumber and 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 KeUsermodeCallback.

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 KiCallbackReturn function.

Finally, NtCallbackReturn does the following:

  1. Copies the return buffer and size to the kernel stack
  2. Restores original trap frame (PreviousTrapFrame)
  3. Restores kernel stack and callback stack(s)
  4. 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.

Complete Walk-through

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.


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


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

Data Checking

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.


For an excellent write-up on how one may abuse user-mode callbacks, including clobbering a process's function table, take a look at this post by @j00ru.

Discussion on the user-mode callback mechanism by Ken Johnson (SkyWing).

Kernel Attacks Through User-Mode Callbacks by Tarjei Mandt (@kernelpool).