一、在用户空间修改系统服务调度表(SSDT)
SSDT位于内核空间,如果要修改SSDT的入口函数,rootkit都是通过驱动程序的方式载入再修改的。然而在用户空间可以通过直接读写\device\physicalmemory来修改SSDT的入口。(译注:修改SSDT入口是可以在用户空间进行,只是SSDT的入口函数必须是在内核空间,所以一般rootkit都是把修改后的程序放在驱动程序中,以便直接加载到内核空间。)
Mark Russinovich在Sysinternals发表了Physmem工具,通过使用\device\physicalmemory来查看物理内存。在"Playing with Windows /dev/(k)mem"文章中详细描述了如何通过\device\physicalmemory读写内核内存。在"Process Hide"文章中描述如何通过直接操作\device\physicalmemory进行进程的隐藏。
以下的步骤描述了如何在管理员权限下用户空间程序获取对\device\phisicalmemory的读写。
1、尝试用ntdll.dll的输出函数NtOpenSection,设置标志为SECTION_MAP_READ | SECTION_MAP_WRITE来获取\device\physicalmemory的句柄。由于系统管理员不具有对内核内存进行SECTION_MAP_WRITE的权限,所以一般情况下都会失败。
2、用NtOpenSection,设置标志为SECTION_MAP_READ | WRITE_DAC来获取\device\physicalmemory的句柄。使用WRITE_DAC标志,将能够对\device\physicalmemory添加新的DACL。
3、给\device\phisicalmemory添加一个DACL,使得系统管理员具有SECTION_MAP_WRITE权限。
4、再次尝试1步骤。
在进行完上述流程以后,用户空间的程序将成功获取\device\physicalmemory的句柄。在用户空间的程序中必须把物理内存页映射到虚拟内存,才能够直接对物理内存进行写操作,这里可以通过NtMapViewOfSection完成。在物理内存映射到虚拟内存以后,用户空间程序就可以像它本身自己分配的内存一样,进行读写操作。
ntStatus = _NtMapViewOfSection(
hPhyMem, // \device\physicalmemory的句柄
(HANDLE)-1,
virtualAddr, // 输出参数,虚拟内存中映射物理内存的地址
0,
*length,
&viewBase, // 输入/输出参数,需要映射的物理内存地址
length, // 输入/输出参数,需要映射的物理内存长度
ViewShare,
0,
PAGE_READWRITE // 读写权限
);
二、定位系统服务调度表的内存地址
为了在用户空间程序中修改SSDT的入口函数,必须把找到SSDT的物理内存并将其映射到虚拟内存空间。SSDT的地址可以从KeServiceDescriptorTable结构体的成员KiServiceTable中找到。在找到SSDT的地址以前,必须先定位KeServiceDescriptorTable的地址。虽然KeServiceDescriptorTable的地址在不同版本的Windows和Serivce Pack中都会变化。但KeServiceDescriptorTable是ntosknl.exe的输出符号,在用户空间程序中仍然可以获取它的可靠值。首先,按照正确的内存对齐方式把ntoskrnl.exe载入内存,通过搜索ntoskrnl.exe的输出表定位KeServiceDescriptorTable的地址。
将获取的KeServiceDescriptorTable的地址转换为物理地址,将对应的物理地址映射到虚拟内存中。为了转换KeServiceDescriptorTable的偏移地址转换到物理地址,必须先找到保护模式下虚拟内核的基地址,这可以直接用ZwQuerySystemInformation来获取,需要设定第一个参数为SystemModuleInformation。这样,包含KeServiceDescriptorTable的物理地址就可以通过如下公式进行计算:
PhyMemAddrKeServiceDescriptorTable =
KernelVirtualBaseAddr + OffsetAddrKeServiceDescriptorTable - 0x80000000
这里都假定保护模式下的虚拟内存是从0x80000000开始。
这之后,通过读取KeServiceDescriptorTable结构体中的第一个成员ServiceDescriptor[0].KiServiceTable,就能够获取SSDT的地址。这里获取的SSDT地址还必须转换为物理地址,公式如下:
PhyMemAddrServiceTable = VirtualMemAddrServiceTable - 0x80000000
KiServiceTable的虚拟内存地址还用来定义ntoskrnl.exe中的原SSDT地址,公式如下:
OffsetAddrServiceTable = VirtualMemAddrServiceTable - KernelVirtualBaseAddr
在SDTrestore工具发布后,90210在rootkit.com提出了一种定位KiServiceTable更有效的办法。该技术主要基于KeServiceDescriptorTable是在KiInitSystem函数中通过下面的指令初始化:
mov ds:KeServiceDescriptorTable, offset KiServiceTable
可以通过搜索ntoskrnl.exe的重定位表找到这条指令。使用重定位表的搜索如上的指令比在整个ntoskrnl.exe的程序区中搜索,更加有效和可靠。一旦找到这条指令,KiServiceTable的偏移地址就找到了。
三、恢复系统服务调度表的入口函数
把包含SSDT的内核物理内容映射以后,对运行内核中的SSDT入口函数和从ntoskrnl.exe磁盘映像的原SSDT入口函数进行对比。由于在运行内核中SSDT入口函数是一个绝对虚拟地址的函
数指针,所以在比较以前必须转换为偏移地址,转换公式如下:
OffsetAddrOfFuncPtr = VirtualMemAbsAddrOfFuncPtr - KernelVirtualBaseAddr
如果某个入口函数的偏移地址和ntoskrnl.exe的原始地址不同,就表示该入口被Hook。被Hook的入口函数可以用ntoskrnl.exe的原始地址进行恢复。当然需要把ntoskrnl.exe的偏移地址转换为绝对虚拟地址。