文章目录
TDI网络过滤驱动之tdifw实现原理分析
在前面文章我们分析过TDI网络过滤驱动的基本开发框架和其相关技术(参见TDI网络过滤驱动开发指南),本文我们来分析一下,基于TDI的网络防火墙(https://sourceforge.net/projects/tdifw/)的基本实现原理。
tdifw是一款基于TDI网络过滤框架的防火墙程序,他实现了一个简要的防火墙功能,主要包括三个代码:
- install.exe:实现了TDI驱动的安装。
- tdifw.exe:实现了防火墙的服务程序,主要涉及到防火墙规则的下发。
- tdifw_drv.sys:防火墙的核心驱动程序。
本文的重点也是来分析一下tdifw_drv.sys的实现原理,来掌握TDI网络过滤驱动的开发。
1. 功能概述
tdifw主要实现了三大块的功能:
- 网络数据包的过滤(包括connect,sendto,recvfrom等)。
- 防火墙规则策略的管理。
- 本地网络连接状态的查询。
其中防火墙规则策略的管理应用以及网络状态信息的查询实现的比较简单,主要是响应用户层的各种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. 数据结构
首先我们先来看一下相关数据结构和管理的变量,在函数入口,主要通过如下函数初始化:
ot_init
:用来初始化对象存储的哈希表。filter_init
:用来初始化过滤规则,以及传递给用户层的消息队列。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对于网络数据接口的过滤,这里有两种方法:
- 设备对象的堆叠挂载。
- 驱动分发函数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
主要在两种情况下被调用:
- 地址对象创建,主要使用
TdiTransportAddress
来标记。 - 连接对象创建,主要使用
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_handler
的TDI_EVENT_CONNECT
事件中,是非常特殊的,需要做如下处理:
status = add_listen(ote_addr);
将当前地址对象设置为连接状态(加入到哈希表g_listen
中)。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
连接。
这个函数的基本流程如下:
ote_addr = ot_find_fileobj(ctx->fileobj, &irql)
从哈希表获取接收connect的地址对象。result = quick_filter(&request, &rule);
过滤防火墙规则,如果防火墙规则是FILTER_DENY
拒绝,那么返回STATUS_CONNECTION_REFUSED
拒绝对端连接。- 调用
ctx->old_handler
完成系统的TDI_EVENT_CONNECT
事件回调函数。 - 设置
AcceptIrp
的完成例程为tdi_evconn_accept_complete
。 - 查找本次连接的连接对象
ote_conn = ot_find_fileobj(irps->FileObject, &irql);
。 - 添加连接状态
TCP_STATE_SYN_RCVD
到g_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);
除了防火墙过滤之外还有三个重要操作:
- 设置连接对象状态为
TCP_STATE_SYN_SENT
(调用函数为:add_tcp_conn(ote_conn, TCP_STATE_SYN_SENT);
)。 - 设置完成例程
tdi_connect_complete
。 - 发送日志
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
主要更新两个数据:
- 总体流量信息。
- 连接对象接收的流量信息。
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 断开与关闭
断开与关闭跟前面建立连接的过程基本相反,这里不再详细分析,相关的接口有如下:
tdi_disconnect
:主动断开连接。tdi_event_disconnect
:被动断开连接(接收到断开连接的信息)。tdi_disassociate_address
:地址解除绑定。tdi_set_event_handler
:部分回调函数接口重置。tdi_cleanup
:地址对象或者连接对象的销毁。
5. 信息查询
通过上面分析,我们可以发现驱动中记录着许多的状态信息,例如:
g_listen
:表示当前处于监听状态的套接字。g_conn
:所有连接对象的状态信息。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的开发技术框架和其实现细节原理了。