ElBlo

Selecting an unmapped Segment Descriptor

Today I found a cool bug in Fuchsia: you could crash the OS from userspace with just one instruction :). Don’t worry, this got fixed by CL 545006

Who would win? A modern operating system (fuchsia) or two tinyinstructions

It all started when my friend David told me that in 32-bits x86 you needed to be really careful with the segment registers upon entering an interrupt: userspace can just change the value of the ds register, and upon entering an interrupt, the ds register is not automatically restored with a safe one.

I didn’t know this was a thing. I remember reading the interrupt code for Fuchsia and never finding anything that restored a safe ds, but then I remembered that in 64 bits the base and limit of the code and data segments are ignored.

Segment Descriptors and Segment Selectors.

In x86 memory is segmented. You can only access memory via a segment descriptor, which characterizes a range of memory (base and limit, plus attributes like code or data, read/write, and permissions). This information is provided via Segment Descriptors that are stored in data structures called Global Descriptor Table and Local Descriptor Table.

On every memory access, a segment selector is used to reference a segment descriptor, sometimes these selectors are implicit in the instruction, sometimes they can be specified in the operand encoding, either directly as an immediate or via a Segment Register.

A Segment Selector is a 16-bit value that references a Segment Descriptor, it contains 3 fields:

  • RPL: The privilege level used by the selector.
  • Table Indicator: Whether the selector references the GDT or the LDT.
  • Index: The index of the descriptor in the GDT/LDT referenced by the selector.

Most operating systems in x86 ignore all the segmentation features and just use a Flat segmentation model, leaving just one segment that ddescribes the whole memory. For this, you would still need 4 segment descriptors:

  • A ring 0 readable code segment, used for running code as the kernel.
  • A ring 0 r/w data segment, used for accesing memory as the kernel.
  • A ring 3 readable code segment, used for running code as a user.
  • A ring 3 r/w data segment, used for accessing memory as a user.

These four segment descriptors share the same base (0) and limit (-1), meaning that they cover the whole memory.

In 64 bits, most of the segmentation stuff goes away, segmentation is assumed to be flat, and the segment descriptors are used only to determine the current privilege level (ring 0, 1, 2, and 3).

Back to the bug…

What if we set the ds to a code segment? In 32 bits, those segments are non-writable, so maybe when we load them everything will be OK and then if the kernel tries to write using them, we can make it crash.

I tested this but I couldn’t get the system to crash. Then I read in the Intel and AMD manuals that in x86-64 mode, the code segments are also writable. But while I was looking for gdt code references in fuchsia, I found an interesting comment:

static inline void gdt_load(uintptr_t base) {
  struct gdtr {
    uint16_t limit;
    uintptr_t address;
  } __PACKED;
  // During VM exit GDTR limit is always set to 0xffff and instead of
  // trying to maintain the limit aligned with the actual GDT size we
  // decided to just keep it 0xffff all the time and instead of relying
  // on the limit just map GDT in the way that accesses beyond GDT cause
  // page faults. This allows us to avoid calling LGDT on every VM exit.
  struct gdtr gdtr = {.limit = 0xffff, .address = base};
  x86_lgdt((uintptr_t)&gdtr);
}

Some parts of the GDT are unmapped? Well, what would happen if we set a segment selector that points to an unmapped region on the gdt? Something like:

  mov eax, 0xfffb
  mov ds, ax

The result is a kernel crash :). This generates a Page Fault, with error code 0 (page not present, read, supervisor mode access). In Fuchsia, all supervisor mode failures are considered critical page faults and non recoverable. This means that any userspace program can crash the kernel with just one or two instructions.

ZIRCON KERNEL PANIC: page fault caused by accessing an unmapped part of theGDT

Other ways of crashing the kernel are: doing a jmp far into an invalid selector, issuing an iretq instructions with either the cs or ss being invalid, doing a pop ds instruction.

Exploitation

Sadly, there doesn’t seem a way to exploit this bug beyond a kernel crash. As soon as we hit the page fault, we can’t recover from it. One idea could be to make the same error from withing a kernel context, but this check happens when we try to load the segment selector into a segment register, so there’s no way to enter the kernel with a selector that points to an unmapped segment descriptor.

© Marco Vanotti 2024

Powered by Hugo & new.css.