win-进入内核
系统调用
MSR寄存器
MSR是一组用于控制CPU的寄存器。
MSR(Model Specific Register,模型专用寄存器)是x86/x64架构中央处理器(CPU)中的一组64位寄存器,主要用于配置硬件参数、监控运行状态及支持特定功能。其操作需通过RDMSR和WRMSR指令执行,并依赖于ECX寄存器指定目标地址,通常在特权级别0或实模式下运行。
MSR涵盖温度控制、频率调节、电源管理(C State)、微码更新及缓存控制等核心模块,涉及14类功能特性。典型应用包括通过IA32_SYSENTER系列寄存器实现快速系统调用(sysenter/sysexit),以及借助EFER寄存器管理64位模式切换。不同厂商或型号CPU的MSR功能可能存在差异,具体定义需参考处理器手册。在ARM架构中,类似功能的系统寄存器也通过MSR/MRS指令访问,但在指令集和寄存器命名上存在差异。
每一个MSR寄存器都有一个Index标识,当在rcx中写入index后,就可以用RDMSR和WRMSR来读取或写入某个MSR寄存器,目标或源均为EDX:EAX。仅r0可读可写
比较重要的有:
- MSR_LSTAR (0xC0000082) 保存 64 位系统调用入口地址。当用户态执行 syscall 指令时,CPU 会读取 MSR_LSTAR 的值跳转到内核的系统调用处理入口。
- MSR_STAR (0xC0000081) 保存系统调用时使用的段描述符信息,包括用户态和内核态的代码段选择符。它在系统调用和返回时确保段选择符得到正确设置。
- MSR_FMASK (0xC0000084) 保存着在从用户态进入内核态过程中需要屏蔽的 EFLAGS 位(比如中断标志等)。这可以防止在执行系统调用处理时出现不安全的中断或其他异常情况。
- MSR_CSTAR (0xC0000083) 在一些系统上用于 32 位兼容系统调用的入口地址(针对 x86 模式下的系统调用)。但在纯64位环境下通常主要使用 MSR_LSTAR。
windbg中可以使用rdmsr 0xC0000082来查看具体的值
1 | kd> rdmsr 0xC0000082 |
syscall和sysret
syscall是intel的快速调用指令,一般在x64使用,x86用sysenter。
基本的格式是:(ntdll.dll中的ZwTestAlert)
1 | .text:00000001800A3DC0 public ZwTestAlert |
这里将传参放入r10,将调用号放入eax,检测的地址是KiSystemCall64表的一位,它表明了用用int 2e中断进入或者syscall进入内核
syscall执行时,CPU将rflags放入r11,将下一条指令地址放入rcx。并以如下方式获取r0的上下文(代码段、指令指针、堆栈段和标志):
- 从MSR的IA32_LSTAR域中加载r0的RIP值
- 从MSR的IA32_FMASK域加载mask并对R11中的rflags进行掩码操作(清除IA32_FMASK置位的位),一般来说会把Interrupt flag清零,防止可屏蔽硬件中断插入执行
- 从MSR的IA32_STAR域的47:32派生值加载cs和ss段选择子。但是它们的段选择器caches不会由它们在gdt或ldt中指向的值获取,而是加载固定值。SYSCALL不保证这些内容的合理
- SYSCALL不保存栈指针
这样就实现了上下文的转换。
sysret从r0返回r3
从MSR的IA32_STAR域63:48加载cs和ss段选择子,其caches也是固定值,同时不修改栈指针
同时将rcx的值加载到rip,将r11的值加载到rflags
KUSER_SHARED_DATA
在刚刚的代码中看到了test byte ptr ds:7FFE0308h, 1
,其中7FFE0000存放了KUSER_SHARED_DATA结构,该结构有4kb,是内核和用户虚拟内存的共享区间,用户可读,内核可读可写。内核:0xFFFFF78000000000到 0xFFFFF78000000FFF。用户:0x7FFE0000到0x7FFE0FFF。
具体内容如下:
1 | 6: kd> dt _KUSER_SHARED_DATA 0xFFFFF78000000000 |
308(SystemCall)这里指定了调用的方式,ProcessorFeatures指明了CPU支持的特性。KdDebuggerEnabled指明是否有内核调试器。
trap frame
trap frame是一个保存了R3进程内容的结构体,具体内容如下:
1 | 6: kd> dt nt!_KTRAP_FRAME ffffbd8d`acc674a0 |
它由R0维护
其作用看下一节
KiSystemCall64
上面说到syscall和sysret不会保存栈指针,在intel-IA32手册中说:这个步骤由操作系统来实现,指令只是用来切换CPL。
使用syscall会固定进入KiSystemCall64函数(由IA32_LSTAR加载),如果开启了内核页表隔离KPTI则会进入KiSystemCall64Shadow,其中Shadow函数在经过自己的检查和初始化(在这之中会进行对trap frame的读写)后进入KiSystemCall64的KiSystemServiceUser标签。
1 | kd> rdmsr 0xc0000082 |
KiSystemCall64可以在ntoskrnl.exe,也就是Windows内核实现文件中查看。ntoskrnl.exe也可能是ntkrpamp.exe这说明cpu支持多核
KiSystemCall64首先进行了栈切换
1 | swapgs |
swapgs切换了gs寄存器指向的内容,在r3中,gs指向TEB,而在内核中,其需要指向KPCR。KPCR地址记录在MSR寄存器的IA32_GS_BASE域。
然后,在内核栈上构造trap frame结构
1 | 000 push 2Bh ; '+' |
然后是和shadow stack有关的检查,其中setssbsy
标记或设置与影子栈相关的状态,rstorssp [rcx]
与saveprevssp
恢复或保存影子栈指针。
1 | 000 mov rcx, gs:95A8h |
然后继续填充_KTRAP_FRAME,似乎还等同于创建栈帧?
1 | mov rcx, r10 |
然后是KeSmapEnabled检查,如果有,set AC flag,使得内核可以访问用户空间
1 | test byte ptr cs:KeSmapEnabled, 0FFh |
继续填充_KTRAP_FRAME
1 | mov [rbp-50h], rax |
似乎和分支预测有关
1 | movzx eax, word ptr gs:866h |
后面一堆add和call似乎是和硬件有关的东西
然后是shadow stack相关的内容
1 | loc_140429C45: |
分支预测相关以及通过各种检查跳转到KiSystemServiceUser
1 | loc_140429C60: |
然后就进入了KiSystemServiceUser,准备调用。在这里,大致进行了优化读写,保存MXCSR寄存器,判断异步扩展
1 | KiSystemServiceUser: |
然后就是解析服务表了
1 | mov [rbx+88h], rcx |
执行完之后就是恢复寄存器,中间还调用了USERAPC,好像基本都是反着写的
1 | loc_140429F08: ; CODE XREF: KiSystemCall64+D58↓j |
然后是结束部分
1 | loc_14042A25D: ; CODE XREF: KiSystemCall64+74A↑p |
SSDT
KeServiceDescriptorTable是系统服务描述符表,具体结构如下:
1 | typedef struct _SERVICE_DESCIPTOR_TABLE |
其表中ServiceTableBase每一项均为一个ULONG,但是里面存的不是地址(因为只有32位)而是相对于ServiceTableBase的偏移。
由上一节的计算过程可得:
1 | PVOID getFunc(DWORD code) |
参考
使用 SYSENTER 和 SYSEXIT 指令执行对系统过程的快速调用