1 of 57

@nickeverdox / @0xNemi

WHY SO SPURIOUS?

How a Highly Error-Prone x86/x64 CPU "Feature" can be Abused to Achieve Local Privilege Escalation on Many Operating Systems

2 of 57

PRESENTATION TOPICS

  • Introductions
  • What is CVE-2018-8897?
  • Prerequisite knowledge
  • How MOV/POP SS functions
  • POC: Local DoS
  • POC: LPE using INT 3
  • POC: LPE using SYSCALL
  • Conclusion

3 of 57

NICK PETERSON

@nickeverdox

www.everdox.net

Anti-Cheat Engineer @ Riot Games

4 of 57

NEMANJA MULASMAJIC

@0xNemi

Anti-Cheat Engineer @ Riot Games

www.triplefault.io

5 of 57

CVE-2018-8897

  • Local privilege escalation: read and write kernel memory from usermode. Execute usermode code with kernelmode privileges
  • Affected Windows, Linux, Mac OS, FreeBSD, some Xen configurations, and many other x86-based operating systems
  • Intel and AMD CPUs were impacted

6 of 57

PREREQUISITE KNOWLEDGE

  • Privilege levels
  • Hardware breakpoints
  • Interrupt handling
  • Segmentation
  • How MOV/POP SS functions

7 of 57

“TRADITIONAL” PRIVILEGE LEVELS

8 of 57

WHAT IN THE WORLD IS A #DB?

  • Not a software breakpoint (INT 3)
  • Data breakpoint = hardware breakpoint
  • Can be set on data access, data write, or instruction execution
  • 4 per processor: DR0-DR3
  • DR6 contains status when a #DB fires
  • Bits of DR7 control what’s active
  • MOV to/from debug registers is privileged, must be done from ring0 (CPL0)
  • Exposed in usermode Windows via kernel32!SetThreadContext (ntdll!NtSetContextThread)

9 of 57

INTERRUPTS AND THE IDT

  • When a #DB fires, CPU transfers execution to the appropriate interrupt handler
  • Lookup is based off of the interrupt descriptor table (IDT), which is registered by the OS through the LIDT instruction during early kernel initialization
  • Hardware breakpoints are transferred to the INT 1 handler, whereas software breakpoints are transferred to the INT 3 handler

10 of 57

In Windows, the INT 1 handler was nt!KiDebugTrapOrFault pre-KPTI.�Nowadays, it’s nt!KiDebugTrapOrFaultShadow.

11 of 57

THE STACK WHEN AN INTERRUPT OCCURS

  • Processor pushes the previous state onto interrupt stack: error code, if appropriate, EIP, CS, EFLAGS, ESP, and SS
  • The OS interrupt handler looks at the CS on the stack to determine what the previous privilege level was
  • The first 2 bits in the CS value on the stack describe the previous mode’s privilege (ring) level.

12 of 57

SEGMENTATION

  • Vestigial part of the x86 architecture now that everything leverages paging
  • Small role in 64-bit mode (IA-32e/AMD64)
  • Just like the IDT, the GDT is setup by the OS during early kernel initialization via the LGDT instruction
    • CS = Code Segment
    • DS = Data Segment
    • ES = Extra Segment
    • SS = Stack Segment
  • FS/GS are “general” purpose segments
  • The value of the segment selector is the index in the GDT, excluding the first 2 bits

13 of 57

SEGMENTATION cont.

  • The first 2 bits describe the RPL (requestor privilege level) of the segment
  • For example, a CS value of 0x10 and 0x13 describe the same index in the GDT, which is 0x10. The first indicates a kernelmode (0) RPL. The latter indicates a usermode (3) RPL
  • On x64, the CS, DS, ES, and SS segments are treated as if each segment base is 0. FS and GS are exceptions
  • OS can set arbitrary base of FS/GS and use it for data structure retrieval, e.g. base of FS is set to 0x12345. Reading fs:100h reads from 0x12445 (0x12345 + 0x100)

14 of 57

THE INT 1 HANDLER, KIDEBUGTRAPORFAULT

  • GS holds data structures relevant to the mode of execution
  • In usermode, this is the _TEB
  • In kernelmode, this is the _KPCR
  • If we’re coming from usermode, we need to SWAPGS to update the GSBASE with the kernelmode equivalent

15 of 57

SWAPGS

  • Exchanges current GSBASE register value with value in MSR address 0xC0000102 (IA32_KERNEL_GS_BASE)
  • Allows the kernel to use GS to read kernel data structures, e.g. gs:188h reads the _KPCR.Prcb.CurrentThread

16 of 57

MOV/POP SS

  • MOV SS and POP SS force the processor to disable external interrupts, NMIs, and pending debug exceptions until the boundary of the instruction following the SS load was reached
  • The intended purpose was to prevent an interrupt from firing immediately after loading SS, but before loading a stack pointer

17 of 57

How did we discover this?

18 of 57

DISCOVERY

  • Discovered it while building VM detection mechanisms
  • What if a VMEXIT occurs during a “blocking” period?
  • CPUID
  • Intel hardware has explicit granularity for these cases, e.g. blocking by MOV SS
  • AMD does not, Zen architecture discards pending #DB exceptions on these VMEXIT cases when blocking by MOV SS
  • Held pending after regular branches...
  • Wondered what would happen in the case of a inter-privilege branch, e.g. INT # and SYSCALL?

19 of 57

IMAGINE THIS SCENARIO...

  • A hardware breakpoint was set at the memory address of RAX (e.g. break on access)
  • Usermode code is executing with EIP at the MOV SS instruction
  • The #DB would normally be tripped before the INT 3 fires, however, MOV SS and POP SS are special - they suppress this behavior until after the INT 3 executes

20 of 57

The INT 3 executes in the context of usermode code.

20

SO, WHAT HAPPENS?

This causes a branch to the INT 3 handler in kernelmode, which is KiBreakpointTrap

Before KiBreakpointTrap executes its first instruction, the pending #DB is fired (which was suppressed by MOV SS) and execution redirects to KiDebugTrapOrFault

KiDebugTrapOrFault is entered with a kernelmode CS.�

1

2

3

4

21 of 57

Demo: A local DoS

22 of 57

github.com/nmulasmajic/

CVE-2018-8897

23 of 57

github.com/nmulasmajic/

CVE-2018-8897

24 of 57

github.com/nmulasmajic/

CVE-2018-8897

25 of 57

github.com/nmulasmajic/

CVE-2018-8897

26 of 57

27 of 57

MOV/POP SS AVOIDS THE SWAPGS

  • As an optimization, there’s no need to use SWAPGS if GSBASE is kernelmode
  • We avoid the SWAPGS since Windows thinks we’re coming from kernelmode
  • We can control GSBASE through the WRGSBASE instruction

28 of 57

WRGSBASE

  • Writes to GSBASE address at any privilege level
  • When the kernel reads from GS memory, e.g. to get kernelmode data structures, it mistakenly reads from memory under our control instead

29 of 57

QUICK RECAP

  • Can fire #DB exception at unexpected location, kernel becomes confused
  • Handler thinks we are trusted, since it came from kernel CS
  • This means we won’t use SWAPGS
  • We control GSBASE
  • ????????
  • Find instructions to capitalize on this
  • ????????
  • Profit

30 of 57

INITIAL WEAPONIZING

  • Erroneously assumed there was no encoding for MOV SS, [RAX] only immediates. e.g. MOV SS, AX
    • That doesn’t dereference memory
    • But POP SS dereferences stack memory
  • Problem though: POP SS only valid in 32-bit compatibility code segment
  • On Intel chips, SYSCALL cannot be used in compatibility mode
    • So, focused on using INT # only, for weaponizing between both architectures

31 of 57

CHALLENGES

  • Find a way to write memory…
  • Luckily, if we cause a page fault (KiPageFault) from kernelmode, we end up calling KeBugCheckEx again
  • This function dereferences GSBASE memory, which is under our control and calls into RtlCaptureContext

32 of 57

CHALLENGES cont.

  • Clobbers surrounding memory
  • Had to early out to avoid destroying too much state…
  • #GP on XMM operation
  • One CPU had to be “stuck” to deal with writing to target location
  • Chose CPU1 since CPU0 had to service other incoming interrupts from APIC
  • CPU1 endlessly page faults, goes to the double fault handler when it runs out of stack space

33 of 57

MORE CHALLENGES...

  • CPU0 does the driver loading
    • Will attempt to send TLB shootdowns
    • This forces CPU0 to wait on the other CPUs, by checking PacketBarrier variable in its _KPCR
    • But CPU1 is in a dead spin... it’s never going to respond
    • Luckily, we have info leak to _KPCR for any CPU, accessible from usermode, so we added this to our list of memory writes
  • Next problem, all CPUs, other than BSP, have their #DF stack flow into the _KPCR without any guard pages. This will corrupt the _KPCR state for that CPU
    • Luckily our _KPCR leak also gives us the TSS pointer for that CPU. We overwrite the #DF stack handler to point to user memory

34 of 57

QUICK RECAP

  • This works, but it’s very complicated. With enough finagling we were able to achieve 100% reliability
  • Firing INT # swaps stack to kernelmode on privilege level change
  • What if we used SYSCALL instead?

35 of 57

THE SYSCALL HANDLER, KISYSTEMCALL64

  • Registered in the IA32_LSTAR MSR (0xC0000082)
  • Not only can we enter kernelmode with a GSBASE under our control, but we can also do so with our usermode stack
  • SYSCALL, unlike INT #, will not immediately swap to a kernel stack
  • Much easier to exploit than our attempt using INT 3

36 of 57

SYSCALL executes in the context of usermode code.�

36

SYSCALL FUNCTIONS SIMILAR TO INT 3

This causes a branch to the SYSCALL handler in kernelmode, which is KiSystemCall64.�

Before KiSystemCall64 executes its first instruction, the pending #DB is fired (which was suppressed by MOV/POP SS) and execution redirects to KiDebugTrapOrFault

KiDebugTrapOrFault is entered with a kernelmode CS and with a usermode stack (since the stack swap doesn’t complete in KiSystemCall64).�

2

1

3

4

37 of 57

Demo: LPE using SYSCALL

38 of 57

github.com/nmulasmajic/�syscall_exploit_CVE-2018-8897�

39 of 57

github.com/nmulasmajic/�syscall_exploit_CVE-2018-8897�

40 of 57

github.com/nmulasmajic/�syscall_exploit_CVE-2018-8897�

41 of 57

github.com/nmulasmajic/�syscall_exploit_CVE-2018-8897�

42 of 57

https://github.com/nmulasmajic/

syscall_exploit_CVE-2018-8897

github.com/nmulasmajic/�syscall_exploit_CVE-2018-8897�

43 of 57

https://github.com/nmulasmajic/

syscall_exploit_CVE-2018-8897

github.com/nmulasmajic/�syscall_exploit_CVE-2018-8897�

44 of 57

github.com/nmulasmajic/�syscall_exploit_CVE-2018-8897�

45 of 57

github.com/nmulasmajic/�syscall_exploit_CVE-2018-8897�

46 of 57

github.com/nmulasmajic/�syscall_exploit_CVE-2018-8897�

47 of 57

github.com/nmulasmajic/�syscall_exploit_CVE-2018-8897�

48 of 57

github.com/nmulasmajic/�syscall_exploit_CVE-2018-8897�

49 of 57

github.com/nmulasmajic/�syscall_exploit_CVE-2018-8897�

50 of 57

51 of 57

MOST OSVS ROLLED OUT FIXES FOR THIS EXPLOIT IN MAY...

52 of 57

MICROSOFT’S FIX

  • Followed our suggestions
  • KiDebugTrapOrFault uses an IST stack upon entry (like the #DF handler). Can’t abuse SYSCALL anymore
  • GS isn’t accessed until everything is known to be good
  • Furthermore, sanity checks against the return address that was pushed onto the stack by the CPU is performed against KiDebugTraps

53 of 57

MICROSOFT’S FIX

  • KiDebugTraps is an array of function pointers initialized by the kernel
  • Contains KiBreakpointTrap, KiSystemCall64, and more (anything that can cause entry to kernelmode from usermode)

54 of 57

SHOUTOUTS TO...

  • Alex Ionescu (@aionescu)
    • http://www.alex-ionescu.com

55 of 57

LESSONS LEARNED

  • Want to make money in bug bounties? Start a hype campaign
    • Get a dope name
    • Pay some graphics artists to design amazing logos
    • Have a great soundtrack
    • Worldstar exclusive

56 of 57

57 of 57