内容回顾
kernel32.dll(ReadProcessMemory)->ntdll.dll(NtReadVirtualMemory)
mov eax, 0BA //提供一个内核函数编号
mov edx, 7FFE0300 //然后提供一个函数的地址,这个函数地址存储在7FFE0300这个位置。
本文将重点介绍该函数到底是什么,要了解该函数到底是什么还需要弄清楚7FFE0000这块内存有什么特殊含义。
_KUSER_SHARED_DATA
- 在user层和kernel层分别定义了一个 _KUSER_SHARED_DATA 结构区域,用于user层和kernel层共享某些数据。
- 它们使用固定的地址值映射,_KUSER_SHARED_DATA 结构区域在user和kernel层,地址如下:
- user层地址:0x7FFE0000,任何一个3环程序都可以访问。
- kernel层地址:0xFFDF0000
虽然指向的是同一个物理页,但在user层是只读的,在kernel层是可读写的。
0x7FFE0000(3环地址):
0xFFDF0000(0环地址):
可以看到储存的内容是完全一致的,别的地方暂时忽略,先来关注内容回顾图中代码 mov edx, 7FFE0300:
查看 _KUSER_SHARED_DATA结构体(0x7FFE0000),然后找到0x300的位置。
0x300的位置名字叫做SystemCall,现在回过头来看一下内容回顾的图:
当调用某个API的时候,进入ntdll的时候,会调用(mov edx, 7FFE0300、call dword ptr [edx])这个位置,这个位置存储的就是一个函数地址。
现在要研究的就是,这个位置存储的到底是什么函数?
0x7FFE0300 到底存储的是什么?
实验:是否支持快速调用
当通过 eax = 1,来执行cpuid指令时,处理器的特征信息被放在ecx和edx寄存器中。
其中edx包含了一个SEP位(下标为11的位置),该位指明了当前处理器是否支持sysenter/sysexit指令。
SEP=1:支持sysenter/sysexit指令。
SEP=0:不支持sysenter/sysexit指令。
支持sysenter/sysexit指令:ntdll.dll!KiFastSystemCall(),不是内核函数,它仍然是ntdll中的函数。
不支持sysenter/sysexit指令:ntdll.dll!KiIntSystemCall()。
CPU如何知道是否支持这种指令?
cpuid指令可以用来查询很多CPU相关信息,但是在查询CPU相关信息的时候需要传递参数,通过EAX寄存器来传参。
比如当前想知道CPU是否支持sysenter/sysexit指令:
在EAX中写入一个1:
然后写上cpuid指令:
当cpuid指令执行完成后,它会将结果存储在ecx和edx寄存器中,所以现在把ecx和edx都置零:
这样,在执行完成后看起来更加直观。
执行完成后:
执行完毕以后,在edx下标为11的位置,也就是所谓的SEP位。
SEP位如果置1,就证明当前的CPU是支持sysenter/sysexit指令的。
如果为0,那就是不支持。
当操作系统启动的时候,操作系统首先就会通过cpuid指令来查询当前CPU是否支持sysenter/sysexit指令。
当支持的时候,它会将 ntdll.dll!KiFastSystemCall() 函数的地址写入到 _KUSER_SHARED_DATA 结构体的0x300的偏移处。
如果不支持的时候,它会将另一个函数的地址,也就是 ntdll.dll!KiIntSystemCall() 的函数地址写入到 _KUSER_SHARED_DATA 结构体的0x300的偏移处。
所以看到类似这种情况,不能武断的论证到底是什么函数,而要先经过测试,通过测试才能论证它存储的到底是哪个函数。
进0环需要更改哪些寄存器?
- CS的权限由3变为0,意味着需要新的CS。
- SS与CS的权限永远一致,需要新的SS。
- 权限发生切换的时候,堆栈也一定会切换,所以需要新的ESP。
- 进入0环后,代码从哪里开始执行的位置,需要EIP。
ntdll.dll!KiFastSystemCall() 与 ntdll.dll!KiIntSystemCall() 的区别
相同点:为了提供4个值(CS、SS、ESP、EIP)怎么找,只不过这两种函数提供了不同的找法。
不支持sysenter/sysexit指令(中断门进入0环):
当不支持sysenter/sysexit指令的时候,将系统调用号(内核函数编号)、参数压入寄存器(eax、edx),然后通过中断门(int 0x2E)进入0环。
所有的API,进入0环的时候,统一的中断门中断号是0x2E。在IDT表中0x2E的位置。
支持sysenter/sysexit指令(快速调用进入0环):
将当前栈顶的值放入edx,edx用来找参数在哪里。
eax存储的仍然是系统调用号(内核函数编号)。
然后就没有使用中断门了,而是使用sysenter。
为什么叫快速调用?
中断门进0环,需要的CS、EIP在IDT表中,需要查内存(SS与ESP由TSS提供)。
而CPU如果支持sysenter指令时,操作系统会提前将CS、SS、ESP、EIP的值存储在MSR寄存器中。
sysenter指令执行时,CPU会将MSR寄存器中的值直接写入相关寄存器,没有读内存的过程,所以叫快速调用,本质是一样的。