TDI网络过滤驱动之tdifw实现原理分析

TDI网络过滤驱动之tdifw实现原理分析

在前面文章我们分析过TDI网络过滤驱动的基本开发框架和其相关技术(参见TDI网络过滤驱动开发指南),本文我们来分析一下,基于TDI的网络防火墙(https://sourceforge.net/projects/tdifw/)的基本实现原理。

tdifw是一款基于TDI网络过滤框架的防火墙程序,他实现了一个简要的防火墙功能,主要包括三个代码:

  1. install.exe:实现了TDI驱动的安装。
  2. tdifw.exe:实现了防火墙的服务程序,主要涉及到防火墙规则的下发。
  3. tdifw_drv.sys:防火墙的核心驱动程序。

本文的重点也是来分析一下tdifw_drv.sys的实现原理,来掌握TDI网络过滤驱动的开发。

1. 功能概述

tdifw主要实现了三大块的功能:

  1. 网络数据包的过滤(包括connect,sendto,recvfrom等)。
  2. 防火墙规则策略的管理。
  3. 本地网络连接状态的查询。

其中防火墙规则策略的管理应用以及网络状态信息的查询实现的比较简单,主要是响应用户层的各种DeviceIoControl消息即可,包括如下消息:

//防火墙规则策略
#define IOCTL_CMD_GETREQUEST	CTL_CODE(FILE_DEVICE_TDI_FW, 0x801, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_CMD_CLEARCHAIN	CTL_CODE(FILE_DEVICE_TDI_FW, 0x802, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_CMD_APPENDRULE	CTL_CODE(FILE_DEVICE_TDI_FW, 0x803, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_CMD_SETCHAINPNAME	CTL_CODE(FILE_DEVICE_TDI_FW, 0x804, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_CMD_SETPNAME		CTL_CODE(FILE_DEVICE_TDI_FW, 0x805, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_CMD_ACTIVATECHAIN	CTL_CODE(FILE_DEVICE_TDI_FW, 0x806, METHOD_BUFFERED, FILE_ANY_ACCESS)

//网络状态查询
#define IOCTL_CMD_ENUM_LISTEN	CTL_CODE(FILE_DEVICE_TDI_FW_NFO, 0x901, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_CMD_ENUM_TCP_CONN	CTL_CODE(FILE_DEVICE_TDI_FW_NFO, 0x902, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_CMD_GET_COUNTERS	CTL_CODE(FILE_DEVICE_TDI_FW_NFO, 0x903, METHOD_BUFFERED, FILE_ANY_ACCESS)

网络数据包的过滤,以及防火墙的实现是本程序的核心,因此下面主要分析一下TDI网络过滤驱动的实现。

2. 数据结构

首先我们先来看一下相关数据结构和管理的变量,在函数入口,主要通过如下函数初始化:

  1. ot_init:用来初始化对象存储的哈希表。
  2. filter_init:用来初始化过滤规则,以及传递给用户层的消息队列。
  3. conn_state_init:用来初始化监听状态的对象表和连接状态的对象表。

g_ot_hash是对象的哈希表,包括地址对象和连接对象,被定义如下:

#define HASH_SIZE	0x1000
#define CALC_HASH(fileobj)  (((ULONG)(fileobj) >> 5) % HASH_SIZE)

static struct ot_entry **g_ot_hash;

g_cte_hash这个是上下文的哈希表,主要用来关联地址对象和连接对象,该结构声明如下:

struct ctx_entry {
	struct ctx_entry *next;
	PFILE_OBJECT addrobj;
	CONNECTION_CONTEXT conn_ctx;
	PFILE_OBJECT connobj;
};

static struct ctx_entry **g_cte_hash;

g_rules这个是防火墙策略的管理结构,如下:

struct flt_rule {
	union {
		struct	flt_rule *next;		// for internal use
		int		chain;				// useful for IOCTL_CMD_APPENDRULE
	};
	int		result;
	int		proto;
	int		direction;
	ULONG	addr_from;
	ULONG	mask_from;
	USHORT	port_from;
	USHORT	port2_from;		/* if nonzero use port range from port_from */
	ULONG	addr_to;
	ULONG	mask_to;
	USHORT	port_to;
	USHORT	port2_to;		/* if nonzero use port range from port_to */
	int		log;			/* see RULE_LOG_xxx */

	UCHAR	sid_mask[MAX_SIDS_COUNT / 8];	/* SIDs bitmask */

	char	rule_id[RULE_ID_SIZE];
};

static struct {
	struct {
		struct		flt_rule *head;
		struct		flt_rule *tail;
		char		*pname;				// name of process
		BOOLEAN		active;				// filter chain is active
	} chain[MAX_CHAINS_COUNT];
	KSPIN_LOCK	guard;
} g_rules;

flt_rule是一条单独的策略,包括协议,地址,方向,进程等。

g_queue是驱动向用户层投递消息的日志消息队列,该队列声明如下:

static struct {
	struct		flt_request *data;
	KSPIN_LOCK	guard;
	ULONG		head;	/* write to head */
	ULONG		tail;	/* read from tail */
	HANDLE		event_handle;
	PKEVENT		event;
} g_queue;

g_listen是监听状态的网络套接字信息,该信息存在的主要目的是让用户层可以查询当前有那些套接字处于监听状态,该结构定义如下:

struct listen_entry {
	struct			listen_entry *next;
	struct			listen_entry *prev;		/* using double-linked list */
	int				ipproto;
	ULONG			addr;		// IPv4 only (yet)
	USHORT			port;
	PFILE_OBJECT	addrobj;
};

static struct listen_entry **g_listen = NULL;

g_conn是连接对象的状态信息,该结构存在的主要目的是为了让用户层可以查询连接对象的各种状态(可以参考函数enum_tcp_conn的实现),该结构被声明为:

struct conn_entry {
	struct			conn_entry *next;
	struct			conn_entry *prev;		/* using double-linked list */
	int				state;
	ULONG			laddr;		// IPv4 only (yet)
	USHORT			lport;
	ULONG			raddr;
	USHORT			rport;
	PFILE_OBJECT	connobj;

	struct			conn_entry *next_to_del;
	LARGE_INTEGER	ticks;
};

static struct conn_entry **g_conn = NULL;

3. 设备的挂载(HOOK)

接下来我们看一下tdifw对于网络数据接口的过滤,这里有两种方法:

  1. 设备对象的堆叠挂载。
  2. 驱动分发函数HOOK。

对于设备对象的堆叠挂载,实现如下:

status = c_n_a_device(theDriverObject, &g_tcpfltobj, &g_tcpoldobj, L"\\Device\\Tcp");
if (status != STATUS_SUCCESS) {
	goto done;
}

status = c_n_a_device(theDriverObject, &g_udpfltobj, &g_udpoldobj, L"\\Device\\Udp");
if (status != STATUS_SUCCESS) {
	goto done;
}

status = c_n_a_device(theDriverObject, &g_ipfltobj, &g_ipoldobj, L"\\Device\\RawIp");
if (status != STATUS_SUCCESS) {
	goto done;
}

对于TCPIP驱动对象回调函数的HOOK,实现如下:

NTSTATUS
hook_tcpip(DRIVER_OBJECT *old_DriverObject, BOOLEAN b_hook)
{
	UNICODE_STRING drv_name;
	NTSTATUS status;
	PDRIVER_OBJECT new_DriverObject;
	int i;

	RtlInitUnicodeString(&drv_name, L"\\Driver\\Tcpip");

	status = ObReferenceObjectByName(&drv_name, OBJ_CASE_INSENSITIVE, NULL, 0,
		IoDriverObjectType, KernelMode, NULL, &new_DriverObject);
	if (status != STATUS_SUCCESS) {
		return status;
	}

	for (i = 0; i < IRP_MJ_MAXIMUM_FUNCTION; i++) {
		if (b_hook) {
			old_DriverObject->MajorFunction[i] = new_DriverObject->MajorFunction[i];
			new_DriverObject->MajorFunction[i] = DeviceDispatch;
		} else
			new_DriverObject->MajorFunction[i] = old_DriverObject->MajorFunction[i];
	}
	
	return STATUS_SUCCESS;	
}

通过上述方法的其中之一,我们就可以对网络接口进行过滤了,下面我们来分析一下每个过滤函数的实现。

4. 回调函数实现

下面我们依次按照TCP服务端,客户端,和UDP的基本流程来分析一下各个过滤函数的具体实现,关于基本调用流程可以参考TDI网络过滤驱动开发指南中的细节。

4.1 tdi_create

tdi_create主要在两种情况下被调用:

  1. 地址对象创建,主要使用TdiTransportAddress来标记。
  2. 连接对象创建,主要使用TdiConnectionContext来标记。

对于地址对象创建,我们主要是用来绑定套接字到本地地址,为了获取绑定的本地地址,因此需要等待tdi_create调用完成之后,再来查询地址信息,查询地址信息使用的是TDI_QUERY_INFORMATION,使用如下函数创建IRP来查询:


query_irp = TdiBuildInternalDeviceControlIrp(TDI_QUERY_INFORMATION,
	devobj, irps->FileObject, NULL, NULL);
if (query_irp == NULL) {
	KdPrint(("[tdi_fw] tdi_create: TdiBuildInternalDeviceControlIrp\n"));
	return FILTER_DENY;
}

//...
TdiBuildQueryInformation(query_irp, devobj, irps->FileObject,
	tdi_create_addrobj_complete2, ctx,
	TDI_QUERY_ADDRESS_INFO, mdl);

status = IoCallDriver(devobj, query_irp);

当创建地址对象的时候,将地址对象加入到哈希表g_ot_hash中:

status = ot_add_fileobj(irps->DeviceObject, irps->FileObject, FILEOBJ_ADDROBJ, ipproto, NULL);

如果创建的是连接对象的时候,将连接对象对象加入到哈希表g_ot_hash中:

status = ot_add_fileobj(irps->DeviceObject, irps->FileObject,
        FILEOBJ_CONNOBJ, ipproto, conn_ctx);

当我们地址对象创建完成之后,如果不是TCP协议,那么就直接将该对象当作监听对象(TCP有专门的监听状态),如下:

if (ote_addr->ipproto != IPPROTO_TCP) {
	// set "LISTEN" state for this addrobj
	status = add_listen(ote_addr);
	if (status != STATUS_SUCCESS) {
		goto done;
	}
}

4.2 tdi_associate_address

当连接对象创建之后,就需要和地址对象进行绑定,绑定的回调进入tdi_associate_address,在该函数中主要的操作流程如下:

ote_conn = ot_find_fileobj(irps->FileObject, &irql);
//...
status = ot_add_conn_ctx(addrobj, ote_conn->conn_ctx, irps->FileObject);

ot_add_conn_ctx将地址对象和连接对象绑定成上下文存放到哈希表g_cte_hash中。

4.3 tdi_set_event_handler

当我们listen调用创建完成连接对象之后,就会设置一个重要的回调函数TDI_EVENT_CONNECT,该事件的回调函数在tdi_set_event_handler回调中被设置。

tdi_set_event_handlerTDI_EVENT_CONNECT事件中,是非常特殊的,需要做如下处理:

  1. status = add_listen(ote_addr);将当前地址对象设置为连接状态(加入到哈希表g_listen中)。
  2. log_request(&request);发送一条日志消息,该消息类型为TYPE_LISTEN

对于通用的处理tdi_set_event_handler函数,主要是替换回调函数:

ctx->old_handler = r->EventHandler;
ctx->old_context = r->EventContext;

if (g_tdi_event_handlers[i].handler != NULL) {
	r->EventHandler = g_tdi_event_handlers[i].handler;
	r->EventContext = ctx;
}
else {
	r->EventHandler = NULL;
	r->EventContext = NULL;
}

4.4 tdi_event_connect

当TCP服务端在accept的时候,就有可能接收客户端的连接信息例如SYN连接请求,此时就会回调tdi_event_connect,该回调是防火墙的一个重要例程,例如我们要阻止外部连接的到来,那就就应该在这个函数里面阻断客户端的connect连接。

这个函数的基本流程如下:

  1. ote_addr = ot_find_fileobj(ctx->fileobj, &irql)从哈希表获取接收connect的地址对象。
  2. result = quick_filter(&request, &rule);过滤防火墙规则,如果防火墙规则是FILTER_DENY拒绝,那么返回STATUS_CONNECTION_REFUSED拒绝对端连接。
  3. 调用ctx->old_handler完成系统的TDI_EVENT_CONNECT事件回调函数。
  4. 设置AcceptIrp的完成例程为tdi_evconn_accept_complete
  5. 查找本次连接的连接对象ote_conn = ot_find_fileobj(irps->FileObject, &irql);
  6. 添加连接状态TCP_STATE_SYN_RCVDg_conn哈希表中(status = add_tcp_conn(ote_conn, TCP_STATE_SYN_RCVD);实现)。

tdi_evconn_accept_complete这个是表示AcceptIrp完成时候的完成例程,在该例程中需要更新连接对象的状态,使用set_tcp_conn_state(param->fileobj, TCP_STATE_ESTABLISHED_IN);设置连接对象状态为TCP_STATE_ESTABLISHED_IN

4.5 tdi_connect

作为TCP客户端,如果主动发起connect调用,那么将会调用tdi_connect函数。该函数也是防火墙需要实现的一个重要操作,可以阻止我们的程序连接外部的一个网络地址和端口。

在这个函数中,防火墙的过滤函数如下:

request.struct_size = sizeof(request);

request.type = TYPE_CONNECT;
request.direction = DIRECTION_OUT;
request.proto = ipproto;

request.pid = (ULONG)PsGetCurrentProcessId();
if (request.pid == 0) {
	request.pid = ote_addr->pid;
}

if ((request.sid_a = copy_sid_a(ote_addr->sid_a, ote_addr->sid_a_size)) != NULL)
request.sid_a_size = ote_addr->sid_a_size;

memcpy(&request.addr.from, &local_addr->AddressType, sizeof(struct sockaddr));
memcpy(&request.addr.to, &remote_addr->AddressType, sizeof(struct sockaddr));
request.addr.len = sizeof(struct sockaddr_in);

memset(&rule, 0, sizeof(rule));

result = quick_filter(&request, &rule);

除了防火墙过滤之外还有三个重要操作:

  1. 设置连接对象状态为TCP_STATE_SYN_SENT(调用函数为:add_tcp_conn(ote_conn, TCP_STATE_SYN_SENT);)。
  2. 设置完成例程tdi_connect_complete
  3. 发送日志log_request(&request);

tdi_connect_complete表示connect操作完成的例程,在该函数中更新TCP连接对象的状态为TCP_STATE_ESTABLISHED_OUT(调用函数为:set_tcp_conn_state(irps->FileObject, TCP_STATE_ESTABLISHED_OUT);)。

4.6 tdi_send

当连接建立之后,我们就可以开发发送数据了,其中tdi_send就是send的回调函数,在该函数中我们应该实现的是流量的处理,例如:

g_traffic[TRAFFIC_TOTAL_OUT] += bytes;

send函数类似,我们还存在recv函数,该函数通过tdi_event_receive来处理,如下:

NTSTATUS
tdi_event_receive(
    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)
{
    //...
    ote_conn->bytes_in += bytes;
    //...
    g_traffic[TRAFFIC_TOTAL_IN] += bytes;
    //...
}

tdi_event_receive主要更新两个数据:

  1. 总体流量信息。
  2. 连接对象接收的流量信息。

4.7 tdi_send_datagram

当使用UDP函数sendto发送数据报文的时候,就会调用tdi_send_datagram函数,该函数时防火墙的UDP协议的一个重要函数,我们需要在该函数中对防火墙策略协议进行判断来决策是否可以运行发送:

request.struct_size = sizeof(request);

request.type = TYPE_DATAGRAM;
request.direction = DIRECTION_OUT;
request.proto = ipproto;

request.pid = (ULONG)PsGetCurrentProcessId();
if (request.pid == 0) {
	request.pid = ote_addr->pid;
}

if ((request.sid_a = copy_sid_a(ote_addr->sid_a, ote_addr->sid_a_size)) != NULL)
request.sid_a_size = ote_addr->sid_a_size;

memcpy(&request.addr.from, &local_addr->AddressType, sizeof(struct sockaddr));
memcpy(&request.addr.to, &remote_addr->AddressType, sizeof(struct sockaddr));
request.addr.len = sizeof(struct sockaddr_in);

memset(&rule, 0, sizeof(rule));

result = quick_filter(&request, &rule);

通用对于数据报文的接收,回调函数时tdi_event_receive_datagram,该函数也有相类似的实现,该函数声明如下:

NTSTATUS tdi_event_receive_datagram(
	IN PVOID TdiEventContext,
	IN LONG SourceAddressLength,
	IN PVOID SourceAddress,
	IN LONG OptionsLength,
	IN PVOID Options,
	IN ULONG ReceiveDatagramFlags,
	IN ULONG BytesIndicated,
	IN ULONG BytesAvailable,
	OUT ULONG *BytesTaken,
	IN PVOID Tsdu,
	OUT PIRP *IoRequestPacket)
{
	//....
	request.struct_size = sizeof(request);

	request.type = TYPE_DATAGRAM;
	request.direction = DIRECTION_IN;
	request.proto = ipproto;
	request.pid = ote_addr->pid;

	if ((request.sid_a = copy_sid_a(ote_addr->sid_a, ote_addr->sid_a_size)) != NULL)
		request.sid_a_size = ote_addr->sid_a_size;

	memcpy(&request.addr.from, &remote_addr->AddressType, sizeof(struct sockaddr));
	memcpy(&request.addr.to, &local_addr->AddressType, sizeof(struct sockaddr));
	request.addr.len = sizeof(struct sockaddr_in);

	memset(&rule, 0, sizeof(rule));

	result = quick_filter(&request, &rule);
}

4.8 断开与关闭

断开与关闭跟前面建立连接的过程基本相反,这里不再详细分析,相关的接口有如下:

  1. tdi_disconnect:主动断开连接。
  2. tdi_event_disconnect:被动断开连接(接收到断开连接的信息)。
  3. tdi_disassociate_address:地址解除绑定。
  4. tdi_set_event_handler:部分回调函数接口重置。
  5. tdi_cleanup:地址对象或者连接对象的销毁。

5. 信息查询

通过上面分析,我们可以发现驱动中记录着许多的状态信息,例如:

  1. g_listen:表示当前处于监听状态的套接字。
  2. g_conn:所有连接对象的状态信息。
  3. g_traffic:当前终端网络流量信息。

tdifw提供了一个设备来查询相关的所有信息,实现如下:

NTSTATUS
process_nfo_request(ULONG code, char *buf, ULONG *buf_len, ULONG buf_size)
{
	switch (code) {
	
	case IOCTL_CMD_ENUM_LISTEN:
		status = enum_listen((struct listen_nfo *)buf, buf_len, buf_size);
		break;

	case IOCTL_CMD_ENUM_TCP_CONN:

		status = enum_tcp_conn((struct tcp_conn_nfo *)buf, buf_len, buf_size);
		break;

	case IOCTL_CMD_GET_COUNTERS: 
		get_traffic_counters((unsigned __int64 *)buf);
		status = STATUS_SUCCESS;
		break;

	default:
		status = STATUS_NOT_SUPPORTED;
	}

	return status;
}

同样防火墙策略规则的添加,以及请求日志的发送和交互也存在类型操作,这里不再重复分析。

6. 总结

tdifw从TDI网络过滤驱动框架出发,基本实现了一个防火墙的基础功能。我们从代码分析可以发现该项目实现时非常粗糙的,他作为一个防火墙的示例代码来说是比较全面的,但是如果将他作为一个真实的项目工程来说,还是不太适合。

作为一个真正的项目产品模块,tdifw还需要更进一步优化和改进;但是我们从这个开源工程的分析,基本就能掌握TDI的开发技术框架和其实现细节原理了。

  • 5
    点赞
  • 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、付费专栏及课程。

余额充值