内核理论基础 || 内核重要数据结构

一、内核对象

     在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过程了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值