TDI网络过滤驱动开发指南

TDI网络过滤驱动开发指南

TDI(Transport Driver Interface)其实是一种过时的网络过滤技术框架,在XP系统下面是非常主流的一种网络数据包过滤框架,如果你的软件需要在XP环境下面运行,那么就需要使用TDI来开发。对于XP以上的系统,Windows提供了一种新的WFP(Windows Filtering Platform)网络过滤层的框架来取代TDI。

TDI常常用来开发网络安全相关的产品,例如:

  1. 防火墙产品。
  2. 网络嗅探工具。
  3. 截包分析工具。

虽然Windows声明了TDI已经是舍弃的技术框架,但是经过实测TDI依旧能够在最新的系统上面稳定运行(Windows10),例如NetFilterSdk提供的TDI的SDK仍旧可以非常稳定运行,甚至比WFP表现得更佳。

为了使得我们开发的代码或者产品能够稳定运行在Windows的各个环境下面,TDI目前也是一种比较重要的技术,本文我们来分析一下TDI的开发技术。

1. 技术概述

首先我们来看一下Windows的网络模块架构,如下:
在这里插入图片描述

在这个框架中:

  1. ws2_32.dll提供了基本的socket相关函数(例如socketbindlisten等)。
  2. Windows在用户层提供了一种过滤网络数据包的HOOK方案,这个就是Layered service provider(也就是我们通常说的LSP),通过这种技术我们对网络包进行HOOK了,国内很多大厂用的都是这种技术。
  3. Socket是一种统一的规范,无论是Windows还是Linux他们对外提供的接口都是一样的。在Windows下面Socket被转换成为设备的IO操作,并且提供了一个AFD.SYS(Ancillary Function Driver for WinSock)的驱动模块来辅助。
  4. tcpip是一个网络协议驱动程序,对底层他提供了一个NIDS协议驱动,对上层他提供了应对TCP,UDP,RAWIP等不同协议的设备对象。

TDI驱动的核心就是对于\\Device\\Tcp\\Device\\Udp以及\\Device\\RawIp三个设备进行过滤,形成设备栈,然后对每个IRP进行处理。

2. 设备的挂载

对于网络设备的过滤,只需要挂载三个设备即可,使用IoAttachDevice就可以挂载设备对象,实现如下:

NTSTATUS
AttachDevice(
	PDRIVER_OBJECT DriverObject, 
	PDEVICE_OBJECT *FltObject, 
	PDEVICE_OBJECT *OldObj,
	CONST WCHAR*NetDevName)
{
	NTSTATUS Status;
	UNICODE_STRING DeviceName;

	Status = IoCreateDevice(DriverObject,
							0,
							NULL,
							FILE_DEVICE_UNKNOWN,
							0,
							TRUE,
							FltObject);
	if (Status != STATUS_SUCCESS) 
	{
		return Status;
	}
	(*FltObject)->Flags |= DO_DIRECT_IO;
	RtlInitUnicodeString(&str, DevName);
	Status = IoAttachDevice(*FltObject, &str, OldObj);
	if (Status != STATUS_SUCCESS)
	{
		return Status;
	}
	return STATUS_SUCCESS;
}

因此我们只需要针对\\Device\\Tcp\\Device\\Udp以及\\Device\\RawIp这三个对象调用AttachDevice就可以对设备上面的网络数据进行过滤了。

3. TDI事件概述

当我们首先来看一下TDI涉及到的事件和IRP有哪些,如下:


#define IRP_MJ_CREATE                   0x00
#define IRP_MJ_CLOSE                    0x02
#define IRP_MJ_CLEANUP                  0x12
#define IRP_MJ_DEVICE_CONTROL           0x0e
#define IRP_MJ_INTERNAL_DEVICE_CONTROL  0x0f

#define TDI_ASSOCIATE_ADDRESS    (0x01)
#define TDI_DISASSOCIATE_ADDRESS (0x02)
#define TDI_CONNECT              (0x03)
#define TDI_LISTEN               (0x04)
#define TDI_ACCEPT               (0x05)
#define TDI_DISCONNECT           (0x06)
#define TDI_SEND                 (0x07)
#define TDI_RECEIVE              (0x08)
#define TDI_SEND_DATAGRAM        (0x09)
#define TDI_RECEIVE_DATAGRAM     (0x0A)
#define TDI_SET_EVENT_HANDLER    (0x0B)
#define TDI_QUERY_INFORMATION    (0x0C)
#define TDI_SET_INFORMATION      (0x0D)
#define TDI_ACTION               (0x0E)

#define TDI_EVENT_CONNECT              ((USHORT)0) // TDI_IND_CONNECT event handler.
#define TDI_EVENT_DISCONNECT           ((USHORT)1) // TDI_IND_DISCONNECT event handler.
#define TDI_EVENT_ERROR                ((USHORT)2) // TDI_IND_ERROR event handler.
#define TDI_EVENT_RECEIVE              ((USHORT)3) // TDI_IND_RECEIVE event handler.
#define TDI_EVENT_RECEIVE_DATAGRAM     ((USHORT)4) // TDI_IND_RECEIVE_DATAGRAM event handler.
#define TDI_EVENT_RECEIVE_EXPEDITED    ((USHORT)5) // TDI_IND_RECEIVE_EXPEDITED event handler.
#define TDI_EVENT_SEND_POSSIBLE        ((USHORT)6) // TDI_IND_SEND_POSSIBLE event handler

这些事件大致可以分为三类:

  1. 基本的IRP操作,最主要的是用来打开IP,UDP等设备使用,以及传送TDI请求。
  2. TDI请求,这个主要是用户层SOCKET发起请求调用。
  3. TDI事件,这个一般是注册接收网络数据引发的回调函数。

对于TCP服务端,这些事件的交互过程可以描述为如下:
在这里插入图片描述

对于TCP客户端时主动发起Connect的一端,这些事件的交互过程可以描述为如下:
在这里插入图片描述

对于TDI的这些所有事件都是通过TidBuildXxx函数来进行创建IRP的,如下:

RequestBuild FunctionAFD/TCPIP Usage
TDI_ASSOCIATE_ADDRESSTdiBuildAssociateAddress()Associate an address and connection object for a TCP socket.
TDI_DISASSOCIATE_ADDRESSTdiBuildDisassociateAddress()Break the association between an address and a connection object that were previously associated with each other.
TDI_CONNECTTdiBuildConnect()Initiate a TCP 3-way setup handshake (SYN, SYN+ACK, ACK) using a particular connection object.
TDI_LISTENTdiBuildListen()Solicit notifications for inbound SYNs on a particular address object.
TDI_ACCEPTTdiBuildAccept()Request the TCP stack to respond to an inbound SYN with a SYN+ACK using a particular connection object.
TDI_DISCONNECTTdiBuildDisconnect()Initialize a TCP 3-way teardown (FIN, FIN+ACK, ACK) handshake with a remote system.
TDI_SENDTdiBuildSend()Transmit stream data using the specified connection object.
TDI_RECEIVETdiBuildReceive()Request TCP to return data received on a specific connection object.
TDI_SEND_DATAGRAMTdiBuildSendDatagram()Transmit datagram packet(s) using the specified address object.
TDI_RECEIVE_DATAGRAMTdiBuildReceiveDatagram()Request UDP to return data received on a specific address object.
TDI_SET_EVENT_HANDLERTdiBuildSetEventHandler()Register/Unregister event callbacks for a given address object with TCPIP.sys.
TDI_QUERY_INFORMATIONTdiBuildQueryInformation()Read Management Information Base (MIB) information from the TCP/UDP/IP stack.
TDI_SET_INFORMATIONTdiBuildSetInformation()Not Supported by TCPIP.sys.
TDI_ACTIONTdiBuildAction()Not Supported by TCPIP.sys.

4. IRP_MJ_CREATE

IRP_MJ_CREATE这个是TDI比较重要的一个点,TDI有两个对象需要创建:

  1. 地址对象:所谓地址对象就是通常我们调用bind函数绑定的本地地址和端口的对象。
  2. 连接对象:所谓连接对象就是我们listen或者connect创建的,用来和远端连接通信的一个对象。

对于IRP_MJ_CREATE,地址或者连接信息在CreateFileEaBuffer参数中指定;对于bindlisten或者connect使用两种不同的方式:

//bind
EaInfo->EaNameLength = TDI_TRANSPORT_ADDRESS_LENGTH;
RtlCopyMemory(EaInfo->EaName,
	TdiTransportAddress,
	TDI_TRANSPORT_ADDRESS_LENGTH);
EaInfo->EaValueLength = sizeof(TA_IP_ADDRESS);
Address =
(PTRANSPORT_ADDRESS)(EaInfo->EaName + TDI_TRANSPORT_ADDRESS_LENGTH + 1); 
TaCopyTransportAddressInPlace(Address, Name);

//connect和listen
EaInfo->EaNameLength = TDI_CONNECTION_CONTEXT_LENGTH;
RtlCopyMemory(EaInfo->EaName,
	TdiConnectionContext,
	TDI_CONNECTION_CONTEXT_LENGTH);
EaInfo->EaValueLength = sizeof(PVOID);
ContextArea = (PVOID*)(EaInfo->EaName + TDI_CONNECTION_CONTEXT_LENGTH + 1); 
*ContextArea = NULL;

因此我们通过通过如下两个字符串来判断当前创建的是地址对象还是连接对象:

#define TdiTransportAddress "TransportAddress"
#define TdiConnectionContext "ConnectionContext"
#define TDI_TRANSPORT_ADDRESS_LENGTH (sizeof (TdiTransportAddress) - 1)
#define TDI_CONNECTION_CONTEXT_LENGTH (sizeof (TdiConnectionContext) - 1)

一般来说,对于我们TDI来说一个非常重要的操作就是获取地址;因为bind的时候我们有时候需要让系统选择一个可以使用的地址和端口,因此我们在IRP_MJ_CREATE过滤的时候并不能获取到绑定的地址,需要使用如下方法:

  1. 设置IRP的完成例程。
  2. 在完成例程中使用TdiBuildInternalDeviceControlIrp创建一个TDI_QUERY_INFORMATION的IRP。
  3. 使用TdiBuildQueryInformation设置查询类型为TDI_QUERY_ADDRESS_INFO,并设置完成例程。
  4. 调用IoCallDriver发起IRP查询请求。
  5. 这样我们在TDI_QUERY_ADDRESS_INFO的完成例程中就可以获取到绑定的地址了。

TdiTransportAddress类型中通过TdiBuildInternalDeviceControlIrpTdiBuildQueryInformation函数获取到绑定的地址信息的声明如下:

PIRP TdiBuildInternalDeviceControlIrp(
  [in] CCHAR            IrpSubFunction, // TDI_QUERY_INFORMATION
  [in] PDEVICE_OBJECT   DeviceObject,
  [in] PFILE_OBJECT     FileObject,
  [in] PKEVENT          Event,
  [in] PIO_STATUS_BLOCK IoStatusBlock
);

VOID TdiBuildQueryInformation(
  [in] PIRP           Irp,
  [in] PDEVICE_OBJECT DevObj,
  [in] PFILE_OBJECT   FileObj,
  [in] PVOID          CompRoutine,
  [in] PVOID          Contxt,
  [in] UINT           QType,  //TDI_QUERY_ADDRESS_INFO
  [in] PMDL           MdlAddr
);

获取到的IP地址信息如下:

typedef struct _TDI_ADDRESS_INFO {
  ULONG             ActivityCount;
  TRANSPORT_ADDRESS Address;
} TDI_ADDRESS_INFO, *PTDI_ADDRESS_INFO;

typedef struct _TRANSPORT_ADDRESS {
  LONG       TAAddressCount;
  TA_ADDRESS Address[1];
} TRANSPORT_ADDRESS, *PTRANSPORT_ADDRESS;

typedef struct _TA_ADDRESS {
  USHORT AddressLength;
  USHORT AddressType;
  UCHAR  Address[1];  //Contains the TDI_ADDRESS_IP structure
} TA_ADDRESS, *PTA_ADDRESS;

typedef struct _TDI_ADDRESS_IP { 
    USHORT  sin_port; 
    ULONG  in_addr; 
    UCHAR  sin_zero[8]; 
} TDI_ADDRESS_IP, *PTDI_ADDRESS_IP; 

5. TDI_ASSOCIATE_ADDRESS

当一个连接对象被创建直接,就需要和本地的地址对象进行绑定,这个绑定的消息就是TDI_ASSOCIATE_ADDRESS

因此一般当我们接收到IRP_MJ_CREATE创建连接对象的消息之后,接下来就会接收到TDI_ASSOCIATE_ADDRESS地址绑定的消息,一般我们IrpSp->Parameters中就是我们需要绑定的地址对象,如下:

//IrpSp->Parameters结构
struct _TDI_REQUEST_KERNEL_ASSOCIATE {
    HANDLE  AddressHandle;
} TDI_REQUEST_KERNEL_ASSOCIATE, *PTDI_REQUEST_KERNEL_ASSOCIATE;

6. TDI_SET_EVENT_HANDLER

TDI_SET_EVENT_HANDLER这个是设置事件处理函数的请求,当下层到来网络操作时,会直接调用TDI_SET_EVENT_HANDLER设置的函数,通过函数参数来传递信息,就不需要创建irp了,那么自然避免了irp的效率损失。

TDI_SET_EVENT_HANDLER可以设置如下事件类型的请求:

#define TDI_EVENT_CONNECT                   0
#define TDI_EVENT_DISCONNECT                1
#define TDI_EVENT_ERROR                     2
#define TDI_EVENT_RECEIVE                   3
#define TDI_EVENT_RECEIVE_DATAGRAM          4
#define TDI_EVENT_RECEIVE_EXPEDITED         5
#define TDI_EVENT_SEND_POSSIBLE             6
#define TDI_EVENT_CHAINED_RECEIVE           7
#define TDI_EVENT_CHAINED_RECEIVE_DATAGRAM  8
#define TDI_EVENT_CHAINED_RECEIVE_EXPEDITED 9
#define TDI_EVENT_ERROR_EX                  10

回调事件是为了优化内存使用,传输时候用底层网络驱动直接传输数据缓冲,整理如下:

EventAFD/TCP Usage
TDI_EVENT_CONNECTAFD.sys指示到来的TCP SYN报文段
TDI_EVENT_DISCONNECTAFD.sys指示到来的TCP FIN报文段
TDI_EVENT_ERROR没被TCPIP.sys使用
TDI_EVENT_RECEIVEAFD.sys指示到来的TCP 数据报文段
TDI_EVENT_RECEIVE_DATAGRAMAFD.sys指示到来的UDP数据报文段
TDI_EVENT_RECEIVE_EXPEDITEDAFD.sys指示带有TCP URGENT标记的数据报文段
TDI_EVENT_SEND_POSSIBLE没被TCPIP.sys使用
TDI_EVENT_CHAINED_RECEIVEAFD.sys用NDIS_BUFFER描述的数据,不使用TCPIP的缓冲区且对该数据进行复制
TDI_EVENT_CHAINED_RECEIVE_DATAGRAM没被TCPIP.sys使用
TDI_EVENT_CHAINED_RECEIVE_EXPEDITED没被TCPIP.sys使用
TDI_EVENT_ERROR_EXTCPIP.sys向AFD.sys通知,远端不可达错误

一般来说我们可以使用TdiBuildSetEventHandler创建TDI_SET_EVENT_HANDLER的IRP,并设置回调函数,然后通过IoCallDriver进行发送。

VOID TdiBuildSetEventHandler(
  [in] PIRP           Irp,
  [in] PDEVICE_OBJECT DevObj,
  [in] PFILE_OBJECT   FileObj,
  [in] PVOID          CompRoutine,
  [in] PVOID          Contxt,
  [in] LONG           InEventType,
  [in] PVOID          InEventHandler,
  [in] PVOID          InEventContext
);

例如我们对于TDI_EVENT_CONNECT事件的处理函数如下:

NTSTATUS ClientEventConnect(
  _In_  PVOID              TdiEventContext,
  _In_  LONG               RemoteAddressLength,
  _In_  PVOID              RemoteAddress,
  _In_  LONG               UserDataLength,
  _In_  PVOID              UserData,
  _In_  LONG               OptionsLength,
  _In_  PVOID              Options,
  _Out_ CONNECTION_CONTEXT *ConnectionContext,
  _Out_ PIRP               *AcceptIrp
);

//RemoteAddress结构 
typedef struct _TRANSPORT_ADDRESS {
  LONG       TAAddressCount;
  TA_ADDRESS Address[1];
} TRANSPORT_ADDRESS, *PTRANSPORT_ADDRESS;

当网络协议栈从地城接收到了Connect事件(例如SYN消息)的时候,就会导致TDI_EVENT_CONNECT事件的处理函数被调用。

TDI各种不同的事件对应不同的函数,如果在项目中需要处理不同事件对应的数据包,应该仔细参考MSDN来实现不同的回调函数。

对于事件的过滤,我们只需要替换回调函数即可,实现如下:

pAddr->ev_connect = pEvent->EventHandler;
pAddr->ev_connect_context = pEvent->EventContext;

pEvent->EventHandler = (PVOID) tcp_TdiConnectEventHandler;
pEvent->EventContext = (PVOID) pAddr;	

7. 其他事件和回调函数

对于其他TDI的消息和事件,都有对应的socket函数与之对应,下面简单看几个典型的消息和事件。

7.1 TDI_CONNECT

TDI_CONNECT是用户层发起connect,对于这个请求,参数信息如下:

//IrpSp->Parameters
typedef struct _TDI_REQUEST_KERNEL {
  ULONG_PTR                   RequestFlags;
  PTDI_CONNECTION_INFORMATION RequestConnectionInformation;
  PTDI_CONNECTION_INFORMATION ReturnConnectionInformation;
  PVOID                       RequestSpecific;
} TDI_REQUEST_KERNEL, *PTDI_REQUEST_KERNEL;

typedef struct _TDI_CONNECTION_INFORMATION {
  LONG  UserDataLength;
  PVOID UserData;
  LONG  OptionsLength;
  PVOID Options;
  LONG  RemoteAddressLength;
  PVOID RemoteAddress;
} TDI_CONNECTION_INFORMATION, *PTDI_CONNECTION_INFORMATION;

从参数RequestConnectionInformation我们可以获取到连接的远程地址。

7.2 TDI_SEND

当用户层调用send发送数据包的时候,TDI驱动就会接收到TDI_SEND类型的事件,对于这个请求有两个参数:

  1. IrpSp->Parameters表示发送数据的信息,为TDI_REQUEST_KERNEL_SEND结构。
  2. Irp->MdlAddress表示发送数据的具体内容。

TDI_REQUEST_KERNEL_SEND定义如下:

struct _TDI_REQUEST_KERNEL_SEND {
    ULONG SendLength;
    ULONG SendFlags;
} TDI_REQUEST_KERNEL_SEND, *PTDI_REQUEST_KERNEL_SEND;

对于SendFlags可以取如下值:

  • TDI_SEND_EXPEDITED:紧急包,必需放到传输层的头部。
  • TDI_SEND_PARTIAL:只是一部分数据,接下来还会发送数据。

7.3 TDI_EVENT_RECEIVE

对于TDI_EVENT_RECEIVE事件,表示接收到数据(recv的被动),响应函数如下:

NTSTATUS ClientEventReceive(
  _In_  PVOID              TdiEventContext,
  _In_  CONNECTION_CONTEXT ConnectionContext,
  _In_  ULONG              ReceiveFlags,
  _In_  ULONG              BytesIndicated,
  _In_  ULONG              BytesAvailable,
  _Out_ ULONG              *BytesTaken,
  _In_  PVOID              Tsdu,
  _Out_ PIRP               *IoRequestPacket
);

在这个函数中:

  1. Tsdu表示接收到的数据。
  2. 返回STATUS_SUCCESS表示Tsdu中的数据全部拷贝完毕(通过BytesAvailableBytesTaken来指示)。
  3. 如果返回STATUS_MORE_PROCESSING_REQUIRED表示通过IoRequestPacket来重新读取剩余的Tsdu中的数据(一般是先读取BytesIndicated长度数据到内部缓存)。
  4. 返回STATUS_DATA_NOT_ACCEPTED表示拒绝接收数据。

对于TDI_EVENT_RECEIVE是在底层接收完成之后,分发数据包的时候调用的回调函数.

8. TDI实现流程

通过上面,我们基本可以大致了解了TDI的基本技术点,现在我们看一下TDI驱动的基本实现流程:

  1. 首先对TCP,UDP,RawIP等设备进行挂载和过滤。
  2. 然后设置各种回调函数,主要是IRP_MJ_CREATEIRP_MJ_CLEANUP/IRP_MJ_CLOSEIRP_MJ_INTERNAL_DEVICE_CONTROL
  3. IRP_MJ_CREATE记录创建的地址对象和连接对象,并将其记录在哈希表中。
  4. IRP_MJ_INTERNAL_DEVICE_CONTROL中的各种TDI_XXX进行处理。
  5. TDI_SET_EVENT_HANDLER各种回调函数进行处理。

例如我们可以对TDI_CONNECTTDI_EVENT_CONNECT创建TCP的主动连接和被动连接的防火墙功能,也可以对TDI_SENDTDI_EVENT_RECEIVE等消息实现网络数据嗅探功能。

9. 引用和参考

关于TDI我们可以参考如下技术链接:

  1. https://codemachine.com/articles/tdi_overview.html
  2. https://sourceforge.net/projects/tdifw/
  3. https://netfiltersdk.com/

对于上述参考链接中NetFilter SDK 2是最全面的,但遗憾的是它并不是开源的。这个SDK提供了网络TDI的基本全部功能,如果有机会这个是我们学习比较全面的代码和资料。后面本人也将会陆续分享基于TDI的各种技术的应用开发示例。

  • 6
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
TDI和WFP是Windows操作系统中的两种不同的驱动技术。TDI是Transport Driver Interface的缩写,它在Windows 2000到Windows Vista期间被支持。TDI是一套接口的集合,用于连接用户态的socket和NDIS协议驱动,实现socket的创建、发送和接收数据。\[1\] WFP是Windows Filtering Platform的缩写,它是取代TDI的新技术。WFP是一种网络过滤平台,用于在网络数据包传输过程中进行过滤和处理。WFP提供了一种灵活的方式来管理和控制网络流量,可以实现防火墙、入侵检测和网络安全等功能。与TDI相比,WFP提供了更高级的网络过滤和处理功能,并且支持更多的Windows操作系统版本。\[1\] 总结来说,TDI是一种用于连接用户态的socket和NDIS协议驱动的接口集合,而WFP是一种网络过滤平台,用于在网络数据包传输过程中进行过滤和处理。WFP相比于TDI提供了更高级的网络过滤和处理功能,并且支持更多的Windows操作系统版本。 #### 引用[.reference_title] - *1* *2* *3* [Windows网络驱动、NDIS驱动(微端口驱动、中间层驱动、协议驱动)、TDI驱动(网络传输层过滤)、WFP(Windows ...](https://blog.csdn.net/zhangge3663/article/details/100918732)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值