2

I know that operating system restricts the access to kernel code & data by using segmentation and privilege level. However, users can change the segment register value and seems that we can access the kernel data if the following code executes successfully:

mov eax, 0x10 
mov es, ax   #point selector to the item 2 in GDT with RPL 0, which is the data segment
les bx, [0]

So I'm wondering what's the mechanism that prevents this code from executing successfully?

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
Changda Li
  • 123
  • 1
  • 8

2 Answers2

5

The mov es, ax instruction will either cause a general protection (#GP) fault because current privilege level (CPL) is greater than the descriptor's privilege level (DPL), or the requested privilege level (RPL) will be ignored because its not numerically higher than the DPL. In your example, since it's running in a user mode, the CPL is 3. This means that he DPL of the descriptor also has to be 3 or the the instruction will fault. If the DPL is 3 then then there won't be a fault, but the RPL is effectively ignored because it can't be higher than the DPL.

(Note that segment privilege level checks are only performed when a segment register is loaded, so it's only the mov es, ax instruction that can crash because of them.)

The documentation for the MOV instruction in the Intel Software Developer's Manual explains when it will cause #GP fault when loading a segment register:

IF DS, ES, FS, or GS is loaded with non-NULL selector
  THEN
      IF segment selector index is outside descriptor table limits
      or segment is not a data or readable code segment
      or ((segment is a data or nonconforming code segment)
      or ((RPL > DPL) and (CPL > DPL))
          THEN #GP(selector); FI;
  IF segment not marked present
      THEN #NP(selector);
  ELSE
      SegmentRegister ← segment selector;
      SegmentRegister ← segment descriptor; FI;
  FI;

The behaviour of the highest of the DPL and RPL being used is documented in the Intel SDM Volume 3, "5.5 PRIVILEGE LEVELS":

  • Requested privilege level (RPL) — [...] Even if the program or task requesting access to a segment has sufficient privilege to access the segment, access is denied if the RPL is not of sufficient privilege level. That is, if the RPL of a segment selector is numerically greater than the CPL, the RPL overrides the CPL, and vice versa. [...]

The RPL field of a selector only allows the effective privilege level to be increased to a numerically higher, or less privileged level than the DPL. It has no effect if you set it to a numerically lower level.

In other words, if selector 0x10 refers to a kernel mode data segment (DPL = 0) then your code will crash. If selector 0x10 is a user mode data segment (DPL = 3), it's treated the same as if you used 0x13 (RPL = 3) instead.


Note that in practice this doesn't really matter much, as all modern operating systems use a flat segment model were every segment has a base of 0 and can access the entire linear address space. User mode code isn't actually restricted from accessing kernel code and data through segment checks, but through page protections. These use only the CPL to determine whether access should be granted to supervisor mode (kernel) pages.

Ross Ridge
  • 38,414
  • 7
  • 81
  • 112
2

In protected and 64-bit mode, mov Sreg, reg faults with #GP(selector) if:

  • If segment selector index is outside descriptor table limits.
  • ...
  • If the DS, ES, FS, or GS register is being loaded and the segment pointed to is a data or nonconforming code segment, and either the RPL or the CPL is greater than the DPL.

But the OS controls the contents of the GDT, and GDT entries have a descriptor-privilege-level field that's required to even load it into a segment reg. (https://wiki.osdev.org/Global_Descriptor_Table). The OS can make some GDT entries unusable for user-space.

(Also, ring 3 user-space can't just far-jump to a ring-0 code segment thanks to similar checks.)

If the GDT is in memory that user-space doesn't have write privileges on, the OS can maintain control. (Same for LDT of course). Some OSes, e.g. Linux, have a modify_ldt system call that privileged user-space can use to ask the OS to set LDT entries.


Since most OSes use a flat memory model (base=0 limit=-1) and do memory protection via paging, there's no need to stop user-space from configuring a data segment however they want. (seg:off to linear happens before virt->phys. i.e. linear addresses are virtual if paging is enabled.)

But segmentation alone does give the OS a mechanism to stop unprivileged ring3 user-space from using arbitrary entries.


Also note that your sequence doesn't use the newly-modified ES register, instead it overwrites it. Have a closer look at https://www.felixcloutier.com/x86/lds:les:lfs:lgs:lss

les bx, [0]     # Load a seg:off from memory at DS:0 into ES:BX

Perhaps you wanted mov bx, [es:0]

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847