-
-
[原创]Windows内核学习笔记之系统调用
-
2021-12-19 19:50 18729
-
一.基本概念
NT内核会把操作系统的代码和数据映射到系统中所有进程的内核空间中。这样,每个进程内的应用程序代码便可以很方便地调用内核空间中的系统服务。这里的”很方便“有多层含义,一方面是内核代码和用户代码在一个地址空间中,应用程序调用系统服务时不用切换地址空间,另一方面是整个系统中内核空间的地址是统一的,编写内核空间的代码时会简单很多。但是,如此设计带来了一个很大的问题,那就是用户空间中的程序指针可以指向内核空间中的数据和代码,因此必须防止用户代码破坏内核空间中的操作系统。做法就是利用权限控制来实现对内核空间的保护。
1.访问模式
Windows定义了两种访问模式,分别是用户模式和内核模式。应用程序运行在用户模式下,操作系统运行在内核模式下。内核模式对应于处理器的最高级别权限(ring 0),在内核模式下执行的代码可以访问所有系统资源并具有使用所有特权指令的权利。相对而言,用户模式对应于较低的处理器优先级(ring 3),在用户模式下执行的代码只可以访问系统运行其访问的内存空间且没有使用特权指令的权力。
因为内核模式下的数据和代码具有更高的优先级,所以用户模式下的代码不可以直接访问内核空间中的数据,也不可以直接调用内核空间中的任何函数或例程。任何这样的尝试都会导致保护性错误。也就是说,即使用户空间中的代码指针正确指向了要访问的数据或代码,但一旦访问发生,那么处理器就会检测到该访问是违法的,会停止该访问并产生保护性异常(#GP)。
2.系统调用
虽然不能直接访问,但是应用程序可以通过调用系统服务来间接访问内核空间中的数据或间接调用,执行内核空间中的代码。当调用系统服务的时候,主调线程会从用户模式切换到内核模式,调用结束后在返回用户模式,也就是所谓的模式切换。在线程的KTHEREAD结构中,定义了UserTime和KernelTime两个字段,分别用来记录这个线程在用户模式和内核模式的运行事件(以时钟中断计数为单位)。
对于应用程序的运行即进程而言,操作系统内核的作用体现在一组可以供其调用的函数,称为”系统调用“,正是这些系统调用加上一些辅助的手段构成了应用软件的运行环境,即日常所说的”运行平台“。从应用软件的角度看,这些系统调用都是操作系统为其提供的服务,所以也称为”系统服务“。
系统调用所提供的服务是在内核中,一般是在”系统空间“中实现的,而应用软件则都在用户空间运行,二者之间有着空间的间隔。所以,在应用软件和内核之间必定存在一个明确定义的”系统调用界面”。
Windows系统服务界面是隐藏不公开的,公开的是由用户空间一些高层库函数构成的界面,称为“Win32应用程序界面”,即Win32 API。用户模式下的代码通过调用这些Win32 API来调用系统服务完成需要的操作。
因运行状态和执行的程序所在的内存区间的不同,CPU既可以运行于非特权的“用户态”,或者说运行于“用户空间”,也可也运行于特权的“系统态”,或者说运行于“系统空间”。CPU要从系统空间转入用户空间是容易的,因为运行于系统态的CPU可以通过一些特权指令来改变其运行状态。但是,反过来要从用户空间转入系统空间就不容易了,因为运行于用户态的CPU是不能执行特权指令的。所以,一般而言只有三种手段或原因可以使运行于用户空间的CPU转入系统空间:
中断:在开启中断机制的情况下,只要有外部设备的中断请求到来,CPU就会自动转入系统空间,并从(系统空间中)某个预定的地址开始执行指令,从而可以在系统空间对外部设备的中断请求作出反应,或者说提供服务。中断只发生在两条指令之间,因而不会使正在执行的指令半途而废。对于CPU而言,因中断而进入系统空间是被动的,并且CPU是完全无法预知何时将发生中断,所以中断的发生又是“异步”的。
异常:不管是用户空间或系统空间,执行指令时的失败都会引起一次”异常“,CPU也会因此而转入系统空间(如果原来不再系统空间),并从某个预定的地址开始执行指令,从而可以在系统空间对发生的失败做出反应。异常在形式上与中断十分相似,只是异常发生在执行一条指令的过程中,而不是两条指令之间,所以当前指令的执行已经半途而废。同样,对于CPU而言,因异常而进入系统空间也是被动的,无法预知的。但是,这只是一般而言,实践中也可也通过故意引起异常而进入内核。例如,在用户空间故意访问系统某个特定地址,就会因访问权限的问题而引起主动的异常,而内核空间中的异常处理程序则可以根据所访问的地址判定其目的,从而做出预定的反应。
自陷:为了让CPU能够主动地进入系统空间,绝大多数CPU都设有专门地”自陷“指令,系统调用通常就是靠自陷指令实现地。一执行这样的指令,CPU就转入系统空间并从某个预定地地址开始执行指令,就像掉进了陷阱一样。自陷指令在形式上与中断相似,就像是一次由CPU主动发出的中断请求。对于CPU而言,这是成功执行了一条指令的结果,所以是主动的,尽管该指令其实是由程序员安排的。对于程序而言,则一条自陷指令的作用相当于一次子程序调用,只是这个子程序存在于系统空间中。
传统的Windows系统调用正是通过自陷指令”int 0x2E"进入内核实现系统调用的,但是,从Pentium II开始,Intel又在x86系列的CPU中增加了一对指令sysenter和sysexit,用来实现一种称为“快速系统调用”的机制,并为此增加了三个寄存器。
二.Intel x86的用户模式-内核模式的切换
应用程序为了调用系统服务,就需要进行模式切换,将处理器从用户模式切换到内核模式下。模式切换的工作并不需要由应用程序自己来完成,Windows提供了一个系统模块ntdll.dll,已经实现了所有系统服务的模式切换工作,在用户模式下只需要通过调用Win32 API就可以由ntdll.dll实现相应的模式切换工作,切换到内核模式以后就可以使用系统提供的系统服务。模式切换依赖于硬件系统结构,接下来以ReadProcessMemory为例,来具体看看切换的过程。
ReadProcessMemory是kernel32.dll中的一个导出函数,根据反汇编的结果可以看到,该函数的主要行为就是调用NtReadVirtualMemory函数来完成功能
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 | .text: 7C8021D0 ; BOOL __stdcall ReadProcessMemory(HANDLE hProcess, LPCVOID lpBaseAddress, LPVOID lpBuffer, SIZE_T nSize, SIZE_T * lpNumberOfBytesRead) .text: 7C8021D0 public _ReadProcessMemory@ 20 .text: 7C8021D0 _ReadProcessMemory@ 20 proc near ; CODE XREF: GetProcessVersion(x) + 2F18B ↓p .text: 7C8021D0 ; GetProcessVersion(x) + 2F1AA ↓p ... .text: 7C8021D0 .text: 7C8021D0 hProcess = dword ptr 8 .text: 7C8021D0 lpBaseAddress = dword ptr 0Ch .text: 7C8021D0 lpBuffer = dword ptr 10h .text: 7C8021D0 nSize = dword ptr 14h .text: 7C8021D0 lpNumberOfBytesRead = dword ptr 18h .text: 7C8021D0 .text: 7C8021D0 mov edi, edi .text: 7C8021D2 push ebp .text: 7C8021D3 mov ebp, esp .text: 7C8021D5 lea eax, [ebp + nSize] .text: 7C8021D8 push eax ; NumberOfBytesRead .text: 7C8021D9 push [ebp + nSize] ; NumberOfBytesToRead .text: 7C8021DC push [ebp + lpBuffer] ; Buffer .text: 7C8021DF push [ebp + lpBaseAddress] ; BaseAddress .text: 7C8021E2 push [ebp + hProcess] ; ProcessHandle .text: 7C8021E5 call ds:__imp__NtReadVirtualMemory@ 20 ; NtReadVirtualMemory(x,x,x,x,x) .text: 7C8021EB mov ecx, [ebp + lpNumberOfBytesRead] .text: 7C8021EE test ecx, ecx .text: 7C8021F0 jnz short loc_7C8021FD .text: 7C8021F2 .text: 7C8021F2 loc_7C8021F2: ; CODE XREF: ReadProcessMemory(x,x,x,x,x) + 32 ↓j .text: 7C8021F2 test eax, eax .text: 7C8021F4 jl short loc_7C802204 .text: 7C8021F6 xor eax, eax .text: 7C8021F8 inc eax .text: 7C8021F9 .text: 7C8021F9 loc_7C8021F9: ; CODE XREF: ReadProcessMemory(x,x,x,x,x) + 3C ↓j .text: 7C8021F9 pop ebp .text: 7C8021FA retn 14h .text: 7C8021FD ; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - .text: 7C8021FD .text: 7C8021FD loc_7C8021FD: ; CODE XREF: ReadProcessMemory(x,x,x,x,x) + 20 ↑j .text: 7C8021FD mov edx, [ebp + nSize] .text: 7C802200 mov [ecx], edx .text: 7C802202 jmp short loc_7C8021F2 .text: 7C802204 ; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - .text: 7C802204 .text: 7C802204 loc_7C802204: ; CODE XREF: ReadProcessMemory(x,x,x,x,x) + 24 ↑j .text: 7C802204 push eax ; Status .text: 7C802205 call _BaseSetLastNTError@ 4 ; BaseSetLastNTError(x) .text: 7C80220A xor eax, eax .text: 7C80220C jmp short loc_7C8021F9 .text: 7C80220C _ReadProcessMemory@ 20 endp |
NtReadVirtualMemory的功能很简单,将服务号0x0BA赋给eax,该服务号决定了进入内核模式以后使用的系统服务,随后将0x7FFE0300赋给edx,在调用edx保存的地址中保存的函数地址
1 2 3 4 5 6 7 8 | .text: 7C92D9E0 public NtReadVirtualMemory .text: 7C92D9E0 NtReadVirtualMemory proc near ; CODE XREF: .text: 7C93FFD1 ↓p .text: 7C92D9E0 ; LdrCreateOutOfProcessImage + 7C ↓p ... .text: 7C92D9E0 mov eax, 0BAh ; 服务号赋给eax .text: 7C92D9E5 mov edx, 7FFE0300h .text: 7C92D9EA call dword ptr [edx] .text: 7C92D9EC retn 14h .text: 7C92D9EC NtReadVirtualMemory endp |
要理解edx保存的内容,首先要知道,在Windows的虚拟地址空间管理中,系统空间有一个特殊的页面会映射到每个进程地址空间中。也就是说,该页面是系统地址空间和进程地址空间共享的。
它在系统空间的地址由以下宏定义决定
1 | #define KI_USER_SHARED_DATA 0xFFDF0000 |
在进程空间的地址则是由以下的宏定义决定
1 | #define MM_SHARED_USER_DATA_VA 0x7FFE0000 |
该共享页面是在内存管理器初始化时分配的,记录了当前系统的一些关键状态信息,数据结构KUSER_SHARED_DATA定义了该页面内部的结构,该结构定义如下:
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 | kd> dt _KUSER_SHARED_DATA nt!_KUSER_SHARED_DATA + 0x000 TickCountLow : Uint4B + 0x004 TickCountMultiplier : Uint4B + 0x008 InterruptTime : _KSYSTEM_TIME + 0x014 SystemTime : _KSYSTEM_TIME + 0x020 TimeZoneBias : _KSYSTEM_TIME + 0x02c ImageNumberLow : Uint2B + 0x02e ImageNumberHigh : Uint2B + 0x030 NtSystemRoot : [ 260 ] Uint2B + 0x238 MaxStackTraceDepth : Uint4B + 0x23c CryptoExponent : Uint4B + 0x240 TimeZoneId : Uint4B + 0x244 Reserved2 : [ 8 ] Uint4B + 0x264 NtProductType : _NT_PRODUCT_TYPE + 0x268 ProductTypeIsValid : UChar + 0x26c NtMajorVersion : Uint4B + 0x270 NtMinorVersion : Uint4B + 0x274 ProcessorFeatures : [ 64 ] UChar + 0x2b4 Reserved1 : Uint4B + 0x2b8 Reserved3 : Uint4B + 0x2bc TimeSlip : Uint4B + 0x2c0 AlternativeArchitecture : _ALTERNATIVE_ARCHITECTURE_TYPE + 0x2c8 SystemExpirationDate : _LARGE_INTEGER + 0x2d0 SuiteMask : Uint4B + 0x2d4 KdDebuggerEnabled : UChar + 0x2d5 NXSupportPolicy : UChar + 0x2d8 ActiveConsoleId : Uint4B + 0x2dc DismountCount : Uint4B + 0x2e0 ComPlusPackage : Uint4B + 0x2e4 LastSystemRITEventTickCount : Uint4B + 0x2e8 NumberOfPhysicalPages : Uint4B + 0x2ec SafeBootMode : UChar + 0x2f0 TraceLogging : Uint4B + 0x2f8 TestRetInstruction : Uint8B + 0x300 SystemCall : Uint4B + 0x304 SystemCallReturn : Uint4B + 0x308 SystemCallPad : [ 3 ] Uint8B + 0x320 TickCount : _KSYSTEM_TIME + 0x320 TickCountQuad : Uint8B + 0x330 Cookie : Uint4B |
其中偏移0x300的SystemCall指示了从用户模式切换到内核模式的函数地址,而偏移0x304的SystemCallReturn则指示了从内核模式返回至用户模式的函数地址。因此,在ntdll.dll中的NtReadVirtualMemory的edx被赋予了SystemCall的值。
在初始化阶段,系统会根据当前处理器的特征信息来决定SystemCall保存的函数。当通过eax=1来执行cpuid指令的时候,处理器的特征信息会被存放到ecx和edx寄存器中,其中edx包含了一个SEP位(第11位),当该位为1说明当前处理器支持快速系统调用,此时SystemCall中保存的就会是通过快速系统调用机制切换到内核模式的KiFastSystemCall的函数地址,否则保存的就是通过自陷指令进入系统内核的KiIntSystemCall。
1.通过自陷指令切换到内核模式
根据KiIntSystemCall反汇编的结果可以知道,函数首先会将传入的参数的地址赋给edx,随后使用"int 0x2E"指令通过中断门来切换到内核模式
1 2 3 4 5 6 7 8 9 10 | .text: 7C92E500 public KiIntSystemCall .text: 7C92E500 KiIntSystemCall proc near ; DATA XREF: .text:off_7C923428↑o .text: 7C92E500 .text: 7C92E500 arg_4 = byte ptr 8 .text: 7C92E500 .text: 7C92E500 lea edx, [esp + arg_4] ;将参数地址赋给edx .text: 7C92E504 int 2Eh ; DOS 2 + internal - EXECUTE COMMAND .text: 7C92E504 ; DS:SI - > counted CR - terminated command string .text: 7C92E506 retn .text: 7C92E506 KiIntSystemCall endp |
Windows将0x2E号向量专门用于系统调用,在启动早期初始化中断描述符表(IDT)时便注册好了服务例程。因此当ntdll.dll中的函数发出int 0x2E指令以后,CPU便会通过IDT找到相应的函数进行执行。由于该函数是位于内核空间的,所以CPU在把执行权交给该函数之前,会做好从用户模式切换到内核模式的各种工作,包括:
权限检测,即检测源位置和目标位置所在的代码段权限,核实是否可以转移
准备内核模式使用的栈,为了保证内核安全,所有线程在内核态执行的时候都必须使用位于内核空间的内核栈,内核栈的大小一般为8KB或12KB
下图是int 0x2E指令的跳转流程,处理器在IDT中查找0x2E的表项,IDTEntry包含一个段选择符和中断例程的段内偏移,所以,处理器还需要在GDT中再查找一次表项,得到段选择符指定的段的虚拟基地址。段基地址加上中断例程偏移,最终得到中断例程的虚拟地址。
在Windows中,IDT的0x2E表项的段选择符是0x0008,中断例程偏移为_KiSystemService例程的地址。这里段选择符0x0008指向第一个段,段内基址为0x0,正是内核中使用的cs段。因此,最终的中断服务例程为内核中的_KiSystemService函数。
Intel x86处理器在不同模式下使用不同的栈,用户模式代码使用用户栈,内核模式代码使用内核栈,因此,当一个线程通过"int 0x2E"指令从用户模式切换到内核模式时,必然会伴随有栈的切换。下图是栈切换的示意图,处理器在将控制器交给目标例程以前,首先将原来的ss, esp, eflags, cs和eip寄存器的值放到内核栈中,并且让新的esp指向内核栈顶位置。
处理器在用户模式下执行,如何知道内核栈的位置,即内核栈的ss和esp?答案在于处理器的当前任务环境。每个处理器都总数在一个任务环境中运行,处理器的任务寄存器(TR寄存器)指向当前任务环境的TSS,其中包含每一特权级使用的栈,其中Ring0的esp位于TSS+4的位置。Windows系统在每个处理器初始化时,会在GDT中为它构造一个TSS段,然后利用ltr指令,设置处理器的任务环境的。另外,Windows每次切换线程时,总会设置好TSS中Ring0的esp,使它指向当前线程的内核栈。
当内核模式下的中断例程完成中断处理以后,它通过iret/iretd指令返回到用户模式代码。iret/iretd指令从内核中弹出eip, cs, eflags以及esp和ss,然后将控制权交给eip所指的用户模式代码。
2.通过快速调用切换到内核模式
由于通过int 0x2E指令进入内核带来的系统开销比较大。所以,从Pentium II处理器开始,Intel引入一对新的指令sysenter/sysexit,实现快速的模式切换。可以看到KiFastSystemCall函数就是通过sysenter指令切换到内核模式
1 2 3 4 5 | .text: 7C92E4F0 public KiFastSystemCall .text: 7C92E4F0 KiFastSystemCall proc near ; DATA XREF: .text:off_7C923428↑o .text: 7C92E4F0 mov edx, esp .text: 7C92E4F2 sysenter .text: 7C92E4F2 KiFastSystemCall endp |
在模式切换过程中,Sysenter要做的事情与"int 0x2E"指令类似,但是,它会尽可能地减少内存访问和权限检查的开销,其做法是,尽可能避免内存访问,而通过处理器的内部寄存器来指定必要的信息。Sysenter使用三个MSR寄存器来指定目标地址和栈信息,如下表所示。操作系统可以在内核模式下通过rdmsr/wrmsr指令来设置这三个寄存器
MSR寄存器名称 | MSR地址 | 含义 |
---|---|---|
IA32_SYSENTER_CS | 0x174 | 低16位指定了特权级0的代码段和栈段的段描述符 |
IA32_SYSENTER_ESP | 0x175 | 内核栈指针的32位偏移 |
IA32_SYSENTER_EIP | 0x176 | 目标例程的32位偏移,指向KiFastCallEntry函数 |
Sysenter指令的内部逻辑是:将IA32_SYSENTER_CS和IA32_SYSENTER_EIP分别装载到cs和eip寄存器中;将IA32_SYSENTER_CS+8和IA32_SYSENTER_ESP分别装载到ss和esp寄存器中;切换到特权级0;清除eflags中的VM标志(虚拟8086模式);执行目标例程。注意,sysenter指令对于跳转目标的代码段和栈段有如下的要求:
代码段和栈段在GDT中必须是相邻的,IA32_SYSENTER_CS指向代码段的GDT表项,紧随其后的表项应该是栈段描述符
代码段描述符指定的是一个基地址是0,段范围可达4GB的特权级0的段,具有执行和读访问权限
栈描述符指定的也是一个基地址为0,范围可达4GB的特权级0的段,但具有读写访问和向上扩展的权限
Sysexit的内部逻辑是:将IA32_SYSENTER_CS+16装载到cs寄存器;将edx寄存器中的指针装载到eip寄存器中;将IA32_SYSENTER_CS+24装载到ss寄存器中;将ecx寄存器中的指针装载到esp寄存器中;切换特权级3;执行eip寄存器中指定的用户模式代码。类似地,sysexit也要求IA32_SYSENTER_CS+16和IA32_SYSENTER_CS+24指定地用户模式代码段和栈段,即GDT中紧跟Sysenter用到地两个段描述符之后地两个GDT表项,符合在特权级3下执行或读写方面的要求。
因此MSR寄存器IA32_SYSENTER_CS实际上指定了GDT中的4个段描述符符合上述sysenter/sysexit要求。而且,用户模式代码在调用sysenter指令以前,必须将要返回的指令地址和栈指针值保存到edx和ecx寄存器中,否则,内核模式代码将无法设置正确的值,以使sysexit还能回到用户模式代码的原来位置。另外,sysenter指令并不负责将用户栈中的参数传递到内核栈中,参数的传递由用户模式代码和内核模式代码来完成,sysenter仅提供跨模式跳转和栈切换的能力。下图显示了sysenter/sysexit指令的跳转示意图,sysenter和sysexit指令旁边的粗箭头表示该指令的跳转依赖于这些寄存器。
从sysexit回到用户模式代码的地址和栈指针,即sysexit指令执行前的edx和ecx寄存器值,需要用户模式diamagnetic和内核例程来约定。也就是说,处理器在执行sysenter指令时并没有将线程信息保存在这两个寄存器中。所以,用户模式代码由责任将返回用户模式后的eip和esp通过某种方式传递到内核例程中,以便内核例程在发送sysexit指令前设置好edx和ecx。
实际上,sysenter/sysexit指令要求代码段和栈段必须执行满足相应的特权级保护要求的0~4GB地址空间,即段的基地址是0,大小可达4GB。Sysenter指令在执行时,cs寄存器被赋予IA32_SYSENTER_CS寄存器的值,但是,cs寄存器内部的段描述符并非拷贝自GDT中的表项,而是硬编码的固定值,ss寄存器也使用同样的方法。类似地,sysexit指令在执行时,cs和ss段寄存器虽然被赋予IA32_SYSENTER_CS+16或IA32_SYSENTER_CS+24的值,但是,内部的段描述符也是硬编码的固定值。所以GDT的4个表项并未真正被使用。
因此,不难理解,sysenter和sysexit只使用固定的值或寄存器的值来完成跳转,从而达到快速切换代码段和栈段的目的。而且,使用固定内容的段描述符,省去了特权级检查的步骤,可进一步提升这两条指令的效率。对于频繁进行模式切换的应用线程来说,使用sysenter指令比"int 0x2E"有显著的性能优势。
三.系统调用函数入口
1._TRAP_FRAME结构体
上面说到,通过"int 0x2E"指令进入内核的时候,KiSystemService函数获得控制权,而如果是通过"Sysenter"指令进入内核,则KiFastCallEntry函数获得控制权。而这两个函数在内核获得控制权以后,首先要做的就是先建立一个陷阱帧(trap frame),陷阱帧对应的结构体是_KTRAP_FRAME,该结构体定义如下:
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 | kd> dt _KTRAP_FRAME nt!_KTRAP_FRAME + 0x000 DbgEbp : Uint4B + 0x004 DbgEip : Uint4B + 0x008 DbgArgMark : Uint4B + 0x00c DbgArgPointer : Uint4B + 0x010 TempSegCs : Uint4B + 0x014 TempEsp : Uint4B + 0x018 Dr0 : Uint4B + 0x01c Dr1 : Uint4B + 0x020 Dr2 : Uint4B + 0x024 Dr3 : Uint4B + 0x028 Dr6 : Uint4B + 0x02c Dr7 : Uint4B + 0x030 SegGs : Uint4B + 0x034 SegEs : Uint4B + 0x038 SegDs : Uint4B + 0x03c Edx : Uint4B + 0x040 Ecx : Uint4B + 0x044 Eax : Uint4B + 0x048 PreviousMode : Uint4B + 0x04c ExceptionList : Ptr32 _EXCEPTION_REGISTRATION_RECORD + 0x050 SegFs : Uint4B + 0x054 Edi : Uint4B + 0x058 Esi : Uint4B + 0x05c Ebx : Uint4B + 0x060 Ebp : Uint4B + 0x064 ErrCode : Uint4B + 0x068 Eip : Uint4B + 0x06c SegCs : Uint4B + 0x070 EFlags : Uint4B + 0x074 HardwareEsp : Uint4B + 0x078 HardwareSegSs : Uint4B + 0x07c V86Es : Uint4B + 0x080 V86Ds : Uint4B + 0x084 V86Fs : Uint4B + 0x088 V86Gs : Uint4B |
可以看到,该结构体保存了是各种寄存器的信息,而要理解内核入口函数是如何填充这个结构体,首先要理解KPCR。
2.KPCR结构体
每一个CPU在内核都有对应的一个结构体来描述CPU的信息,该结构体就是KPCR结构体。该结构体中保存了CPU需要使用的一些数据,比如GDT,IDT和线程相关的一些数据,结构体定义如下:
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 | kd> dt _KPCR nt!_KPCR + 0x000 NtTib : _NT_TIB + 0x01c SelfPcr : Ptr32 _KPCR + 0x020 Prcb : Ptr32 _KPRCB + 0x024 Irql : UChar + 0x028 IRR : Uint4B + 0x02c IrrActive : Uint4B + 0x030 IDR : Uint4B + 0x034 KdVersionBlock : Ptr32 Void + 0x038 IDT : Ptr32 _KIDTENTRY + 0x03c GDT : Ptr32 _KGDTENTRY + 0x040 TSS : Ptr32 _KTSS + 0x044 MajorVersion : Uint2B + 0x046 MinorVersion : Uint2B + 0x048 SetMember : Uint4B + 0x04c StallScaleFactor : Uint4B + 0x050 DebugActive : UChar + 0x051 Number : UChar + 0x052 Spare0 : UChar + 0x053 SecondLevelCacheAssociativity : UChar + 0x054 VdmAlert : Uint4B + 0x058 KernelReserved : [ 14 ] Uint4B + 0x090 SecondLevelCacheSize : Uint4B + 0x094 HalReserved : [ 16 ] Uint4B + 0x0d4 InterruptMode : Uint4B + 0x0d8 Spare1 : UChar + 0x0dc KernelReserved2 : [ 17 ] Uint4B + 0x120 PrcbData : _KPRCB |
偏移 | 成员 | 作用 |
---|---|---|
0x000 | NtTib | 包含异常,线程栈等信息 |
0x01C | SelfPrc | 指向_KPCR结构体本身的地址 |
0x020 | Prcb | 指向扩展结构体PRCB |
0x038 | IDT | IDT表基址 |
0x03C | GDT | GDT表基址 |
0x040 | TSS | 指向TSS |
0x051 | Number | CPU编号 |
0x120 | PrcbData | 指向_KPRCB结构体 |
其中NtTib结构体的定义如下:
1 2 3 4 5 6 7 8 9 10 | kd> dt _NT_TIB ntdll!_NT_TIB + 0x000 ExceptionList : Ptr32 _EXCEPTION_REGISTRATION_RECORD + 0x004 StackBase : Ptr32 Void + 0x008 StackLimit : Ptr32 Void + 0x00c SubSystemTib : Ptr32 Void + 0x010 FiberData : Ptr32 Void + 0x010 Version : Uint4B + 0x014 ArbitraryUserPointer : Ptr32 Void + 0x018 Self : Ptr32 _NT_TIB |
偏移 | 成员 | 作用 |
---|---|---|
0x000 | ExceptionList | 指向当前线程内核异常链表(SEH) |
0x004 | StackBase | 内核栈的基地址 |
0x008 | StackLimit | 内核栈的边界 |
0x018 | Self | 指向_NT_TIB结构本身地址 |
_KPRCB结构体的定义如下:
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 | kd> dt _KPRCB nt!_KPRCB + 0x000 MinorVersion : Uint2B + 0x002 MajorVersion : Uint2B + 0x004 CurrentThread : Ptr32 _KTHREAD + 0x008 NextThread : Ptr32 _KTHREAD + 0x00c IdleThread : Ptr32 _KTHREAD + 0x010 Number : Char + 0x011 Reserved : Char + 0x012 BuildType : Uint2B + 0x014 SetMember : Uint4B + 0x018 CpuType : Char + 0x019 CpuID : Char + 0x01a CpuStep : Uint2B + 0x01c ProcessorState : _KPROCESSOR_STATE + 0x33c KernelReserved : [ 16 ] Uint4B + 0x37c HalReserved : [ 16 ] Uint4B + 0x3bc PrcbPad0 : [ 92 ] UChar + 0x418 LockQueue : [ 16 ] _KSPIN_LOCK_QUEUE + 0x498 PrcbPad1 : [ 8 ] UChar + 0x4a0 NpxThread : Ptr32 _KTHREAD + 0x4a4 InterruptCount : Uint4B + 0x4a8 KernelTime : Uint4B + 0x4ac UserTime : Uint4B + 0x4b0 DpcTime : Uint4B + 0x4b4 DebugDpcTime : Uint4B + 0x4b8 InterruptTime : Uint4B + 0x4bc AdjustDpcThreshold : Uint4B + 0x4c0 PageColor : Uint4B + 0x4c4 SkipTick : Uint4B + 0x4c8 MultiThreadSetBusy : UChar + 0x4c9 Spare2 : [ 3 ] UChar + 0x4cc ParentNode : Ptr32 _KNODE + 0x4d0 MultiThreadProcessorSet : Uint4B + 0x4d4 MultiThreadSetMaster : Ptr32 _KPRCB + 0x4d8 ThreadStartCount : [ 2 ] Uint4B + 0x4e0 CcFastReadNoWait : Uint4B + 0x4e4 CcFastReadWait : Uint4B + 0x4e8 CcFastReadNotPossible : Uint4B + 0x4ec CcCopyReadNoWait : Uint4B + 0x4f0 CcCopyReadWait : Uint4B + 0x4f4 CcCopyReadNoWaitMiss : Uint4B + 0x4f8 KeAlignmentFixupCount : Uint4B + 0x4fc KeContextSwitches : Uint4B + 0x500 KeDcacheFlushCount : Uint4B + 0x504 KeExceptionDispatchCount : Uint4B + 0x508 KeFirstLevelTbFills : Uint4B + 0x50c KeFloatingEmulationCount : Uint4B + 0x510 KeIcacheFlushCount : Uint4B + 0x514 KeSecondLevelTbFills : Uint4B + 0x518 KeSystemCalls : Uint4B + 0x51c SpareCounter0 : [ 1 ] Uint4B + 0x520 PPLookasideList : [ 16 ] _PP_LOOKASIDE_LIST + 0x5a0 PPNPagedLookasideList : [ 32 ] _PP_LOOKASIDE_LIST + 0x6a0 PPPagedLookasideList : [ 32 ] _PP_LOOKASIDE_LIST + 0x7a0 PacketBarrier : Uint4B + 0x7a4 ReverseStall : Uint4B + 0x7a8 IpiFrame : Ptr32 Void + 0x7ac PrcbPad2 : [ 52 ] UChar + 0x7e0 CurrentPacket : [ 3 ] Ptr32 Void + 0x7ec TargetSet : Uint4B + 0x7f0 WorkerRoutine : Ptr32 void + 0x7f4 IpiFrozen : Uint4B + 0x7f8 PrcbPad3 : [ 40 ] UChar + 0x820 RequestSummary : Uint4B + 0x824 SignalDone : Ptr32 _KPRCB + 0x828 PrcbPad4 : [ 56 ] UChar + 0x860 DpcListHead : _LIST_ENTRY + 0x868 DpcStack : Ptr32 Void + 0x86c DpcCount : Uint4B + 0x870 DpcQueueDepth : Uint4B + 0x874 DpcRoutineActive : Uint4B + 0x878 DpcInterruptRequested : Uint4B + 0x87c DpcLastCount : Uint4B + 0x880 DpcRequestRate : Uint4B + 0x884 MaximumDpcQueueDepth : Uint4B + 0x888 MinimumDpcRate : Uint4B + 0x88c QuantumEnd : Uint4B + 0x890 PrcbPad5 : [ 16 ] UChar + 0x8a0 DpcLock : Uint4B + 0x8a4 PrcbPad6 : [ 28 ] UChar + 0x8c0 CallDpc : _KDPC + 0x8e0 ChainedInterruptList : Ptr32 Void + 0x8e4 LookasideIrpFloat : Int4B + 0x8e8 SpareFields0 : [ 6 ] Uint4B + 0x900 VendorString : [ 13 ] UChar + 0x90d InitialApicId : UChar + 0x90e LogicalProcessorsPerPhysicalProcessor : UChar + 0x910 MHz : Uint4B + 0x914 FeatureBits : Uint4B + 0x918 UpdateSignature : _LARGE_INTEGER + 0x920 NpxSaveArea : _FX_SAVE_AREA + 0xb30 PowerState : _PROCESSOR_POWER_STATE |
偏移 | 成员 | 作用 |
---|---|---|
0x004 | CurrentThread | 指向当前线程 |
0x008 | NextThread | 指向即将切换的下一个线程 |
0x00C | IdleThread | 指向空闲线程 |
0x30段选择子对应的段描述符所指的基地址就是KPCR的地址
3.KiSystemService
接下来看看KiSystemService是如何填充陷阱帧,首先由于是KiSystemService获得控制权,所以是通过"int 0x2E"指令切换进内核。那么此时栈中已经保存了ss, esp, eflags, cs, eip。所以这个时候是从Errcode开始填充
1 2 3 4 5 6 7 8 9 10 11 | .text: 004067D1 _KiSystemService proc near ; CODE XREF: ZwAcceptConnectPort(x,x,x,x,x,x) + C↓p .text: 004067D1 ; ZwAccessCheck(x,x,x,x,x,x,x,x) + C↓p ... .text: 004067D1 .text: 004067D1 arg_0 = dword ptr 4 .text: 004067D1 .text: 004067D1 push 0 ; 填充ErrCode .text: 004067D3 push ebp ; 填充Ebp .text: 004067D4 push ebx ; 填充ebx .text: 004067D5 push esi ; 填充esi .text: 004067D6 push edi ; 填充edi .text: 004067D7 push fs ; 填充fs段寄存器 |
将0x30赋给fs寄存器,让fs寄存器指向KPCR后继续填充
1 2 3 4 5 6 | .text: 004067D9 mov ebx, 30h .text: 004067DE mov fs, ebx ; 将 0x30 赋值给fs,此时fs指向的就是KPCR .text: 004067E0 push large dword ptr fs: 0 ; 填充ExceptionList .text: 004067E7 mov large dword ptr fs: 0 , 0FFFFFFFFh .text: 004067F2 mov esi, large fs: 124h ; 取出ETHREAD赋给esi .text: 004067F9 push dword ptr [esi + _KTHREAD.PreviousMode] ; 填充PreviousMode |
填充到PreviousMode的时候,此时的esp所指的是陷阱帧偏移0x48的地址,接下来将esp减去0x48,就是将esp移动到陷阱帧的头部,此时esp + 0x68 + arg_0就会是陷阱帧偏移0x6C的位置,保存的是切换到内核模式前的cs段寄存器的内容,将其除去与1进行与操作,如果为1则说明是从用户模式切换进内核模式,将其保存到线程的先前模式中
1 2 3 4 | .text: 004067FF sub esp, 48h ; 让esp指向陷阱帧头部 .text: 00406802 mov ebx, [esp + 68h + arg_0] ; 将进入内核前的cs的值赋值给ebx .text: 00406806 and ebx, 1 ; 将ebx与 1 进行与运算 .text: 00406809 mov [esi + _KTHREAD.PreviousMode], bl ; 将bl赋值到当前线程的先前模式 |
将esp赋给ebp,这样ebp也指向陷阱帧的顶部,随后将线程的TrapFrame填充到正在填充的TrapFram的到edx中,在将当前正在填充的TrapFrame赋值给线程的TrapFrame
1 2 3 4 | .text: 0040680F mov ebp, esp ; 让ebp指向陷阱帧的顶部 .text: 00406811 mov ebx, [esi + _KTHREAD.TrapFrame] ; 将线程的TrapFrame保存到ebx中 .text: 00406817 mov [ebp + 3Ch ], ebx ; 填充edx .text: 0040681A mov [esi + _KTHREAD.TrapFrame], ebp ; 将当前陷阱帧顶部的地址赋给线程的TrapFrame |
继续对陷阱帧进行填充
1 2 3 4 5 6 7 | .text: 00406820 cld .text: 00406821 mov ebx, [ebp + 60h ] ; 将ebp赋值给ebx .text: 00406824 mov edi, [ebp + 68h ] ; 将eip赋值给edi .text: 00406827 mov [ebp + 0Ch ], edx ; 用edx填充DbgArgPointer .text: 0040682A mov dword ptr [ebp + 8 ], 0BADB0D00h ; 为DbgArgMark赋值 .text: 00406831 mov [ebp + 0 ], ebx ; 将ebx保存的内容赋值给DbgEbp .text: 00406834 mov [ebp + 4 ], edi ; 将edi的内容赋给DbgEip |
判断当前线程是否处于调试模式
1 2 | .text: 00406837 test [esi + _ETHREAD.Tcb.DebugActive], 0FFh .text: 0040683B jnz Dr_kss_a |
如果DebugActive不为-1,则处于调试模式,会跳转到Dr_kss_a执行,如果不处于调试模式,则会直接跳转到loc_406932地址继续执行
1 2 3 4 5 | .text: 00406841 loc_406841: ; CODE XREF: Dr_kss_a + 10 ↑j .text: 00406841 ; Dr_kss_a + 7C ↑j .text: 00406841 sti .text: 00406842 jmp loc_406932 .text: 00406842 _KiSystemService endp |
4.KiFastCallEntry
当KiFastCallEntry获得控制权的时候,说明是通过sysenter指令进入内核,此时并为将esp指针指向内核栈中,且也没有像KiSystemService一样进入内核的时候就已经填充了5个字段。所以,这个时候就首先需要获得内核栈的esp,然后开始从SS寄存器开始填充
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | .text: 0040689F _KiFastCallEntry proc near ; DATA XREF: _KiTrap01 + 71 ↓o .text: 0040689F ; KiLoadFastSyscallMachineSpecificRegisters(x) + 24 ↓o .text: 0040689F .text: 0040689F var_B = byte ptr - 0Bh .text: 0040689F .text: 0040689F ; FUNCTION CHUNK AT .text: 0040686C SIZE 00000024 BYTES .text: 0040689F .text: 0040689F mov ecx, 23h .text: 004068A4 push 30h .text: 004068A6 pop fs ; fs执行KPCR .text: 004068A8 mov ds, ecx .text: 004068AA mov es, ecx ; 将ds和es都用 0x23 赋值 .text: 004068AC mov ecx, large fs: 40h ; 将TSS赋给ecx .text: 004068B3 mov esp, [ecx + 4 ] ; 取出在ring 0 执行时候使用的esp .text: 004068B6 push 23h ; 填充HardwareSegSs .text: 004068B8 push edx ; 填充HardwareEsp .text: 004068B9 pushf ; 填充EFlags |
由于快速系统调用的方式进入内核的时候,直接用esp为edx赋值,所以edx要加上8,这样才能让esp指向参数的地址
1 2 3 4 5 | .text: 004068BA loc_4068BA: ; CODE XREF: _KiFastCallEntry2 + 23 ↑j .text: 004068BA push 2 .text: 004068BC add edx, 8 ; edx加 8 ,指向保存参数的栈地址 .text: 004068BF popf ; 将eflags赋值为 2 .text: 004068C0 or [esp + 0Ch + var_B], 2 ; 将Esp与 2 进行或操作 |
从cs开始继续填充
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 | .text: 004068C5 push 1Bh ; 填充CS .text: 004068C7 push dword ptr ds: 0FFDF0304h ; 填充EIP .text: 004068CD push 0 ; 填充ErrCode .text: 004068CF push ebp ; 填充ebp .text: 00408D0 push ebx ; 填充ebx .text: 004068D1 push esi ; 填充esi .text: 004068D2 push edi ; 填充edi .text: 004068D3 mov ebx, large fs: 1Ch ; 将KPCR地址赋值给ebx .text: 004068DA push 3Bh ; 填充fs .text: 004068DC mov esi, [ebx + 124h ] ; 将当前线程赋给esi .text: 004068E2 push dword ptr [ebx] ; 填充ExceptionList .text: 004068E4 mov dword ptr [ebx], 0FFFFFFFFh ; 为KPCR的ExceptionList赋值 .text: 004068EA mov ebp, [esi + _ETHREAD.Tcb.InitialStack] ; 取出栈地址赋给ebp .text: 004068ED push 1 ; 填充PreviousMode .text: 004068EF sub esp, 48h ; 将esp指向陷阱帧头部 .text: 004068F2 sub ebp, 29Ch ; 将ebp减去 0x29C .text: 004068F8 mov [esi + _KTHREAD.PreviousMode], 1 .text: 004068FF cmp ebp, esp ; 比较esp与ebp是否相等 .text: 00406901 jnz loc_40686C .text: 00406907 and dword ptr [ebp + 2Ch ], 0 ; 将Dr7清 0 .text: 0040690B test [esi + _ETHREAD.Tcb.DebugActive], 0FFh ; 判断是否处于调试模式 .text: 0040690F mov [esi + _KTHREAD.TrapFrame], ebp ; 为当前线程的TrapFrame赋值 .text: 00406915 jnz Dr_FastCallDrSave .text: 0040691B .text: 0040691B loc_40691B: ; CODE XREF: Dr_FastCallDrSave + 10 ↑j .text: 0040691B ; Dr_FastCallDrSave + 7C ↑j .text: 0040691B mov ebx, [ebp + 60h ] ; 取出正在填充的TrapFrame的ebp .text: 0040691E mov edi, [ebp + 68h ] ; 取出正在填充的TrapFrame的Eip .text: 00406921 mov [ebp + 0Ch ], edx ; 填充DbgArgPointer .text: 00406924 mov dword ptr [ebp + 8 ], 0BADB0D00h ; 填充DbgArgMark .text: 0040692B mov [ebp + 0 ], ebx ; 填充DbgEbp .text: 0040692E mov [ebp + 4 ], edi ; 填充DbgEip .text: 00406931 sti |
填充完了以后就会继续向下执行loc_406932的代码,KiSystemService填充完陷阱帧以后也是跳转到这里开始执行。
四.Windows中的系统服务分发
1.系统服务表
填充完陷阱帧以后,接下来就需要调用内核的服务函数。在ntdll.dll的存根函数中,在进入内核之前会为eax指定一个服务号,内核就是根据这个服务号来知道要调用内核中哪个系统服务例程,以及从用户栈拷贝多少数据到内核栈中,这一过程被称为系统服务分发。
Windows实现系统服务分发的关键是一个称为SDT(服务描述符表)的表。Windows支持多个SDT,系统服务存根函数指定的系统服务号,即eax寄存器的值,包含了表的索引和表内的服务索引。eax的低12位(即0~11位)指定了表内的系统服务索引,12~13位指定了表的索引。内核全局变量KeServiceDescriptorTable是一个数组,每个元素指向一个SDT,所以,系统服务分发代码根据系统服务号,首先定位到KeServiceDescriptorTable的元素,然后找到特定的系统服务项。
以下是与系统服务表有关的定义:
1 2 3 4 5 6 7 8 9 10 11 12 | typedef struct _KSERVICE_TABLKSERVICE_TABLE_DESCRIPTORE_DESCRIPTOR { PULONG_PTR Base; PULONG Count; PULONG Limit; PUCHAR Number; }KSERVICE_TABLE_DESCRIPTOR, * PKSERVICE_TABLE_DESCRIPTOR; #define NUMBER_SERVICE_TABLES 2 KSERVICE_TABLE_DESCRIPTOR KeServiceDescriptorTable[NUMBER_SERVICE_TABLES] KSERVICE_TABLE_DESCRIPTOR KeServiceDescriptorTableShadow[NUMBER_SERVICE_TABLES] |
以上可以看出,Windows系统支持2个SDT。KeServiceDescriptorTableShadow是一个内部数组,它的第二个元素,即KeServiceDescriptorTableShadow[1]专门用于Windows子系统,其他表项与KeServiceDescriptorTable完全相同。KeServiceDescriptorTable[1]元素不再使用。因此,Windows内核系统服务号范围是0x0000000~0x00000FFF;而Windows子系统服务号的范围是0x00001000~0x00001FFF。
KSERVICE_TABLE_DESCRIPTOR数组结构描述的每一个SDT中,Base域指向一个由系统服务例程构成的32位地址数组;Count域是一个指针成员,指向一个计数器数组,数组中的每个元素记录了相应的系统服务被调用的次数,次域仅用于内核的调试版本;Limit域记录了该SDT中系统服务的数量;Number域也是一个指针,指向一个字节数组,数组中的每个元素对应于一个系统服务所需要传递的参数区的长度,该长度是以字节为单位。
2.系统函数的调用
接下来从loc_406932继续分析,看看内核是如何完成系统调用的。
将服务号赋给edi以后,edi右移了8位,此时保留了系统服务号的8~13位,又将edi与0x30异或,那就会保留服务号的12~13位,此时edi会等于0x00或0x10,接下来就从线程结构体中将KeServiceDescriptorTable取出与edi进行相加就可以得到想要使用的系统服务表基址
1 2 3 4 5 6 7 | .text: 00406932 loc_406932: ; CODE XREF: _KiBBTUnexpectedRange + 18 ↑j .text: 00406932 ; _KiSystemService + 71 ↑j .text: 00406932 mov edi, eax ; 将服务号赋值给edi .text: 00406934 shr edi, 8 ; 服务号右移八位 .text: 00406937 and edi, 30h ; 服务号与 0x30 进行与操作 .text: 0040693A mov ecx, edi ; 将计算以后结果保存在ecx中 .text: 0040693C add edi, [esi + _KTHREAD.ServiceTable] ; 将edi加上系统服务表基地址 |
将系统服务号保存到ebx以后,保留系统服务号的低12位,与系统服务表中的Limit域进行比较,如果超过Limit域的内容则说明越界,跳转到想要代码执行
1 2 3 4 | .text: 00406942 mov ebx, eax ; 将服务号赋值给ebx .text: 00406944 and eax, 0FFFh ; 保留系统服务号低 12 位 .text: 00406949 cmp eax, [edi + 8 ] ; 判断是否超过界限 .text: 0040694C jnb _KiBBTUnexpectedRange |
判断ecx是否等于0x10,ecx的值在上面计算edi的最后进行赋值,所以它会等于0x00或0x10,如果等于0x10则说明此时要查询的是子系统SDT表,否则就是系统使用的基本SDT
1 2 | .text: 00406952 cmp ecx, 10h .text: 00406955 jnz short loc_406972 |
通过检查以后,就要准备调用相应的函数,但是首先需要将参数从用户栈赋值到内核栈中,此时edx保存的就是用户栈参数的地址,将它赋给esi已备后面的赋值。从Number域和Base域中分别将要调用的函数和参数长度取出赋给ebx和ecx。但是要注意此时参数的长度是以字节为单位,而程序在进行参数传递的时候会将4个字节当作一个参数,所以ecx需要进行右移操作算出参数个数,有了参数个数就可以移动相应大小的栈顶用来保存参数
1 2 3 4 5 6 7 8 9 10 11 12 | .text: 00406972 loc_406972: ; CODE XREF: _KiFastCallEntry + B6↑j .text: 00406972 ; KiSystemServiceAccessTeb() + 6 ↑j .text: 00406972 inc large dword ptr fs: 638h .text: 00406979 mov esi, edx ; 将参数地址赋给esi .text: 0040697B mov ebx, [edi + 0Ch ] ; 将Number域赋给ebx .text: 0040697E xor ecx, ecx ; ecx清 0 .text: 00406980 mov cl, [eax + ebx] ; 将所需的参数长度赋给cl .text: 00406983 mov edi, [edi] ; 取出Base域赋给edi .text: 00406985 mov ebx, [edi + eax * 4 ] ; 取出要调用的函数地址赋给ebx .text: 00406988 sub esp, ecx ; 将栈顶向上移动存放参数 .text: 0040698A shr ecx, 2 ; ecx右移两位,就是除以 4 ,得到的就是参数个数 .text: 0040698D mov edi, esp ; 将栈顶赋给edi |
在对陷阱帧中的保存的eflags和cs进行判断以后
1 2 3 4 | .text: 0040698F test byte ptr [ebp + 72h ], 2 .text: 00406993 jnz short loc_40699B .text: 00406995 test byte ptr [ebp + 6Ch ], 1 .text: 00406999 jz short _KiSystemServiceCopyArguments@ 0 |
就会跳转到KiSystemServiceCopyArguments函数执行,在这个函数中就会完成参数的赋值,注意此时的esi指向的是用户栈参数地址,而edi指向的是用来保存参数的内核栈的地址,ebx保存的就是要调用系统服务函数的地址
1 2 3 4 5 6 | .text: 004069A7 _KiSystemServiceCopyArguments@ 0 proc near .text: 004069A7 ; CODE XREF: KiSystemServiceAccessTeb() + 39 ↑j .text: 004069A7 ; DATA XREF: KiPreprocessAccessViolation(x,x,x):loc_4628DF↓o .text: 004069A7 rep movsd .text: 004069A9 call ebx .text: 004069A9 _KiSystemServiceCopyArguments@ 0 endp |
当系统服务函数调用返回以后,接下来就需要返回到用户层继续执行,所以接下来就需要恢复相应的寄存器来返回到用户层。
五.系统调用的返回
将esp恢复到陷阱帧的顶部,在将保存在edx中的线程原来的TrapFrame取出赋值到当前线程的TrapFrame中
1 2 3 4 5 6 7 8 9 10 | .text: 004069AB _KiSystemServicePostCall@ 0 proc near ; CODE XREF: KiSystemServiceAccessTeb() + 1F4 ↓j .text: 004069AB ; DATA XREF: KiPreprocessAccessViolation(x,x,x) + 59 ↓o .text: 004069AB mov esp, ebp .text: 004069AD .text: 004069AD loc_4069AD: ; CODE XREF: _KiBBTUnexpectedRange + 38 ↑j .text: 004069AD ; _KiBBTUnexpectedRange + 43 ↑j .text: 004069AD mov ecx, large fs: 124h ; 将当前线程赋值给ecx .text: 004069B4 mov edx, [ebp + 3Ch ] ; 将保存的TrapFrame地址赋给edx .text: 004069B7 mov [ecx + _KTHREAD.TrapFrame], edx ; 将edx赋给线程的TrapFrame .text: 004069B7 _KiSystemServicePostCall@ 0 endp |
程序继续向下执行会执行到KiServiceExit,该函数从TrapFrame中取出保存的Eflagls和cs判断是否是虚拟8086模式,是否是从用户模式发起的调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | .text: 004069BD _KiServiceExit proc near ; CODE XREF: _KiSetLowWaitHighThread + 7F ↓j .text: 004069BD ; NtContinue(x,x) + 43 ↓j ... .text: 004069BD .text: 004069BD arg_C = dword ptr 10h .text: 004069BD arg_10 = dword ptr 14h .text: 004069BD arg_40 = dword ptr 44h .text: 004069BD arg_44 = dword ptr 48h .text: 004069BD arg_48 = dword ptr 4Ch .text: 004069BD arg_60 = dword ptr 64h .text: 004069BD arg_64 = dword ptr 68h .text: 004069BD arg_68 = dword ptr 6Ch .text: 004069BD arg_6C = dword ptr 70h .text: 004069BD .text: 004069BD ; FUNCTION CHUNK AT .text: 00406AC7 SIZE 00000088 BYTES .text: 004069BD .text: 004069BD cli .text: 004069BE test dword ptr [ebp + 70h ], 20000h ; 判断是否是虚拟 8086 模式 .text: 004069C5 jnz short loc_4069CD .text: 004069C7 test byte ptr [ebp + 6Ch ], 1 ; 判断是否是用户层发起的调用 .text: 004069CB jz short loc_406A23 |
如果是8086模式或者是从用户层发起的请求就会执行以下代码,如果不是上述情况就会跳转到loc_406A23执行,而以下代码是与APC相关,主要内容就是先通过KfRaiseIrql将CPU的运行级别提升到APC_LEVEL,然后就调用KiDeleverApc来执行用户APC,随后在通过KfLowerIrql将运行级别降低到PASSIVE_LEVEL。
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 | .text: 004069CD loc_4069CD: ; CODE XREF: _KiServiceExit + 8 ↑j .text: 004069CD ; _KiServiceExit + 64 ↓j .text: 004069CD mov ebx, large fs: 124h .text: 004069D4 mov [ebx + _KTHREAD.Alerted], 0 .text: 004069D8 cmp [ebx + _KTHREAD.ApcState.UserApcPending], 0 ; 判断是否有用户APC需要执行 .text: 004069DC jz short loc_406A23 .text: 004069DE mov ebx, ebp ; 将陷阱帧栈顶地址赋给ebx .text: 004069E0 mov [ebx + 44h ], eax ; 为eax赋值 .text: 004069E3 mov dword ptr [ebx + 50h ], 3Bh ; 为fs寄存器赋值 .text: 004069EA mov dword ptr [ebx + 38h ], 23h ; 为ds寄存器赋值 .text: 004069F1 mov dword ptr [ebx + 34h ], 23h ; 为es寄存器赋值 .text: 004069F8 mov dword ptr [ebx + 30h ], 0 ; 为gs寄存器赋值 .text: 004069FF mov ecx, 1 ; NewIrql .text: 00406A04 call ds:__imp_@KfRaiseIrql@ 4 ; KfRaiseIrql(x) .text: 00406A0A push eax .text: 00406A0B sti .text: 00406A0C push ebx .text: 00406A0D push 0 .text: 00406A0F push 1 .text: 00406A11 call _KiDeliverApc@ 12 ; KiDeliverApc(x,x,x) .text: 00406A16 pop ecx ; NewIrql .text: 00406A17 call ds:__imp_@KfLowerIrql@ 4 ; KfLowerIrql(x) .text: 00406A1D mov eax, [ebx + 44h ] .text: 00406A20 cli .text: 00406A21 jmp short loc_4069CDKfRaiseIrql@ 4 |
如果不用执行APC代码,就会接着执行以下的代码,将陷阱帧中的保存的数据复制到线程中,这样就恢复了切换进内核模式前的一部分数据,然后判断是否处于调试模式
1 2 3 4 5 6 7 8 9 10 | .text: 00406A23 loc_406A23: ; CODE XREF: _KiServiceExit + E↑j .text: 00406A23 ; _KiServiceExit + 1F ↑j .text: 00406A23 mov edx, [esp + 4Ch ] ; 将ExceptionList赋给edx .text: 00406A27 mov ebx, large fs: 50h ; 将DebugActive赋给ebx .text: 00406A2E mov large fs: 0 , edx ; 将ExceptionList赋给KPCR的ExceptionList .text: 00406A35 mov ecx, [esp + 48h ] ; 将PreviousMode赋值给ecx .text: 00406A39 mov esi, large fs: 124h .text: 00406A40 mov [esi + _KTHREAD.PreviousMode], cl .text: 00406A46 test ebx, 0FFh ; 判断是否处于调试模式 .text: 00406A4C jnz short loc_406AC7 |
从陷阱帧中恢复几个寄存器的值
1 2 3 4 5 6 | .text: 00406A89 loc_406A89: ; CODE XREF: _KiServiceExit + C5↑j .text: 00406A89 lea esp, [ebp + 54h ] ; 将陷阱帧中edi的地址赋给esp .text: 00406A8C pop edi ; 恢复edi .text: 00406A8D pop esi ; 恢复esi .text: 00406A8E pop ebx ; 恢复ebx .text: 00406A8F pop ebp ; 恢复ebp |
此时esp指向陷阱帧中的ErrorCode,然后继续判断保存的cs寄存器的值,如果cs寄存器的值小于0x80就会判断是否是用户模式发起的调用还是内核模式发起的调用,如果是从内核模式发起的调用,则会把返回地址赋值到edx中,然后通过jmp指令跳转
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | .text: 00406A90 cmp word ptr [esp + 8 ], 80h ; 判断cs寄存器是否高于 0x80 .text: 00406A97 ja loc_407376 .text: 00406A9D add esp, 4 ; esp加 4 ,指向EIP .text: 00406AA0 test dword ptr [esp + 4 ], 1 ; 判断保存的cs寄存器是否等于 1 .text: 00406AA0 _KiServiceExit endp ; sp - analysis failed .text: 00406AA0 .text: 00406AA8 .text: 00406AA8 ; = = = = = = = = = = = = = = = S U B R O U T I N E = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = .text: 00406AA8 .text: 00406AA8 .text: 00406AA8 _KiSystemCallExitBranch proc near ; DATA XREF: KiEnableFastSyscallReturn():loc_43AAE3↓r .text: 00406AA8 ; KiEnableFastSyscallReturn() + 26 ↓w ... .text: 00406AA8 jnz short _KiSystemCallExit .text: 00406AAA pop edx ; 返回地址赋给edx .text: 00406AAB pop ecx ; cs寄存器赋给ecx .text: 00406AAC popf ; 恢复eflags寄存器 .text: 00406AAD jmp edx |
如果是用户模式发起的调用,则会进行跳转,而跳转的地址会根据是否支持快速调用而不同,如果不支持快速调用,跳转的地址就会是KiSystemCallExit,如果支持快速调用,系统就会将跳转地址从KiSystemCallExit修改为KiSystemCallExit2。
如果执行的是KiSystemCallExit,就会通过iret指令返回用户层
1 2 3 4 5 | .text: 00406AAF _KiSystemCallExit: ; CODE XREF: _KiSystemCallExitBranch↑j .text: 00406AAF ; _KiSystemCallExit2 + 5 ↓j .text: 00406AAF ; DATA XREF: ... .text: 00406AAF iret .text: 00406AAF _KiSystemCallExitBranch |
如果执行的是KiSystemCallExit2,就会将eip和esp分别赋值给edx和ecx以后调用sysexit指令来返回到用户层
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | .text: 00406AB0 _KiSystemCallExit2 proc near ; DATA XREF: KiRestoreFastSyscallReturnState() + 16 ↓o .text: 00406AB0 .text: 00406AB0 arg_5 = byte ptr 9 .text: 00406AB0 .text: 00406AB0 test byte ptr [esp + 9 ], 1 .text: 00406AB5 jnz short _KiSystemCallExit .text: 00406AB7 pop edx ; 将陷阱帧中eip赋值给edx .text: 00406AB8 add esp, 4 ; esp加 4 ,指向陷阱帧中的EFlags .text: 00406ABB and byte ptr [esp + 1 ], 0FDh .text: 00406AC0 popf ; 恢复eflags寄存器 .text: 00406AC1 pop ecx ; 将陷阱帧中保存的esp赋给ecx .text: 00406AC2 sti .text: 00406AC3 sysexit .text: 00406AC5 iret .text: 00406AC5 _KiSystemCallExit2 endp ; sp - analysis failed |
六.参考资料
《Windows内核原理与实现》
《Windows内核情景分析》(上册)
《软件调试》(第二版)卷2
[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界