A great way to learn any operating system (OS) is studying the OS’s APIs. Today we will be looking at SetThreadContext, a powerful and commonly seen API that is capable of changing the registers of remote threads. There are many use cases for such API, but security folk in particular will encounter this API often; whether it’s a malware analyst examining code injection techniques or a vulnerability researcher injecting code into a remote process for fuzzing/hooking. Understanding this API on a deeper level can be very beneficial, so here, we’ll cover that and more by exploring the internals of SetThreadContext API.
SetThreadContext function is exported by Kernel32.dll, which calls into ntdll’s NtSetContextThread. The signature for SetThreadContext is quite simple:
BOOL SetThreadContext(HANDLE hThread,const CONTEXT *lpContext);
The HANDLE is for the target thread you wish to change the CONTEXT of, and the CONTEXT structure is simply a structure of registers (which can be seen here https://docs.microsoft.com/en-us/windows/desktop/api/winnt/ns-winnt-_context) which we want the target thread to have. Funny thing about SetThreadContext, is that there is no real user-mode functionality besides directly SYSCALL-ing into the kernel. So we can skip directly to kernel-mode functionality, since everything is implemented there.
From the SYSCALL instruction in NtSetContextThread, we transfer execution from user-mode to kernel-mode, this also changes the RIP register to the value held in IA32_LSTAR, which is an MSR (Model Specific Register). What is stored in IA32_LSTAR? We can see in ntoskrnl!KiInitializeBootStructures, the address of KiSystemCall64 is stored in this MSR.
This means KiSystemCall64 is the kernel-mode function that is invoked right upon SYSCALL. The KiSystemCall64 is responsible for looking up the SYSCALL number we passed from user-mode. The SYSCALL number will be looked up in KiServiceTable to find corresponding kernel function — in this case, NtSetContextThread. Yes, NtSetContextThread is the same function name as the one that was called from user-mode (ntdll!SetContextThread), but this NtSetContextThread is the one in ntoskrnl (ntoskrnl!SetContextThread), which contains the core functionality.
Now our calling thread is in kernel-mode and executing NtSetContextThread in Ntoskrnl. Below, I annotated the function to show what is happening at a high level.
We can see some interesting restrictions going on here:
- The mitigation flag “RestrictSetContextThread” is an EPROCESS flag that can be set to prevent local threads from changing each other’s context within the same process.
- Pico threads (from Windows Subsystem for Linux) and System threads cannot have their contexts changed.
Interestingly, Pico CONTEXT manipulation was one of the security design issues that was brought up in Alex Ionescu’s talk on WSL here https://youtu.be/_p3RtkwstNk?t=2622. It appears NtSetContextThread now prevents this.
If these restrictions are passed, execution flow continues to PspSetContextThreadInternal.
Here we see the ContextsFlags from the CONTEXT structure that was passed from user-mode (https://docs.microsoft.com/en-us/windows/desktop/api/winnt/ns-winnt-_context) are extracted. PreviousMode is checked to see if it can safely trust the buffer before dereferencing. The pink area is if the call originated from kernel-mode, and purple is if call came from user-mode. You will see these checks all over Windows kernel components, which basically tell the kernel routine if it should blindly trust pointer arguments that are passed. For example — you don’t want user-mode passing arbitrary kernel-mode memory buffers which may later get written/read by the routine, so this precheck prevents that by seeing where the call originated from (PreviousMode) and trusting accordingly. Here, since the call came from user-mode, the kernel can’t trust our arguments and our CONTEXT buffer is checked that it resides within user-mode memory range and that memory is valid by safely dereferencing in a try/catch.
The RtlpSanitizeContextFlags function is called directly after and will fix up quirky ContextFlags members. ContextFlags are a member in the CONTEXT structure that tells the operating system which parts of CONTEXT the caller intends to change, for example, you can use flag CONTEXT_DEBUG_REGISTERS, which means you intend to manipulate/retrieve debug registers for the CONTEXT. Each group of registers has its own ContextFlag that will need to be set if register manipulation/retrieval is desired. One additional ContextFlag is for architecture. Part of the fix up done on my Windows version will ensure CONTEXT_AMD64 was the intended architecture flag used, and if not, it’s nice enough to fix it up for you, allowing code compatibility between different architectures.
I mentioned earlier that there was a RestrictSetContextThread mitigation checked. After sanitizing context flags, we hit another mitigation check which checks if the calling thread and target thread are within the same process and branches execution to KeVerifyContextRecord if they are.
KeVerifyContextRecord makes a call into RtlGuardIsValidStackPointer, which is a CFG (Control Flow Guard) feature that will validate the new RSP value from the CONTEXT structure by checking that its new address is still within the valid range of stack memory of the target thread. The valid stack memory range used to check against is taken from the TEB (Thread Environment Block) of the target thread. This validation is to protect against stack pivoting attempts, a common technique used in exploitation.
Once this check is passed, we’re almost to the part where we actually change the CONTEXT of our target thread. In PspSetContextThreadInternal, one check is made to see if the target thread is the same as the calling thread. If the calling thread and target thread are the same, then PspGetSetContextSpecialApc is executed directly while still in the caller’s thread context. If the target and caller thread are not the same however, a kernel-mode APC (https://docs.microsoft.com/en-us/windows/desktop/sync/asynchronous-procedure-calls) is queued for the target thread, with the routine of PspGetSetContextSpecialApc. An APC is an Asynchronous Procedure Call. In short, they allow procedures to be queued for a thread to be executed in the target thread’s context when thread executes again. APIs such as KeInsertQueueApc (for kernel-mode APCs) or QueueUserAPC (for user-mode APCs) can be used for queuing these.
To learn more, an excellent APC primer can be found here.
In remote thread scenario, we can see the queued APC for the target thread with KernelRoutine of PspGetSetContextSpecialApc and corresponding arguments. This is the routine that will be executed when APC is delivered to target thread, along with arguments.
When the target thread is ready to execute this procedure, execution flow will divert to its KernelRoutine of PspGetSetContextSpecialApc. Here, we broke on the target thread’s call to PspGetSetContextInternal to see this.
You can see the APC was delivered when calling KiSwapThread function. KiSwapThread is kernel function for dispatching threads to run and it ends with a call to KiDeliverApc, which checks if there are any pending APCs in the target thread and executes them.
The APC is now executing PspGetSetContextSpecialApc in our target thread context. This routine immediately calls into PspGetSetContextInternal, where the running thread’s TrapFrame is extracted and passed to PspSetContext, along with the CONTEXT structure we passed from user-mode. The TrapFrame (KTRAP_FRAME) is the saved user-mode’s CONTEXT state of the target thread before kernel transition. Simply modifying this directly will affect the user-mode registers upon this thread exiting the kernel, which is exactly what this routine does.
From here, it’s a simple copying of our passed CONTEXT registers directly into the target thread’s KTRAP_FRAME (based on the ContextFlags that we passed in the CONTEXT structure).
Now that the TrapFrame has been modified for our target thread, when it returns to user-mode, the entire thread CONTEXT will now be our new CONTEXT.
We’re back in NtSetContextThread in NtosKrnl.exe and end with a call for Event Tracing using ETW.
This step notifies that the SetThreadContext call occurred so that consumers of this provider may be notified. We actually see two different ETW writes occur here, each for different providers:
From this, it’s clear that SetThreadContext event logging is not just for diagnostic/audit consumers, but also for security consumers. From the little I found online, it appeared some Microsoft security tools, such as Windows Defender APT, consume such Threat-Intel events for EDR purposes, and it makes sense being that SetThreadContext can have malicious usages that should be monitored for.
Now that we’ve changed the target thread’s CONTEXT (or at least queued the APC for it) and logged the event, it’s time to return back to user-mode, where the SetThreadContext functionality is finished. This ends our deep dive on SetThreadContext. Next, we plan to cover another interesting API — until next time!