SSDT 表介绍
ntdll.dll 模块中的函数有些以 nt 或 zw 开头的函数为了完成相应的功能需要进入内核, 调用内核中以 nt 开头的函数来完成相应的功能. ntdll.dll 里的函数在进入内核层之前首先将系统服务号传入 eax 寄存器中, 然后调用 KiSystemService 函数进入内核层. 进入内核后会根据 eax 值索引 ssdt 表里的函数进行执行相应地址的函数.
SSDT 的每一项是一个系统服务函数的地址, 可以通过 HOOK 这些函数完成特定的功能.
32 位系统上 SSDT 是导出的, 64 位是不会导出的.
通过 PCHunter 查看 win7 x64 系统的 SSDT 表:
,
如何获得 SSDT 表的地址和每一个项对应的服务名称呢?
注意: ntoskrnl.exe - 单处理器, 不支持 PAE;ntkrnlpa.exe - 单处理器, 支持 PAE;ntkrnlmp - 多处理器, 不支持 PAE;ntkrpamp - 多处理器, 支持 PAE.
32 位系统
32 系统上 ntdll.dll 使用 mov eax,xxx 传入索引值, 可以通过遍历 ntdll.dll 查看每一个函数对应的服务号. 从而找到服务函数名和服务编号的关系. 另外, 内核中 32 位的 SSDT 的起始地址是直接在 ntoskrnl.exe 中通过 KeServiceDescriptorTable 符号导出, 不需要使用工具来获得, 可以直接在驱动程序中引用该符号的地址. 注意: 在代码实现上应当引入头文件 #include <ntimage.h > 之后使用语句
extern SSDTEntry __declspec(dllimport) KeServiceDescriptorTable;
来获得 KeServiceDescriptorTable 的地址.
32 位系统中 KeServiceDescriptorTable 结构如下图所示
- #pragma pack(1)
- typedef struct _SERVICE_DESCRIPTOR_TABLE
- {
- PULONG ServiceTableBase;//SSDT 的起始地址
- PULONG ServiceCounterTableBase;//
- ULONG NumberOfService;//SSDT 表中服务函数的总数
- PUCHAR ParamTableBase;// 服务函数的参数个数数组的起始地址, 数组单位是字节, 记录了对应函数的参数个数
- } SSDTEntry, *PSSDTEntry;
- #pragma pack()
ServiceTableBase 的内容是 SSDT 表的起始地址, 然后从 ServiceTableBase 开始是一个长度为 NumberOfService 的指针数组, 每一项是 4 个字节, 是 SSDT 表中每一个服务的函数地址.
在内核调试器 windbg 中使用 dd KeServiceDescriptorTable 命令查看 KeServiceDescriptorTable 数据, 就可以看到 SSDTEntry 结构的每一项数据.
64 位系统
64 位系统上 ntdll.dll 使用 mov r10,rcx;mov eax,xxx 传入索引值, 可以通过遍历 64 位的 ntdll.dll 查看每一个函数对应的服务号. 从而找到服务函数名和服务编号的关系.
如下图所示, 使用 IDA 查看 Windows 7 x64 的 64 位 ntdll.dll 的函数
通过 SSDT 表可以看出二者却是相互对应(这是因为, 就是通过 ntdll.dll 来逆推出 SSDT 表的每一项对应的函数名的)
但是, 由于 64 位的内核文件并未导出 KeServiceDescriptorTable 的信息, 所以 64 位系统的 SSDT 表的起始地址无法直接获得. 但是却同样可以在 windbg 中使用 dd KeServiceDescriptorTable 命令查看 KeServiceDescriptorTable 内容:
但是, 我们却无法使用某种方式直接获得 KeServiceDescriptorTable 的地址, 于是常采用间接方式.
使用 windbg 观察 nt!KiSystemServiceRepeat 函数的反汇编如下
可以发现, x64 系统会在 KiSystemServiceRepeat 函数使用 lea r10,[nt!KeServiceDescriptorTable]指令获得 KeServiceDescriptorTable 的地址, 因此只要在 KiSystemServiceRepeat 函数内搜索 4c 8d 15 得到这条指令的起始地址.
KeServiceDescriptorTable 地址保存在这条指令之后的地址即 fffff800`03e98779 + 指令操作数 (是偏移量) 得到的新地址中(指令反汇编的知识, 用四个字节作为偏移量), 即
pKeServiceDescriptorTable= fffff800`03e98779+2320c7= FFFFF800`040CA840
通过 windbg 验证 KeServiceDescriptorTable 的地址
而 KeServiceDescriptorTable 结构如下:
- #pragma pack(1)
- typedef struct _SERVICE_DESCIPTOR_TABLE
- {
- PULONG ServiceTableBase; // SSDT 基址, 8 字节大小
- PVOID ServiceCounterTableBase; // SSDT 中服务被调用次数计数器, 8 字节大小
- ULONGLONG NumberOfService; // SSDT 服务函数的个数, 8 字节大小
- PVOID ParamTableBase; // 系统服务参数表基址, 8 字节大小. 实际指向的数组是以字节为单位的记录着对应服务函数的参数个数
- }SSDTEntry, *PSSDTEntry;
- #pragma pack()
通过以上信息, 可以看出 SSDT 的首地址是 fffff800`03e9a300,SSDT 中以每 4 个字节为单位描述一个服务的地址项信息, 不过其内容并非实际的地址, 因为 4 个字节无法保存 64 位下的地址, 实际上其内容左移 4 位得到的是地址相对偏移量, 是真实服务地址相对 SSDT 起始地址的偏移量. 下面的内容可以帮助理解真实服务地址的计算过程.
通过 windbg 查看 fffff800`03e9a300 的数据
通过以上信息, 验证第一项和第二个项指向的地址, SSDT 的首地址是 fffff800`03e9a300:
- fffff800`03e9a300+040d9a00>>4= fffff800`03e9a300+040d9a0= FFFFF800`042A7C0
- fffff800`03e9a300+02f55c00>>4= fffff800`03e9a300+02f55c0= FFFFF800`0418F8C0
很明显这与 PCHunter 一致
上面的操作需要直到 KiSystemServiceRepeat 地址, 然而 KiSystemServiceRepeat 函数越没有导出. 所以无法知道其地址.
如何获得 SSDT 表的基址
方式一: 硬编码, 对每一个系统的不同版本, 分别使用 windbg 工具获得 KiSystemServiceRepeat 函数的地址.
方式二: 读取 msr 寄存器 (特别模块寄存器) 的 0xc0000082 的值, 即在微软的 C 语言中使用__readmsr(0xc0000082)获得 KiSystemCall64 函数的地址.
pKiSystemCall64=(PVOID)__readmsr(0xc0000082);
获得地址后遍历该地址后的数据会经过 KiSystemServiceRepeat 函数, 就可以通过特征码 4c 8d 15 找到目标指令.
具体计算公式是:
特征码起始地址是 pKiSystemCall64+i 则(i 表示相对 KiSystemCall64 函数开始处偏移 i 个字节)
- pKeServiceDescriptorTable=(PVOID)(pKiSystemCall64+i+7+*(PLONG)( pKiSystemCall64+i+3));
- pSSDT=*(PLONG)pKeServiceDescriptorTable
方式三: 解析本操作系统下的内核文件的符号文件.
来源: http://www.bubuko.com/infodetail-2973340.html