4

My goal is to access the IDT from a MacOS kernel module.

I'm running macOS 10.13.2 under VMFusion 10.0.1, and it seems like the sidt assembly command points to corrupted table structure.

This opcode should return the address to "Interrupt Descriptor Table" which holds the descriptors for each interrupt.

Each interrupt contain pointer for callback function that should be called when a specific interrupt is called.

For example, here are 2 interrupt descriptor that points to valid callbacks from kernel __TEXT section in previous older versions.

  • int3 points to idt64_int3
  • int80 points to idt64_unix_scall

etc...

For some reason, starting from high Sierra 10.13.2, the sidt return memory range that doesn't contain valid pointers (all are outside the boundary of the kernel __TEXT section). perhaps there's another way of getting the table pointer ?

here's my source code for finding the idt pointer:

unsigned char idt_out[10];
__asm__ volatile ("sidt %0": "=m" (idt_out));

// skip the first 2 bytes as they are not part of the address
uint64_t idt_address = *((unsigned long long *)(idt_out+2));
printf("idt address is %llx \n",  idt_address);

int80_desc = (struct descriptor_idt*)(idt_address + sizeof(struct idt_desc)*0x80);
int80_address = (mach_vm_address_t)(((unsigned long)int80_desc->offset_high << 32) + ((unsigned int)int80_desc->offset_middle << 16) + int80_descriptor->offset_low); // this should point to idt64_unix_scall
Michael Petch
  • 46,082
  • 8
  • 107
  • 198
Zohar81
  • 4,554
  • 5
  • 29
  • 82
  • 1
    `sidt` returns a physical address as opposed to a virtual address. Did you make sure to translate the virtual address to a virtual address before trying to understand its contents? – fuz Dec 20 '17 at 16:50
  • I'm trying to access the idt from kernel module so yes, and when running the same code from older version of macOS, and it works perfectly. – Zohar81 Dec 20 '17 at 16:56
  • 1
    Even in a kernel module addresses are virtual. There is really no way around converting the address to a virtual one. – fuz Dec 20 '17 at 16:58
  • 3
    @fuz also it is not **physical**, the descriptor table bases are **linear** not **physical addresses**. In 64-bit mode with segmenting mostly gone, they *are* the virtual addresses. – Antti Haapala -- Слава Україні Dec 20 '17 at 17:07
  • @anttihaapala, perhaps you can add the idt descriptor struct to your answer and some code example for how to assemble the address of callback function to interrupt 80. thanks ! – Zohar81 Dec 20 '17 at 17:12
  • @AnttiHaapala Indeed! Sorry for this mistake. – fuz Dec 20 '17 at 17:16
  • 3
    It's possible SIDT isn't being emulated properly. If your CPU doesn't support hardware virtualization or you're not using it then some VMs fall back on just running kernel code in ring 3 and handling the exceptions that are generated when the code tries execute privileged instructions. The problem is that SIDT isn't privileged so you get the real IDT virtual address of the VM host rather than emulated one of the VM guest. Generally though this isn't a problem because operating systems don't actually use the SIDT instruction. – Ross Ridge Dec 20 '17 at 17:17
  • @rossridge, my host machine is 10.12 and the guest VM is 10.13, but somehow I get different idt addresses in both guest and host (host is 0xffffff8000000000 and guest is 0xfffffd0000007000) .. what can lead for this difference if the idt is not emulated ? – Zohar81 Dec 20 '17 at 17:23
  • @Zohar81 Each logical CPU gets allocated its own IDT so you need to find out the linear addresses of all the host's IDTs to compare. – Ross Ridge Dec 20 '17 at 17:38
  • 1
    @Zohar81: For 64-bit code the default operand size for most instructions is still 32-bit, and I suspect that you're doing a 32-bit "SIDT" that only sets the lowest 32-bits of `idt_address`. To test this theory, try `unsigned char idt_out[10] = 0xCC,0xCC,0xCC,0xCC,0xCC,0xCC,0xCC,0xCC,0xCC,0xCC;`. – Brendan Dec 20 '17 at 19:04
  • @fuz: re discussion in comments on Antti's deleted answer: It can be a problem to create misaligned pointers in C, even when targeting x86. Gcc doesn't explicitly support it, it just happens to work most of the time with normal x86 code-gen. It [will break if gcc decides to auto-vectorize](https://stackoverflow.com/questions/47510783/why-does-unaligned-access-to-mmaped-memory-sometimes-segfault-on-amd64) and assumes that some number of scalar iterations will reach an alignment boundary. – Peter Cordes Dec 20 '17 at 19:12
  • @Brendan: [Intel's ISA reference manual entry for `sidt`](https://github.com/HJLebbink/asm-dude/wiki/SIDT) says the operand size is fixed at 8+2 bytes. Checking with poison values is probably worth trying in case something weird is happening, though. – Peter Cordes Dec 20 '17 at 19:28
  • @Zohar81: Are you running this code from a kernel module? Is that how you're able to read the memory at the address you get from `SIDT`? I expect it would fault in a normal user-space process. Anyway, are the pointer addresses even close to what you expect? Do they have the right *relative* offset from each other? If so, maybe there's ASLR for the kernel in 10.13 and the "expected" addresses you're generating are the non-ASLRer values. Can you find another pointer to `idt64_unix_scall` inside the kernel? Or simply make one yourself in your module so you can print the relocated value. – Peter Cordes Dec 20 '17 at 19:37
  • @AnttiHaapala That is interesting! I consider this to be a compiler bug. – fuz Dec 20 '17 at 19:46
  • @fuz: were you replying to my comment? See my linked answer and discussion in comments about whether this is a gcc bug or not. Conclusion: no, a `uint16_t*` with less than `alignof(uint16_t)` is UB, so gcc is allowed to make that assumption. If the source had used `memcpy` for the unaligned load / store, there would be no UB, and it would compile to a scalar load just fine, but auto-vectorization would then have to account for the possibility that aligned SIMD loads wouldn't line up with element boundaries. https://godbolt.org/g/AscgSu – Peter Cordes Dec 20 '17 at 21:37
  • @Zohar81 Also it just occurred to me that a VM doing what I suggested would probably allocate its own IDTs separate from the host's IDTs, so it can catch all the exceptions generated while executing client code. – Ross Ridge Dec 20 '17 at 21:56

1 Answers1

0

Apparently, the reason for the sidt stopped working is intentional and part of the Meltdown fix.

That's all I managed to find out so far, if anybody got more clues about the reasoning, please enlighten us.

Zohar81
  • 4,554
  • 5
  • 29
  • 82
  • Are you sure it's part of the Meltdown fix and not Spectre workarounds? A kernel can fully protect itself from Meltdown by never leaving kernel memory mapped (depending on the U/S bit set for protection) when running user-space code. I don't see how the IDT has anything to do with it. Or is this something to do with the VM, rather than OS X kernel patches? – Peter Cordes Jan 16 '18 at 14:24
  • Hi, from what I know as a fact (without reversing so far), is that the patch in 10.13.2 obviously shows new IDT behavior.. I can assume from the timing of 10.13.2 release that this changes come to improve the memory separation between kernel and user apps since this opcode doesn't require any special permission. it just seems reasonable. I wish I could understand exactly how the IDT is working now. if it's managed by the kernel (and not the hardware) so I guess they somehow changed the accessing to the interrupt callbacks so that it wouldn't be exposed in anyway from user-space. – Zohar81 Jan 17 '18 at 06:31
  • Maybe it is for Meltdown. I think the IDT has to be mapped for interrupts to work (including when user-space code is executing), so user-space could read it with Meltdown. This could expose pointers into the kernel, defeating kernel ASLR. So maybe they do some trickery. Like maybe an extra level of indirection. In your question you say the structure is "corrupt", but surely it must point somewhere that has pointers to handlers. (Unless there's some extra level of VM trickery happening and it doesn't actually emulate `sidt` inside the VM, like Ross suggested.) – Peter Cordes Jan 17 '18 at 07:06
  • Oh, or maybe when you try to dereference those pointers, the page tables are different from when interrupts can be taken. So Meltdown mitigation might be affecting your code's mappings from virtual->physical, and you're reading the wrong memory? – Peter Cordes Jan 17 '18 at 07:07