65.9K
CodeProject 正在变化。 阅读更多。
Home

内核查找

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.74/5 (21投票s)

2006年4月4日

5分钟阅读

viewsIcon

83962

downloadIcon

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 的包装类。该类包含两个方法:InstallServiceUnInstallServiceServiceInstaller 负责加载和卸载我们的小型内核驱动程序。代码摘自 www.c-sharpcorner.com 上的一篇文章。

Driver:这是程序的核心。它与驱动程序交互并评估接收到的数据。我将用几句话解释主要方法及其工作原理

public bool Open(string name)

此方法使用 kernel32.dllCreateFile 导入,简单地打开与我们驱动程序的设备。

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 方法与驱动程序通信。它发送 inBufferIoControlCode,并在 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 - 初始发布。
© . All rights reserved.