Windows系统调用学习笔记(四)—— 系统服务表&SSDT
- 要点回顾
- 系统服务表
- 实验:分析 KiSystemService 与 KiFastCallEntry 共同代码
- SSDT
- 实验:在SSDT中查找内核函数信息
- 第一步:查看函数地址
- 第二步:查看参数个数
- 第三步:查看内核函数反汇编
- 练习
要点回顾
API进入0环后调用的函数:
- 中断门 – KiSystemService
- 快速调用 – KiFastCallEntry
上一篇留了几个练习:
- 进0环后,原来的寄存器存在哪里?
- 如何根据系统服务号(eax中存储)找到要执行的内核函数?
- 调用时参数是存储到3环的堆栈,如何传递给内核函数?
- 两种调用方式是如何返回到3环的?
本篇将对第二个和第三个练习进行说明
系统服务表
描述:
- 系统服务表:System Service Table
- 系统服务表共有两张,第一张表后紧接第二张表
- 系统服务表里的函数都是来自内核文件导出的函数
- 它并不包含内核文件导出的所有函数,而是3环最常用的内核函数
- 系统服务表位于 _KTHREAD +00xE0
结构体:
typedef struct _SERVICE_DESCRIPTOR_TABLE
{
PULONG ServiceTableBase; // 指针,指向函数地址,每个成员占4字节
PULONG ServiceCounterTableBase; // 当前系统服务表被调用的次数
ULONG NumberOfService; // 服务函数的总数
PUCHAR ParamTableBase; // 服务函数的参数总长度,以字节为单位,每个成员占一个字节
// 如:服务函数有两个参数,每个参数占四字节,那么对应参数总长度为8
// 函数地址成员 与 参数总长度成员 一一对应
} SSDTEntry, *PSSDTEntry;
结构图:
实验:分析 KiSystemService 与 KiFastCallEntry 共同代码
IDA反汇编:
.text:00406932 loc_406932: ; CODE XREF: _KiBBTUnexpectedRange+18↑j
.text:00406932 ; _KiSystemService+71↑j
.text:00406932 mov edi, eax ; 从 eax 寄存器中取出3环传进来的系统调用号
.text:00406934 shr edi, 8 ; 系统调用号右移8位
.text:00406937 and edi, 30h ; 右移后的值和0x30进行与运算
; 目的是检测第12位是否为1
; WindowsNT基本的(Native)系统调用有两百多个,编号都小于0x1000
; 编号大于0x1000的系统调用号是微软扩展出来的
; 这些扩展出的系统调用位于动态安装的模块中,即win32k.sys
; 若第12位为0x00,则将低12位作为下标在 ntoskrl.exe 中寻找对应的系统调用
; 若第12位为0x10,则将低12位作为下标在 win32k.sys 中寻找对应的系统调用
.text:0040693A mov ecx, edi ; 将运算结果赋值给 ecx
.text:0040693C add edi, [esi+0E0h] ; 将系统服务表的指针赋值给 edi
; nt!_KTHREAD
; +0x0e0 ServiceTable : Ptr32 Void
; 这里将系统服务表所在地址直接加上edi的运算结果
; 巧妙地得到要查哪张表(两张表是连续的),每张表占16字节
.text:00406942 mov ebx, eax ; 将系统调用号赋值给 ebx
.text:00406944 and eax, 0FFFh ; 将系统调用号与 0xFFF进行与运算
; 目的是保留低12位,作为函数地址的下标
.text:00406949 cmp eax, [edi+8] ; typedef struct _SYSTEM_SERVICE_TABLE
; {
; PVOID ServiceTableBase; //这个指向系统服务函数地址表
; PULONG ServiceCounterTableBase;
; ULONG NumberOfService; //服务函数总数
; ULONG ParamTableBase; //参数总长度
; }SYSTEM_SERVICE_TABLE,*PSYSTEM_SERVICE_TABLE;
;
; +8 是服务函数的个数
.text:0040694C jnb _KiBBTUnexpectedRange ; 若大于系统调用号的个数则跳转,即系统调用号越界
.text:00406952 cmp ecx, 10h ; 将 ecx 与0x10进行比较
; ecx 保存的是 edi 与0x30与运算后的结果,只能是0x00货0x10
.text:00406955 jnz short loc_406972 ; 若系统调用号小于0x1000,则跳转
.text:00406957 mov ecx, large fs:18h ; 只有当ecx == 0x10才会向下执行
; 作用是动态加载GUI等图形相关函数
.text:0040695E xor ebx, ebx
.text:00406960
.text:00406960 loc_406960:
.text:00406960 or ebx, [ecx+0F70h]
.text:00406966 jz short loc_406972
.text:00406968 push edx
.text:00406969 push eax
.text:0040696A call ds:_KeGdiFlushUserBatch
.text:00406970 pop eax
.text:00406971 pop edx
.text:00406972
.text:00406972 loc_406972: ; CODE XREF: _KiFastCallEntry+B6↑j
.text:00406972 ; KiSystemServiceAccessTeb()+6↑j
.text:00406972 inc large dword ptr fs:638h ; _KPRCB -> +0x518 KeSystemCalls 增加1
.text:00406979 mov esi, edx ; edx保存的是3环参数的指针
.text:0040697B mov ebx, [edi+0Ch] ; edi指向要使用的系统服务表
; +0Ch是ParamTableBase(参数表指针)
.text:0040697E xor ecx, ecx ;ecx清零
.text:00406980 mov cl, [eax+ebx] ; eax保存的是3环传入的系统调用号
; ebx保存的是是参数表指针
; 这条指令的目的是得到内核函数的参数总长度,存入cl
.text:00406983 mov edi, [edi] ; 取出系统调用表的第一个成员(函数地址指针)
.text:00406985 mov ebx, [edi+eax*4] ; 函数地址指针 + 系统调用号*4(乘4是因为每个成员占4字节)
; 目的是找到函数地址,存入ebx
.text:00406988 sub esp, ecx ; 提升堆栈,大小为参数总长度
.text:0040698A shr ecx, 2 ; 参数总长度/4 = 参数个数
.text:0040698D mov edi, esp ; 设置要COPY的目的地
.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 loc_4069A7
.text:0040699B
.text:0040699B loc_40699B: ; CODE XREF: KiSystemServiceAccessTeb()+33↑j
.text:0040699B cmp esi, ds:_MmUserProbeAddress ; 全局变量存储用户能访问的最大范围
; 这条指令的作用是判断3环参数是否越界
.text:004069A1 jnb loc_406B4F ; 越界则跳转至异常处理
.text:004069A7
.text:004069A7 loc_4069A7:
.text:004069A7 rep movsd ; 将3环参数复制到0环堆栈
.text:004069A9 call ebx ; 调用函数
SSDT
描述:
- 全称:System Services Descriptor Table(系统服务描述符表)
- SSDT的每个成员叫做系统服务表
- SSDT的第一个成员是导出的,声明一下即可使用
- SSDT的第二个成员是未导出的,需要通过其它方式查找
- 在Windows中,SSDT的第三个成员和第四个成员未被使用
在WinDbg中查看已导出成员:
kd>dd KeServiceDescriptorTable
在WinDbg中查看未导出成员:
实验:在SSDT中查找内核函数信息
实验说明
- 在之前的实验中,我们通过分析三环的ReadProcessMemory函数,一步步了解三环函数是如何进入内核的
- 当ReadProcessMemory即将进入内核时,传递了一个系统服务号,为0BAh
- 本次实验查找编号 0BAh 在 SSDT 表中的相关信息
第一步:查看函数地址
函数地址表:
[函数地址表 + 系统服务号*4] = 内核函数地址
第二步:查看参数个数
参数表:
[参数表 + 系统服务号] = 内核函数参数个数(单位:字节)
第三步:查看内核函数反汇编
练习
要求:在SSDT表中追加一个函数地址(NtReadVirtualMemory),自己编写API的3环部分调用这个新增的函数(注意:使用2-9-9-12分页)。
答案:略(待补充)