老掉牙的话题了,不过这是学习研究系统的基础。
简单的说说windows系统调用,我们编写win32程序的时候为了完成特定的功能就要去调用win32应用程序接口,就是win32 API,windows通过win32 API调用界面所定义的库函数,这些函数基本上都是系统dll中导出的,然后调用流程一直往系统底层走,知道通过某些特殊的命令进入内核进而完成系统调用,当然不是每一个API都能进入系统调用流程。
这里以ReadFile函数作为研究对象,这个API是kernel32导出的,通过windbg跟踪调用可以直白的看出函数内部调用了NtReadFile,内涵源代码提供了NtReadFile函数,但是用户空间的程序是不可能直接调用内核函数的,其实这个NtReadFile是用户空间提供的一个函数。
以NtReadFile为例:
lkd> u ntdll!ntreadfile
ntdll!NtReadFile:
7c92d9b0 b8b7000000 mov eax,0B7h
7c92d9b5 ba0003fe7f mov edx,offset SharedUserData!SystemCallStub (7ffe0300)
7c92d9ba ff12 call dword ptr [edx]
7c92d9bc c22400 ret 24h
7c92d9bf 90 nop
看到在对eax赋值之后,将一个函数指针传给了edx。
lkd> dd SharedUserData!SystemCallStub
7ffe0300 7c92e4f0 7c92e4f4 00000000 00000000
7ffe0310 00000000 00000000 00000000 00000000
7ffe0320 00000000 00000000 00000000 00000000
7ffe0330 fb1fc0d5 00000000 00000000 00000000
7ffe0340 00000000 00000000 00000000 00000000
7ffe0350 00000000 00000000 00000000 00000000
7ffe0360 00000000 00000000 00000000 00000000
7ffe0370 00000000 00000000 00000000 00000000
继续反汇编这个地址7c92e4f0
lkd> u 7c92e4f0
ntdll!KiFastSystemCall:
7c92e4f0 8bd4 mov edx,esp
7c92e4f2 0f34 sysenter
ntdll!KiFastSystemCallRet:
7c92e4f4 c3 ret
7c92e4f5 8da42400000000 lea esp,[esp]
7c92e4fc 8d642400 lea esp,[esp]
ntdll!KiIntSystemCall:
7c92e500 8d542408 lea edx,[esp+8]
7c92e504 cd2e int 2Eh
7c92e506 c3 ret
系统使用快速系统调用实现ring3到ring0的跨越。使用sysenter快速系统调用就如内核,那么系统怎么找到进入内核该执行什么指令呢?
[快速系统调用]
快速系统调用
系统使用sysenter sysexit和三个msr寄存器实现的快速系统调用,具体的流程大致为:
执行sysenter时,cpu进入系统态,随后将SYSENTER_CS_MSR的内容附给CS寄存器,将SYSENTER_EIP_MSR的内容附给EIP,将SYSENTER_CS_MSR + 8附给SS,将SYSENTER_ESP_MSR附给SS寄存器。
1. CS = SYSENTER_CS_MSR
2. EIP = SYSENTER_EIP_MSR
3. SS = SYSENTER_CS_MSR + 8
4. ESP = SYSENTER_ESP_MSR
因此只要事先设置好MSR就可以顺利进入系统空间从CS:EIP开始执行内核指令
[摘自windows内核情景分析代码-reactOS]
ULONG_PTR NTAPI KiLoadFastSyscallMachineSpecificRegister(IN ULONG_PTR Context)
{
//设置MSR寄存器
//CS被设置成0x8, 那么SS就是0x8+0x8 = x010
Ke386Wrmsr(0x174, 0x8, 0);
//ESP被设置成一个中立的不属于任何线程的堆栈,作为过渡的系统空间堆栈
Ke386Wrmsr(0x175, (ULONG)KeGetCurrentPrcb->DpcStack, 0);
//EIP设置为系统快速调用总入口KiFastCallEntry
Ke386Wrmsr(0x176, (ULONG)KiFastCallEntry, 0);
Retun 0;
}
可以看出三个MSR寄存器的编号分别为0x174,0x175,0x176,系统通过Ke386Wrmsr函数对这个三个MSR寄存器赋值完成快速调用前的准备。可以看到EIP指向了KiFastCallEntry这个函数。
lkd> u nt!kifastcallentry
nt!KiFastCallEntry:
8053e540 b923000000 mov ecx,23h
8053e545 6a30 push 30h
8053e547 0fa1 pop fs
8053e549 8ed9 mov ds,cx
8053e54b 8ec1 mov es,cx
8053e54d 8b0d40f0dfff mov ecx,dword ptr ds:[0FFDFF040h]
8053e553 8b6104 mov esp,dword ptr [ecx+4]
8053e556 6a23 push 23h
KiFastCallEntry的实现就不详细说了,函数开头将0x30附给了FS,待会在后边会讲解为什么这里首先要将0x30给FS。
以上是通过sysenter进行快速系统调用的过程,其实在以前的系统中也可以通过int 2e中断进入内核,当然现在有些函数也是通过int 2e指令进入内核的,在此不进行举例了。
[int 2e中断指令进入内核]
不得不提的是IDT,系统终端描述表,也可以叫中断向量表,显而易见的是这是一张表,但不是普通的一张表,这个表中的每一项都记录了一个中断编号和对应的中断处理函数。比如我们常用的“int 3”就是调用编号为3的中断处理函数,通过windbg可以看到编号为3的中断处理函数:
lkd> !idt 3
Dumping IDT:
03: 8053f6e4 nt!KiTrap03
可以看到对应的函数是 KiTrap03。那么int 2e也是一样的,它调用的就是编号为2e的中断处理函数,首先看看这个函数到底是什么。
lkd> !idt 2e
Dumping IDT:
2e: 8053e481 nt!KiSystemService
其对应的函数是KiSystemService,那么就是说系统通过KiSystemService进入内核。
接下来我们来模拟系统,看看究竟是如何通过中断编号找到中断处理函数KiSystemService的。
保护模式下的中断的实现方式是通过IDT表来实现,IDT表中存放的是中断描述向量,表中的每个记录长8个字节,其中第0个字节和第一个字节是一个16位的offset,第2,3字节是一个选择字selector,第4,5字节是属性,剩下两个字节也是offset,头尾共四个字节组成了一个32位偏移地址。我们要找的函数地址其实就是要通过这个32位偏移和选择子selector进行定位,具体的过程:
首先要知道IDT的地址,这样才能知道第2e个向量在哪儿
lkd> !pcr
KPCR for Processor 0 at ffdff000:
Major 1 Minor 1
NtTib.ExceptionList: f4de9c7c
NtTib.StackBase: f4de9df0
NtTib.StackLimit: f4de7000
NtTib.SubSystemTib: 00000000
NtTib.Version: 00000000
NtTib.UserPointer: 00000000
NtTib.SelfTib: 7ffde000
SelfPcr: ffdff000
Prcb: ffdff120
Irql: 00000002
IRR: 00000000
IDR: ffff20f8
InterruptMode: 00000000
IDT: 8003f400
GDT: 8003f000
TSS: 80042000
CurrentThread: 819323a0
NextThread: 00000000
IdleThread: 8055a9c0
通过!pcr可以找到处理器对应的IDT(本人测试机器是单核的XP),那么IDT表所在的地址就是8003f400,GDT的地址就是8003f000,那么第2e项可以通过windbg这样找到:
lkd> dq 8003f400+2e*8
8003f570 804dee00`0008f631 804e8e00`0008297c
8003f580 806f8e00`00085d54 81e08e00`000855a4
8003f590 804d8e00`0008ed04 804d8e00`0008ed0e
8003f5a0 804d8e00`0008ed18 804d8e00`0008ed22
8003f5b0 804d8e00`0008ed2c 804d8e00`0008ed36
8003f5c0 806e8e00`0008fef0 81f98e00`000890d4
8003f5d0 81e18e00`0008165c 81ed8e00`00080aac
8003f5e0 81eb8e00`00089744 804d8e00`0008ed72
根据上面所讲的结构,可以看到这里对应的selector = 0x0008,而offset = 804df631。我们要找的函数入口地址其实就是[段基址]+offset,既然找到了offset,那么寻找这个基址就是接下来的任务了。段选择子共16位,其中低三位暂时不管,剩下的13位是一个编号,就是要通过这个编号作索引去GDT(全局描述符)拿到段的信息。同样GDT的每一项占8个字节,我们关心的是第2,3,4,7这四个字节,因为他们组成了段基地址。有上面selector = 0x0008,可以知道索引值为1,也就是要去找GDT第一项的内容:
lkd> dq 8003f000+1*8
8003f008 00cf9b00`0000ffff 00cf9300`0000ffff
8003f018 00cffb00`0000ffff 00cff300`0000ffff
8003f028 80008b04`200020ab ffc093df`f0000001
8003f038 7f40f3fd`e0000fff 0000f200`0400ffff
8003f048 00000000`00000000 80008955`22000068
8003f058 80008955`22680068 00009302`2f40ffff
8003f068 0000920b`80003fff ff0092ff`700003ff
8003f078 80009a40`0000ffff 80009240`0000ffff
根据上面说的GDT结构可以知道基地址是0x0000。那么我们所找到的函数入口地址就是:[基地址]+offset = 0x000 + 804df631 = 0x804df631。反汇编这个地址:
lkd> u 0x804df631
nt!KiSystemService:
804df631 6a00 push 0
804df633 55 push ebp
804df634 53 push ebx
804df635 56 push esi
804df636 57 push edi
804df637 0fa0 push fs
804df639 bb30000000 mov ebx,30h
804df63e 8ee3 mov fs,bx
果然也还是KiSystemService这个函数。
这里有一个问题,函数什么时候使用int 2eh进行中断系统调用什么时候使用快速系统调用呢?原来在内核初始化的时候,系统会根据是否支持快读调用设置SharedUserData!SystemCallStub的具体指向,也就是说SharedUserData!SystemCallStub可以指向KiFastSystemCall也可以指向KiIntSystemCall,具体指向那一个函数由系统决定。
不会编辑图片,所以上面所说的IDT,GDT的结构有些潦草,其实用图片可能比较直观。还有这个格式....就不说了。帖子中所说的有些地方还是一带而过,但其实里面有很多点都是可以深钻的。
哦,差点忘记了,上面还提到为什么将0x30附给FS。通过对GDT的描述,就能知道为什么这样做。0x30就是一个选择子,根据选择子的格式,可以知道我们需要将3作为索引去查找GDT。
lkd> dq 8003f000+6*8
8003f030 ffc093df`f0000001 7f40f3fd`e0000fff
8003f040 0000f200`0400ffff 00000000`00000000
8003f050 80008954`af000068 80008954`af680068
8003f060 00009302`2f40ffff 0000920b`80003fff
8003f070 ff0092ff`700003ff 80009a40`0000ffff
8003f080 80009240`0000ffff 00009200`00000000
8003f090 00000000`00000000 00000000`00000000
8003f0a0 890089a2`d3200068 00000000`00000000
拿到的地址就是ffdff000,眼熟吧--
lkd> !pcr
KPCR for Processor 0 at ffdff000:
Major 1 Minor 1
NtTib.ExceptionList: b1ea0c7c
NtTib.StackBase: b1ea0df0
NtTib.StackLimit: b1e9d000
NtTib.SubSystemTib: 00000000
NtTib.Version: 00000000
NtTib.UserPointer: 00000000
NtTib.SelfTib: 7ffde000
SelfPcr: ffdff000
Prcb: ffdff120
Irql: 00000000
IRR: 00000000
IDR: ffffffff
InterruptMode: 00000000
IDT: 8003f400
GDT: 8003f000
TSS: 80042000
CurrentThread: 894cc560
NextThread: 00000000
IdleThread: 80553740
没错,就是KPCR 的地址。说明进入内核的时候FS指向了KPCR。
好了,内容有些混乱,因为有很多点都比较深入而本人能力有限,加上不好编辑,不管怎么调整都觉得别扭......
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课