系统调用的学习
上文回顾
上篇文章分析得到:
1.3环进0环的两种方式,分别是中断门和快速调用,CPU支持快速调用,那么_KUSER_SHARED_DATA 结构体的 SystemCall 属性指向的函数是 KiFastSystemCall,执行 KiFastSystemCall,使用快速调用的方式进0环;如果不支持,那么SystemCall 指向的函数是KiIntSystemCall,执行 KiIntSystemCall,使用中断门的方式进0环。
2.快速调用不需要访问内存,而中断门需要读TSS和IDT表
3.int 0x2e 和 sysenter 指令进0环后,分别调用了两个函数 KiSystemService 和 KiFastCallEntry。
4.原来的寄存器存储到了_KTRAP_FRAME 结构体里,3环API参数指针通过EDX传给0环。
上篇文章分析结果,两个函数最后执行同一段代码,如图
图中标记的是407781函数,之后是执行的相同代码部分,7781函数在_KiFastCallEntry函数内部。图片太大没法截图全部。
KiSystemService / KiFastCallEntry 填充_KTRAP_FRAME 后续部分
这两个函数虽然入口不同,但是填充完 _KTRAP_FRAME 后,就会执行相同的代码。
预备知识:
eax中存储的系统服务号 0BAh
edx存储的三环的参数指针
系统服务表
_KTHREAD+0xE0= +0x0e0 ServiceTable : Ptr32 Void
有两张系统服务表,第一张是用来找内核函数的,第二张是找Win32k.sys驱动函数的
结构:0x10大小
typedef struct _SERVICE_DESCRIPTOR_TABLE
{
PULONG ServiceTableBase; // 指针,指向函数地址,每个成员占4字节
PULONG ServiceCounterTableBase; // 当前系统服务表被调用的次数
ULONG NumberOfService; // 服务函数的总数
PUCHAR ParamTableBase; // 服务函数的参数总长度,以字节为单位,每个成员占一个字节
// 如:服务函数有两个参数,每个参数占四字节,那么对应参数总长度为8
// 函数地址成员 与 参数总长度成员 一一对应
} SSDTEntry, *PSSDTEntry;
- 系统服务表里的函数都是来自内核文件导出的函数
- 它并不包含内核文件导出的所有函数,而是3环最常用的内核函数
SSDT表
全称:System Services Descriptor Table(系统服务描述符表)
SSDT的每个成员叫做系统服务表
查看SSDT表
kd> dd nt!KeServiceDescriptorTable
第二张表为0,使用KeServiceDescriptorTable这个公开的导出函数,我们无法看到win32k.sys这张表结构
win32k.sys系统服务表已经可见
系统服务号:低12位就是函数参数表和函数地址表的下标,而第13位(下标12)如果是0,表示找第一张系统服务表Ntoskrl.exe,如果是1,那么找第二张表win32k.sys.后12位是函数地址表和函数参数表的索引。
分析
.text:00407781
.text:00407781 loc_407781: ; CODE XREF: _KiBBTUnexpectedRange+18↑j
.text:00407781 ; _KiSystemService+6E↑j
.text:00407781 mov edi, eax ;eax中是系统服务号
.text:00407783 shr edi, 8 ;edi右移8位
.text:00407786 and edi, 30h ;判断系统服务号12位,0:edi == 0x00 ;1:edi == 0x10
.text:00407789 mov ecx, edi
.text:0040778B add edi, [esi+0E0h]; [esi+0E0h]:_KTHREAD+0xE0=ServiceTable,edi指向系统服务表,这里将系统服务表所在地址直接加上edi的运算结果,巧妙地得到要查哪张表(两张表是连续的),每张表占16字节
.text:00407791 mov ebx, eax
.text:00407793 and eax, 0FFFh ;与运算后,只保留系统服务号低12位
.text:00407798 cmp eax, [edi+8] ;edi指向系统服务表,[edi+8]:NumberOfService,判断要找的函数是否超出范围
.text:0040779B jnb _KiBBTUnexpectedRange ;若大于系统调用号的个数则跳转,即系统调用号越界
.text:004077A1 cmp ecx, 10h ;ecx 保存的是 edi 与0x30与运算后的结果,只能是0x00或0x10
.text:004077A4 jnz short loc_4077C0 ;若系统调用号小于0x1000,则跳转
.text:004077A6 mov ecx, ds:0FFDFF018h ;只有当ecx == 0x10才会向下执行,作用是动态加载GUI等图形相关函数
.text:004077AC xor ebx, ebx
.text:004077AE
.text:004077AE loc_4077AE: ; DATA XREF: _KiTrap0E+110↓o
.text:004077AE or ebx, [ecx+0F70h]
.text:004077B4 jz short loc_4077C0
.text:004077B6 push edx
.text:004077B7 push eax
.text:004077B8 call ds:_KeGdiFlushUserBatch
.text:004077BE pop eax
.text:004077BF pop edx
.text:004077C0
.text:004077C0 loc_4077C0: ; CODE XREF: _KiFastCallEntry+B4↑j
.text:004077C0 ; _KiFastCallEntry+C4↑j
.text:004077C0 inc dword ptr ds:0FFDFF638h;
.text:004077C6 mov esi, edx ;edx:三环参数指针
.text:004077C8 mov ebx, [edi+0Ch] ; [edi+0Ch]:ParamTableBase(参数表指针)
.text:004077CB xor ecx, ecx ;eax保存的是3环传入的系统调用号
.text:004077CD mov cl, [eax+ebx] ;eax保存的是3环传入的系统调用号,ebx保存的是是参数表指针,这条指令的目的是得到内核函数的参数总长度,存入cl
.text:004077D0 mov edi, [edi] ;取出系统调用表的第一个成员(函数地址指针)
.text:004077D2 mov ebx, [edi+eax*4] ;函数地址指针 + 系统调用号*4(乘4是因为每个成员占4字节),ebx中存入函数地址
.text:004077D5 sub esp, ecx ;提升堆栈,提升高度为cl,目的是要把三环堆栈中的参数存进来
.text:004077D7 shr ecx, 2 ;参数总长度/4=参数个数
.text:004077DA mov edi, esp ;设置参数的0环堆栈地址
.text:004077DC cmp esi, ds:_MmUserProbeAddress ;判断esi与用户程序能访问的最大地址范围,是否越界
.text:004077E2 jnb loc_407990 ;如果越界了 进行跳转,处理异常
.text:004077E8
.text:004077E8 loc_4077E8: ; CODE XREF: _KiFastCallEntry+2A4↓j
.text:004077E8 ; DATA XREF: _KiTrap0E+106↓o
.text:004077E8 rep movsd ;复制参数(执行几次取决于参数个数<-004077D7结果 )到0环的堆栈
.text:004077EA call ebx ;调用内核函数NtReadVirtualMemory
实验:
查看函数地址:
[函数地址表 + 系统服务号*4] = 内核函数地址 805aa712
查看参数地址
[参数表 + 系统服务号] = 内核函数参数个数(单位:字节)14
查看内核函数反汇编: