Lutas has already provided a remarkable job explaining the vulnerable code in his paper , if you need explanations about those, definitely read his paper.

The vulnerability is located in the emulation of hlt , lgdt , lidt and lmsw instructions :

Exploitation

Among the vulnerable instructions, only two of them could lead to a potential privilege escalation: lgdt and lidt . They respectively allow to change the value of the Global Descriptor Table Register and Interrupt Descriptor Table Register. Both GDTR and IDTR have the same format: the upper bits contain the base address and the lower bits define the limit . These values define the Global Descriptor Table (GDT) and the Interrupt Descriptor Table (IDT) addresses.

According to Intel manuals, a non privileged code is not allowed to execute these instructions. If a user is able to load his own GDT or IDT, this can lead to an arbitrary code execution and a privilege escalation. Let's see how.

Interrupt Descriptor Table (IDT) The IDT is the x86 interrupt vector table . It is a basic table that associates an interrupt number with an interrupt handler. The entry number determines the interrupt number and each entry contains some fields such as: a type, a segment selector, an offset, a privilege level, etc. The interrupt handler address is determined by adding the segment base (determined with the segment selector) and the offset. If a user is able to load his own IDT, he can specify a malicious entry which links an interrupt to his own handler using kernel code segment selector. In order to avoid stability issues, the interrupt must be fowarded to the original handler. This can be done because the handler runs in kernel space, and it can read entries from the previous IDT. This IDT must have been previously saved using the sidt instruction because it must be restored before returning to user space. However, we have not tested it. We chose to use the GDT approach, despite the IDT solution adopted by Andrei Lutas .

Global Descriptor Table (GDT) The GDT is used to define memory segments. Each entry contains: a base, a limit, a type, a Descriptor Privilege Level (DPL), read/write bit, and so on: struct desc_struct { union { struct { unsigned int a ; unsigned int b ; }; struct { unsigned short limit0 ; unsigned short base0 ; unsigned int base1 : 8 , type : 4 , s : 1 , dpl : 2 , p : 1 ; unsigned int limit : 4 , avl : 1 , l : 1 , d : 1 , g : 1 , base2 : 8 ; }; }; } __attribute__ (( packed )); Nowadays, the most used memory segmentation pattern is a flat model. Each descriptor maps the whole memory but with differents privileges and flags (all security checks are performed with paging). Most of the time there are at least six GDT entries: 32-bit kernel code segment (dpl = 0)

64-bit kernel code segment (dpl = 0)

kernel data segment (dpl = 0)

32-bit user code segment (dpl = 3)

64-bit user code segment (dpl = 3)

user data segment (dpl = 3) The current memory segments are specified in the segment registers. There are several segment registers: code selector, stack selector, data selector, etc. Each segment selector is 16-bit long. Bits 3 through 15 are an index in the GDT, bit 2 is the LDT/GDT selector, bit 0 and 1 are the Requested Segment Privilege (RPL). There is another kind of entry which is very interesting in our case: call gate entry. The aim of a call gate is to facilitate the transfer between different privilege levels. Such entries are twice larger than memory descriptors (in 64-bit mode) and have others fields: a segment selector

an offset in the selected segment

a DPL To access a call gate, the user has to perform a far call. The far call must specify the call selector. This selector has exactly the same format as any selector (index in the GDT, LDT/GDT selector, requested privilege). Then the CPU takes the segment selector specified in the call gate entry, takes the base of this segment, add the call gate offset and reaches procedure entry point. Of course, there are some privilege checks and four levels of privileges are involved: the current privilege level (CPL)

the requested privilege level in the far call selector (RPL)

the call gate descriptor privilege level (CDPL)

the segment descriptor privilege level (SDPL) Three conditions must be satisfied: CPL <= CDPL

RPL <= CDPL

SDPL <= CPL If these conditions are satisfied the call gate procedure is executed. The idea is to create a call gate with a DPL set to 3, a segment selector pointing to the kernel code segment, and procedure giving us supervisor privileges. Then: CPL = 3

RPL = 0

CDPL = 3

SDPL = 0

CPL <= CDPL == True

RPL <= CDPL == True

SDPL <= CPL == True