一、回顾
前两篇博客,我逆向分析了 KiSystemService 和 KiFastCallEntry 填充_KTRAP_FRAME 结构体的代码,二者大同小异,主要的区别是 sysenter 只改了eip,cs,ss,虽然esp也改了,但是windows不使用,而是从TSS里取esp0;另外sysenter并没有像中断门那样压栈,所以3环的 ss, esp, eflags, cs,eip都要在函数里依次保存到 _KTRAP_FRAME 。
这次课后作业是逆向 KiSystemService / KiFastCallEntry 调用内核函数部分,放在一块讲是因为这两个函数虽然入口不同,但是填充完 _KTRAP_FRAME 后,就会执行相同的代码。他们两个函数就像两头蛇一样,有两个入口,初始化的工作有区别,但是往后就共用一个函数体。
在逆向之前,先介绍所需的前置知识。同时,思考两个问题:
如何根据系统服务号(eax中存储)找到要执行的内核函数?
调用时参数是存储到3环的堆栈,如何传递给内核函数?
二、KeServiceDescriptorTable SSDT
注意:此SSDT不是系统服务调度表
KeServiceDescriptorTable SSDT是一个全局变量,这个全局变量是Windows内核导出,这个全局变量的结构如下;每一个成员又是一个结构体,这个结构体就是系统服务表;这个表只包含了一张表也就是SST结构,如图:
//KeServiceDescriptorTable 是 ntoskrnl.exe 所导出的全局变量 声明一下就可以直接使用了,里面已经有值了
//extern 是告诉编译器,不是我定义的,是别人定义的
extern PKSERVICE_TABLE_DESCRIPTOR KeServiceDescriptorTable
kd> dd KeServiceDescriptorTable SSDT结构体
843769c0 8428ad9c 00000000 00000191 8428b3e4 KSYSTEM_SERVICE_TABLE ntoskrnl; //ntoskrnl.exe 的函数
843769d0 00000000 00000000 00000000 00000000 KSYSTEM_SERVICE_TABLE win32k; //win32k.sys 的函数
843769e0 842e96af 00000000 02ca1dac 000000bb
843769f0 00000011 00000100 5385d2ba d717548f
84376a00 8428ad9c 00000000 00000191 8428b3e4
84376a10 99c96000 00000000 00000339 99c9702c
84376a20 00000000 00000000 84376a24 00000340
84376a30 00000340 867f3f78 00000007 00000000
SST结构体:
8428ad9c 是参数1 指向函数地址表 SSDT
00000000 是参数2
00000191 是参数3
8428b3e4 是参数4 指向函数参数表 说明函数中参数所占的字节数,一个参数占4个字节; 这个参数本身占1个字节
SSDT:系统服务调
#pragma pack(1)
typedef struct _KSYSTEM_SERVICE_TABLE{
unsigned int *ServiceTableBase; //函数地址表(SSDT)基址
unsigned int *ServiceCounterTableBase; //Used only in checked build;SSDT函数被调用的次数
unsigned int NumberOfServices; //函数的个数
unsigned char *ParamTableBase; //函数参数表基址
} KSYSTEM_SERVICE_TABLE, *PKSYSTEM_SERVICE_TABLE;
#pragma pack()
//KeServiceDescriptorTable 结构体
typedef struct _KSERVICE_TABLE_DESCRIPTOR
{
KSYSTEM_SERVICE_TABLE ntoskrnl; //ntoskrnl.exe 的函数
KSYSTEM_SERVICE_TABLE win32k; //win32k.sys 的函数
KSYSTEM_SERVICE_TABLE notUsed1;
KSYSTEM_SERVICE_TABLE notUsed2;
} KSERVICE_TABLE_DESCRIPTOR, *PKSERVICE_TABLE_DESCRIPTOR;
SST:SystemServiceTable 系统服务表
SSDT:系统服务调度表 SSDT(system service dispatch table) 系统服务分派表
SSPT:系统服务参数表 SSPT(system service parameter table) 系统服务参数表
关系:
3.
看示意图:
通过此图,我们可以得知以下信息:
通过 _KTHREAD 可以找到系统服务表
系统服务表又指向了函数地址表和函数参数表
有两张系统服务表,第一张是用来找内核函数的,第二张是找Win32k.sys驱动函数的。
通过逆向,我们还可以判定,其实两张系统服务表是线性地址连续的,每张16字节。
KeServiceDescriptorTableShadow SSDT表是指向Win32k.sys的表;黄色的表,也就是win32k.sys这张表只有在GDI进程才会挂,否则不会挂,可以dd 99c96000 验证,发现都是问号,运行一个需要GDI的的程序就可会挂上
kd> dd KeServiceDescriptorTableShadow
84376a00 8428ad9c 00000000 00000191 8428b3e4 KSYSTEM_SERVICE_TABLE ntoskrnl; //ntoskrnl.exe 的函数
84376a10 99c96000 00000000 00000339 99c9702c KSYSTEM_SERVICE_TABLE win32k; //win32k.sys 的函数; 99c96000 就是指向Win32k.sys的指针
84376a20 00000000 00000000 84376a24 00000340
84376a30 00000340 867f3f78 00000007 00000000
84376a40 867f3eb0 867f3c58 867f3de8 867f3d20
84376a50 00000000 867f3b90 00000000 00000000
84376a60 84284809 84291eed 842a03a5 00000003
84376a70 86745000 86746000 00000120 ffffffff
说明一下表的4个属性:
ServiceTable 指向函数地址表,Count没有用,ArgmentTable 指向函数参数表,ServiceLimit 是这两张表的长度。
要注意函数参数表每项存储的是对应函数参数占的字节数,每项只有1字节。 这个在逆向中也可以验证。
最后补充一点,我们之前逆向API三环部分时,它进0环之前,无论是中断门还是快速调用,都会在 eax 里存一个值,我们称之为系统调用号或者服务号,这个东西的低12位就是函数参数表和函数地址表的下标,而第13位(下标12)如果是0,表示找第一张系统服务表(绿色的表),如果是1,那么找第二张表(黄色的表)。这点可以先记住,待会逆向的时候可以印证这个结论。
三、KeServiceDescriptorTableShadow和KeServiceDescriptorTable 拓展
早先“盗用”过公开的Re SSDT的源代码,对SSDT多少还是了解些,也Hook 过SSDT,但一直对另外一个SSDT——Shadow SSDT不甚了解,只知道它跟GUI调用有莫大的关系,具体怎么联系起来的,说不清楚。
最近研究起了Hook ShadowSSDT这个东西,经过查找资料、阅读Win2k的源码,以及WinDbg闹腾了半天,才终于明白了KeServiceDescriptorTable 和 KeServiceDescriptorTableShadow到底是什么样的关系。
众所周知,KeServiceDescriptorTable在ntoskrnl.exe中被导出了,在我们的代码中只需要extern一下就可以方便的对其进行引用。网上流行的代码,把KeServiceDescriptorTable的类型定义为指向类似下面结构的指针(这是我从Win2k源码中摘出来的):
typedef struct _KSERVICE_TABLE_DESCRIPTOR {
PULONG_PTR Base;
PULONG Count;
ULONG Limit;
PUCHAR Number;
} KSERVICE_TABLE_DESCRIPTOR, *PKSERVICE_TABLE_DESCRIPTOR;
遗憾的是,这些代码让我有一个误判断,认为SSDT就是一个KSERVICE_TABLE_DESCRIPTOR结构,而KeServiceDescriptorTable就是指向这个KSERVICE_TABLE_DESCRIPTOR的指针。这种理解让我对ShadowSSDT相当的迷糊。真正今天,才算是明白了这个ShadowSSDT到底是个什么东西。
实际上,KeServiceDescriptorTable描述成下面的结构更便于描述和理解:
typedef struct _KSERVICE_TABLE_DESCRIPTOR_TABLE {
KSERVICE_TABLE_DESCRIPTOR NativeApiTable;
KSERVICE_TABLE_DESCRIPTOR Win32kApiTable;
KSERVICE_TABLE_DESCRIPTOR NotusedTable;
KSERVICE_TABLE_DESCRIPTOR NotusedTable;
} KSERVICE_TABLE_DESCRIPTOR_TABLE, * KSERVICE_TABLE_DESCRIPTOR_TABLE;
其中所谓的ShadowSSDT,就是由KSERVICE_TABLE_DESCRIPTOR_TABLE结构的第二个表:Win32kApiTable(注意这个名字是我起的,仅便于理解)来描述的。但是KeServiceDescriptorTable的这个成员是没有被使用的,全值为0。这就到KeServiceDescriptorTableShadow上场了。
KeServiceDescriptorTableShadow其实就是KeServiceDescriptorTable的复制品,唯一的区别就是它的Win32kApiTable是有效的。当Win32k.sys初始化之前,KeServiceDescriptorTableShadow与KeServiceDescriptorTable完全一致,Win32kApiTable同样为0,没有任何的意义。Win32k.sys初始化时,会调用KeAddSystemService将GUI相关的系统调用添加进KeServiceDescriptorTableShadow中的Win32kApiTable中,直到这时,KeServiceDescriptorTableShadow才和KeServiceDescriptorTable有了区别。
也就是说,Win32k的GUI系统调用表,保存在KeServiceDescriptorTableShadow的第二个成员:Win32kApiTable中,也就是KeServiceDescriptorTableShadow的0x10偏移处。这么简单点东西,让我花了一整个下午的时间来闹腾,真是笨到家了。
最后附上Windows 2000源代码中KeServiceDescriptorTableShadow 和 KeServiceDescriptorTable的声明:
#define NUMBER_SERVICE_TABLES 4
extern KSERVICE_TABLE_DESCRIPTOR KeServiceDescriptorTable[NUMBER_SERVICE_TABLES];
extern KSERVICE_TABLE_DESCRIPTOR KeServiceDescriptorTableShadow[NUMBER_SERVICE_TABLES];
四、函数调用过程分析
(1)用户调用kenel32.dll中的ReadFile,kenel32.dll中都是包装函数,kenel32.dll会用这些包装函数完成参数的有效性检查,将所有东西转换为unicode,接着锁定NTDLL.dll中的NtReadFile函数。
(2)NTDLL.dll中的都是服务的包装函数,当调用其中的NtReadFile时,这些服务包装函数将所需的Servcie ID送入EAX寄存器,将参数堆栈帧的指针送入EDX寄存器,然后发出INT 2e中断。这条指令会将处理器切换到内核模式。INT 2e对应的处理程序是由windows NT 的 executive(估计是内核)建立的,它将参数从用户模式的堆栈拷贝到内核模式的堆栈。堆栈帧的基址为EDX寄存器的值。而这个中断程序被称为KiSystemService()
(3)接着进入内核态,NTOSKRNL.exe开始工作,由它进行系统服务的最终调用,它的系统服务的用户接口是以包装函数(wrapper functions)的形式提供的。这些函数都在一个叫做NTDLL.DLL的dll里。NTOSKNL.EXE先初始化,初始化的过程中,先为NTOSKRNL提供的不同服务创建一个函数表即SSDT,表中的每一项都指定了Service ID所需函数的地址,每个函数代码都位于内核之中。类似的,SSPT也开始创建。
转载于:https://my.oschina.net/magicdmer/blog/408081
原文链接:https://blog.youkuaiyun.com/Kwansy/article/details/109476899