一、内核对象
在Windows内核中有一种很重要的数据结构管理机制,那就是内核对象。应用层的进程、线程、文件、驱动模块、事件、信号量等对象或者打开的句柄在内核中都有与之对应的内核对象。
如图7.10所示,一个Windows内核对象可以分为对象头和对象体两部分。在对象头中至少由1个OBJECT_HEADER和对象额外信息。对象体紧接着对象头中的OBJECT_HEADER。一个对象指针总是指向对象体而不是对象头。如果要访问对象头,需要将对象体指针减去一个特定的偏移值,以获取OBJECT_HEADER结构,通过OBJECT_HEADER结构定位从而访问其它对象辅助信息。对象体内部一般会有1个type和1个size成员,用来表示对象的类型和大小。
(1)Dispatcher对象
这种对象在对象体开始位置放置了一个共享的公共数据结构DISPATCHER_HEADER,其结构代码如下。包含DISPATCHER_HEADER结构的内核对象的名字都以字母“K”开头,表明这是一个内核对象,例如KPROCUSS、KTHREAD、KEVENT、KSEMAPHORE、KTIMER、KQUEUE、KMUTANT、KMUTEX,但以字母“K”开头的内核对象不一定是Dispatcher对象。包含DISPATAHER_HEADER结构的内核对象都是可以等待的(waitable),也就是说,这些内核对象可以作为参数传给内核的KeWaitForSingleObject()和KeWaitForMultipleObjects()函数,以及应用层的waitForSingleObject()和WaitForMultiple Objects()函数。
typedef struct _DISPATCHER_HEADER{
UCHAR Type; //DISP_TYPE_*
UCHAR Absolute;
UCHAR Size; //number of DWORDs
UCHAR Inserted;
LIST_ENTRY WaitListHead;
}
DISPATCHER_HEADER,
*PDISPATCHER_HEADER,
**PPDISPATCHER_HEADER;
(2)I/O对象
I/O对象在对象体开始位置并未防止DISPATCHER_HEADER结构,但通常会放置一个与type和size有关的整型成员,以表示该内核对象的类型(例如文件内核对象的类型为26)和大小。常见的I/O对象包括DEVICE_OBJECT、DRIVER_OBJECT、FILE_OBJECT、IRP、VPB、KPROFILE等。
(3)其它对象
除了Dispatcher对象和I/O对象,剩下的都属于其它内核对象。其中有两个常用的内核对象,分别是进程对象(EPROCESS)和线程对象(ETHREAD)。
EPROCESS用于在内核中管理进程的各种信息,每个进程都对应于一个EPROCESS结构,用于记录进程执行期间的各种数据。尽管EPROCESS结构非常大,但它是一个不透明的结构(Opaque Sturcture),具体成员并未导出,并随着操作系统版本的变化而变化。因此,要想查看EPROCESS结构中的成员,只能查阅网上资料或者在使用WinDbg调试器加载内核符号后进行。
所有进程的EPROCESS内核结构都被放入一个双向链表,R3在枚举系统进程的时候,通过遍历这个链表获得了进程的列表。因此,有的Rootkit会试图将自己进程的EPROCESS结构从这个链表中摘掉,从而达到隐藏自己的目的。
EPROCESS结构中的一些关键数据如下。
KPROCESS pcb; //进程的内核对象
PVOID UniqueProcessId; //进程的PID
PVOID DebugPort; //调试端口,设置为0,禁止进程被调试
EX_PAST_REF Token; //进程的权限token
UCHAR ImageFileName[16]; //进程名字,只支持16字节
PPEB Peb; //进程的环境块
PEJOB Job; //指向正在运行的系统进程列表
PHANDLE_TABLE ObjectTable; //进程的handle表
调用下面两个内核函数可以获得进程的EPROCESS结构。PsLookupProcessByProcessId函数的结构如下。
NTSTATUS PsLookupProcessByProcessId(
//根据进程PID拿到进程的EPROCESS结构
IN HANDLE ProcessId,
OUT PEPROCESS *Process
);
PsGetCurrentProcess函数的结构如下
PEPROCESS PsGetCurrentProcess(
//直接获取当前进程的EPROCESS结构
VOID
);
ETHREAD结构是线程的内核管理对象。每个线程都有一个对应的ETHREAD结构。ETHREAD结构也是一个不透明的结构,具体成员并未导出,而且会随着操作系统版本的变化而变化。在ETHREAD结构中,第1个成员就是线程对象KTHREAD成员,所有的ETHREAD结构也被放在一个双向链表里进行管理。
ETHREAD结构中的一些重要成员如下
KTHREAD Tcb; //线程内核对象
CLIENT_ID Cid; //进程PID
EPROCESS、KPROCESS、ETHREAD、KTHREAD结构之间的关系图如图7.11所示。可以看出,EPROCESS和ETHREAD结构都是通过双向循环链表组织管理的。一个EPROCESS结构中包含了一个KPROCESS结构,而在一个KPROCESS结构中又有一个指向ETHRAD结构的指针。在ETHREAD结构中,又包含了KTHREAD结构成员。
二、SSDT
" SSDT "的全称是 “System Services Descriptor Table” (系统服务描述符表),在内核中的实际名称是 “KeServiceDescriptorTable”.这个表已通过内核ntoskrnl.exe导出(在x64里不导出)。
SSDT用于处理应用层通过kernel32下发的各个API操作请求。ntdll.dll中的API是一个简单的包装函数,当kernel32.dll中的API通过ntdll.dll时,会先完成对参数的检查,再调用一个中断(int 2Eh 或者SysEnter指令),从而实现从R3层进入R0层,并将要调用的服务号(也就是SSDT数据中的索引号index值)存放到寄存器EAX中,最后根据存放在EAX中的索引值在SSDT数组中调用指定的服务(Nt*系列函数),如图7.12所示。
SSDT表的结构定义如下。
#pragma pack(1)
typedef struct ServiceDescriptorEntry
{
unsigned int *ServiceTableBase; //表的基地址
unsinged int *ServiceCounterTableBase;
unsinged int NumberOfServices; //表中服务函数的个数
unsigned char *ParamTableBase;
}ServiceDescriptorTableEntry_t,
*PServiceDescriptorTableEntry_t;
#pragma pack()
其中最重要的两个成员SeviceTableBase(SSDT表的及地址)和NumberOfServices(表示系统中SSDT服务函数的个数)。SSDT表其实就是一个连续存放这个函数指针的数组。
SSDT表的导入方法如下。
—declspec(dllimport) ServiceDescriptorTableEntry_t keServiceDescriptorTable;
由此可以知道SSDT表的基地址(数组的首地址)和SSDT函数的索引号(index),从而求出对应的服务函数的地址。在x86平台上,它们之间满足如下规则:
FuncAddr = KeServiceDescriptortable + 4*index
与x86平台上直接在SSDT中存放SSDT函数地址不同,在x64平台上,SSDT中存放的是索引号所对应SSDT函数地址和SSDT表基地址的偏移量*16(即左移4位)的值,因此计算公式变为:
FuncAddr = ([KeServiceDescriptortable + index * 4]>>4 + KeServiceDescriptortable)
通过这个公式,只要知道SSDT表的首地址和对应函数的索引号,就可以将对应位置的服务函数替换为自己的函数,从而完成SSDT Hook过程了。