SSDT
概述
SSDT全称是”System Services Descriptor Table”(系统服务描述符表),在内核中的实际名称是”KeSeriveDescriptorTable“。这个表已通过内核ntoskrnl.exe导出(在x64里不导出)。
SSDT用于处理应用层通过kernel32.dll下发的各个API操作请求。ntdll.dll中的API是一个简单的包装函数,当kernel32.dll中的API通过ntdll.dll时,会先完成对参数的检查,在调用一个中断(int 2Eh 或者 Sys Enter指令),从而实现从R3层进入R0层,并将要调用的服务号(也就是SSDT数组中的索引号index值)存放到寄存器EAX中,最后根据存放在EAX中的索引值在SSDT数组中调用指定的服务(Nt*系列函数)如下图所示。
定义
SSDT表的结构定义如下
1 |
|
其中最重要的2个成员为ServiceTableBase(SSDT表的基地址)和 NumberOfServices(表示系统中SSDT服务函数的个数)。SSDT表其实就是一个连续存放这个函数指针的数组。
原理与Hook
SSDT表的导入方法如下。
1 | __declspec(dllimport) ServiceDescriptorTableEntry_t KeServiceDescriptorTable; |
由此可以知道SSDT表的基地址(数组的首地址)和SSDT函数的索引号(index),从而求出对应的服务函数的地址。在x86平台上,它们之间满足如下规则。
1 | FuncAddr = KeServiceDescriptortable + 4 * index |
与x86平台上直接在SSDT中存放SSDT函数地址不同,在x64平台上,SSDT中存放的时索引号所对应SSDT函数地址和SSDT表基地址的偏移 * 16 的值,计算公式如下。
1 | FuncAddr = ([KeServiceDescriptortable + index * 4] >> 4 + KeServiceDescriptorTable) |
通过这个公式,只要知道SSDT表的首地址和对应函数的索引号,就可以将对应位置的服务函数替换成自己的函数,从而完成SSDT Hook过程了。
Shadow SSDT的原理比SSDT类似,它对应的表名为KeServiceDescriptorTableShadow,是内核未导出的另一张表,包含Ntoskrnel.exe和win32k.sys服务函数,主要处理来自User32.dll和GDI32.dll的系统调用。与SSDT不同,Shadow SSDT是未导出的,因此不能在自己的模块中导入和直接引用。
挂钩该表中的NtGdiBitBlt、NtGdiStretchBlt 可以实现截屏保护。挂钩NtUserSetWindowsHookEx函数可以防止或保护键盘钩子,挂钩与按键相关的函数NtUserSendInput可以防止模拟按键,挂钩NtUserFindWindowEx函数可以防止或保护键盘钩子,挂钩与按键相关的函数NtUserSendInput可以防止模拟按键,挂钩NtUserFindWindowEx函数可以防止搜索窗口,挂钩与窗口相关的函数NtUserPostMessage、NtUserQueryWindow可以防止窗口被关闭。
Shadow SSDT的管沟原理和SSDT的挂钩原理一样,只不过由于未导出,需要使用不同的方法来获取该表的地址及服务函数的索引号。例如,硬编码与KeServiceDescriptorTable在不同系统中的位置偏移,搜索KeAddSystemServiceTable、KTHREAD.ServiceTable,以及有效内存搜索等。
KeServiceDescriptorTableShadow实际上也是一个SSDT结构数组,也就是说,KeServiceDescriptorTableShadow是一组系统描述表。在Windows XP中,KeServiceDescriptorTableShadow表位于KeServiceDescriptorTable表上方便宜0x40处。
KeServiceDescriptorTableShadow包含四个子结构,示例如下。第一个子结构是”ntoskrnl.exe(native api)”,与KeServiceDescriptorTable的指向相同。真正需要获得的是第二个子结构,即”win32k.sys(gdi/user support)”。第三个和第四个子结构一般不使用。
1 | typedef struct _SERVICE_DESCRIPTOR_TABLE |
TEB
TEB和PEB一样,不在系统内核空间中,而是之外应用层中的结构。TEB结构比较重要。
概述
TEB 全称 Thread environment block,线程环境块,结构中包含了系统频繁使用的一些与线程相关的数据。进程中的每个线程(系统线程除外)都有一个自己的TEB。一个进程的所有TEB都存放在0x7FFDE00开始的线性内存中,,每4KB未一个完整的TEB。
1. TEB结构体
与EPROCESS类似(其他博文已有描述),在不同的Windows中,TEB结构略有差异。例如,在R3级的应用程序中,fs:[0]的地址指向TEB结构,这个结构的开头是一个NT_TIB结构,具体如下。
1 | 0:000> dt _nt_tib |
(摘自 Windbg)
NT_TIB结构的0x18偏移处是一个Self指针,指向这个结构自身,也就是TEB结构的开头。TEB结构的0x30偏移处是指向PEB的指针。
利用Windbg的本地调试可以查看系统中的TEB结构。启动WinDbg,选择 File –> Kernel Debug –> Local 选项,然后在弹出的对话框中单击 Local 标签,就可以打开WinDbg的本机调试功能。在 Windows Vista 及以后的版本中会弹出信息,提示系统不支持本地内核调试,这时可以使用管理员模式打开cmd ,输入命令 bcdedit -debug on,重新启动计算机,在以管理员身份打开WinDbg。
输入 !teb 即可查看TEB结构数据
1 | 0:000> !teb |
继续查看,代码如下。
1 | 0:000> dt _teb 00620000 |
在TEB中,0x30偏移处时PEB结构,地址为0x00620000。可以使用dt命令进一步查看PEB结构成员中的值。
2. 访问TEB
可以通过NtCurrentTeb函数调用和FS段寄存器访问这两种方法访问TEB结构。
(1)NtCurrentTeb函数调用
从ntdll.dll中道出了一个函数NtCurrentTeb函数,该函数可以返回当前线程的TEB结构体的地址。通过喜爱按的代码,就i可以从ntdll.dll中找到对应的NtCurrentTeb函数地址并调用它,返回TEB结构的地址。
1 | typedef struct _TEB{ |
(2)FS段寄存器访问
FS是段寄存器,当代码运行在R3时,基地址即为当前线程的线程环境块(TEB),所以该段也称为 TEB段 。运行如下代码可获得TEB的指针。
1 | mov eax, dword ptr fs:[18h] |
PEB
概述
PEB 全称 ProcessEnvironment Block,进程环境块,存在于用户地址空间中,记录了进程的相关信息。每个进程有自己的PEB信息。
PEB访问
TEB中的ProcessEnvironmentBlock 就是PEB结构的地址,其结构的0x30偏移处是一个指向PEB的指针。PEB的0x2偏移处是一个UChar成员,名叫“BeginDebugged”,进程被调试时值为1,否则为0.因此访问PEB有两种方法。
直接获取
1
mov eax , dword ptr fs:[30] ;fs[30]里面存放的即为PEB地址
通过TEB获取
1
2mov eax,dword ptr fs:[18h] ;此时eax里为TEB的指针
mov eax,dword ptr [eax + 30h] ;此时eax里为PEB的指针
此外,在内核结构对象EPROCESS结构中,同样记录了PEB结构的地址。因此,可以通过查看EPROCESS找到进程的PEB信息。
PEB结构体
与TEB一样,PEB也是随着Windows系统版本的变化而略有差异的结构。可以查阅MSDN或winternl.h,获取TEB结构定义。
1 | typedef struct _PEB { |
MSDN中定义的PEB是不完整的,微软隐藏了很多细节,其他结构也是一样,下面是windbg获得的结果
1 | 0:000> dt _PEB 0X0061d000 |
其中,BeingDebugged成员适用于指定该进程是否处于被调试状态CheckRemoteDebuggerPrecset()函数用于判断进程是否处于调式状态。ProcessParameters是一个RTL_USER_PROCESS_PARAMMETERS,即用于记录进程的参数信息(例如命令行参数等)。
下图表示EPROCESS、ETHREAD、PEB、TEB的关系,可以看出,EPROCESS和ETHREAD结构处于内核空间中,它们分别拥有一个指针,指向处于应用层空间的PEB结构和TEB结构,而在TEB中也有一个指针指向PEB结构。
EPROCESS、ETHREAD、TEB、PEB之间的关系