记一次有关smap的错误排查
起因
最近在写VT驱动,卸载驱动时发现老是会发生蓝屏,错误信息:
PAGE_FAULT_IN_NONPAGED_AREA (50)
Invalid system memory was referenced. This cannot be protected by try-except.
Typically the address is just plain bad or it is pointing at freed memory.
Arguments:
Arg1: 000001b839cd7e98, memory referenced.
Arg2: 0000000000000003, X64: bit 0 set if the fault was due to a not-present PTE.
bit 1 is set if the fault was due to a write, clear if a read.
bit 3 is set if the processor decided the fault was due to a corrupted PTE.
bit 4 is set if the fault was due to attempted execute of a no-execute PTE.
- ARM64: bit 1 is set if the fault was due to a write, clear if a read.
bit 3 is set if the fault was due to attempted execute of a no-execute PTE.
Arg3: fffff8002d2a0282, If non-zero, the instruction address which referenced the bad memory
address.
Arg4: 000000000000000f, (reserved)
而且这个错误并非发生在我的驱动程序中,而是一些系统相关的进程中,并且是在内核态访问(写)用户态地址,通过IDA查看发生错误的内核地址处,发现是在SEH中访问用户态内存,按理说这个操作没什么毛病。这个r3地址通过windbg访问查看也是正常的。
通过查阅微软的文档,参数4=0xf
0xF - NONPAGED_BUGCHECK_USER_VA_ACCESS_INCONSISTENT - 内核模式代码尝试访问用户模式虚拟地址,但这种访问是不允许的。
按我的理解,r0访问r3地址是正常操作,内核中大量这种操作,搞不懂。
分析
找到发生错误的堆栈,最后调用的是KeBugCheckEx,找到前一个函数MiValidFault,拉到IDA F5看一下
找到蓝屏代码:
if ( !(v5[10] & 0x40) && trap_frame && v2 < 0xFFFF800000000000ui64 && !v8 && !KeIsUserVaAccessAllowed(trap_frame) )// 应该是这里,是否允许用户虚拟内存访问,返回0
{
if ( KeInvalidAccessAllowed((_KTRAP_FRAME *)trap_frame, 0) != 1 )// 执行条件是KeInvalidAccessAllowed返回0
KeBugCheckEx(0x50u, v2, v3, trap_frame, 0xFui64);// 这里蓝屏,参数4为f
v9 = v54;
v15 = -140737488355328i64;
v6 = v53;
}
先看一下KeInvalidAccessAllowed
bool __fastcall KeInvalidAccessAllowed(_KTRAP_FRAME *trap_frame, char a2) {
if (!trap_frame) {
return 0;
}
unsigned __int16 cs = trap_frame->SegCs;
bool isTraceMemoryAccess = KiIsTraceMemoryAccess(trap_frame->Rip);
bool result = false;
if (cs == 0x10) {
// 检查EFlags标志和Rsp是否在指令栈内
result = (trap_frame->EFlags & 0x200) || !KiRspInIstStack(3u, trap_frame->Rsp) || !KiRspInIstStack(2u, trap_frame->Rsp);
} else if (cs == 0x33) {
// 检查是否是用户模式的异常
result = true;
}
// 检查是否允许无效访问
result = result && (a2 & 1) || ((void *)trap_frame->Rip == &ExpInterlockedPopEntrySListFault || (void *)trap_frame->Rip == KeUserPopEntrySListFault);
return result || isTraceMemoryAccess; // 如果是跟踪内存访问,也允许
}
看了一下这个函数,并非是判断地址是否非法,而是说地址已经被判断为非法访问的情况下是否允许访问。
虽然对于这个流程这个函数总是返回0,但是还是有两个知识点,一个是isTraceMemoryAccess,查了下资料和内核的dtrace有关,猜测是非法访问也需要记录下来,便于追踪分析。
还有一个是通过cs段选择子判断当前代码是处于R0还是R3,如果是R0则cs=0x10,如果是R3则cs=0x33,学到了。
eflags & 0x200是判断中断使能标志
这边关键逻辑还是KeIsUserVaAccessAllowed,点进去查看,逻辑很简单:
char __fastcall KeIsUserVaAccessAllowed(_KTRAP_FRAME *a1)
{
unsigned int v2; // eax
unsigned int v3; // [rsp+0h] [rbp-8h]
if ( !KeSmapEnabled )
return 1;
if ( a1 )
v2 = a1->EFlags;
else
v2 = v3;
return (v2 >> 18) & 1;
}
这里KeSmapEnabled是一个全局变量(实际上是CR4寄存器的第21位,我猜这个全局变量是个缓存,会同步CR4的21位),表示Smap是否打开,Smap实际上就是处理器和操作系统加入的一个开关,用于限制R0对于R3内存的访问,如果这个开关开了,则无法从R0访问R3内存。
但是实际上有很多R0访问R3的场景,怎么办呢,这里还有一个位控制是否能够访问,那就是eflags的18位ac位,如果ac位为1,则能够访问,对ac位的控制应该是在编译时自动加入的开关代码。
结论
找到原因后,我在进入vmm前将cr4的smap位置为0,关闭后再进行就不会出错了。
看似解决了,但是我发现在win11和win10 1809中,smap是默认打开的,我在vmm没有错误,而卸载VT的时候报错。
也就是说,我在卸载VT时,还有些操作有问题,可能是没有恢复eflags寄存器,在VT卸载流程中加入恢复rflags,问题解决。
本文使用 markdown.com.cn 排版