3

I find out that it's possibly to read GDTR by SGDT assembly command. Inserting this piece of assembly in my C code I get Error: operand type mismatch for 'sgdt'

unsigned long j;
asm("sgdt %0" : "=r"(j));
Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
Pirate
  • 97
  • 8
  • 2
    Note that there's a few instructions (e.g. `SGDT`) that were originally allowed at CPL=3 but useful for breaking security features (e.g. address layout randomisation); so Intel created a "User Mode Instruction Prevention" feature where (if enabled by OS) these instructions are no longer allowed and cause an exception; allowing the OS to assume your code is malicious and terminate your process or emulate the instructions (e.g. so that they give you lies instead of correct information). Support for UMIP was added to Linux a few years ago. – Brendan Jul 29 '19 at 00:41
  • I'd say it's easiest to assume this is running in freestanding code at ring 0. – reeeee Jun 20 '20 at 10:10

2 Answers2

7

sgdt can only take a memory operand, not a register, so it has to be "=m". The operand-size is 2+8 bytes (for ; limit then address in that order) so you need a struct; using a long will result in storing outside the object.

Read the manual! https://www.felixcloutier.com/x86/sgdt


Other caveats:

  • UMIP (User Mode Instruction Prevention) lets a kernel stop user-space (privilege level 3) from running this instruction, because it only helps user-space defeat kernel ASLR or with other exploits; user-space has no legitimate use for this address. Under a normal kernel like Linux, user-space can't dereference the virtual address it gets from this. So Linux does enable UMIP if supported by hardware (Zen 2, Cannon Lake, Goldmont Plus).

  • The Linux kernel has a macro for this: store_gdt(dtr), which uses
    asm volatile("sgdt %0":"=m" (*dtr)); with struct desc_ptr *dtr.


On my Linux 5.18 system with a Skylake CPU (no UMIP support), I put sgdt [rsp] (NASM syntax) into a static executable so I could single-step it with GDB (starti / stepi). After that instruction:

  • x /1hx $rsp shows the limit was 0x007f (stored in 2 bytes, what GDB calls a half-word, what Intel calls a word).
  • x /1gx $rsp+2 shows the qword address happened to be 0xfffffe00000ed000, which is a valid kernel address (48-bit sign-extended, but fairly far from the very top of the upper half of the canonical range of address space.) According to docs/x86/x86-64/mm.txt, the 0.5TB starting at fffffe0000000000 holds the cpu_entry_area mapping, so that's an unsurprising place to find the GDT, along with other kernel stuff whose address is exposed to user-space (on CPUs without UMIP), and has to be mapped all the time, even on CPUs with Meltdown.
Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
2

By the way, in the Linux kernel it's also possible to perform the same action by already defined macros store_gdt(dtr). It contains the same inline-assembly code inside. Header of macros is asm/desc.h

Pirate
  • 97
  • 8
  • According to Pirate's comment, I found the code from the linux kernel like below: static inline void native_load_gdt(const struct desc_ptr *dtr) { asm volatile("lgdt %0"::"m" (*dtr)); } static inline void native_store_gdt(struct desc_ptr *dtr) { asm volatile("sgdt %0":"=m" (*dtr)); } – Navy Jul 19 '22 at 03:58