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 的。
版权声明:本文标题:防火墙的数据包拦截方式小结 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.freenas.com.cn/free/1705969562h496152.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论