admin 管理员组

文章数量: 887021


2024年1月23日发(作者:asp命令是什么意思)

防火墙的数据包拦截方式小结

网络防火墙都是基于数据包的拦截技术之上的。在 Windows 下,数据包的拦截方式有很多种,

其原理和实现方式也千差万别。总的来说,可分为“用户级”和“内核级”数据包拦截两大类。

用户级下的数据包拦截方式有:

* Winsock Layered Service Provider (LSP)。

* Win2K 包过滤接口 (Win2K Packet Filtering Interface)。

* 替换 Winsock 动态链接库 (Winsock Replacement DLL)。

内核级下的数据包拦截方式有:

* TDI过滤驱动程序 (TDI-Filter Driver)。

* NDIS中间层驱动程序 (NDIS Intermediate Driver)。

* Win2K Filter-Hook Driver。

* Win2K Firewall-Hook Driver。

* NDIS-Hook Driver。

在这么多种方式面前,我们该如何决定采用哪一种作为自己项目的实现技术?这需要对每一种

方式都有一个大致的了解,并清楚它们各自的优缺点。技术方案的盲目选用往往会带来一些技术

风险。以自己为例,我需要在截包的同时得到当前进程文件名,也就是说,需向用户报告当前是

哪个应用程序要访问网络。在选用 Win2K Filter-Hook Driver 这一方案之后(很多小型开源项

目都采用这一方案),便开始编码。但之后发现 Win2K Filter-Hook Driver 的截包上下文处于内

核进程中,即 IRQL >= DISPATCH_LEVEL,根本无法知道当前应用程序的名字。相比之下,

TDI-Filter Driver 和 NDIS-Hook Driver 则可以得知这些信息。其中 TDI-Filter Driver

比 NDIS-Hook Driver 更能准确地获知当前应用程序文件名,后者的接收数据包和少数发送数据

包的场景仍然处于内核进程中。

下面列出了各种截包方式的特点:

* Winsock Layered Service Provider (LSP)

该方式也称为 SPI (Service Provider Interface) 截包技术。SPI是由 Winsock2 提供的一个

接口,它需要用户机上安装有 Winsock 2.0。Winsock2 SPI 工作在 API 之下的 Driver

之上,

可以截获所有基于 Socket 的网络数据包。

优点:

* 以DLL形式存在,编程方便,调试简单。

* 数据封包比较完整,未做切片,便于做内容过滤。

缺点:

* 拦截不够严密,对于不用 Socket 的网络通讯则无法拦截 (如 ICMP),木马病毒很容易绕过。

* Win2K Packet Filtering Interface

这是 Win2K 中一组 API 提供的功能 (PfCreateInterface,

PfAddFiltersToInterface, ...)。

优点:

* 接口简单,实现起来没什么难度。

缺点:

* 功能过于简单,只能提供IP和端口的过滤,可能无法满足防火墙的复杂需求。

* 处于 API 层,木马病毒容易绕过。

* 只能在 Win2K 以上(含)系统中使用。

* Winsock Replacement DLL

这种方法通过替换系统 Winsock 库的部分导出函数,实现数据报的监听和拦截。

缺点:

* 由于工作在 Winsock 层,所以木马病毒容易绕过。

* TDI-Filter Driver

TDI 的全称是 Transport Driver Interface。传输层过滤驱动程序通过创建一个或多个设备对象

直接挂接到一个现有的驱动程序之上。当有应用程序或其它驱动程序调用这个设备对象时,会首

先映射到过滤驱动程序上,然后由过滤驱动程序再传递给原来的设备对象。

优点:

* 能获取到当前进程的详细信息,这对开发防火墙尤其有用。

缺点:

* 该驱动位于 之上,所以没有机会得到那些由 直接处理的包,比如ICMP。

* TDI驱动需要重启系统方能生效。

* NDIS Intermediate Driver

也称之为 IM Driver。它位于协议层驱动和小端口驱动之间,它主要是在网络层和链路层之间对

所有的数据包进行检查,因而具有强大的过滤功能。它能截获所有的数据包。

可参考DDK中附带的例子 Passthru。

优点:

* 功能非常强大,应用面广泛,不仅仅是防火墙,还可以用来实现VPN,NAT 和 VLan 等。

缺点:

* 编程复杂,难度较大。

* 中间层驱动的概念是在 WinNT SP4 之后才有的,因此 Win9X 无法使用。

* 不容易安装,自动化安装太困难。

* Win2K Filter-Hook Driver

这是从 Win2K 开始提供的一种机制,该机制主要利用 所提供的功能来拦截

网络

数据包。Filter-Hook Driver 的结构非常简单,易于实现。但是正因为其结构过于简单,并且

依赖于 ,微软并不推荐使用。

可参考 CodeProject 上的例子:/KB/IP/

优点:

* 结构简单,易于实现。

* 能截获所有的IP包(包括ICMP包)。

缺点:

* 工作于内核进程中,无法取得当前应用程序进程的信息。

* 虽能截获所有IP包,但无法取得数据包的以太帧(Ethernet Frame)。

* 只能在 Win2K 以上(含)系统中使用。

* Win2K Firewall-Hook Driver

这是一种和 Win2k Filter-Hook Driver 差不多的机制,所不同的是,Firewall-Hook Driver

能在 IP Driver 上挂接多个回调函数,所以和前者相比,它引起冲突的可能性更小一些。

可参考 CodeProject 上的例子:/KB/IP/

这种方式的优缺点和 Win2K Filter-Hook Driver 基本相同。

* NDIS-Hook Driver

这是一种要重点讲述的截包方式。它是目前大多数网络防火墙所使用的方法。这种方式的做法

是安装钩子到 中,替换其中的某些关键函数,从而达到截包的目的。在下一节中我

们将详细地介绍它的实现方法。

优点:

* 安装简单,可即时安装和卸载驱动,无需重启系统。

* 能截获所有的IP包,同时能取得数据包的以太帧(Ethernet Frame)。

* 安全性高,木马病毒不容易穿透。

* 在大多数情况下,能获取到当前应用程序的进程信息。

* 能在 Win98 以上(含)系统中使用。

缺点:

* 接收数据包、或偶尔发送数据包时,驱动工作在内核进程中,无法获得应用程序进程信息。

◎ NDIS-Hook 技术

微软和 3COM 公司在1989年制定了一套开发 Windows 下网络驱动程序的标准,称为

NDIS。

NDIS 的全称是 Network Driver Interface Specification。NDIS为网络驱动的开发提供了一套

标准的接口,使得网络驱动程序的跨平台性更好。

NDIS提供以下几个层次的接口:

1. NDIS 小端口驱动 (NDIS Miniport Driver)。

这也就是我们常说的网卡驱动。

2. NDIS 协议驱动 (NDIS Protocol Driver)。

用来实现某个具体的协议栈,如 TCP/IP 协议栈,并向上层导出 TDI 接口。

3. NDIS 中间层驱动 (NDIS Intermediate Driver)。

这是位于小端口驱动和协议驱动之间的驱动。

NDIS为了给出上述三种接口,提供了一个系统的、完整的 Wrapper。这个 Wrapper 即

上面提到的 Miniport Driver、Protocol Driver、Intermediate Driver 均属于插入到这个

Wrapper 中的“模块”,它们调用 Wrapper 提供的函数,同时也向 Wrapper 注册回调函数。

在简单了解了NDIS的机制之后,不难得知,网络防火墙只需要将自己的函数挂钩(Hook)到

中即可截获网络数据包。NDIS-Hook 技术有两种实现方案:

1. 修改 的 Export Table。

在 Win32 下,可执行文件(EXE/DLL/SYS)都遵从PE格式。所有提供接口的驱动都有

Export Table,

因此只要修改 的 Export Table,就可实现对关键函数的挂接。在实现步骤中,首先

需要得到 的内存基址,再根据PE格式得到DOS头部结构(IMAGE_DOS_HEADER),进一步得

到NT头部结构(IMAGE_NT_HEADER),最后从头部结构中查得 Export Table 的地址。

由于协议驱动程序(NDIS Protocol Driver)在系统启动时会调用 NdisRegisterProtocol()

来向

系统注册协议,因此这种方法关键在于修改 所提供的 NdisRegisterProtocol、

NdisDeRegisterProtocol、NdisOpenAdapter、NdisCloseAdapter、NdisSend 这几个函数的地址。

对于处于系统核心的 而言,要修改它的内存区域,只有驱动程序才能做到,所以我们

必须编写驱动程序来达到这个目的。

该方案的缺点是加载或卸载驱动后无法立即生效,必须重启系统。且挂钩方法较为复杂。早期凡

使用 NDIS-Hook 的防火墙都采用这一方法,包括著名的费尔防火墙的早期版本(v2.1)。

直到 2004 年, 上一名黑客公布了一种全新的 NDIS-Hook 技术(即下文即将提

到的第2种方法),诸多防火墙产品才都悄悄对自己的核心技术进行了升级。由于新的挂钩技术更

好,故本文不打算详述修改 Export Table 这一方法的具体细节。

2. 向系统注册假协议(Bogus Protocol)。

NDIS提供了一个API: NdisRegisterProtocol(),这个API的职责是向系统注册一个协议(如TCPIP),

并将该协议作为一个链表节点插入到“协议链表”的头部,最后返回该链表头节点(即新节点)的

地址。正常情况下,只有NDIS协议驱动程序(NDIS Protocol Driver)才会调用这个API。

既然如此,如果我们也调用 NdisRegisterProtocol() 向系统注册一个新的协议,我们也就能轻

易地得到“协议链表”的首地址,通过走访这个链表,就能修改其中的某些关键信息,比如关键

函数的地址。修改完毕后,再调用 NdisDeRegisterProtocol() 注销掉新协议。这看似一切都没

发生,但事实上目的已经达到了。这个新协议我们称之为假协议(Bogus Protocol)。

通过这种方法,我们可以不用重启系统就能轻松挂接截包函数。当今大多数网络防火墙都采用了

这一方法。近来网上又有人提出了获取协议链表首地址的新的怪异途径,比如获取

中全局变量 _ARPHandle 值的方法。不管怎样,相比之下,注册假协议仍不失为一种经典且简单

的方法。

本文将详细叙述第2种方案的内部原理和实现细节,即通过注册假协议获取协议链表首地址,遍历

链表并修改其中的函数地址,挂钩自己的函数,从而实现网络截包。在这么做之前,需要先对NDIS

内部维护的几个结构有清楚的认识。另外,由于历史原因,NDIS存在诸多并不完全向下兼容的版本,

不同的版本中关键数据域的偏移地址也不尽相同。微软并没有以文档形式提供这些变化的列表。本

文稍后给出这些变化。

* NDIS_PROTOCOL_BLOCK 和 NDIS_OPEN_BLOCK

在NDIS中,所有已注册的协议是通过一个单向的协议链表来维护的。这个单向链表保存了所有已注册

的协议,每个协议对应一个节点。链表节点由 NDIS_PROTOCOL_BLOCK 结构来描述,在这个结构中保存

了注册协议驱动时所指定的各种信息,如支持协议即插即用的回调函数地址等。同时,每个协议驱动

还对应一个 NDIS_OPEN_BLOCK 节点结构的单向链表来维护其所绑定的网卡信息,协议驱动发送和接收

数据包的回调函数地址就保存在这个结构中,是我们要重点修改的对象。

协议与网卡绑定的示意图如下:

┌───┐

│ Head │

└─┬─┘

┌──────────────┐ ┌───────────┐

│ TCPIP Protocol Block ├──→│ RTL8168 Open Block │

└──────┬───────┘ └─────┬─────┘

↓ ↓

┌──────────────┐ ┌───────────┐

│ TCPIP_WANARP Protocol Block│ │ Wireless Open Block │

└──────┬───────┘ └─────┬─────┘

↓ ↓

┌───┐ ┌───┐

│ NULL │ │ NULL │

└───┘ └───┘

* 得到 NDIS_PROTOCOL_BLOCK 链表的首地址

上文已提到,通过向系统注册假协议,我们即可得到协议链表的首地址。

从DDK中可查到 NdisRegisterProtocol() 的原型:

EXPORT

VOID

NdisRegisterProtocol(

OUT PNDIS_STATUS Status,

OUT PNDIS_HANDLE NdisProtocolHandle,

IN PNDIS_PROTOCOL_CHARACTERISTICS ProtocolCharacteristics,

IN UINT CharacteristicsLength

);

可以看出,我们在调用它时需要传入一个结构 NDIS_PROTOCOL_CHARACTERISTICS,这个结构是我们

在注册协议时必须填写的一张表格,这个表格描述了协议的相关信息。不过既然我们注册的是一个

假协议,所以可以尽量简单地填写它。

NDIS_STATUS

DummyNdisProtocolReceive(

IN NDIS_HANDLE ProtocolBindingContext,

IN NDIS_HANDLE MacReceiveContext,

IN PVOID HeaderBuffer,

IN UINT HeaderBufferSize,

IN PVOID LookAheadBuffer,

IN UINT LookAheadBufferSize,

IN UINT PacketSize

)

{

return NDIS_STATUS_NOT_ACCEPTED;

}

NDIS_HANDLE

RegisterBogusNdisProtocol(void)

{

NTSTATUS Status = STATUS_SUCCESS;

NDIS_HANDLE hBogusProtocol = NULL;

NDIS_PROTOCOL_CHARACTERISTICS BogusProtocol;

NDIS_STRING ProtocolName;

NdisZeroMemory(&BogusProtocol,

sizeof(NDIS_PROTOCOL_CHARACTERISTICS));

disVersion = 0x04;

disVersion = 0x0;

NdisInitUnicodeString(&ProtocolName, L"BogusProtocol");

= ProtocolName;

eHandler = DummyNdisProtocolReceive;

NdisRegisterProtocol(&Status, &hBogusProtocol, &BogusProtocol,

sizeof(NDIS_PROTOCOL_CHARACTERISTICS));

if (Status == STATUS_SUCCESS) return hBogusProtocol;

else return NULL;

}

函数 RegisterBogusNDISProtocol() 的返回值即是我们想要的协议链表首地址。不过须注意的是,

在函数挂钩完成后,应调用 NdisDeregisterProtocol() 将假协议注销。另外,在遍历协议链表

进行函数挂钩时,应从首节点的下一个节点开始,因为首节点是我们的假协议节点。

* 修改原有函数地址值实现函数挂钩

上文已提到了和NDIS相关的三个结构:

NDIS_PROTOCOL_BLOCK,

NDIS_OPEN_BLOCK,

NDIS_PROTOCOL_CHARACTERISTICS。

那么我们要替换的函数在哪儿呢?答案是在 NDIS_OPEN_BLOCK 和

NDIS_PROTOCOL_CHARACTERISTICS

这两个结构中,而且重点是前者,因为前者是协议驱动和网卡绑定的纽带。现在的主流网卡都只

调用 NDIS_OPEN_BLOCK 中的收发函数进行发送和接收数据包。但据试验,虚拟机

VMware 有时会

调用 NDIS_PROTOCOL_CHARACTERISTICS 中的函数进行数据包收发。所以为了严谨,我们应该对两

个结构中的函数进行替换。关于这两个结构的定义,读者可以自行查阅DDK文档和头文件。

下面给出示意性代码。简单起见,下列代码均假设当前NDIS的版本为5.0。

BOOLEAN

InstallHook(void)

{

NDIS_STATUS nStatus;

NDIS_HANDLE hBogusProtocol = NULL;

BYTE *pProtocolChain;

// Get the address of the first NDIS_PROTOCOL_BLOCK node.

hBogusProtocol = RegisterBogusNDISProtocol();

if (hBogusProtocol == NULL) return FALSE;

pProtocolChain = (BYTE*)hBogusProtocol;

while (TRUE)

{

// Get the address of the next node.

DWORD dwOffset = 0x10; // for NDIS 5.0

pProtocolChain = ((BYTE **)(pProtocolChain + dwOffset))[0];

if (!pProtocolChain) break;

HookNdisProtocolBlock(pProtocolChain);

}

NdisDeregisterProtocol(&nStatus, hBogusProtocol);

return TRUE;

}

void

HookNdisProtocolBlock(

IN BYTE *pProtocolBlock

)

{

PNDIS_PROTOCOL_CHARACTERISTICS pProtoChar;

PNDIS_OPEN_BLOCK pOpenBlock;

pProtoChar = (PNDIS_PROTOCOL_CHARACTERISTICS)(pProtocolBlock +

0x14);

HookNdisProc(MyReceive, (PVOID *)&pProtoChar->ReceiveHandler);

HookNdisProc(MyReceivePacket, (PVOID

*)&pProtoChar->ReceivePacketHandler);

HookNdisProc(MyBindAdapter, (PVOID

*)&pProtoChar->BindAdapterHandler);

pOpenBlock = ((PNDIS_OPEN_BLOCK *)pProtocolBlock)[0];

while (pOpenBlock)

{

HookNdisProc(MySend, (PVOID *)&pOpenBlock->SendHandler);

HookNdisProc(MyReceive, (PVOID

*)&pOpenBlock->ReceiveHandler);

HookNdisProc(MyReceivePacket, (PVOID

*)&pOpenBlock->ReceivePacketHandler);

HookNdisProc(MySendPackets, (PVOID

*)&pOpenBlock->SendPacketsHandler);

pOpenBlock = pOpenBlock->ProtocolNextOpen;

}

}

void

HookNdisProc(

IN PVOID pMyProc,

IN PVOID *ppOrgProc

)

{

// TODO: Save the address of the original proc.

*ppOrgProc = pMyProc;

}

InstallHook() 首先得到协议链表的首地址,接着遍历链表,针对系统中的每个(第一个除外)

NDIS_PROTOCOL_BLOCK 调用 HookNdisProtocolBlock() 函数。

HookNdisProtocolBlock() 对 NDIS_PROTOCOL_BLOCK 中

NDIS_PROTOCOL_CHARACTERISTICS 和

NDIS_OPEN_BLOCK 链表的每个节点进行函数挂接。

HookNdisProc() 用于替换函数地址。给出的代码中它只是简单地替换函数地址,在实际应用中,

它还应当保存原始函数的地址值,以供新的函数调用。

* 关键数据域在不同NDIS版本中的差异

由于 NDIS-Hook 并非受微软官方支持的技术,所以相关文档非常缺乏。不仅如此,操作系统的

每次升级,都会同时升级NDIS,而NDIS中的某些数据结构并没有保持向下兼容。最需要注意的

是 NDIS_PROTOCOL_BLOCK。

在 Win9x/Me/NT 的DDK中,NDIS_PROTOCOL_BLOCK 都有明确的定义,但在

Win2K/XP 的DDK中,

并没有该结构的详细定义,也就是说该结构在 Win2K 以后(含)的系统中是非公开的。因此开发

人员只能利用各种调试工具来发掘该结构的详细定义。也正是因为如此,NDIS-Hook 方法对平台

的依赖性比较大,需要在程序中判断不同的操作系统版本而使用不同的结构定义。

NDIS_PROTOCOL_BLOCK 的定义可大致认为是这个样子:

typedef struct _NDIS_PROTOCOL_BLOCK

{

PNDIS_OPEN_BLOCK OpenQueue;

REFERENCE Ref;

UINT Length;

NDIS_PROTOCOL_CHARACTERISTICS ProtocolChars;

struct _NDIS_PROTOCOL_BLOCK* NextProtocol;

ULONG MaxPatternSize;

// ...

} NDIS_PROTOCOL_BLOCK, *PNDIS_PROTOCOL_BLOCK;

其中 OpenQueue 为 PNDIS_OPEN_BLOCK 链表的首节点地址,NextProtocol 指向下一个

NDIS_PROTOCOL_BLOCK 节点。

在不同的NDIS版本中,该结构中的某些域的偏移地址是不同的,现列于下:

┌───────┬───────────┬───────────┐

│ NDIS Version │ ProtocolChars offset │ NextProtocol offset │

├───────┼───────────┼───────────┤

│ │ 0x14 │ 0x04 │

│ │ 0x14 │ 0x60 │

│ 4.01 │ 0x14 │ 0x8C │

│ │ 0x14 │ 0x10 │

└───────┴───────────┴───────────┘

* 如何在驱动中得到当前NDIS版本?

有两种方法可得到当前NDIS版本。一种是先取得当前操作系统的版本信息,在根据操作系统

的版本得到NDIS的版本。操作系统版本和NDIS版本有一个映射关系,读者可在DDK帮助中查到。

┌───────┬───────┐

│ OS Version │ NDIS Version │

├───────┼───────┤

│ Win95 │ 3.1 │

│ Win95 OSR2 │ 4.0 │

│ Win98 │ 4.1 │

│ Win98 SE │ 5.0 │

│ WinMe │ 5.0 │

│ WinNT 3.5 │ 3.0 │

│ WinNT 4.0 │ 4.0 │

│ WinNT 4.0 SP3│ 4.1 │

│ Win2K │ 5.0 │

│ WinXP │ 5.1 │

│ WinVista │ 6.0 │

└───────┴───────┘

还有一种方法,通过调用 NdisReadConfiguration() 直接获取NDIS版本。代码如下:

BOOLEAN

GetNdisVersion(

OUT DWORD *pMajorVersion,

OUT DWORD *pMinorVersion

)

{

NDIS_STATUS nStatus;

NDIS_STRING VersionStr = NDIS_STRING_CONST("NdisVersion");

PNDIS_CONFIGURATION_PARAMETER ReturnedValue;

BOOLEAN bResult;

NdisReadConfiguration(

&nStatus,

&ReturnedValue,

NULL,

&VersionStr,

NdisParameterInteger);

bResult = ((nStatus == NDIS_STATUS_SUCCESS)? TRUE : FALSE);

if (bResult)

{

//

// The returned value has the NDIS version of the form

// 0xMMMMmmmm, where MMMM is major version and mmmm is minor

// version so 0x00050000 is 5.0

//

DWORD dwVersion = ReturnedValue->rData;

if (pMajorVersion)

*pMajorVersion = dwVersion >> 16;

if (pMinorVersion)

*pMinorVersion = dwVersion & 0xFFFF;

}

return bResult;

}

须注意的是,GetNdisVersion() 必须在 PASSIVE_LEVEL 下运行。所以此函数适合于在

驱动的 DriverEntry() 中调用,因为 DriverEntry() 一定是处于 PASSIVE_LEVEL 的。


本文标签: 协议 函数 驱动 地址 链表