ChefZ -- 磨劍錄 (A Coder's Log)

慣看秋月春風 一壺濁酒喜相逢 古今多少事 皆賦談笑中
posts - 42, comments - 3, trackbacks - 0, articles - 6

原文出处:http://www.driverdevelop.com/article/znsoft_ndis-sniffer.mht#xx907xx

发布者:soarlove

本文简单地介绍了NDIS (Network Driver Interface Specification 即网络驱动接口规范),以及应用程序如何与一个驱动程序交互,如何最好地利用驱动程序。作为例子,本文提供了一个应用程序使用Packet.sys的网络 协议层驱动程序的例子,读者在这个例子的基础上可以实现象Netxray等局域网数据包截获程序的功能。

  Packet.sys是DDK中的一个非常有用的驱动程序,通过它你能够接收以太网中所有经过你的电脑的数据包,并且可以脱离系统的TCP/IP协议栈独立发送数据包,即通过Packet.sys建立的与TCP/IP同层次的协议发送数据包。

基础知识介绍

1. 驱动程序 Driver
设备驱动程序是拥有与Windows内核相同的最高特权的程序,它是在操作系统与输入/输出设备之间的一层必不可少的"胶水"。它的作用相当于转换器,将从操作系统发来的原始的请求转换成某种外围设备能够理解的命令。
系 统程序员的主要工作就是编写驱动程序,与系统的底层打交道。许多在应用程序中称为"mission impossible"即不可能完成的任务在使用了驱动程序后就可以轻易解决。编写驱动程序最主要的目的当然是为了驱动真正的硬件,使系统能够顺利地控制 各种不同型号的外围设备或内部硬件,称为硬件驱动程序,象显卡驱动程序、网卡驱动程序、PCI总线驱动程序等等;还有的驱动程序是为了实现一些应用程序不 能够完成的功能的,有的虽然在逻辑上实现了一个硬件的功能,但是物理上并不存在这个硬件,象虚拟光驱,这一大类则称为软件驱动程序,象TCP/IP驱动程 序、防火墙的驱动程序、虚拟光驱的驱动程序等等。

在Windows 9x/Me中支持Vxd驱动程序和WDM(Windows Driver Model)驱动程序,Windows NT中支持Kernel Driver即内核式驱动程序,Windows 2000及以后版本的Windows使用WDM驱动程序。Windows NT的内核式驱动程序与WDM驱动程序很相似,只是少了部分功能,而Vxd式驱动程序行将淘汰,所以我们这里用的是WDM驱动程序。

2. 网络接口卡 Network Interface Card (NIC)

网 络接口卡俗称网卡,它是一种硬件设备,作用是在电脑的内部总线和网络的传输介质中充当大门的作用,通过它,我们可以向网络上发送和接收数据包。一般网卡的 名称随着它所在网络的类型不同而不同,象处于以太网中的网卡叫做以太网卡,处于令牌网中的网卡叫做令牌网卡。我们这篇文章中讲的是在以太网中的应用。

3. 网络驱动接口规范 Network Driver Interface Specification(NDIS)

随着计算机网络蓬勃发展,网络相关的驱动程序成为驱动中的热点。为了提高编写网络驱动程序的效率,也为了使各种协议驱动在各种网卡之间独立, Microsoft创建了一个网络驱动程序界面规范,即Network Device Interface Specification (NDIS),这个规范是为原本复杂的网络驱动程序编写框架提供一个并不严格的封装,在这个规范下,编写网络驱动程序中原来应该使用系统有关函数都变换为 通过NDIS.sys这个接口,而内部实现的细节由NDIS.sys实现,这样,不仅提高了编写效率,程序员不易出错,而且也增强了驱动程序的强壮性、可 维护性,设备独立性等性能。

  目前最新的NDIS是5.1版本,Windows 2K及以后版本的NDIS是5.0,我的例子中使用的是Windows 2K DDK。
NDIS 程序库(NDIS.sys)提供了一个面向NIC驱动程序的完全抽象的接口,如下图所示,网卡驱动程序与协议层驱动程序及操作系统通过这个接口进行通信。 我们可以把这个接口看做Microsoft为网络驱动设计者提供了一个设计网络驱动程序所必须的抽象的伪"类"。(我个人认为,Microsoft引入 NDIS是一个在C++的面向对象和C的高编译效率之间的一个折衷,就象MFC封装了WinAPI一样,以后Microsoft迟早会推出真正面向对象标 准的DDK,就像现在DriverStudio等某些驱动编写工具所做的那样)NDIS库输出了所有的能够在NIC驱动开发中使用的NT内核模式函数。 NDIS库还参与管理操作系统中的与网络有关的特定任务,管理所有底层的NIC驱动的绑定与状态信息。



NDIS驱动程序有三种类型,分别是网络接口卡驱动程序、中间层驱动程序、高层协议驱动程序。

A. 网络接口卡驱动程序 Miniport Network Interface Card drivers

网络接口卡驱动程序管理网络接口卡,NIC驱动程序在它的下端直接控制网络接口卡硬件,在它的上端提供一个较高层的驱动能够使用的接口,这个接口一般完成以下的一些任务:初始化网卡,停止网卡,发送和接收数据包,设置网卡的操作参数等等。
NIC驱动程序分为以下两种:

l 无连接的微端口驱动程序 (Connectionless Miniport Drivers)

无连接的微端口驱动程序是控制无连接的网络介质上的网卡的驱动程序,象以太网(Ethernet)、光纤分布式数据接口(FDDI)、令牌网(Token Ring)。

l 面向连接的微端口驱动程序 (Connection-oriented Miniport Drivers)

面向连接的微端口驱动程序是控制面向连接的网络介质上的网卡的驱动程序,象异步传输模式(ATM)。

B. 中间层驱动程序 Intermediate Protocol Driver

中 间层驱动程序在协议驱动程序和微端口驱动程序之间。在高层的传输层驱动程序看来,中间层驱动程序象一个微端口驱动程序,而在底层的微端口驱动程序看来,它 象一个协议驱动程序。使用中间层驱动程序的最主要的原因可能是在一个已经存在的传输层驱动程序和一个使用新的,传输层驱动程序并不认识的媒体格式的微端口 驱动程序中相互转换格式,即充当翻译的角色。

C. 高层的协议驱动程序 Upper Level Protocol Driver

象各种TCP/IP协议,一个协议驱动程序完成TDI接口或者其他的应用程序可以识别的接口来为它的用户提供服务。这些驱动程序分配数据包,将用户发来 的数据拷贝到数据包中,然后通过NDIS将数据包发送到低层的驱动程序,这个低层的驱动程序可能是中间层驱动程序,也可能是微端口驱动程序。当然,它在自 己的下端也提供一个协议层接口,用来与低层驱动程序交互,其中最主要的功能就是接收由低层传来的数据包,这些通讯基本上都是由NDIS完成的。

4. 驱动程序与应用程序的交互

在windows NT/2K下编写的驱动程序都必须要包括一个名叫DriverEntry入口函数,这个函数是作为系统载入驱动程序时的入口点,它主要进行一些初始化及告 诉系统各个回调函数的位置,系统只有通过DriverEntry函数才能够知道驱动程序中其他的函数。应用层的调用象CreateFile, ReadFile等等将导致NT输入/输出管理器生成一个与应用层的调用相对应的IRP(Input/Output Request Packet 输入/输出请求包)。在Windows NT下,几乎所有的输入/输出操作都是包驱动的,也就是每个I/O操作都是输出输入管理器向各个相关驱动程序发送IRP来实现的。IRP是一个数据结构, 里面包含了完成这个I/O操作需要的各个参数和最终的状态等返回值。

网络监视器例子原理

  好,基础知识都介绍完毕,下 面我将讲解一个非常有用的例子,要应用这个例子,必须要有微软的驱动程序开发包,即DDK,我们使用的是开发包里的一个例子:协议层驱动程序 Packet.sys,今天我们只解决如何在应用层使用这个驱动程序,以后再讲解Packet.sys的细节。Packet.sys是一个协议层驱动程 序,它工作在OSI中的传输层,见下图,也就是说,


  如果你将它加载到系统中的话,你就拥有一个与TCP/IP、IPX等等协 议层驱动程序同等级的协议层,这是一个令人兴奋的事情,至少我当初是这么想的:你可以脱离TCP/IP而自己发送接收数据包,你可以完成许多原来不能完成 的事情,象截获局域网(我讲的是以太网,由于以太网的广播性质,所以网上的所有机器都可以获得网上数据包的复本)上的经过你的机器这个网络结点的所有的数 据包,这个功能就是Netxray等网络监视软件的基本原理,如果你想做局域网访问限制器的话,只有根据接收的数据包稍加修改再发送出去就可以了。所以 说,这个驱动的应用范围是非常广的,当然也是非常有用的。

  下面讲讲网卡的工作过程,下面所说的都是在一个局域网里的情况:当一个机器向网上发送出一个数据包的时候,网上的所有机器上的网卡都将接收到这个数据包,它将判断这个数据包的目的地是不是它,如果是的话就接纳,如果不是就丢弃。
网卡有几个工作模式:广播模式、多播模式、直接模式和混杂模式。

  网卡在设置为广播模式时,它将会接收所有目的地址为广播地址的数据包,一般所有的网卡都会设置为这个模式。

  网卡在设置为多播模式时,当数据包的目的地址为多播地址,而且网卡地址是属于那个多播地址所代表的多播组时,网卡将接纳此数据包,即使一个网卡并不是一个多播组的成员,程序也可以将网卡设置为多播模式而接收那些多播的数据包。

  网卡在设置为直接模式时,只有当数据包的目的地址为网卡自己的地址时,网卡才接收它。

  网卡在设置为混杂模式时,它将接收所有经过的数据包,这个特性是我们要编写网络监视程序的关键。

  当然一般的应用程序是不能轻易设置网卡的工作模式的,不过我们借助Packet.sys驱动程序就可以将网卡设置为以上的任意模式。在我们的例子中,我们将网卡设置为混杂模式以接收所有的数据包。

Packet.sys是NT DDK中的一个例子。这个驱动程序能够把网卡设置为我们需要的任意模式,允许应用程序通过它向网上发送和接收数据包。这个例子中还包括了一个方便使用驱动 程序的DLL,Packete32.dll,它提供给应用程序一个方便的接口,而与驱动程序通讯相关的复杂的内部操作由DLL完成,面向应用层的程序员不 需要了解这些细节。下面的图形描述了我们的程序是如何同网卡通信的。应用程序调用了Packet32.dll中的函数,函数接着调


  用了Packet.sys中与请求相对应的入口点,驱动程序使用了Ndis.sys中输出的函数来与网卡通信。
在上面的过程中使用了以下的数据结构:

typedef struct _ADAPTER
{
HANDLE hFile; // 包含由CreateFile 函数返回的句柄
TCHAR SymbolicLink[MAX_LINK_NAME_LENGTH];
// 驱动程序的符号链接SymbolicLink
} ADAPTER, *LPADAPTER;

typedef struct _PACKET
{
HANDLE hEvent; // 一个用于Adapter对象的事件的句柄
OVERLAPPED OverLapped; // 用于与驱动程序异步输入输出的Overlapped结构
PVOID Buffer; // 发送或接收的数据包的缓冲区首指针
UINT Length; // Buffer的实际长度
} PACKET, *LPPACKET;

typedef struct _CONTROL_BLOCK
{
LPADAPTER hFile; // 网卡对象的指针
HANDLE hEvent; // event 的句柄
TCHAR AdapterName[64]; // 网卡的名称
// 接收的数据包的缓冲区
HANDLE hMem;
LPBYTE lpMem;
// 发送的数据包的缓冲区
HGLOBAL hMem2;
LPBYTE lpMem2;
ULONG PacketLength; // 数据包的长度
ULONG LastReadSize; // 最后一次读取的长度
UINT BufferSize; // 缓冲区的长度
} CONTROL_BLOCK, *PCONTROL_BLOCK;

下面是在应用程序中调用DLL的关键代码。

// 变量定义
CONTROL_BLOCK cbAdapter;
// 从注册表中得到网卡的名称
ULONG NameLength=64;
PacketGetAdapterNames(
CbAdapter.AdapterName,
&NameLength
);
CbAdapter.BufferSize=1514; // 以太网帧的最大长度

// 分配并锁定内存用来作为发送或接受缓冲区
CbAdapter.hMem=GlobalAlloc(GPTR, 1514);
CbAdapter.lpMem=(LPBYTE)GlobalLock(CbAdapter.hMem);
CbAdapter.hMem2=GlobalAlloc(GPTR,1514);
CbAdapter.lpMem2=(LPBYTE)GlobalLock(CbAdapter.hMem2);

// 打开网卡以接收发送数据包,这些代码调用DLL中的PacketOpenAdapter函数,这个
// 函数调用CreateFile函数,这样就打开了网卡与我们的协议驱动程序的绑定,使我们
// 可以在以后进行读写操作
CbAdapter.hFile=(ADAPTER*)PacketOpenAdapter(CbAdapter.AdapterName);

// OpenAdapter失败
if (CbAdapter.hFile = = NULL)
{
AfxMessageBox("Open Adapter failed");
}

// 一个接收PacketAllocatePacket函数返回值的void 指针
PVOID Packet;

// 将网卡的工作模式设置为混杂模式
Filter=NDIS_PACKET_TYPE_PROMISCUOUS;
PacketSetFilter(
CbAdapter.hFile,
Filter
);

// 分配缓冲区用来接收数据包
Packet=PacketAllocatePacket(CbAdapter.hFile);

// 初始化接收缓冲区
// 这些代码调用DLL中的PacketInitPacket来初始化Packet对象,这个Packet对象是用
// 来保存接收网上的数据包
if(Packet != NULL)
{
PacketInitPacket(
(PACKET *)Packet,
(char *)pdData[nCurrentWriteLocation].pData,
1514
);

// 从网卡驱动程序中读取数据包
PacketReceivePacket(
CbAdapter.hFile,
(PACKET *)Packet,
TRUE,
&pdData[nCurrentWriteLocation].nPacketLength
);
}

以上代码清楚地描述了应用程序如何使用Packet.sys驱动程序将网卡设置为混杂模式,这样我们就可以接收到经过我们电脑的所有数据包了。

下面是一些我们在应用程序中主要用到的Packet32.dll中的函数。

l PacketGetAdapterNames

PacketGetAdapterNames函数从注册表中获得每个网卡的名称。

ULONG
PacketGetAdapterNames(
PTSTR pStr,
PULONG BufferSize
)
{
HKEY SystemKey;
HKEY ControlSetKey;
HKEY ServicesKey;
HKEY NdisPerfKey;
HKEY LinkageKey;
LONG Status;
DWORD RegType;

// 依次打开键,并读取键值
Status=RegOpenKeyEx(
HKEY_LOCAL_MACHINE,
TEXT("SYSTEM"),
0,
KEY_READ,
&SystemKey
);

if (Status == ERROR_SUCCESS) {
Status=RegOpenKeyEx(
SystemKey,
TEXT("CurrentControlSet"),
0,
KEY_READ,
&ControlSetKey
);

if (Status == ERROR_SUCCESS) {
Status=RegOpenKeyEx(
ControlSetKey,
TEXT("Services"),
0,
KEY_READ,
&ServicesKey
);

if (Status == ERROR_SUCCESS) {
Status=RegOpenKeyEx(
ServicesKey,
TEXT("Packet"),
0,
KEY_READ,
&NdisPerfKey
);

if (Status == ERROR_SUCCESS) {
Status=RegOpenKeyEx(
NdisPerfKey,
TEXT("Linkage"),
0,
KEY_READ,
&LinkageKey
);

if (Status == ERROR_SUCCESS) {
Status=RegQueryValueEx(
LinkageKey,
TEXT("Export"),
NULL,
&RegType,
(LPBYTE)pStr,
BufferSize
);

// 关闭上面所有打开的键
RegCloseKey(LinkageKey);
}
RegCloseKey(NdisPerfKey);
}
RegCloseKey(ServicesKey);
}
RegCloseKey(ControlSetKey);
}
RegCloseKey(SystemKey);
}
return Status;
}

l PacketOpenAdapter

下面的PacketOpenAdapter函数的流程:

上面的流程是从应用程序的PacketOpenAdapter函数调用开始的,这也适用于所有其他的函数调用。函数PacketOpenAdapter 为设备(Device)定义了一个新的DOS设备名(DOS Device Name,通过这个DOS设备名,我们应用层的程序才可以向驱动程序提出请求),接着调用CreateFile函数来建立并打开一个联系设备的文件句柄。 这个函数必须在我们进行其他操作比如读写数据包之前完成。CreateFile函数将进入驱动程序的IRP_MJ_CREATE入口点,在这里,它调用了 NDIS库中输出的函数NdisOpenAdapter来完成操作。

PVOID
PacketOpenAdapter(
LPTSTR AdapterName
)
{
LPADAPTER lpAdapter;
BOOLEAN Result;

ODS("Packet32: PacketOpenAdapter\n");
// 为Adapter 对象分配全局内存
lpAdapter=(LPADAPTER)GlobalAllocPtr(
GMEM_MOVEABLE | GMEM_ZEROINIT,
sizeof(ADAPTER)
);

if (lpAdapter==NULL) {
ODS("Packet32: PacketOpenAdapter GlobalAlloc Failed\n");
return NULL;
}

// 将名称拷贝到Symbolic link中
wsprintf(
lpAdapter->SymbolicLink,
TEXT("\\\\.\\%s%s"),
DOSNAMEPREFIX,
&AdapterName[8]
);

// 为设备定义一个DOS设备名
Result=DefineDosDevice(
DDD_RAW_TARGET_PATH,
&lpAdapter->SymbolicLink[4],
AdapterName
);

if (Result)
{
// 创建一个设备的文件句柄(file handle)
lpAdapter->hFile=CreateFile(lpAdapter->SymbolicLink,
GENERIC_WRITE | GENERIC_READ,
0,
NULL,
CREATE_ALWAYS,
FILE_FLAG_OVERLAPPED,
0
);

if (lpAdapter->hFile != INVALID_HANDLE_VALUE) {
return lpAdapter;
}
}
ODS("Packet32: PacketOpenAdapter Could not open adapter \n");
GlobalFreePtr(
lpAdapter
);
return NULL;
}

l PacketAllocatePacket

下面的函数PacketAllocatePacket为packet对象分配内存。

PVOID
PacketAllocatePacket(
LPADAPTER AdapterObject
)
{
LPPACKET lpPacket;

// 为Packet对象分配内存
lpPacket=(LPPACKET)GlobalAllocPtr(
GMEM_MOVEABLE | GMEM_ZEROINIT,
sizeof(PACKET)
);

if (lpPacket==NULL) {
ODS("Packet32: PacketAllocateSendPacket: GlobalAlloc Failed\n");
return NULL;
}

// 建立一个事件,这个事件将在操作完成后激活
lpPacket->OverLapped.hEvent=CreateEvent(
NULL,
FALSE,
FALSE,
NULL
);

if (lpPacket->OverLapped.hEvent==NULL) {
ODS("Packet32: PacketAllocateSendPacket: CreateEvent Failed\n");
GlobalFreePtr(lpPacket);
return NULL;
}

return lpPacket;
}

l PacketInitPacket

函数PacketInitPacket初始化packet对象,即将packet对象中的buffer设置为传递的buffer指针。

VOID
PacketInitPacket(
LPPACKET lpPacket,
PVOID Buffer,
UINT Length
)
{
lpPacket->Buffer=Buffer;
lpPacket->Length=Length;
}

l PacketReceivePacket

函数PacketReceivePacket调用驱动程序的相应的入口点来从网络上读取一个数据包。这个操作是通过调用ReadFile函数来实现的。

BOOLEAN
PacketReceivePacket(
LPADAPTER AdapterObject,
LPPACKET lpPacket,
BOOLEAN Sync,
PULONG BytesReceived
)
{
BOOLEAN Result;
// 设置偏移量(Offset)为0
lpPacket->OverLapped.Offset=0;
lpPacket->OverLapped.OffsetHigh=0;
if (!ResetEvent(lpPacket->OverLapped.hEvent)) {
return FALSE;
}

// 调用ReadFile 来读取一个数据包
Result=ReadFile(
AdapterObject->hFile,
lpPacket->Buffer,
lpPacket->Length,
BytesReceived,
&lpPacket->OverLapped
);

if (Sync) {
// 调用者设定为未接收到数据包将等待,即同步调用
// 所以我们使用Overlapped中的同步对象来等待数据包
Result=GetOverlappedResult(
AdapterObject->hFile,
&lpPacket->OverLapped,
BytesReceived,
TRUE
);
}
else
{
// 如果调用者不想等待,则直接退出,他们会调用PacketWaitPacket来获得这次请
// 求的最终结果
Result = TRUE;
}
return Result;
}


具体应用
当我们实现了网络监视器后,成功地从网络上截获数据之后,怎么办呢?在下篇中,我将讲如何具体应用。


参考资料

1. Windows NTDDK Help
2. Windows NTDDK Packet.sys Sample

首 先,我声明一点,我们的网络监视功能是不能够阻止系统的一般协议栈对数据包的发送和接收的,它只是在比协议栈更低层次的地方(即网卡驱动程序的上端)获得 了进入机器的数据包的一个"复本",而"源本"则按照正常的流程向上传递给了相应的协议,"截获"这个词可能会让大家产生误解,我们的监视器只能实现"拷 贝"这个功能,但说"拷贝"确实不太舒服,所以以后我还是使用"截获"这个词。

  当我们完成了一个能够成功地截获网上数据包的监视器了,但是这 只是我们实现监视器的基础,还有许多工作要做,比如,当监视器截获数据包时,下一步应该干什么呢?什么事都不做当然不是目的。而且,我们不必要也不应该截 获所有的数据包,我们要有目的的过滤那些需要的数据包,否则我们将看着海量的数据包而无所适从。

下面我就从过滤数据包、增加功能和优化性能三个方面谈谈怎么在应用层实践网络监视。

1.过滤数据包

根据用户的需要过滤拷贝的数据包,提供给分析者分析,这可能是网络监视器的所能增加的最基本的功能了。要实现过滤,使用者必须要有网络的基本概念,特别 要了解以太网帧的结构和IP,TCP/UDP等等数据包是如何封装在一个帧中的,这一节讲的是就是如何根据自己的需要识别各种数据包的结构并过滤它。

为了在一个分层次的网络上传输数据,我们将数据从我们的应用程序传送到一个协议栈上,当数据在栈上一层一层地向下传送时,每一层的相应协议将把上一层传 送下来的数据封装为自己的格式,举一个最普通的数据传递过程,即应用->TCP->IP->以太网流程,如下图所示:

在图中我们可以清楚地看到TCP/IP协议栈及以太网中的数据传送的层次关系:当我们在应用程序(一般应用有HTTP、FTP等等协议)中将应用数据 (包括用户数据和应用首部)向网络传送,它首先到达TCP层,TCP协议根据应用层的要求在TCP首部填写好各个字段,比如端口号、序号、标志等等,重要 的一个步骤是填写数据校验和到校验和字段,然后将包括TCP首部的段(数据包在TCP协议层称为段segment)向协议栈的下一层即IP层传送;IP层 则与TCP层一样,填写IP首部的各个字段,比如地址、协议类型等等,然后将在头部包括IP首部和TCP首部的整个数据报(数据包在IP协议层称为数据报 datagram)向下传送;到了以太网驱动程序,他将继续进行封装工作,将以太网首部和以太网尾部添加到从IP层传下来的数据报上。

下面我们从外向内看各个封装的格式。

l 以太网帧首部

  以太网帧的首部的组成是:6字节的目的硬件地址、6字节的源硬件地址和2字节的类型字段。如下图所示。对于类型字段我们主要使用以下几种:

协议 类型字段
IP 0800h
ARP 0806h
RARP 0835h


l IP首部

IP的全称是Internet Protocol即网际协议,这个协议是TCP/IP协议族中的核心协议。下面是它的数据报格式:

从图中可以看出,如果IP数据报没有选项的话,那么IP首部有20字节。对网络监视器来说,IP首部中各字段中重要的有:IP地址、协议类型、总长度。
协议类型说明是什么协议(TCP,UDP,ICMP,IGMP)向它传递数据。下面是各个主要协议的代码:
协议类型 协议代码(十进制)

TCP 6
UDP 17
ICMP 1
IGMP 2

所以我们可以根据协议的代码来判断数据报内部封装的数据是属于什么协议。

下面的四个协议(ICMP、IGMP、TCP、UDP)都是封装在IP数据报中的。

l ICMP首部

ICMP的全称是Internet Control Message Protocol即网间控制报文协议。著名的Ping程序用的就是这个协议。它的首部结构见下图。


l IGMP首部

IGMP的全称是Internet Group Manage Protocol即因特网组管理协议。它是用来支持主机和路由器进行多播的协议。


l TCP首部

TCP的全称是Transport Control Protocol即传输控制协议。它是非常重要的协议。我们的FTP,HTTP,TELNET等我们经常使用的应用都是使用TCP来传输的。它提供一种面向连接的、可靠的字节流服务。下面是它的首部的结构。

如图所示,TCP首部长20字节,包括了源端口、目的端口、序号、确认序号、首部长度(以四字节记)、六个标志字段、窗口大小、校验和等等。
六个标志字段的意义见下表:

标志 意义
URG 紧急指针(urgent pointer)有效
ACK 确认序号有效
PSH 接收方应该尽快将这个报文段交给应用层
RST 重建连接
SYN 同步序号用来发起一个连接
FIN 发端完成发送任务


l UDP首部

UDP的全称是User Datagram Protocol即用户数据报协议,与TCP区别,它是面向无连接应用的协议,象我们的OICQ、ICQ等聊天软件都是用的UDP协议。下面是UDP首部的结构。

我们可以看到,UDP首部比TCP首部要简单得多,这是因为UDP是无连接的,比TCP的有连接中双方复杂的交互要简单,它并不保证数据传输的质量,但是我们可以在更高层的应用中自己进行质量保证。

l HTTP

HTTP的全称是Hyper Text Transfer Protocol。我们浏览网页用的就是这个协议。HTTP数据是在TCP包中的,而且一般来说网页服务是在80或8080端口。所以如果你只需要分析浏 览网页的情况,只需要截获端口号有80或8080的TCP包就可以了。更进一步,我们需要分析HTTP协议中请求的内容。

  我们可能需要得到用 户浏览网页的URL地址。假设我们现在得到了包含在TCP包中的HTTP头信息。任何我们在浏览器里对HTTP服务器发送的请求都是GET或POST请求 中的一种。浏览器在HTTP头里添加了与请求及系统相关的信息然后将请求发送给相应的服务器。那些信息当然包括了我们想要的URL。所以我们分析HTTP 头就可以得到URL。下面是一个正常的包含于HTTP头中的URL:

GET / HTTP/1.1

  上面的请求是得到服务器的主页(即默认页)的HTTP请求。"/"表示服务器的主页,后面的"HTTP/1.1"表示这是HTTP的1.1版本。这是现在的主流版本,也有1.0的老版本。

下面是一个我们访问服务器上其他的网页的请求。

GET /source/index.html HTTP/1.1

上面的请求是得到"/source/index.html"的请求。

从上面我们可以知道如何解析HTTP头。

l 小结

通过上面对数据格式的介绍,我们可以轻松灵活地进行数据包的过滤。

2.增强我们的程序的功能

我们当然不会满足于实现仅仅能够捕获数据包的简单的应用程序,要不是,我们干嘛这么辛苦编程呢,直接用Netxray等软件得了,我们的目的是能够让程 序实现自己的功能。一个有意思的功能就是能够得到局域网上他人使用的代理服务器,实现方法很简单,别人使用代理服务器的连接特征一般是在服务器返回给用户 端的HTTP头中有"Proxy-Connection"关键字存在。

下面我从通过实现一个访问限制器讲讲怎么增强程序的功能。

由于局域网的特殊性,我们可以实现一个局域网的访问限制器,可以限制网上其他用户对因特网的访问权限。要实现这样的功能,我们先讲讲如何通过协议驱动程序发送数据包。

使用Packet32.dll中的函数PacketSendPacket来发送数据包。下面是这个函数的定义:

BOOL
PacketSendPacket(
LPADAPTER AdapterObject, // 我们使用PacketOpenAdapter打开的网络适配卡对象
LPPACKET lpPacket, // 使用PacketAllocatePacket等函数建立的数据包对象,
BOOLEAN Sync // 是否同步
);

如果函数发送数据包成功,则返回True,否则返回False。

  下面我讲讲构建数据包中需要注意的地方。

首先是字与双字在各种系统中内部存储的方式的不同,在Windows中字与双字是高位在低地址排列的,而网络传输的标准是低位在低地址排列,比如一个十 进制数字4660在Windows系统中存储成3412h,而在网络上表示是1234h。所以我们在设置或读取协议首部中有关用字或双字表示(一般象 TCP中的端口、序号,而IP地址则不是)的字段时要切记转换他们的排列顺序。下面是一个转换字排列顺序的转换算法:

WORD SwapWord(WORD WordToReverse)
{
WORD lo,hi;
WORD result;

lo= WordToReverse & 0xff;
hi= WordToReverse & 0xff00;
lo=lo<<8;
hi=hi>>8;
result=hi | lo;

return result;
}

在我们建立发送包的过程中,除了设置包中IP首部、TCP首部中各种字段为我们需要的值,一个非常重要的工作是计算TCP、UDP、IP的校验和。我们 遇到的校验和计算就是把一个范围的数据按字(16 bit,WORD,即两个字节)反码相加,如果数据不是字对齐的,则将在最后补上一个填充字节0使之字对齐再进行计算(在IP校验和计算中,由于只要计算 IP首部,所以没可以出现这种情况,但是我们在后面的TCP、UDP校验和计算中碰到这种情况),以上计算得到的结果就是校验和。下面是一个公用校验和计 算函数,它可以用在IP、TCP、UDP校验和的计算中:

WORD CheckSum(WORD *addr,WORD len)
{
DWORD lSum;
WORD wOddByte;
WORD ChecksumAnswer;

lSum=0l;

while(len>1) {
lSum+= *addr++;
len-=2;
}

if(len==1) {
wOddByte=0;
*((unsigned char*)&wOddByte)=*(unsigned char*)addr;
lSum+=wOddByte;
}

lSum=(lSum>>16)+(lSum&0xffff);
lSum+=(lSum>>16);
ChecksumAnswer=(unsigned int)~lSum;

return CheckSumAnswer;
}

  IP首部的校验和只计算IP首部的数据,而UDP校验和是计算整个UDP首部和UDP数据。

UDP的校验和是可选的,尽管UDP校验和的基本计算方法与上面描述的IP首部校验和计算方法相类似(16 bit 字的二进制反码和),但是它们之间存在不同的地方。首先,UDP数据报的长度可以为奇数字节,但是校验和算法是把若干个16 bit 字相加。如前所述,我们可以在最后增加填充字节0 ,这只是为了校验和的计算(也就是说,可能增加的填充字节不被传送)。其次,UDP数据报包含一个12字节长的伪首部,它是为了计算校验和而设置的。伪首 部包含IP首部一些字段。由于UDP可以不计算校验和,所以规定如果发送端没有计算校验和的话,校验和字段将设置为0。UDP数据报中的伪首部格式如下图 所示。


  TCP校验和的计算方法与UDP大致相同,只是TCP伪首部中的长度为16位TCP长度。而且TCP的校验和是必须计算的。
  另外在我们发送TCP报文段的时候要注意序号的顺序,不然发送的报文段将得不到对方的承认。

知道了以上发送数据包的必要知识,我们现在可以使用发送数据包来进行应用了:比如,前面我提到了访问限制器,我们可以在截获了某用户的对以太网的非法访 问后(可以通过对IP地址的检查或者对HTTP头中URL的检查来实现),根据截获的TCP包的序号重新构建一个伪装成从服务器发送的TCP包,其中 TCP包中的RST标志设为1,将它发送到网上的话,他们建立的连接将被断开,所以就实现了阻止用户访问非法站点的功能。

  从上面的应用我们也看到了局域网的不安全性,每个结点都可以得到任何结点的网络信息,甚至可以轻松地阻断别人的网络连接,那种方法如果用在黑客手里,你也不能有如何防御的手段,幸好这只是在局域网上,如果有人能够在路由器上获得截获,那将是不幸的事情。

3.优化应用程序的性能

  用过Netxray等网络监视器的读者都知道,如果在一个繁忙的网络上进行截获,而且不设置任何过滤,那得到的数据包是非常多的,可能在一秒钟内得到上千的数据包。如果应用程序不进行必要的性能优化,那么将会大量的丢失数据包,下面就是我对性能的一个优化方案。

这个方案使用了多线程来处理数据包。在程序中建立一个公共的数据包缓冲池,这个缓冲池是一个LILO的队列。程序中使用三个线程进行操作:一个线程只进 行捕获操作,它将从驱动程序获得的数据包添加到数据包队列的头部;另一个线程只进行过滤操作,它检查新到的队尾的数据包,检查其是否满足过滤条件,如果不 满足则将其删除出队列;最后一个线程进行数据包处理操作,象根据接收的数据包发送新数据包这样的工作都由它来进行。上面三个线程中,考虑尽可能少丢失数据 包的条件,应该是进行捕获操作的线程的优先级最高,当然具体问题具体分析,看应用的侧重点是什么了。

4.参考资料

TCP/IP详解 卷1:协议 (本文的图摘自本书)
IPMan from HiHint

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理