通过直接恢复SSDT(系统服务分派表)的方式解除核心原生API钩子

简介

WIN32核心RootKit通过挂钩核心原生API的方式改变了系统的行为.这项技术最典型的实现方式是更改内核的SSDT(系统服务分派表).这种更改保证了被rootkit安装的钩子函数可以预先调用初始原生API.钩子函数通常调用初始的原生API,然后在原生API将结果返回到用户态程序之前将其输出结果改变.这种技术可以使核心态rootkit隐藏文件,进程或者阻止恶意进程被结束.

此篇论文会对核心原生API挂钩技术做简要的介绍,并且会给出一项对抗技术.对抗技术可以修复被rootkit的钩子修复过的SSDT,并且是直接在用户态进行修正.

通过修改SSDT实现的内核原生API挂钩

在Windows中,用户态应用程序会调用一些API来请求系统服务,这些api被很多dll导出.比如说,向一个打开的文件或者管道或者设备中写入数据,通常会调用WriteFile这条API来实现,WriteFile是被kernel32.dll导出的.在kernel32.dll中,被执行的WriteFile API会调用ZwWriteFile这条被ntdll.dll导出的原生API.这项工作实际是由ZwWriteFile在内核态完成.因此,ZwWriteFile在ntdll.dll中的执行过程仅仅是将一些极少量的代码传送到内核空间调用0x2E中断来完成.ZwWriteFile在Win2K平台上的反汇编信息如下:

1- MOV EAX, 0ED
2- LEA EXD, DWORD PTR SS:[ESP+4]
3- INT 2E
4- RETN 24

这个在第一行的魔力般的数字0xED就是ZwWriteFile在Win2k系统的服务号.在内核中的系统服务分派表(SSDT)中,它是一个索引,这个索引用于定位这个完成写文件,管道或者设备的硬编码的服务函数的地址.SSDT的地址可以通过SDT(服务描述符号表)找到.

SDT可以通过KeServiceDescriptorTable符号来引用,他是被ntoskrnl.exe导出的.其结构定义如下:

typedef struct ServiceDescriptorTable{
SDE ServiceDescriptor[4];
} SDT;

typedef struct ServiceDescriptorEntry{
PDWORD KiServiceTable;
PDWORD CounterTableBase;
DWORD                 ServiceLimit;
PBYTE                 ArgumentTable;
} SDE

结构体的第一个成员,SDT.ServiceDescriptor[0].KiServiceTable,拥有一个指针,它指向由ntoskrnl.exe实现的系统服务的SSDT.正如早先提到的那样,SSDT拥有一系列的函数指针,指向相应的被原生API呼叫的服务函数.ServiceLimit成员提供SSDT入口值.

那个处在KiServiceTable[0xED]处的DWORD类型的值是一个指向NtWriteFile的指针,该函数有实现写文件,管道或者设备功能的硬编码.因此,如果想改变用户模式下WriteFile这条API的行为,最简单的就是编写一个钩子(替换)函数,以驱动的方式加载到内核,然后修改KiServiceTable[0xED]使他指向那个钩子函数.这个钩子函数需要保存一个初始函数指针的副本(KiServiceTable[0xED]的初始值),以便于原始的函数可以在必要时被调用.

下面的来自WinDbg调试器的截屏展示了KeServiceDescriptorTable和KeServiceDescriptorTable.KiServiceTable的一些内容:

通过直接恢复SSDT(系统服务分派表)的方式解除核心原生API钩子_第1张图片
通过直接恢复SSDT(系统服务分派表)的方式解除核心原生API钩子_第2张图片

接下来的例子说明了核心原生API钩子是如何改变了一些特定API的行为.

示例1-通过挂钩ZwQuerySystemInformation来隐藏进程

用户模式下的程序可以使用被ToolHelp Dll导出的一些API来将所有正在运行的进程构建一个列表.这些API会转过去调用ZwQuerySystemInformation这个被ntdll.dll导出的原生API来完成活动进程列表的构建动作(指定ZwQuerySystemInformation的第一个参数为systemProcessesAndThreadsInformation).欲隐藏进程,Win2K平台上一个以驱动形式加载的内核态rootkit,可以改变处在KiServiceTable[0x97]位置的函数指针(ZwQuerySystemInformation)并将指针重定向到钩子函数.

这个钩子函数首先调用原始的ZwQuerySystemInformation API来构建一个装载着所有活动进程信息的数组.然后再将这个即将被返回的数组中想要被rootkit隐藏的那条进程信息抹去.最终,这个被更改的结果被返回到了用户态应用程序.从而阻止了用户态的应用程序发现那条被隐藏的进程.

示例2-通过挂钩ZwQuerySystemInformation来隐藏驱动/模块

用户模式下的应用程序可以通过调用原生API ZwQuerySystemInformation来构建一个包含所有已加载的驱动模块的列表(指定ZwQuerySystemInformation的第一个参数为SystemModuleInformation).正如先前所提到的,ZwQuerySystemInformation由ntdll.dll导出并且可以被用户模式下的应用程序直接调用.在和心态,原生API ZwQuerySystemInformation通过遍历PsLoadedModileList链表来构建一个已加载驱动的列表.

一个Win2K核心态的rootkit可以通过改动KiServiceTable[0x97](ZwQuerySystemInformation)使其指向一个钩子函数来篡改ZwQuerySystemInformation的返回值.狗子函数首先会调用原始的ZwQuerySystemInformation来得到一个包含所有已加载驱动的数组.然后将需要隐藏的驱动(比如那个rootkit需要的驱动)从数组中抹掉.最后这个篡改过的数组被返回到用户态应用程序.

示例3-通过挂钩ZwQueryDirectoryFile来隐藏文件

用户态应用程序使用FindFirstFile和FindNextFile这两条被kernel32.dll导出的API来构建一个文件夹中所包含的文件的列表.这些API最终会调用ZwQueryDirectoryFile这条原生API来获得这个文件列表.一个和心态的rootkit可以篡改ZwQueryDirectoryFile的输出,在这个结果被返回到用户态应用程序之前将所有包含想要隐藏的那个文件的入口抹掉.

恢复SSDT(后面的尤其精彩,因为完全是在用户态完成的--译注)

通过以上的示例,我们便可以确信--如果我们能够将SSDT链回复到其原始状态,便可以使所有通过挂接SSDT入口点来改变系统行为的核心态rootkit失效.接下来的章节将会讨论这项技术的实现方式.一个概念性但是却有效(缩写为POC,下同--译注)的rootkit对抗工具--SDTTrestore被开发出来用于阐释文献中所描述的技术.这个POC工具可以通过以下地址下载:

http://www.security.org.sg/code/sdtrestore.html

在用户态修改SSDT

SSDT通常存在于核心态,欲修改SSDT里面的入口点,rootkit必须以驱动的方式加载其自身.但是,通过直接用/device/physicalmempry来写核心内存的方式来从用户态修正SSDT也是行的通的.

来自Sysinternals的Mark Russinovich第一个将/device/physicalmempry应用在他的物理内存工具使得直接查看物理内存成为可能.一篇完美的文章描述这项技术的细节.一份有趣的代码阐述了如何通过/device/physicalmempry来直接修改核心内存从而实现进程隐藏.

接下来的几个部分描述了一个以Administrator权限运行的用户态应用程序是如何通过/device/physicalmempry来获得直接访问核心内存的权限.

1. 调用原生API NtOpenSection(由ntdll.dll导出),注意权限标记为SECTION_MAP_READ|SECTION_MAP_WRITE,这样可以获取一个/device/physicalmempry的句柄.通常这样使不能成功的,因为管理员权限想对/device/physicalmempry拥有SECTION_MAP_WRITE的权限是不可能的.

2. 以SECTION_MAP_READ|WRITE_DAC访问标记来调用原生API NtOpenSection从而获得/device/physicalmempry的句柄.这样允许一个新的DACL被添加到/device/physicalmempry对象上.

3. 添加一个DACL到/device/physicalmempry中,使得Administrator也能获取SECTION_MAP_WRITE的访问特权.

4. 再一次尝试以SECTION_MAP_READ|SECTION_MAP_WRITE的访问标记调用原生API NtOpenSection来获取/device/physicalmempry的句柄.

上面的几步完成之后,用户态的应用程序便可以成功的获取/device/physicalmempry的句柄.为了写物理内存,程序必须首先将物理内存页映射到其自身的虚拟内存空间中.这个可以通过调用原生API NtMapViewOfSection来实现,方法如下:

通过直接恢复SSDT(系统服务分派表)的方式解除核心原生API钩子_第3张图片

将物理内存页面映射到虚拟内存空间之后,一个用户态的程序就可以像读写他自身分配的内存一样读写物理内存.输出参数为virualAddr,给出了物理内存页面映射到虚拟内存的地址.

定位SSDT的内存地址

为了能让用户模式应用程序修改SSDT中的入口值,必须事先确定他的物理内存地址并且将页面映射到其自身的虚拟内存空间中.SSDT的地址可以通过KeServiceDescriptorTable结构的成员KiServiceTable来找到.这意味着在获取SSDT的地址之前我们必须首先定位KeServiceDescriptorTable.然而,KeServiceDescriptor的内存地址却会因为内核补丁的版本不同而不同.但是,用户态应用程序依旧可以根据从ntoskrnl.exe中导出的符号来准确的侦测到KeServiceDescriptorTable地址.为了获取这个地址,用户态的应用程序必须首先将ntoskrnl.exe按照内存对齐的方式加载.然后KeServiceDescriptorTable的偏移地址便可以通过搜索ntoskrnl.exe导出符号来侦测到.

紧接着KeServiceDescriptorTable的偏移地址被转换成物理内存地址并且相应的物理内存页被映射到用户模式应用程序的虚拟内存空间.为了将KeServiceDescriptorTable的偏移地址转换成物理内存地址,我们必须首先确定内核在保护模式虚拟内存中的基址.这个可以通过调用ZwQuerySystemInformation(第一个参数设定为SystemModuleInformation)来很轻松的办到.结合内核的基址,拥有KeServiceDescriptorTable那块物理内存的地址可以按照如下方式计算到:

在这种情况下,我们假设保护模式下虚拟内存由0x80000000起始.

在包含KeServiceDescriptorTable的物理内存页被映射之后(使用/device/physicalmemory),我们可以通过读取其第一个结构单元--ServiceDescriptor[0].KiServiceTable来确定SSDT的地址,这个地址可以很方便的用如下的方式计算,假设保护模式的虚拟内存起始点为0x80000000.

KiServiceTable的虚拟地址通常也可以用来定位初始的SSDT在ntoskrnl.exe内部的副本.原始的SSDT在ntoskrnl.exe的磁盘镜像中的偏移位置可以按如下方式计算:

在我的SDTrestore工具发布不久,90210在rootkit.com提出了一种改进的定位KiServiceTable[10]的方案.该方案基于KeServiceDesciptorTable在KiInitSystem函数中被初始化的指令分析:

      mov ds:KeServiceDescriptorTable, offset KiServiceTable

这样的话我们就可以定位这条在ntoskrnl.exe的指令,通过扫描其自身的重定位表同时参照与"mov KeServiceDescriptorTable, imm32"相一致的指令来实现定位.使用重定位表来协助扫描将比扫描ntoskrnl.exe入口代码段来获取上面的指令更加行之有效.一旦这条指令被定位,KiServiceTable的偏移地址就可以马上被确定.

恢复SSDT内被篡改的入口

当包含运行在核心的SSDT的物理内存页被映射之后,我门可以将所有的正在运行的核心SSDT入口点与ntoskrnl.exe的磁盘镜像中的初始SSDT进行循环比较.每一个正在内核运行的SSDT实际都是一个包含绝对虚地址的函数指针,因此在比较之前需要将其转换成相对偏移地址.转换方式如下:


只要存在丝毫的差异则说明绝对有原生API被钩住了,并且任何被挂钩的SSDT的入口点均可以根据我们从磁盘镜像文件(ntoskrnl.exe)中获取的初始值来恢复.恢复之前,从磁盘镜像文件中获取的初始值同样需要经过一次从相对偏移地址到绝对虚拟地址的转换.

通过SSDT还原来禁用He4Hool(详见www.rootkit.com--译注)的原生API钩子

He4Hook是一款内核态的rootkit,并且可以通过挂钩核心原生API来实现文件/文件夹的隐藏.使用我们的SDTrestore工具,可以发现He4Hook钩住了一些原生API.如下图:

通过直接恢复SSDT(系统服务分派表)的方式解除核心原生API钩子_第4张图片

ZwQueryDirectoryFile被钩住了用于隐藏文件和文件夹.挂钩ZwCreateFile和ZwOpenFile使得He4Hook可以限制某些文件和文件夹的访问方式从而实现文件保护.使用SDTrestore,我们可以将SSDT恢复到初始状态.SSDT的还原有效的禁止了He4Hook的功能,截屏如下:

以原生API挂钩来实现功能的一些安全工具

通过修改SSDT挂钩核心原生API的技术不仅仅用于rootkit.使用我们的KProcCheck工具,我们可以发现一些安全软件也使用了同样的技术.接下来这些就是使用核心原生API挂钩技术的安全软件:

DiamondCS Process Guard(v2.000)
Kerio Personal Firewall 4(v4.0.16)
Sebek(v2.1.5)

DiamondCS Process Guard(v2.000)

PG锁这款安全工具可以保护系统和信任进程(比如用户指定的进程)免受其他恶意进程/服务/驱动以及一切在系统中存在的可执行代码的破坏.它可以保护一条进程免被终止/挂起
或者阻止一份恶意驱动被加载.

使用KProcCheck检查之后,我们发现PG锁的工作方式是挂钩如下几个原生API:

更进一步的测试证实了使用我们的SDTrestore工具,我们可以让PG锁失效.换句话说,那些被PG锁保护的进程现在也可以轻易的使用Windows任务管理器来终止了.

你可能感兴趣的:(通过直接恢复SSDT(系统服务分派表)的方式解除核心原生API钩子)