系列索引
【原创】TP驱动保护分析系列一 定位TenProtect保护
【原创】TP驱动保护分析系列二 代码定位内核函数
【原创】TP驱动保护分析系列三 SSDT定位内核函数
前言
前面在【原创】TP驱动保护分析系列二 代码定位内核函数 中介绍了通过驱动函数MmGetSystemRoutineAddress
直接定位和特征码定位法两种定位方式。接下来继续介绍代码定位内核函数的方法:SSDT定位法
SSDT定位内核函数
SSDT
既然要通过SSDT来定位,首先就要知道什么是SSDT
SSDT是什么
SSDT全称System Service Descriptor Table
或System Service Dispatch Table
,即系统服务描述表或系统服务分派表。一般被称作系统服务描述表居多,但要了解它和系统服务分派表是同一个东西
SSDT本质是32位操作系统内核routine的地址数组,或是64位操作系统相同routine的相对偏移量数组
这里的routine是指SSDT函数,如NtOpenProcess
也就是说:SSDT本质是32位操作系统SSDT函数的地址数组,或是64位操作系统相同SSDT函数的相对偏移量数组
简单来说:SSDT是个数组,这个数组在32位系统存储SSDT函数的地址;在64位系统存储SSDT函数的相对偏移量
SSDT是Service Descriptor Table内存结构的第一个成员,结构如下:
typedef struct tagSERVICE_DESCRIPTOR_TABLE {
SYSTEM_SERVICE_TABLE nt; //实际上是指向SSDT本身的指针
SYSTEM_SERVICE_TABLE win32k;
SYSTEM_SERVICE_TABLE sst3; //指向内存地址的指针,该地址包含表中定义了多少个SSDT函数
SYSTEM_SERVICE_TABLE sst4;
} SERVICE_DESCRIPTOR_TABLE;
PS:由于先前在WINDOWS XP上各种SSDT HOOK等修改内核的操作被大家玩得飞起,导致微软为后来的x64系统引入了PatchGuard机制。该机制会检测系统内核是否被修改,若发现被修改就无情地BSOD蓝屏,以此来对抗SSDT HOOK
通过SSDT可以干什么
通过前面的介绍,不难发现SSDT存储着和SSDT函数地址相关的数据,32位系统直接存储的就是地址,64位系统则是相对偏移量。因此通过SSDT就能够定位到SSDT函数
以NtOpenProcess为例,定位该函数的过程可简略为:
(SSDT 和 NtOpenProcess的索引)某种运算→ NtOpenProcess
关于如何获取SSDT函数对应的索引和运算的方法放在本篇的后面介绍
定位SSDT
既然知道了通过SSDT可以定位到SSDT函数,就得想办法定位SSDT
前面提到了SSDT是Service Descriptor Table内存结构的第一个成员,而我们可以通过KeServiceDescriptorTable
得到Service Descriptor Table内存结构的地址
通过windbg可以通过下面的指令直接查看:
dps nt!keservicedescriptortable L4
dqs指令是将每qword(8字节)视为一个符号进行解析并显示出来,L4表示显示4个qword的长度
除此之外还有dds和dps,dds是每dword(4字节),dps是根据当前处理器架构来选择最合适的长度
查看得到:
即:
kd> dps nt!keservicedescriptortable L4
8055d700 80505570 nt!KiServiceTable
8055d704 00000000
8055d708 0000011c
8055d70c 805059e4 nt!KiArgumentTable
这里得到的数据分别对应先前的结构体,填入对应的结构得到:
|
值 |
对应结构 |
说明 |
nt |
80505570 |
nt!KiServiceTable |
SSDT的首地址为80505570 |
win32k |
00000000 |
|
|
sst3 |
0000011c |
|
SSDT函数一共有0x11c=284个 |
sst4 |
805059e4 |
nt!KiArgumentTable |
|
拿到SSDT的首地址后让我们尝试从SSDT中打印出几个值
在windbg中输入指令:
dd /c1 KiServiceTable L2
/c? 指定显示中使用的列数。如果省略,则默认的列数取决于显示类型;这里为指定显示使用列数为1
查看得到:
即:
kd> dd /c1 KiServiceTable L2
80505570 805a5664
80505574 805f23ea
因为测试的环境为Windows XP 32位,所以SSDT存储的直接就是SSDT函数的地址
PS:关于64位系统如何通过偏移量得到对应的SSDT函数,这里不做说明,感兴趣的可以查看下面的参考链接中的第一个参考链接,里面有说明
这里得到的就是头两个SSDT函数的地址,为了验证这一点,分别查看对应地址的反汇编:
u 805a5664
u 805f23ea
查看得到:
得到了前两个SSDT函数:NtAcceptConnectPort和NtAccessCheck
对应并验证了SSDT表中存储着SSDT函数的地址
获取SSDT函数对应索引
想要定位到SSDT函数,首先要获取SSDT函数对应的索引
下面以NtOpenProcess为例,介绍如何获取对应的索引
首先要载入ntdll.dll模块,该模块中有SSDT函数相关的调用,RING3也是通过该模块调用SSDT函数的
具体的内容不展开,留做之后的系统调用再做讲解(挖坑`(>﹏<)′)
依旧使用Windbg工具(其实用IDA Pro或其他工具也可以查看):
!process 0 0 calc.exe
.process /p eprocess
.reload /f ntdll.dll
lm ntdll
!process 扩展显示关于指定进程的信息,或关于所有进程的信息,包括EPROCESS块
.process 指定进程上下文使用哪个进程
/p 在访问之前将此进程的所有转换页表项转换为物理地址
.reload命令删除指定模块的所有符号信息,并根据需要重新加载这些符号。在某些情况下,该命令还会重新加载或卸载模块本身
/f 强制调试器立即加载符号。此参数覆盖惰性符号加载
lm 显示指定加载的模块。输出包括模块的状态和路径
指令说明:
- 在加载ntdll.dll模块前,需要先用windbg附加到一个非系统进程上,这里选用了系统自带的计算器(要先打开计算器)进行附加。通过!processs指令得到对应的eprocess
- 得到eprocess后就可以使用.process指令附加到计算器上了
- 附加完毕后,重新载入ntdll.dll模块
- 最后查看模块的加载情况,确保ntdll已成功加载
过程图如下:
接下来使用指令查看对应SSDT函数的反汇编:
u ntdll!NtOpenProcess
显示得到:
这里就获取到了NtOpenProcess对应的索引为:0x7A
根据索引找到对应的SSDT函数
得到SSDT的首地址,再加上索引就可以计算得到SSDT
计算公式为:
SSDT函数地址 = SSDT首地址 + 4 × SSDT函数对应函数索引
前面得到的SSDT首地址为:0x80505570 ,NtOpenProcess对应的索引为:0x7A
代入计算:
存放NtOpenProcess地址的地址 = 0x80505570 + 4 × 0x7A =0x80505758
接着验证一下计算得到的地址是否正确:
dd 0x80505758 L1
查看得到:
得到NtOpenProcess的地址为:0x805cc486
反汇编查看这个地址,验证是否为NtOpenProcess
u 0x805cc486
查看得到:
验证完毕,结果正确O(∩_∩)O
小结
通常定位一个SSDT函数时会有以下步骤:
确定要定位的SSDT函数→到ntdll.dll中查看对应的函数反汇编→通过反汇编得到索引→通过KeServiceDescriptorTable获得SSDT首地址→通过首地址和索引计算得到SSDT函数地址
代码实现SSDT定位
知道了原理以后就不难用代码实现SSDT定位了
要注意的是,在WINDOWS XP中,KeServiceDescriptorTable是导出的,可以声明后直接使用
但在WIN7和WIN7之后的系统中,KeServiceDescriptorTable没有被导出,可以使用读取MSR寄存器+特征码定位法来获得KeServiceDescriptorTable的地址,这里不做赘述
代码
代码如下:
#include <ntifs.h>
//这里的结构正是之前在 SSDT是什么中 说明的
typedef struct tagSERVICE_DESCRIPTOR_TABLE {
PULONG nt; //实际上是指向SSDT本身的指针
PULONG win32k;
ULONG sst3; //指向内存地址的指针,该地址包含表中定义了多少个SSDT函数
PUCHAR sst4;
} SERVICE_DESCRIPTOR_TABLE, * PSERVICE_DESCRIPTOR_TABLE;
//这里直接引入即可
extern PSERVICE_DESCRIPTOR_TABLE KeServiceDescriptorTable;
/*
根据SSDT函数对应索引获取SSDT函数地址
index 要获取的SSDT函数对应的索引
*/
PVOID GetSSDTFunctionAddr(ULONG index)
{
PVOID Addr;
__asm
{
mov ebx, index //ebx = index
shl ebx, 2 //shl xxx,2 左移两位 就是乘以4 即ebx=ebx*4=index*4
mov eax, KeServiceDescriptorTable // eax = KeServiceDescriptorTable
// eax = KeServiceDescriptorTable的dword(四个字节 正好是其第一个成员的数据宽度)的值
// 而KeServiceDescriptorTable的第一个成员就指向SSDT本身,故 eax = SSDT首地址
mov eax, dword ptr ds:[eax]
add eax, ebx // eax = eax + ebx = SSDT首地址 + index*4 就是前面的公式
mov ecx, [eax]
mov Addr, ecx
}
return Addr;
}
VOID DriverUnload(IN PDRIVER_OBJECT DriverObject)
{
DbgPrint("卸载完成!\n");
}
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
DbgPrint("加载完成!\n");
DbgPrint("NtOpenProcess:%p\n", GetSSDTFunctionAddr(0x7A));
DriverObject->DriverUnload = DriverUnload;
return STATUS_SUCCESS;
}
运行结果
运行结果如下图所示:
可以看到输出的NtOpenProcess地址为:0x805CC486
验证结果
使用Windbg验证结果:
u 0x805CC486
查看得到:
验证完毕o(^▽^)┛
总结
代码实现SSDT其实并没有什么难点,直接声明后引入KeServiceDescriptorTable,然后通过之前的公式计算出对应的SSDT函数地址即可
在WINDOWS XP系统下由于KeServiceDescriptorTable有直接导出,所以实现起来相对简单。若要在WIN7和WIN7之后的系统定位SSDT函数,则需要注意要先定位KeServiceDescriptorTable,且SSDT存储的是相对偏移,计算方式略有差异。其他方面,原理都差不多,想实现的朋友可以自己研究研究,这里就不再赘述≡(▔﹏▔)≡
附件
附上本篇编译出的打印NtOpenProcess地址的驱动文件,想试验能不能用的可以测试下╰( ̄ω ̄o)
PS:环境一定要是WINDOWS XP 32位
不同版本系统的SSDT函数对应的索引号不一定相同,注意查看要编写驱动环境的ntdll.dll文件中的索引号
如果本篇提供的成品中打印的地址不对,是其他的SSDT函数的地址,可能就是由于索引号不相同导致的
附件下载地址:点我下载
本篇中用到的其他工具可回顾:【原创】TP驱动保护分析系列二 代码定位内核函数 的附件部分
参考链接
System Service Descriptor Table - SSDT - Red Teaming Experiments (ired.team)
SSDT-System-Service-Descriptor-Table - aldeid
d, da, db, dc, dd, dD, df, dp, dq, du, dw (Display Memory) - Windows drivers | Microsoft Docs
dds, dps, dqs (Display Words and Symbols) - Windows drivers | Microsoft Docs
.reload (Reload Module) - Windows drivers | Microsoft Docs
lm (List Loaded Modules) - Windows drivers | Microsoft Docs
process - Windows drivers | Microsoft Docs
.process (Set Process Context) - Windows drivers | Microsoft Docs