sysenter调用
以CreateFile为例,因为它常见,且对应于nt中的函数NtCreateFile。
对于程序中队CreateFile的调用,编译器会编译成如下汇编码:
先会将参数压栈,然后调用函数:
01031017 call dword ptr[__imp__CreateFileW@28 (1032000h)]
上面的1032000处的值为0x7664cc56,也就是跳到这个值处。这是应用其它模块函数的标准方式。(通过导入表跳转)
看下面的代码,在kernel32模块里面,
_CreateFileWImplementation@28:
7664CC56 mov edi,edi
。。。
7664CC98 push dword ptr [ebp+8]
7664CC9B call _CreateFileW@28(7664CCA9h)
。。。
又调用函数了,看调用方式应该还在kernel32里面
_CreateFileW@28:
7664CCA9 jmp dword ptr[__imp__CreateFileW@28 (766019E0h)]
貌似和exe中的处理方式一样,跳转到其他模块的函数时会使用导入表中的对应函数地址。
果然,到KernelBase.dll中了,(发现VS2010每次加载dll都会从定位,这是为了安全着想,不过调试的时候很讨厌)
_CreateFileW@28:
75B9A850 mov edi,edi
。。。
准备参数,入栈(44个字节的参数啊),
75B9AA1B lea eax,[ebp-8]
75B9AA1E push eax
75B9AA1F call esi
75B9AA21 mov ebx,eax
75B9AA23 mov edi,0C0000022h
。。。
call了就到了ntdll里面的_NtCreateFile@44了(这个函数在msdn上有介绍,参数中有处理过的文件名)。这个函数应该是软件生成的,因为很多这样的函数,只是调用号和弹出字节数不一样,她有个名字叫stub function。
_NtCreateFile@44:
778455C8 mov eax,42h
778455CD mov edx,7FFE0300h
778455D2 call dword ptr [edx]
778455D4 ret 2Ch
上面的42h是win7中nt!NtCreateFile的系统调用编号(和win2003的不一样了,微软真恶心)。7FFE0300h是一个固定的位置,该位置根据cpu是否支持sysenter设置成不同的函数。
下面就到了KiFastSystemCall了:
_KiFastSystemCall@0:
778470B0 mov edx,esp
778470B2 sysenter
调用上面这个又进无回的函数之前,eax是系统调用的调用号,那edx是什么了?看下面的栈结构:
KiFastSystemCall@0的返回后的eip地址
_NtCreateFile@44返回后的eip地址
参数1
参数2
参数3这个参数里面含有路径信息。验证方法看下面.
。。。
参数11
所以,edx-8为第一个参数的地址。参数3验证为:
找到参数3的内容(4字节),该地址指向POBJECT_ATTRIBUTES结构,该结构在msdn中有定义,第3个成员就是文件名。
好了,用户态就这么多了,剩下了就要进入内核了。
看一下sysenter干的事情,intel文档上说该指令是为了快速的系统调用(从ring3 到ring0),在执行sysenter之前了,必须要设置好ring0中的代码段、入口函数地址、ring0栈所在的段、栈指针,设置方式是写下列MSRs:
IA32_SYSENTER_CS:32bit,前16个字节为代码段选择符,其确定的索引+1为栈段的索引,所以在全局描述符表中这两个必须相连。
IA32_SYSENTER_EIP:32bit,入口指令地址。
IA32_SYSENTER_ESP:32bit,内核栈地址。
通过上面的介绍,进入进入内核的步骤应该就清楚了。执行sysenter时会将对应的段设置好,并且从IA32_SYSENTER_EIP给出的地址开始执行。下面看看这个地址的内容(rdmsr 176):
nt!KiFastCallEntry:
804dee0f b923000000 mov ecx,23h
804dee14 6a30 push 30h
804dee16 0fa1 pop fs
明显,进入内核开始执行的为上面的函数。
nt!KiFastCallEntry:
804dee0f b923000000 mov ecx,23h
804dee14 6a30 push 30h
…
这个函数主要流程如下:
1.建立陷阱帧,准备eax为调用号,edx为用户态栈的esp(注意这里加过8了,即将用户栈的两个返回值排除了,直接是参数列表),esi为ETHERAD地址
2.调用_KiSystemServiceRepeat.
KiSystemServiceRepeat会根据ETHERAD中的系统服务描述表盒调用号决定该是使用nt里面的服务还是win32k里面的服务。确定了需要使用的系统服务描述符表,根据参数个数和edx的地址从用户栈拷贝参数到内核栈上。然后将服务地址放到ebx中,如下调用服务函数:
call ebx ; call system service
ok,调用完了就要返回用户空间了。
先根据当前的陷阱帧恢复旧的陷阱帧。然后会处理APC,再然后就是执行退出代码了,这个代码在wrk中是在EXIT_ALL宏中完成的。这个宏最后一点代码是:
_KiSystemCallExit2:
test dword ptr [esp+8],EFLAGS_TF
jne short _KiSystemCallExit
pop edx ; pop EIP
add esp, 4 ; Remove CS
and dword ptr [esp], NOTEFLAGS_INTERRUPT_MASK ; Disable interrupts in the flags
popfd
pop ecx ; pop ESP
sti ;sysexit does not reload flags
iSYSEXIT
执行sysexit时(iSYSEXIT宏),它做的事情如下:
IA32_SYSENTER_CS:根据这个量设置ring3的cs和ss。
EDX:设置为用户态的EIP。
ECX:设置用户态的ESP。(刚进入KiSystemCallEntry是保存在了栈上push dword ptrds:[USER_SHARED_DATA+UsSystemCallReturn] ;)
ok切换完成,系统调用结束。
那这个返回地址是在哪了?很奇怪,这个和0x2e进入时时不一样的。
原来,在内核空间的一个特殊的地方(0xffdf0000),存放了一个nt!_KUSER_SHARED_DATA结构的变量,该变量里面存放了进入内核之前要执行的代码和刚出来时会执行的代码。那用户空间为啥能访问了?这是应为系统同时将这块内存(貌似是64kB大小)映射到了用户空间(0x7ffe0000),该部分对于用户来说是可访问但不可修改(想想要是能修改这就算是直接写内核空间了)
那返回时要执行什么代码了?根据进入时用户栈的内容可以知道,如下的语句就满足要求了:
ret。
还记得吗?栈顶是一个返回值(函数_KiFastSystemCall@0),这样代码就连贯了。
ok,这就差不多了吧。