浅谈系统服务分发
欢迎转载,转载请注明出处:http://www.cnblogs.com/uAreKongqi/p/6597701.html
0x00.说在前面
就我们所知,Windows操作系统内核的陷阱处理器会分发中断、异常和系统服务调用,这里我们就其中的系统服务分发简单解析一下。
0x01.粗看不同处理器进入系统调用
(1).在PentiumII 之前的x86处理器上,Windows使用int2e指令产生一个陷阱,导致执行线程转到内核模式,进入系统服务分发器,eax保存系统服务号,edx指向参数列表。最后通过iret指令返回用户模式;
我们查看IDT的2e成员,得知该成员保存的地址是系统调用分发器的地址,紧接着u一下KiSystemService,会发现在保存完寄存器等状态之后,他走到了KiFastCallEntry里面!(在Win7 x86下测试)
(2).在x86Pentium II 处理器上,Windows使用了sysenter指令,内核的系统服务分发器例程的地址保存在与该指令相关联的一个MSR中,eax,edx保存与int2e相同的内容。最后通过sysexit指令返回用户模式;
这里读取MSR的0x176处,其中包含了系统服务分发器地址,发现实际上调用的就是KiFastCallEntry(入口)!(在Win7 x86下测试)
(3).在x64体系架构上,Windows使用syscall指令,将系统调用号保存在eax中,前四个参数放在寄存器(rcx/rdx/r8/r9)中,剩下的参数在栈中。
64位平台读取MSR的0xC0000082处,里面保存的是64位的syscall,当我们u一下这个地址,发现这个就是x64系统调用分发的入口KiSystemCall64(在Win7 x64下测试)
Ps:通过KiSystemCall64的地址可以通过硬编码得到SSDT、SSSDT地址
......
我们发现,32位下,系统调用分发操作都会走到KiFastCallEntry里面,而64位下,系统调用分发操作会走到KiSystemCall64,然后去完成相应的系统服务调用。那么我们有个疑问,系统是怎么进入到这些系统调用的呢?
0x02.举例查看系统调用如何发生
这里以Win7 x86 平台下的 NtOpenProcess为例,切换到一个进程内(如explorer.exe),u一下NtOpenProcess,这里显示的是ntdll里的NtOpenProcess的反汇编:
我们有两个收获,一个是看到了系统服务号放在了eax里了,另一个是它呼叫了一个地址,call指令将执行由内核建立起来的系统服务分发代码,该地址保存在ntdll!_KUSER_SHARED_DATA+0x300处,我们接着进这个地址看看:
我们似乎有了点儿眉目了,系统调用在ntdll里面发生了,也就是说,在ntdll里面完成了从ring3到ring0的切换,其中eax保存服务号,edx保存参数列表首地址,通过服务号可以在SSDT中定位目标服务例程
32位下的KeServiceDescriptorTable每个成员就是目标系统服务的绝对地址,64位下的目标系统服务真实地址是KeServiceDescriptorTable每个成员保存的偏移量(右移4位后的)+KeServiceDescriptorTable基地址;
一开始线程的系统服务表地址指向Ntoskrnl.exe中的SSDT表,但当调用了一个USER或GDI服务时,服务表地址被修改成指向win32k.sys中的系统服务表。
0x03.从用户层到内核层完整的调用过程
(1). 当一个Windows应用程序调用Kernel32.dll中的OpenProcess时,其导入并调用了API-MS-Win-Core-File-L1-1-0.dll(一个MinWin重定向Dll)中的NtOpenProcess函数;
(2). 接着上述的NtOpenProcess函数又会调用KernelBase.dll中的OpenProcess函数,这里是函数的真正实现,它会对子系统相关的参数做了检查;
(3). 然后KernelBase!OpenProcess就会调用ntdll.dll中的NtOpenProcess函数,在这儿就会触发系统调用(ntdll!KiFastSystemCall),传递NtOpenProcess的系统服务号和参数列表;
(4). 系统服务分发器(Ntoskrnl.exe中的KiSystemService函数)就会调用真正的NtOpenProcess函数来处理该I/O请求。
0x04.内核模式下的系统分发
在系统调用中,如果原先模式为用户模式,在给系统服务传递的参数指向了用户空间缓冲区时,内核模式代码在操作该缓冲区前会检查是否可以访问该缓冲区,而原先模式就是内核模式的时候,默认参数有效,则不会对参数进行检查。而既然已经在内核模式了,那就不需要int2e中断或者sysenter之类的操作了,但如果直接像调用API一般直接调用NtOpenProcess之类的系统服务函数时,内核保存的原先模式值仍然是用户模式(进内核之前当然是用户模式咯~),但又检测到传递来的地址是一个内核模式地址(因为在当前内核模式下调用),于是会导致调用失败(STATUS_ACCESS_VIOLATION)。
这里要介绍内核的Zw系列函数了,他们不仅是Nt版本函数的别名或包装,而是对应Nt系列系统调用的翻版,使用了同样的系统调用分发机制。他们会建立一个假的中断栈(CPU在中断后生成的栈),并直接调用KiSystemService例程,这个过程就在模拟CPU中断,仿佛调用来自用户模式一般,而在检测到该调用的实际特权级后将原先模式修改为内核模式,这样也省去了参数校验,成功调用到NtOpenProcess!
0x05.简单小结
Ring3 ---> Ring0 的系统调用: Kernel32.dll(API)--->ntdll.dll(Nt/Zw)--->用户模式转内核模式--->Ntoskrnl.exe(Nt)--->完成I/O请求(原路返回)
Ring0 ---> Ring0 的系统调用:Ntoskrnl.exe(Zw)--->Ntoskrnl.exe(Nt)
以上理解参考自《深入解析Windows操作系统6》第三章,如果有不正确的地方还请指出,我将虚心请教!