内核查找






4.74/5 (21投票s)
2006年4月4日
5分钟阅读

83962

2432
如何在 C# 应用程序中显示内核信息。
引言
去年索尼的丑闻导致人人皆知“root kit”这个词。但 root kit 技术是如何工作的?如今更重要的是:如何检测此类软件?本文将解释一种方法,并附带的源代码将展示如何在 C# Windows 应用程序中实现它。
背景
什么是 root kit? root kit 通常指的是一种恶意软件,其主要特点是:该进程试图通过操纵文件、注册表和其他重要访问查询来隐藏自身。更改 KiServiceTable 中的条目是实现此目的最常用的方法。
简要解释:一个普通的 Windows 应用程序无法直接访问内核函数,如“打开文件”或“打开注册表键”。因此,WinNT 提供了 ntdll.dll 层。ntdll.dll 中的大多数导出函数都是直接内核调用。举个小例子:我们需要访问一个 Windows 注册表键。因此,我们将使用 RegOpenKey
API(advapi32.dll)。RegOpenKey
将执行内部工作,然后将调用转发到 ntdll.dll 中的 NtOpenKey
。您会惊讶地发现 NtOpenKey
是一个非常简短的函数。
public NtOpenKey ; WinXP SP2
mov eax, 77h ; NtOpenKey service number
mov edx, 7FFE0300h
call dword ptr [edx] ; Kernel call
retn 0Ch
该函数将服务号 0x77(NtOpenKey
)移入 eax
寄存器,然后直接跳转到内核模式。内核地址空间中的一个分派例程将根据 eax
寄存器中的值重定向代码流。为什么采用如此复杂的方法?它确保用户可以访问选定的内核函数,因为直接调用是不可能的。服务号取决于操作系统版本。
这里的一个薄弱点是 KiServiceTable。KiServiceTable 是一个包含每个服务地址的大区域。对于我们的例子,分派器将查看 0x77 的位置。在该位置,应该有 NtOpenKey
的相应地址。root kit 仅将此地址更改为其自己的代码。每次用户应用程序尝试打开注册表键时,分派器都会将调用转发到 root kit,而不是真正的 NtOpenKey
服务。
我们将如何检测这种行为?我们无法直接访问 KiServiceTable,因为它位于内核地址空间中。因此,我们的这个小程序分为两部分。第一部分是 C# GUI,第二部分是 C 内核驱动程序。
内核驱动程序响应三种不同的 IO 请求
IOCTL_GET_MODULE_NAME
:驱动程序尝试查找特定地址的模块名称。IOCTL_GET_SERVICE_TABLE
:驱动程序将整个 KiServiceTable 复制到输出缓冲区。IOCTL_GET_INT_TABLE
:驱动程序将整个中断描述符表复制到输出缓冲区。
C# 程序验证每个服务是否指向 ntoskrnl.exe 模块,否则将其声明为已挂钩。您可以在上面的屏幕截图中看到一个示例。NtCreateKey
由 Daemon Tools 重定向,NtCreateFile
由 SoftIce 扩展重定向。为了将每个服务分配一个名称,我们使用 ntdll.dll 导出表。程序遍历导出项,并在第一个指令是 mov eax, serviceNumber
的条件下,将导出名称与服务号关联起来。
关于代码
类 MainForm
:此类处理 UI,借助 Driver
类调用内核服务,并用接收和评估的数据填充列表视图。
//load driver as a service
LookupDriver.Load();
//create the kiServiceTable and get it from the kernel service
kiServiceTable = LookupDriver.GetKiServiceTable();
//resolve every service number into its name from ntdll.dll
LookupDriver.GetKiServiceNames(kiServiceTable);
//put the entries into the list field
FillServiceTableList();
类 ServiceInstaller
:它是 Windows 服务 API 的包装类。该类包含两个方法:InstallService
和 UnInstallService
。ServiceInstaller
负责加载和卸载我们的小型内核驱动程序。代码摘自 www.c-sharpcorner.com 上的一篇文章。
类 Driver
:这是程序的核心。它与驱动程序交互并评估接收到的数据。我将用几句话解释主要方法及其工作原理
public bool Open(string name)
此方法使用 kernel32.dll 的 CreateFile
导入,简单地打开与我们驱动程序的设备。
private unsafe int Interact(
uint IoControlCode, //control code for the driver
byte[] inBuffer, //input buffer, send to the driver
byte[] outBuffer) //output buffer, receive from the driver
Interact
方法与驱动程序通信。它发送 inBuffer
和 IoControlCode
,并在 outBuffer
中接收信息。用于此目的的 API 是 DeviceIoControl
。
public unsafe KiServiceTable GetKiServiceTable()
此方法将 IOCTL_GET_SERVICE_TABLE
控制码发送到驱动程序,并接收完整的 KiServiceTable。
public unsafe string GetModuleName(uint addr)
此方法将 IOCTL_GET_MODULE_NAME
控制码发送到驱动程序。内核驱动程序尝试查找特定地址的模块。返回值是模块名称字符串或“unknown”。
public unsafe void GetKiServiceNames(KiServiceTable kiServiceTable)
上述方法遍历 ntdll.dll 导出表,并尝试将服务号与导出的函数关联起来。
第二部分是位于 sys 目录下的内核驱动程序,文件名为 lookup.c。驱动程序响应三种不同的 IO 请求。IO 请求在 DrvDeviceControl
函数中处理。DrvDeviceControl
通过 DeviceIoControl
API 调用。代码本身很容易理解。您将在 DriverEntry
函数中找到分配。DriverEntry
是入口点,Windows 在初始化驱动程序时会调用它。
case IOCTL_GET_MODULE_NAME: //get a module name for a address { //address from the input buffer, first dword addr = ((DWORD *)inBuf)[0]; //call getmodulename name = GetModuleName(addr); if(name != 0) //module name found memcpy(outBuf, name, strlen(name)); else memcpy(outBuf, "unkown\0", 7); Irp->IoStatus.Information = outBufLength; ntStatus = STATUS_SUCCESS; break; }
IOCTL_GET_MODULE_NAME
是驱动程序处理的第一个 IO 请求代码。输入缓冲区包含用户需要模块名称的内存地址。GetModuleName
函数使用内核函数 ZwQuerySystemInformation
遍历每个模块,当地址位于开始和结束之间时,该函数返回指向其名称的指针。
case IOCTL_GET_SERVICE_TABLE: //get complete service table { //number of services avaible in the current os ((DWORD *)outBuf)[0] = KeServiceDescriptorTable->ServiceTable[0].NumberOfServices; //first dword in the output buffer = number of services ptr = (char *)KeServiceDescriptorTable->ServiceTable[0].TableBase; outBuf += 4; //copy each entry into the output buffer for(i = 0;i < KeServiceDescriptorTable-> ServiceTable[0].NumberOfServices;i++) ((DWORD *)outBuf)[i] = ((DWORD *)ptr)[i]; Irp->IoStatus.Information = outBufLength; ntStatus = STATUS_SUCCESS; break; }
IOCTL_GET_SERVICE_TABLE
是我们这个小程序第二个也是最重要的 IO 请求。KiServiceTable 地址无法直接访问。但我们可以在 ntoskrnl.exe 的导出函数 KeServiceDescriptorTable
中找到它。当前操作系统版本中可用的服务数量也是如此。有了这两个信息,我们只需将每个条目复制到输出缓冲区。
case IOCTL_GET_INT_TABLE: //get complete interrupt descriptor table { dwIdt = GetIntTable(); //base address from the idt ((DWORD *)outBuf)[0] = 255; //hardcoded 255 interrupt entries outBuf += 4; for(i = 0;i < 255;i++) //copy each entry into the output buffer ((DWORD *)outBuf)[i] = GetIntAddress(dwIdt, i); Irp->IoStatus.Information = outBufLength; ntStatus = STATUS_SUCCESS; break; }
IOCTL_GET_INT_TABLE
是第三个也是最后一个 IO 请求。它只是将整个中断描述符表复制到输出缓冲区。
关注点
您将需要 Windows DDK 和 VC# 来构建项目。演示程序本身在 Windows XP SP2、Windows Server 2003 下进行了测试,并在 VC#2005 / .NET 2.0 中构建。
这是我为 CodeProject 写的第一个文章。希望您喜欢。
历史
- 1.0 - 初始发布。