通过对系统分派表的直接还原,防御内核本地API钩子(翻译) 分享

original url from : http://taianmonkey.blog.sohu.com/84297540.html

通过对系统分派表的直接还原,防御内核本地API钩子

介绍
win32内核rootkit通过挂钩本地内核的API去修改系统的行为。这项技术通常是通过修改内核中的系统服务分派表(System Service Dispatch Table)来实现的。这样的修改确保了通过rootkit安装的一个钩子函数先于本地原始的API被调用。钩子函数通常调用本地原始的API并在返回给用户空间程序之前修改其输出。这项技术允许内核rootkit去隐藏文件,进程和预防恶意进程的结束。

本文章将对本地内核API挂钩进行一个简短的介绍,并为通过对修改系统服务分派表而挂钩本地API的内核rootkit防御提供一些建议。所建议的技术是从用户空间中直接恢复系统服务分派表,并且不再要求加载一个内核驱动。

通过修改系统服务分派表挂钩本地内核API

在windows中,用户空间的应用程序,通过调用由相关dll所导出的API去请求系统服务。比如,要向一个打开的文件,管道或设备中写入数据,通常会使用由kernel32.dll所导出的API WriteFile。在kernel32.dll内部,API WriteFile则将依次去执行由ntdll.dll所导出的本地API ZwWriteFile。而由ZwWriteFile所做的工作实际上是执行在内核空间内。因此在ntdll.dll中ZwWriteFile所执行的仅仅是很少的一段代码,是使用0X2E中断转入内核空间中。ZwWriteFile在win2k上的反汇编代码如下:
1-MOV EAX,0ED
2-LEA EDX,DWORD PTR SS:[ESP+4]
3-INT 2E
4-RETN 24
其中1行中的0ED是ZwWriteFile在win2k上服务号。在内核系统服务分派表(SSDT)中作为索引值使用,用于定位实际执行写文件,管道或设备的系统函数代码的地址。SSDT的地址可以在系统描述符表(Service Descriptor Table , SDT)中找到。SDT可以使用KeServiceDiscriptorTable符号进行参考,它由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]的原始值)值的一个复本,以便原始函数可被调用执行其原有的任务。

下面的例子示例了如何使用挂钩本地内核API去修改特定API的行为。

例子1:通过挂钩ZwQuerySystemInformation隐藏进程

用户空间的程序可以使用由ToolHelp DLL导出的API获取所有正在运行的进程的列表。API依次调用由ntdll.dll导出的本地API ZwQuerySystemInformation指定其第一个参数为(SystemProcessAndThreadInformation)来获取正在运行的进程的列表。要隐藏进程,一个win2k的内核空间的rootkit,它以一个驱动加载,可以修改在KiServiceTable[0x97](ZwQuerySystemInformation)的函数指针,使其转向一个钩子函数调用。

钩子函数首先调用原始的ZwQuerySystemInformation API来获取一个包含有所有正在运行的进程信息的数组。接着在返回的数组中去删的进程的结点。最后,将修改的结果返回给用户空间程序。这样就有效的防止用户空间的程序“看到”隐藏的进程。

例子2:通过挂钩ZwQuerySystemInformation去隐藏驱动/模块

用户空间程序可以使用ZwQuerySystemInformation API来获取所有已加载的驱动的列表,第一个参数指定为SystemModuleInformation。就像先前所提到的,ZwQuerySystemInformation是由ntdll.dll导出的并且可以被用户空间的程序直接调用。在内核空间中,从本地API ZwQuerySystemInformation获取加载的驱动的列表中截去PsLoadedModuleList。

一个win2k内核空间的rootkit可以操作由ZwQuerySystemInformation返回的结果,是通过修改KiServiceTable[0x97](ZwQuerySystemInformation)的函数指针,使其转向一个钩子函数调用。钩子函数首先调用原始的ZwQuerySystemInformation API来获取一个包含有所有已被加载的驱动的数组。接着在返回的数组中去删除的要隐藏的驱动的结点。最后,将修改的结果返回给用户空间程序。操作后的数组返回给用户空间程序。

例子3:通过挂钩ZwQueryDirectoryFile隐藏文件:
用户空间程序使用由kernel32.dll导出的FindFirstFile和FindNextFile API在一个目录中获取所有文件的列表。这些API最终将要调用本地API ZwQueryDirectoryFile去返回文件列表的请求。一个内核空间的rootkit可以操作ZwQueryDirectoryFile的输出,在把结果返回给用户空间程序前,删除其中要隐藏的文件的结点。

恢复SSDT(系统服务分派表):

 从上面的例子,很明显,如果我们想要恢复SSDT到原始的状态,我们必须禁止任何通过挂钩SSDT内部结点去修改系统行为的内核rootkit。接下来的章节将详细描述应该如何去做。一个POC rootkit防御工具,SDTrestore,例示了本文章所描述的技术。POC工具可以从下面的URL中下载到:
http://www.security.org.sg/code/sdtrestore.htm

从用户空间修改SSDT(系统服务分派表):

SSDT存在于系统空间内,并且通常要修改SSDT中的结点,rootkit必须以一个驱动的方式加载自己到内核空间中。然而,用户空间程序可以使用\device\physicalmemory直接写内核内存去修改的SSDT结点。

来自Sysinternals的Mark Russinovich在其Physmem工具中首先使用\device\physicalmemory,允许去查看物理内存[3]。另一篇详细描述如何使用\device\physicalmemory读取和写入内核内存的好文章可以在[2]中找到。展示如何通过\device\physicalmemory直接操作内核内存去隐藏进程的代码可以在[4]中找到。

下列步骤描述了一个运行在管理员权限下用户空间程序,如何通过\device\physicalmemory来对内核内存进行读写访问:
1、使用本地API NtOpenSection(由ntdll.dll导出),访问参数为SECTION_MAP_READ | SECTION_MAP_WRITE,以获取一个\device\physicalmemory句柄。当管理员没有SECTION_MAP_WRITE对\device\physicalmemory的访问权限时通常会失败。
2、使用带参数READ_CONTROL | WRITE_DAC的本地API NtOpenSection获取一个\device\physicalmemory句柄。这将允许添加一个新的DACL到\device\physicalmemory对象。
3、为\device\physicalmemory对象添加一个DACL,授予管理员账户SECTION_MAP_WRITE访问。
4、使用带SECTION_MAP_READ | SECTION_MAP_WRITE访问参数的本地API NtOpenSection,试图再次获取\device\physicalmemory的句柄。

依次执行完上述的步骤后,用户空间程序已成功获取了一个\device\physicalmemory的句柄。为了可写入物理内存,程序首先映射物理内存页到其虚拟地址空间内。可以使用本地API NtMapViewOfSection完成,如下所示。

ntStatus = NtMapViewOfSection(
           hPhyMem ,          // handle to \device\physicalmemory
           (HANDLE)-1 ,
           virtualAddr ,      // OUT - Virtual memory where the physical memory is mapped to
           0 ,
           *length ,
           &viewBase ,        // IN / OUT - Physical memory address to map in
           length ,           // IN / OUT - Size of the mapped physical memory
           ViewShare ,
           0 ,
           PAGE_READWRITE      // Map for READ / WRITE access
           );
映射物理内存页到其虚拟内存空间后, 一个用户程序接着可以读取和写入物理内存中,就像写入已分配的内存中。输出参数virtualAddr , 给出物理内存页映射到虚拟内存空间的虚拟内存地址。

定位SSDT的内存地址

为了让用户空间程序可以可以修改SSDT的成员,他必须首先确定其物理内存地址并映射页到他的虚拟内存空间中。SSDT的地址可以使用KeServiceDescriptorTable的成员KiServiceTable获取。这将意味着在我们获取SSDT的地址之前首先定位KeServiceDescriptorTable。然而,KeServiceDescriptorTable的内存地址对于不同的内核服务包版本是不同的。尽管如此,自从这个符合由ntoskrnl.exe导出后,用户空间程序还是有可能真正确定KeServiceDescriptorTable的地址。要获取这个地址,用户空间程序首先加载ntoskrnl.exe到一个适当的内存队列中。KeServiceDescriptorTable的偏移地址接着可以通过在ntoskrnl.exe的导出表中查询其符号获取。

KeServiceDescriptorTable的偏移地址接着转换为物理内存地址,并且对应的物理内存页被映射到虚拟内存空间中的用户空间程序中。要转换KeServiceDescriptorTable的偏移地址到物理内存地址,我们必须确定内核在保护模式虚拟内存的基地址。这可以使用第一参数为SystemModuleInformation的ZwQuerySystemInformation调用尽早获取。有了内核的基地址,KeServiceDescriptorTable的物理地址可以使用如下的方法计算出:
PhyMemAddrKeServiceDescriptorTable = KernelVirtualBaseAddr + OffsetAddrKeServiceDescriptorTable - 0x80000000
在本例中,我们假定保护模式虚拟内存起始于0x80000000。

映射完包含有KeServiceDescriptorTable(使用\device\physicalmemory)的物理内存页后,我们可以通过读取其先前的数据结构成员ServiceDescriptor[0].KiServiceTable去确定SSDT的地址。要读取的地址必须在它被用于映射含有SSDT的页面之前转换为物理内存地址。这个地址很容易按下述的方法计算,假定保护模式虚拟内存起始于0x80000000:
PhyMemAddrServiceTable = VirtualMemAddrServiceTable - 0x80000000
KiServiceTable的虚拟地址也通常用于定位ntoskrnl.exe内SSDT的原始副本。在ntoskrnl.exe磁盘镜像中,原始SSDT偏移定位也很容易的通过下面的方法计算出:
OffsetAddrServiceTable = VirtualMemAddrServiceTable - KernelVirtualBaseAddr

稍后发布我们的SDTrestore工具后,在rootkit.com上,推荐90210为定位KiServiceTable[10]的一个改进的技术。这项技术是基于观察KeServiceDescriptorTable在KiInitSystem函数中的初始化时的下述命令:
            mov ds:KeServiceDescriptorTable , offset KiServiceTable
能通过检索其重定位表参考的对应的指令“mov KeServiceDescriptorTable,imm32”定位ntoskrnl.exe内这条指令。使用重定位表有助于比检索ntoskrnl.exe整个代码节中上述指令,更加高效和可靠。一旦这条指令被定位,KiServiceTable的偏移地址能接着确定。


恢复在SSDT中修改的入口

当含有正在内核运行的SSDT物理内存页被映射后,将对运行在内核中的SSDT的所有的入口与原始的存放在磁盘上ntoskrnl.exe内的原始SSDT入口进行比较。正运行在内核中的每个入口,实际上是一个绝对的虚拟地址的指针。它在与原始的SSDT中对应的入口进行比较之前,必须被转换为偏移地址。这个转换的方法如下:
OffsetAddrOfFuncPtr = VirtualMemAbsAddrOfFuncPtr - KernelVirtualBaseAddr
任何的差异,说明本地特定的API已经被挂钩了,并且任何挂钩的SSDT入口可以使用取自磁盘镜像(ntoskrnl.exe)的原始值复原。先前的恢复,取自磁盘镜像的原始值必须首先将偏移地址转换为绝对的虚拟地址。

通过SSDT还原去禁止He4Hook的内核本地API挂钩

He4Hook是一种内核的rootkit,它用于本地内核API的挂钩,作为隐藏和保护文件/目录的一种方式。使用我们的SDTrestore rootkit防御工具,我们发现当He4Hook的文件系统挂钩特征使用-hk:1选项允许时,挂钩了如下的本地API:
ZwCreateFile,ZwOpenFile,ZwQueryDirectoryFile。
ZwQueryDirectoryFile被He4Hook挂钩,用于从目录中隐藏文件和目录。挂钩ZwCreateFile和ZwOpenFile,允许He4Hook限制对特定类型的保护文件和目录的访问。使用SDTrestore,我们能将SSDT恢复到原始的状态。SSDT的恢复,有效的禁止了He4Hook使用-hk:1选项时对文件和目录的保护特性。

使用挂钩本地内核API的安全工具:
通过对SSDT操作对本地内核API挂钩的技术不仅仅被rootkit使用。使用我们的KProcCheck工具[5],我们找到了几个安全工具为了多个目的也使用了这项技术。以下是某些使用挂钩本地内核API的安全工具:
     DiamondCS Process Guard(v2.000)
     Kerio Personal Firewall 4(v4.0.16)
     Sebek(v2.1.5)

DiamondCS Process Guard(v2.000):
  它是一个win32 安全系统,用于保护系统和安全进程(也就是用户进程)不被其他进程,服务,驱动,和系统中执行的代码的攻击。它可以保护一个进程不被结束,挂起并阻止恶意内核驱动的加载。

使用KProcCheck,我们将发现Process Guard通过挂钩下面的本地内核API进行工作的:
ZwCreateFile,ZwCreateKey,ZwCreateThread,ZwOpenFile,ZwOpenKey,ZwOpenProcess,ZwOpenThread,ZwRequestWaitReplyPort,ZwSetValueKey,ZwWriteVirtualMemory。

更多的测试显示表明,通过使用我们的rootkit防御工具SDTrestore复原SSDT,我们能禁止由Process Guard提供的保护。还句话说,由Process Guard所保护的进程可以很容易的被windows的任务管理器终止。

Kerio Personal Firewall 4(v4.0.16)
Kerio Personal Firewall(KPF)是一个高水平的个人防火墙,它用于帮助用户限制他们的计算机与在互联网活本地网络上其他计算机数据的交换。KPF有一个系统安全特性,它允许用户去控制在其系统上程序的运行。KPF通过当一个未知/新的或修改的程序被执行时的提示用户的动作,去防止恶意代码在用户系统上产生出进程。

其系统安全特性是通过对下面的本地内核API进行挂钩来工作的:
ZwCreateFile,ZwCreateProcess,ZwCreateThread,ZwResumeThread。
通过使用我们的rootkit防御工具SDTrestore复原SSDT,我们能禁止KPF4的系统安全特性。特性被禁止后,当未知/新的或修改的程序被执行时,KPF4将不再提示用户的动作。

Sebek(v2.1.5):
Sebek是一个数据俘获工具,被设计为用于在入侵者不知道的情况下,俘获其蜜罐活动。他有两个组件。第一个是运行在蜜罐上的客户端,它的目的是用于俘获入侵者的所有的活动(按键,上载的文件,密码)并接着转发数据给服务器端。另一个组件是服务器端,用于收集蜜罐的数据。
Sebek通过挂钩内地内核的API来防止他自身被检测到。挂钩的执行是在Sebek.sys模块中,它通过替换SDT服务表中的入口来实现。Sebek通过挂钩ZwCreateFile和ZwWriteFile来记录所有的控制台事件。挂钩这两个本地API允许Sebek去跟踪所有到控制台的读写请求并发送他们到记录服务器。

使用KProcCheck,我们能检测到Sebek挂钩了如下的本地API:
ZwClose,ZwCreateFile,ZwCreateKey,ZwEnumerateKey,ZwEnumerateValueKey,ZwOpenFile,ZwOpenKey,ZwQueryDirectroyFile,ZwQuerySystemInformation,ZwReadFile,ZwRequestWaitReplyPort,ZwSecureConnectPort,ZwWriteFile。
通过使用我们的rootkit防御工具SDTrestore复原SSDT,我们能禁止控制台的记录动作和Sebek的反检测能力。

结论:
在本论文中,我们已经给出了一个通过操控SSDT对本地内核API挂钩技术的简短的介绍。这项技术被内核rootkit使用于对系统行为的修改。我们已经展示了一个用户空间程序可以通过\device\physicalmemory直接写受保护的内存,恢复SSDT到其原始的状态来禁止这样的rootkit。

在我们的研究中,我们页发现有几个安全工具使用常用的挂钩技术去实现他们的某些特性。我们已经展示通过复原SSDT用户空间程序能禁止他们的安全特性。因此,我们所推荐的这些安全工具应该增加附加的步骤去阻止SSDT入口的恢复,这可以导致他们的安全特性将被禁用。比如,要防止用户空间程序加载内核驱动并阻断对\device\physicalmemory的写访问。

参考资料:
[1] Greg Hoglund,"NT Rootkit - The original and first public NT ROOTKIT",http://www.rootkit.com。
[2] crazylord "Playing with Windows /dev/(k)mem",Phrack Volume 0x0b,Issue 0x3b,Phile #0x10 of 0x12 , Jul 2002,http://www.phrack.org/59/p59-0x10.txt
[3] Mark Russinovich,"Physmem",Systems Internals. http://www.sysinternals.com/files/physmem.zip
[4] 90210 , "Process Hide" , 29A#7 magazine,VX Heavens,Jan 2004 http://vx.netlux.org/vx.php?id=ep12
[5] Tan Chew Keong,"Win2K Kernel Hidden Process/Module Checker 0.1(Proof-Of-Concept)",May 2004 . http://www.security.org.sg/code/kproccheck.html
[6] He4Hook,http://www.rootkit.com/vault/hoglund/He4Hook215b6.zip
[7] fuzen_op , "FU Rootkit" , http://www.rootkit.com/vault/fuzen_op/FU_Rootkit.zip
[8] joanna,"klister",http://www.rootkit.com/vault/joanna/klister-0.4.zip
[9] fireworker,"Kernel-mode backdoors for Windows NT",Phrack 62, Volume 0x0b,Issue 0x3e,Phile #0x06 of 0x10,July 2004
[10] 90210 ,"A more stable way to locate real KiServiceTable",http://www.rootkit.com/newsread.php?newsid=176
[11] David A.Soloman and Mark E.Russinovich,"Inside Microsoft windows 2000 Third Edition"
[12] Sven B.Schreiber,"Undocumented Windows 2000 Secrets, A Programmer's Cookbook"

鸣谢:
  作者感谢SIG^2 G-TEC Lab (http://www.security.org.sg/webdocs/g-tec.html)对本研究的支持。


你可能感兴趣的:(通过对系统分派表的直接还原,防御内核本地API钩子(翻译) 分享)