Windows定义了两种访问模式:用户模式和内核模式。应用程序代码运行在用户模式下,操作系统代码运行在内核模式下。
内核模式对应处理器的最高权限级别。在内核模式下执行的代码可以访问所有资源并可以执行所有特权指令。用户模式具有较低的优先级,用户模式只能访问用户空间,且不能执行特权指令。
如果用户代码不慎访问了系统空间的数据或执行了特权指令将会导致保护性异常的发生。但是用户代码可以通过调用系统服务来间接的访问系统空间中的数据或间接执行系统空间中的代码。当调用系统服务时,调用线程会从用户模式切换到内核模式,调用结束后再返回用户代码。这就是所谓的模式切换,也被称为上下文切换。
一、使用 INT 2E 切换到内核模式
2e 号向量号专门被用来做系统调用。在 windbg 中可以输入: !idt 2e 来查看该向量号在 IDT 对应的异常处理函数。如:
可以看到 2e 号向量对应的异常处理函数为: nt!KiSystemService。该函数是内核中用以分发系统调用的。
下面我们以调用 ReadFile 为例来展示使用 int2e 指令进行系统调用的步骤:
因为 ReadFile 是从 Kernel32 导出的,所以我们看到调用首先转到了Kernel32 的 ReadFile 函数。在 ReadFile 中又调用了 ntdll!NtReadFile 函数 ntdll.dll是内核空间和用户控件的桥梁,用户空间的代码通过这个 dll 来调用内核空间的系统服务。它会被加载到所有用户进程的进程空间中,且位于同一位置。
下图为 ntdll !NtReadFile 函数反汇编代码:
通过反汇编代码可以看到 ntdll!NtReadFile 非常的短,首先将请求的读文件的系统调用的系统服务号 0xa1 放入 eax 寄存器,然后便通过 INT 2e 指令引发系统调用异常。 INT 2e 会导致陷阱异常 。异常发生时,发生异常代码处得 CS 和EIP 寄存器会被压入堆栈,用于在处理完异常后的返回。然后系统会在 IDT 表中查询 2e 号向量对应的异常处理函数,得到 KiSystemService 函数地址。
在调用内核态的 KiSystemService 函数值前, cpu 会做一些模式切换工作。包括权限检查和准备内核态使用的栈空间。 N t!KisystemService ,得到传入的系统调用号,并将传入的参数从用户态复制到内核态,调用系统读取文件的系统调用。执行完毕后, nt!KiSystemService 会将操作结果复制到用户态。弹出 CS 和EIP 将执行权交给 NtReadFile 用以执行 INT 2e 后面的指令。
二、快速系统调用
前面介绍了 Windows 使用 INT 2e 来实现系统调用。但是它是使用中断机制实现的,伴随着中断产生的还有权限检查和查询 IDT 表等操作,这会导致额外的开销。因此在最新版本的 Windows 中,微软采用了被称为快速系统调用的机制。这主要是得益于 Intel 从奔腾 2 开始在处理器新加的三个特殊的 MSR 寄存器以及sysenter 和 sysexit 指令。
现在我们通过Windows API OpenProcess的调用过程,来看看Windows 7中快速系统调用过程:
前面的部分还是一样的:因为OpenProcess是从Kernel32导出的,所以调用首先转到了Kernel32的OpenProcess函数。在OpenProcess中又调用了ntdll!NtOpenProcess函数。
我们再来看看ntdll!NtOpenProcess的反汇编代码。
eax中保存系统调用号,此处NtOpenProcess的为BEh;edx是SharedUserData!SystemCallStub的地址,里面保存着KiFastSystemCall的地址。SharedUserData总是存放在0x7ffe0000处,其偏移0x300处正是SystemCall。
我们来看看0x7ffe0300处到底是不是SystemCall。
好了,我们看到了sysenter!!!
Sysenter时eax中保存着系统调用号,edx中保存着用户空间线程栈栈顶地址即保存着NtOpenProcess中call KiFastSystemCall的返回地址。
与指令对INT/IRET不同,快速系统调用指令对SYSENTER/SYSEXIT不具有调用、返回关系,因为指令SYSENTER并不会为指令SYSEXIT保存任何返回信息,不指示SYSEXIT返回到何处继续执行,也就是说指令SYSEXIT并不一定返回到指令SYSENTER后的下一个条指令继续执行。这两条指令所需的相关信息由处理器内部的一组相关的寄存器(MSR)提供,这些寄存器的名称及用途如下。
MSR_IA32_SYSENTER_CS:保存了系统调用处理过程所使用的内核态代码段的段选择子,用于在通过指令SYSENTER进入内核态时,设置代码段段选择子寄存器cs。同时,紧随在该寄存器所指示段描述符后面的三个段描述符被依次认为是内核数据段、用户代码段、用户数据段的段描述符(这些段描述符保存在全局描述符表GDT中,有先后次序)。快速系统调用指令SYSENTER/SYSEXIT依赖这些次序来完成内核态、用户态执行环境的设置工作。
MSR_IA32_SYSENTER_EIP:保存了系统调用处理过程的入口地址,用于在通过指令SYSENTER进入内核态时,设置指令指针寄存器eip。
MSR_IA32_SYSENTER_ESP:保存了系统调用处理过程所使用内核态的栈指针信息,用于在通过指令SYSENTER进入内核态时,设置内核态栈的栈指针寄存器esp。
1.请求快速系统调用处理过程
在用户态的代码执行了SYSENTER指令之后,处理器中的控制单元将会完成以下操作,然后进入内核态进行系统调用的处理。
(1)将寄存器MSR_IA32_SYSENTER_CS所指示的段描述符装载到代码段段选择子寄存器cs中。
(2)将寄存器MSR_IA32_SYSENTER_EIP的值装载到指令指针寄存器eip中。
(3)将寄存器MSR_IA32_SYSENTER_CS的值加8作为一个段选择子,然后将该段选择子对应的段描述符装载到栈基址寄存器ss中。
(4)将寄存器MSR_IA32_SYSENTER_ESP的值装载到栈指针寄存器esp中。
2.快速系统调用返回处理过程
在内核态对系统调用服务完毕之后,执行指令SYSEXIT完成系统调用的返回过程。在该过程中处理器的控制单元完成以下处理,设置用户态的执行环境。
(1)将寄存器MSR_IA32_SYSENTER_CS的值加16作为一个段选择子,然后将该段选择子对应的段描述符装载到代码段段选择子寄存器cs中。
(2)将寄存器edx的值装载到指令指针寄存器eip中。
(3)将寄存器MSR_IA32_SYSENTER_CS的值加24作为一个段选择子,然后将该段选择子对应的段描述符装载到栈段段选择子寄存器ss中。
(4)将寄存器ecx的值装载到栈指针寄存器esp中。
由上面的分析可知,在使用指令SYSENTER请求快速系统调用之前,需要初始化好相关的型号相关寄存器;在使用指令SYSEXIT进行快速系统调用返回之前,还要保证寄存器ecx、 edx的正确性,以便能正确地返回到用户态继续运行。
在内核的初始化过程中,函数enable_sep_cpu()负责完成这组寄存器的初始化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | void enable_sep_cpu(void) { //调用宏定义get_cpu()首先禁用内核态抢占,然后返回当前处理器在系统中的编号. int cpu = get_cpu(); //获得当前处理器对应的任务状态段的地址,并将该地址保存在局部变量tss中。 //宏定义per_cpu()用于获得每处理器变量的本地拷贝。 struct tss_struct *tss = &per_cpu(init_tss, cpu); //判断负责系统引导的处理(BootSstrap Processor)是否支持快速系统调用指令SYSENTER/SYSEXIT,如果不支持, //则调用宏定义put()使能内核态抢占,然后返回。也就是说,只有在系统引导处理器支持快速系统调用的情况下, //才为系统中的每个处理器设置快速系统调用所需要的参数;否则,系统中不支持快速系统调用。 if (!boot_cpu_has(X86_FEATURE_SEP)) { put_cpu(); return; } //设置当前任务状态段中的成员变量ss1、esp1分别为快速系统调用所使用的代码段段选择子(内核代码段__KERNEL_CS) //和快速系统调用使用的临时(紧急)内核态栈的栈底。 tss->ss1 = __KERNEL_CS; tss->esp1 = sizeof(struct tss_struct) + (unsigned long) tss; //调用宏定义wrmsr()设置快速系统调用所需的3个MSR寄存器。 //分别设置MSR_IA32_SYSENTER_CS为内核代码段段择子__KERNEL_CS; wrmsr(MSR_IA32_SYSENTER_CS, __KERNEL_CS, 0); //设置MSR_IA32_SYSENTER_ESP为任务状态段中预留的临时(紧急)内核态栈的栈底地址; wrmsr(MSR_IA32_SYSENTER_ESP, tss->esp1, 0); //设置MSR_IA32_SYSENTER_EIP为快速系统调用处理函数的入口地址sysenter_entry。 wrmsr(MSR_IA32_SYSENTER_EIP, (unsigned long) sysenter_entry, 0); //宏定义put_cpu()与get_cpu()相对,用于使能内核态抢占。 put_cpu(); } |
WRMSR可以设置上面说的MSR寄存器,对应关系如下:
SYSENTER_CS_MSR 174H
SYSENTER_ESP_MSR 175H
SYSENTER_EIP_MSR 176H
我们来看看MSR寄存器相应位置保存的什么:
sysenter切换堆栈时是从用户栈切换到DPC栈,这是因为MSR寄存器中的值是操作系统安排好的固定的值,它与具体的线程上下文无关,所以需要在DPC栈中再切换到线程的内核栈。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 | kd> u KiFastCallEntry l 80 nt!KiFastCallEntry: 8288a300 b923000000 mov ecx,23h ; KGDT_R3_DATA 0x23 = 0x20 + 011b(CPL Ring3) 8288a305 6a30 push 30h ; KGDT_R0_PCR 0x30 = 0x30 + 000b(CPL Ring0), DPC Stack 8288a307 0fa1 pop fs ; DPC Stack 8288a309 8ed9 mov ds,cx 8288a30b 8ec1 mov es,cx 8288a30d 648b0d40000000 mov ecx,dword ptr fs:[40h] ;fs Base:ffdff000 Limit:1fff Data RW, KPCR 0x40处为TSS Ptr32 _KTSS 8288a314 8b6104 mov esp,dword ptr [ecx+4] ; KTSS 0x4处为Esp0 Uint4B, Ring0下的esp 8288a317 6a23 push 23h ; 此时已在内核堆栈, Ring3下的ss, KTRAP_FRAME.HardwareSegSs 8288a319 52 push edx ; Ring3下的esp, KTRAP_FRAME.HardwareEsp 8288a31a 9c pushfd ; eflags ; KTRAP_FRAME.EFlags 8288a31b 6a02 push 2 8288a31d 83c208 add edx,8 ; Ring3堆栈的参数 8288a320 9d popfd ; 初始eflags为2, 即各位清零 8288a321 804c240102 or byte ptr [esp+1],2 ; 4字节eflags第二个字节的IF中断允许位置1 8288a326 6a1b push 1Bh ; KGDT_R3_CODE 0x1B = 0x18 + 011b(CPL Ring3), KTRAP_FRAME.SegCs 8288a328 ff350403dfff push dword ptr ds:[0FFDF0304h] ; KTRAP_FRAME.EIP, 0x7ffe0000与0xffdf000映射到同一块物理内存, 0xffdf0304处存放的是KiFastSystemCallRet 8288a32e 6a00 push 0 ; KTRAP_FRAME.ErrCode ; 以下四个寄存器从Ring3到Ring0没修改过, 直接保存 8288a330 55 push ebp 8288a331 53 push ebx 8288a332 56 push esi 8288a333 57 push edi 8288a334 648b1d1c000000 mov ebx,dword ptr fs:[1Ch] ; 指向自己的指针, 头部即是TIB 8288a33b 6a3b push 3Bh ; KTRAP_FRAME.SegFs, fs Base:00000000 Limit:fff Data RW, Ring3 8288a33d 8bb324010000 mov esi,dword ptr [ebx+124h] ; KPCR.SelfPcr->PrcbData.CurrentThead Ptr32 _KTHREAD 8288a343 ff33 push dword ptr [ebx] ; KTRAP_FRAME.ExceptionList, TIB的头部即是ExceptionList 8288a345 c703ffffffff mov dword ptr [ebx],0FFFFFFFFh 8288a34b 8b6e28 mov ebp,dword ptr [esi+28h] ; KTHREAD.InitialStack Ptr32 Void 8288a34e 6a01 push 1 ; KTRAP_FRAME.PreviousPreviousMode 8288a350 83ec48 sub esp,48h ; 预留KTRAP_FRAME中Eax到DbgEbp的空间 8288a353 81ed9c020000 sub ebp,29Ch 8288a359 c6863a01000001 mov byte ptr [esi+13Ah],1 ; KTHREAD.PreviousMode 8288a360 3bec cmp ebp,esp 8288a362 7597 jne nt!KiFastCallEntry2+0x49 (8288a2fb) 8288a364 83652c00 and dword ptr [ebp+2Ch],0 ; KTRAP_FRAME第12个(0x2C/4+1)参数Dr7 8288a368 f64603df test byte ptr [esi+3],0DFh ; KTHREAD.DebugActive 8288a36c 89ae28010000 mov dword ptr [esi+128h],ebp ; KTHREAD.TrapFrame Ptr32 _KTRAP_FRAME 8288a372 0f8538feffff jne nt!Dr_FastCallDrSave (8288a1b0) ; if(KTHREAD.DebugActive != 0) ... 8288a378 8b5d60 mov ebx,dword ptr [ebp+60h] 8288a37b 8b7d68 mov edi,dword ptr [ebp+68h] 8288a37e 89550c mov dword ptr [ebp+0Ch],edx ; 将Ring3参数地址赋给KTRAP_FRAME.DbgArgPointer 8288a381 c74508000ddbba mov dword ptr [ebp+8],0BADB0D00h ; KTRAP_FRAME.DbgArgMark = 0xBADB0D00 8288a388 895d00 mov dword ptr [ebp],ebx ; KTRAP_FRAME.DbgEbp = KTRAP_FRAME.Ebp 8288a38b 897d04 mov dword ptr [ebp+4],edi ; KTRAP_FRAME.DbgEip = KTRAP_FRAME.Eip 8288a38e fb sti ; 当调用KeServiceDescriptorTableShadow中的系统服务时, eax是系统调用号再加0x1000 8288a38f 8bf8 mov edi,eax 8288a391 c1ef08 shr edi,8 8288a394 83e710 and edi,10h 8288a397 8bcf mov ecx,edi ; 经过以上4条指令后, 若ecx为0则为调用KeServiceDescriptorTable中的系统服务, 否则ecx为0x10调用KeServiceDescriptorTableShadow中的系统服务 8288a399 03bebc000000 add edi,dword ptr [esi+0BCh] ; edi = KTHREAD.ServiceTable + 0x00或0x10 8288a39f 8bd8 mov ebx,eax 8288a3a1 25ff0f0000 and eax,0FFFh ; 系统调用号 8288a3a6 3b4708 cmp eax,dword ptr [edi+8] ; 系统调用号必须小于系统服务数 8288a3a9 0f8333fdffff jae nt!KiBBTUnexpectedRange (8288a0e2) 8288a3af 83f910 cmp ecx,10h 8288a3b2 751a jne nt!KiFastCallEntry+0xce (8288a3ce) ; KeServiceDescriptorTable 8288a3b4 8b8e88000000 mov ecx,dword ptr [esi+88h] 8288a3ba 33f6 xor esi,esi 8288a3bc 0bb1700f0000 or esi,dword ptr [ecx+0F70h] ; TEB.GdiBatchCount 8288a3c2 740a je nt!KiFastCallEntry+0xce (8288a3ce) ; KeServiceDescriptorTable 8288a3c4 52 push edx ; Ring3参数地址 8288a3c5 50 push eax ; 系统调用号 8288a3c6 ff154cf99a82 call dword ptr [nt!KeGdiFlushUserBatch (829af94c)] 8288a3cc 58 pop eax 8288a3cd 5a pop edx 8288a3ce 64ff05b0060000 inc dword ptr fs:[6B0h] ; KeServiceDescriptorTable直接跳转到这来, ++KPCR.KPRCB.KeSystemCalls 8288a3d5 8bf2 mov esi,edx 8288a3d7 33c9 xor ecx,ecx 8288a3d9 8b570c mov edx,dword ptr [edi+0Ch] ; SSDT参数表的地址 8288a3dc 8b3f mov edi,dword ptr [edi] ; 系统服务表地址 8288a3de 8a0c10 mov cl,byte ptr [eax+edx] ; 该系统调用的参数个数 8288a3e1 8b1487 mov edx,dword ptr [edi+eax*4] ; 该系统服务地址 8288a3e4 2be1 sub esp,ecx ; 在内核栈中分配参数空间 8288a3e6 c1e902 shr ecx,2 ; 参数以DWORD大小拷贝 8288a3e9 8bfc mov edi,esp 8288a3eb 3b351cf79a82 cmp esi,dword ptr [nt!MmUserProbeAddress (829af71c)] 8288a3f1 0f832e020000 jae nt!KiSystemCallExit2+0xa5 (8288a625) ; 若参数地址超过用户空间地址(0x7fff0000) 8288a3f7 f3a5 rep movs dword ptr es:[edi],dword ptr [esi] 8288a3f9 f6456c01 test byte ptr [ebp+6Ch],1 8288a3fd 7416 je nt!KiFastCallEntry+0x115 (8288a415) 8288a3ff 648b0d24010000 mov ecx,dword ptr fs:[124h] 8288a406 8b3c24 mov edi,dword ptr [esp] 8288a409 89993c010000 mov dword ptr [ecx+13Ch],ebx 8288a40f 89b92c010000 mov dword ptr [ecx+12Ch],edi 8288a415 8bda mov ebx,edx 8288a417 f60588c8978240 test byte ptr [nt!PerfGlobalGroupMask+0x8 (8297c888)],40h 8288a41e 0f954512 setne byte ptr [ebp+12h] 8288a422 0f858c030000 jne nt!KiServiceExit2+0x17b (8288a7b4) 8288a428 ffd3 call ebx ; 调用! 8288a42a f6456c01 test byte ptr [ebp+6Ch],1 8288a42e 7434 je nt!KiFastCallEntry+0x164 (8288a464) 8288a430 8bf0 mov esi,eax 8288a432 ff1568818482 call dword ptr [nt!_imp__KeGetCurrentIrql (82848168)] 8288a438 0ac0 or al,al 8288a43a 0f853b030000 jne nt!KiServiceExit2+0x142 (8288a77b) 8288a440 8bc6 mov eax,esi 8288a442 648b0d24010000 mov ecx,dword ptr fs:[124h] 8288a449 f68134010000ff test byte ptr [ecx+134h],0FFh 8288a450 0f8543030000 jne nt!KiServiceExit2+0x160 (8288a799) 8288a456 8b9184000000 mov edx,dword ptr [ecx+84h] 8288a45c 0bd2 or edx,edx 8288a45e 0f8535030000 jne nt!KiServiceExit2+0x160 (8288a799) 8288a464 8be5 mov esp,ebp 8288a466 807d1200 cmp byte ptr [ebp+12h],0 8288a46a 0f8550030000 jne nt!KiServiceExit2+0x187 (8288a7c0) 8288a470 648b0d24010000 mov ecx,dword ptr fs:[124h] 8288a477 8b553c mov edx,dword ptr [ebp+3Ch] 8288a47a 899128010000 mov dword ptr [ecx+128h],edx nt!KiServiceExit: 8288a480 fa cli 8288a481 f6457202 test byte ptr [ebp+72h],2 ; eflags是否是Virtual-8086 Mode 8288a485 7506 jne nt!KiServiceExit+0xd (8288a48d) ; 非Virtual-8086 Mode则跳 8288a487 f6456c01 test byte ptr [ebp+6Ch],1 8288a48b 7467 je nt!KiServiceExit+0x74 (8288a4f4) ; KTRAP_FRAME.SegCs, CPL为Ring0则跳 8288a48d 648b1d24010000 mov ebx,dword ptr fs:[124h] ; 非Virtual-8086 Mode, 以及下面的交付APC, 跳到这来 8288a494 f6430202 test byte ptr [ebx+2],2 8288a498 7408 je nt!KiServiceExit+0x22 (8288a4a2) 8288a49a 50 push eax 8288a49b 53 push ebx 8288a49c e8a4dc0900 call nt!KiCopyCounters (82928145) 8288a4a1 58 pop eax 8288a4a2 c6433a00 mov byte ptr [ebx+3Ah],0 ; KPCR.KPRCB.CurrentThread->Alerted = 0 8288a4a6 807b5600 cmp byte ptr [ebx+56h],0 8288a4aa 7448 je nt!KiServiceExit+0x74 (8288a4f4) ; if(KPCR.KPRCB.CurrentThread->ApcState.UserApcPending == 0)跳 8288a4ac 8bdd mov ebx,ebp 8288a4ae 894344 mov dword ptr [ebx+44h],eax ; KTRAP_FRAME.Eax, 系统调用号 8288a4b1 c743503b000000 mov dword ptr [ebx+50h],3Bh ; KTRAP_FRAME.SegFs = 0x3B 8288a4b8 c7433823000000 mov dword ptr [ebx+38h],23h ; KTRAP_FRAME.SegDs = KGDT_R3_DATA 8288a4bf c7433423000000 mov dword ptr [ebx+34h],23h ; KTRAP_FRAME.SegEs = KGDT_R3_DATA 8288a4c6 c7433000000000 mov dword ptr [ebx+30h],0 ; KTRAP_FRAME.SegGs = 0 |
下图为快速系统调用的完整过程:
现在我相信大家对用户模式到内核模式的切换有了一个比较清晰的认识。