http://feikoo.bokee.com/viewdiary.10774705.html
译者: feikoo 作者: NDIS.com 日期: 2006-4-3
这篇文章的目的是探讨一下在网络上截取的包(如 IP 包)与在 NDIS 驱动中代表相同内容的 NDIS_PACKET 之间的关系。
标准化组织:
我们经常在新闻组上看到如下的内容:
1. 微软的开发文档是如何描述 Window2000 网络包的?
2. 有谁知道在哪可以找到 Window2000 IP 包的详细描述?
3. 我想知道 NDIS_PACKET 的基本结构,例如:我想知道哪部分是源 IP 地址,哪部分是端口号与数据等
这些问题可以分以下两个独立问题:
1. 在哪儿可以找到网络协议的详细说明?如 IP 协议
2. 网络数据是怎样封装在 NDIS_PACKET 中的?
第一个问题的答案是:广泛使用的协议是标准化组织制定的,不是微软。以下列出一些标准化组织(这些组织读者自己去找中文翻译吧):
ETSI - European Telecommunications Standards Institute
IEEE - Institute of Electrical and Electronics Engineers
IETF - Internet Engineering Task Force
IPv6 - IPv6 Forum
ISO - International Standards Organization
OMG - Object Management Group Open Group
World Wide Web Consortium
IP 协议是世界上广泛使用的网络协议。 IP 的详细说明由 IETF 来维护。 IETF 是由网络设计者,使用者,厂商,研究人员组成的一个开放组织,他们共同关注网络的结构以及网络的顺畅运行,它对所有感兴趣的个人开放。
关于 IP 协议的相关信息可以在 IETF 的网站上找到: RFC791
你应该访问该网站来获取其它协议的相关信息。
当然, IP 不是网络的唯一协议, IETF 也不是唯一的标准化机构。那么,我们还能在哪儿找到相关信息呢?
就近的网站有 Protocols.com ,它们发布了一个协议目录,其中包括了各种协议及它们的标准化机构。
接下来我们来看看数据是怎样封装在 NDIS_PACKET 结构中的。
网络上的数据包:
接下来我们来真正理解一下在网络上观测到的数据包。下面是一个 ICMP 包的 HEX Dump 。
000000:
00 A0 CC 63 08 1B 00 40 : 95 49 03 5F 08 00
45 00
...c...@.I._..E.
000010:
00 3C 82 47 00 00 20 01 : 94 C9 C0 A8 01 20 C0 A8
.<.G.. ...... ..
000020:
01 40
08 00 48 5C 01 00 : 04 00 61 62 63 64 65 66
.@..H/....abcdef
000030:
67 68 69 6A 6B 6C 6D 6E : 6F 70 71 72 73 74 75 76
ghijklmnopqrstuv
000040:
77 61 62 63 64 65 66 67 : 68 69
wabcdefghi......
Ping 是由下面的命令发起的:
C:> ping 192.168.1.64
发送 ICMP 回显请求,带 32 字节的数据。 Ping 的总长度是 74 字节,上面的数据中没包含帧前导(用于同步的部分)以及 FCS (帧校验和)。
以上是用 PCAUSA Rawether for Windows HookPeek 程序捕获的, HOOKPEER 不是网络监控程序,但它有这方面的功能。
PING 包的解包在下面的链接中有详细说明,可以作为参考:
http://www.ndis.com/papers/ndispacket/ndispacket_decode.htm
数据包的 NDIS 表示:
当然,我们感兴趣的信息是包数据。即包含有 74 字节 VM (虚拟内存)的地方,它表示的是在网络上观察到的 74 字节的数据。首先, NDIS 用来管理包数据的机制看起来似乎有点复杂而且不必要。但是,这种基础的机制是经常深思熟虑的,它给编写协议的作者提供了许多灵活性。
NDIS_PACKET 的简单表示
通常,最好将 NDIS_PACKET 结构(与之关联的 NDIS_BUFFER 结构也一样)认为是“透明”的 - 除了一些在以后将要讨论的特殊保留域。这意味着在结构中定义的域不能直接访问,并且这些结构可能随着 NDIS 的版本不同而不同。
然而,对这些结构以及它们与包数据是如何关联的有一个大概的了解是很有用的。
如下的图表向我们展示了 NDIS_PACKET 封装包数据的最简单的方法:
图 1. 一个简单 NDIS_PACKET 展示
在这种简单情形下,所有的 74 字节的包数据均位于连续的 74 字节的数组中。
NDIS_PACKET 包描述:
NDIS_PACKET 包有一个链接的 NDIS_BUFFER 。 NDIS_BUFFER 描述了一个包含了所有包数据的 74 字节的虚拟内存范围。
NDIS_PACKET 更复杂的表示
如下的图表向展示了 NDIS_PACKET 封装包数据的另一种方法。
图 2 :多缓冲区的 NDIS_PACKET 展示
在这种情形下,包数据分布在两个分开的虚拟内存范围中。
以下是第二种 NDIS_PACKET 包的描述:
NDIS_PACKET 包拥有两个链接的 NDIS_BUFFER ,第一个 NDIS_BUFFER 描述了一个 14 字节的虚拟内存范围,它包含了 Ethernet 头。第二个 NDIS_BUFFER 描述了一个 60 字节范围的虚拟内存,它包含了 Ethernet 的负载(净荷域)。
很明显,没一种关于 NDIS_PACKET 包构成的安全假设。包结构首先是由构建包的软件来决定的。
理解 NDIS_PACKET 包
尽管以上的图表给我们提供了一个 NDIS_PACKET 包是如何使用的画面,但是你必须记住,这些结构是透明的(即你最好别改它)。你决不能直接访问这些结构的某些域。相反,你应该使用 NDIS 包提供的访问函数。
在接下来的话题中,我们讨论部分 NDIS 库函数。我们的目光主要集中在那一小部分函数,如果给你一个包让你检查,你就可以使用这些函数来检查和解释包数据。
你用来检查 NDIS_PACKET 和与之相链接的 NDIS_BUFFER 的函数仅有一小部分,它们是:
NdisQueryPacket- 返回给定的一个包描述符的信息。
BufferCount – 包描述符上的缓冲区描述符个数
FirstBuffer – 指向链接在包描述上的第一个缓冲区描述符。
TotalPacketLength – 所有链接的缓冲区描述符所映射的包数据总数。
NdisQueryBuffer 返回所给的缓冲区描述符的相关信息
VirtualAddress – 缓冲区描述符所描述的地址范围的基地址指针
Length – 缓冲区描述符所描述的地址范围中所包含的字节数。
NdisGetNextBuffer – 提供当前缓冲区描述符指针,返回链上的下一个缓冲区描述
NDIS 5.1 注: NDIS 5.1 ( Windows XP )引入几个 NDIS 函数的安全版本。这些函数的安全版本只能用在 NDIS 5.1 驱动中。
NdisQueryBufferSafe – NdisQueryBuffer 的安全版本
以下是如何用 NdisQueryPacket 函数来检查 pNdisPacket 指针所指向的 NDIS_PACKET
PNDIS_BUFFER
pCurrentBuffer;
UINT
nBufferCount, TotalPacketLength;
//
// Query Packet
//
NdisQueryPacket(
(PNDIS_PACKET )pNdisPacket,
(PUINT )NULL,
// Physical Buffer Count
(PUINT )&nBufferCount,
// Buffer Count
&pCurrentBuffer,
// First Buffer
&TotalPacketLength
// TotalPacketLength
);
这段代码先获取链在包上的 NDIS_BUFFER 的个数和通过缓冲区描述符映射的包数据长度。同时,它还取得了指向第一个 NDIS_BUFFER 的指针。
NdisQueryBuffer 函数可以用来从 NDIS_BUFFER 中提取信息,如下所示:
PUCHAR
VirtualAddress;
UINT
CurrentLength, CurrentOffset;
//
// Query The First Buffer
//
NdisQueryBuffer(
CurrentBuffer,
&VirtualAddress,
&CurrentLength
);
以上代码获取缓冲区描述符所描述的虚拟内存的长度和虚拟地址。
可以通过 VirtualAddress 指针,像操作普通无符号字符数组那样来修改内存的内容( VirtualAddress 所指)。
但是,我们必须明白,有可能第一个缓冲区指针并不包含所有的包数据。如果 NDIS_PACKET 是如上图一所示的简单情形,那么 CurrentLength 应为 74 ,所有的数据都在 VirtualAddress 所指的内存中。
如果 pNdisPacket 指向的是如图二所示的包,那情况将有所不同。 NdisQueryPacket 返回的 TotalPacketLength 就该为 74 ;但是,当用 NdisQueryPacket 查询第一个缓冲区时返回的应该是 14 。
这就意味着我们必须检查链接在包描述符上的其它缓冲区,以便查看余下的包数据。这时我们可以使用 NdisGetNextBuffer 来获取下一个缓冲区描述符。这种迭代方法(有时也叫做遍历缓冲区链)是检查包数据所必须的。
UTIL_ReadOnPacket 函数展示了通过任一包描述符指针来遍历缓冲区链表读数据的方法。 PCAUSA 用 这个函数来安全“ PEEK ”示知结构的包描述符。它可能不是所有情况下的最优的方法;例如,如果你试图拷贝一大片的网络数据(如:在 MTU 较大的网络上的 IP 包),那么需要一个更有效的函数来完成。
以下是 UTIL_ReadOnPacket 函数的代码:
/
UTILReadOnPacket
//
// Purpose
// Logical read on the packet data in a NDIS_PACKET.
//
// Parameters
//
// Return Value
//
// Remarks
// The purpose of this function is to provide a convenient mechanism to
// read packet data from an NDIS_PACKET that may have multiple chained
// NDIS_BUFFERs.
//
VOID
UTILReadOnPacket(
PNDIS_PACKET Packet,
PUCHAR lpBuffer,
ULONG nNumberOfBytesToRead,
ULONG nOffset,
// Byte Offset, Starting With MAC Header
PULONG lpNumberOfBytesRead
)
{
PNDIS_BUFFER
CurrentBuffer;
UINT
nBufferCount, TotalPacketLength;
PUCHAR
VirtualAddress;
UINT
CurrentLength, CurrentOffset;
UINT
AmountToMove;
*lpNumberOfBytesRead = 0;
if (!nNumberOfBytesToRead)
return;
//
// Query Packet
//
NdisQueryPacket(
(PNDIS_PACKET )Packet,
(PUINT )NULL,
// Physical Buffer Count
(PUINT )&nBufferCount,
// Buffer Count
&CurrentBuffer,
// First Buffer
&TotalPacketLength
// TotalPacketLength
);
//
// Query The First Buffer
//
NdisQueryBuffer(
CurrentBuffer,
&VirtualAddress,
&CurrentLength
);
CurrentOffset = 0;
while( nOffset || nNumberOfBytesToRead )
{
while( !CurrentLength )
{
NdisGetNextBuffer(
CurrentBuffer,
&CurrentBuffer
);
// If we've reached the end of the packet.
We return with what
// we've done so far (which must be shorter than requested).
if (!CurrentBuffer)
return;
NdisQueryBuffer(
CurrentBuffer,
&VirtualAddress,
&CurrentLength
);
CurrentOffset = 0;
}
if( nOffset )
{
// Compute how much data to move from this fragment
if( CurrentLength > nOffset )
CurrentOffset = nOffset;
else
CurrentOffset = CurrentLength;
nOffset -= CurrentOffset;
CurrentLength -= CurrentOffset;
}
if( nOffset )
{
CurrentLength = 0;
continue;
}
if( !CurrentLength )
{
continue;
}
// Compute how much data to move from this fragment
if (CurrentLength > nNumberOfBytesToRead)
AmountToMove = nNumberOfBytesToRead;
else
AmountToMove = CurrentLength;
// Copy the data.
NdisMoveMemory(
lpBuffer,
&VirtualAddress[ CurrentOffset ],
AmountToMove
);
// Update destination pointer
lpBuffer += AmountToMove;
// Update counters
*lpNumberOfBytesRead +=AmountToMove;
nNumberOfBytesToRead -=AmountToMove;
CurrentLength = 0;
}
}
PCASIM_SEND_FILTER_ACTION
IPBlock_FilterSendPacket(
IN PADAPTER
Adapter,
IN PNDIS_PACKET
pOriginalPacket
)
{
USHORT
nEtherType;
ULONG
nNumberOfBytesRead = 0;
<snip...>
//
// Ignore Non-IP Packets
// ---------------------
// Packets that are presented to IPBlock_FilterRcvIndication include
// non-IP packets. These should, of course, be ignored for TCP.
//
// The switch could be extended to to ARP, REVARP, etc.
//
UTILReadOnPacket(
pOriginalPacket,
(PUCHAR )&nEtherType,
sizeof( USHORT ),
MEtherType, // Offset From MAC Header To Length/Type. Value is 12.
&nNumberOfBytesRead
);
//
// Check For IEEE 802.2/802.3 (RFC 1042) Encapsulation
//
if( htons( nEtherType ) <= MAX_802_3_LENGTH )
{
return( SEND_FILTER_ACTION_PASS_PACKET );
// Allow The Packet To Pass Through
}
else
{
//
// Check For Ethernet Encapsulation (RFC 894)
//
switch( htons( nEtherType ) )
{
case ETHERTYPE_IP:
break;
case ETHERTYPE_ARP:
case ETHERTYPE_REVARP:
default:
ImDbgOut( DBG_TRACE, DBG_FILTERS, ( "IPBlock_FilterSendPacket: Ignoring 0x%4.4X EtherType/n",
htons( nEtherType ) ) );
return( SEND_FILTER_ACTION_PASS_PACKET );
// Allow The Packet To Pass Through
}
}
<Snip...>
return( SEND_FILTER_ACTION_PASS_PACKET );
}
其它考虑:
以下是其它需要考虑的地方:
为 NdisMIndicateReceive 构建一个 NDIS_PACKET
在 Windows2000 或更高版本中 TCP/IP 栈存在影响使用 NDIS_BUFFER (调用 NdisMIndicateReceive ) 的 BUG.
(原文未译)
The TCP/IP ReceivePacket handler does not support multiple NDIS_BUFFERs (MDLs) if the virtual memory for what would be the LookaheadBuffer spans multiple NDIS_BUFFERs.
You need to set the packet status to NDIS_STATUS_RESOURCES if you are indicating a packet up with more than one chained NDIS_BUFFER.
第一种情况是一个已经被提升为设计风格的 BUG 。你必须确保头( header ) +lookahead 字节的内容在第一个 NDIS 缓冲区描述中( Windows2000 或更高)。
第二种情况是一个 BUG ,可望在 Widnows Server 2003 中修复。
NDIS_BUFFER 是一个内存描述符列表( MDL )
至少,对于 NT,windows 2000 和 Windows XP 来说上面这句话是对的。
MDL 是一种相当复杂的结构,它根据物理页面来描述一个虚拟缓冲区的页面。读者可以通过阅读关于 NT 内存结构的文章来获得更多的关于缓冲区描述符的信息。
在 Windows 9X 中, NDIS_BUFFER 与 WINDOWS NT 的 MDL 类似。当你坚持使用 NDIS 函数来操作 NDIS_BUFFER 结构,那么跨平台的差异被有效地的隐藏了。
在所有的 Windows 平台上, NDIS_BUFFER 是管理与硬件设备 ( 如 NIC) 相关的内存的必须结构。
在这种 level 的设备驱动编程中使用这种结构来管理的方法并不是 Windows 的发明。在 Unix 系统中有 mbuf 结构,在 Macintosh 中有 BDS 结构。如果你工作在一台虚拟内存主机上,那么将有一类似 MDL 的结构( a.k.a NDIS_BUFFER ),它用于同样的目的。
在 NT 和 windows2000 上,最高级别的 NDIS 驱动可真正交互地使用 MDL 和 NDIS_BUFFER 。事实上,你在 DDK 示例中能看到,一个传递给 NDIS 协议驱动的 MDL 被简单地用 NdisChainBufferAtFront 链接在了 NDIS_PACKET 中。
仅最高层 NT 协议可以使用由同样高层的驱动建立的 MDL ,用它作为 NDIS_BUFFER 描述符的一个替换。
不要修改不属于你的对象
1. 不要修改不是你自己分配的包数据的虚拟内存。
2. 不要修改或调整不是你自己分配的包描述符(除了在特殊情况下修改其中的 ProtocoReserved of MiniportReserved 域)
如果你正在写一个过滤驱动程序,它对对数据进行加密或压缩,那么你必须建立一种不修改或改变原始数据的方法。通常,这意味着你必须考贝要加密或压缩的数据,要么你在发送的过程( on-the-fly )执行加密或压缩的操作。
NDIS 5.1 注: NDIS 5.1 ( Windows XP )引入了一种新的功能,叫作“包栈”。这机制充许 NDIS IM 驱动传递一个包到邻近的驱动而不用分配一个新的包描述符。