C++实现原始套接字捕获数据包
引言
原始套接字是允许访问底层传输协议的一种套接字类型,提供了普通套接字所不具备的功能,能够对网络数据包进行某种程度的控制操作。因此原始套接字通常用开发简单网络性能监视程序以及网络探测、网络攻击等工具。今天我们来探索一下,从实现原始套接字到捕获数据包的整个过程。
原始套接字与TCP套接字和UDP套接字的区别
Berkeley套接字将流式套接字和数据报套接字定义为标准套接字,用于在主机之间通过TCP和UDP来传输数据。为了保证Internet的使用效率,除了传输数据之外,操作系统的协议栈还处理了大量的非数据流量,如果程序员在创建应用时也需要对这些非数据流量进行控制的话,那么此时就需要另一种套接字,即原始套接字。这种套接字越过了TCP/IP协议栈的部分层次,为程序员提供了完全且直接的的数据包级别的Internet访问能力,如下图所示。
- 具有发送和接收ICMPv4、IGMPv4、ICMPv6等分组
- 具有发送和接收内核不处理其协议字段的IPv4数据包
- 可以控制IPv4首部
从图中我们可以清晰看出,对于普通流式套接字和数据包套接字的应用程序,他们只能控制数据包的数据部分,也就是除了传输层首部和网络层首部以外的,需要通过网络传输的数据部分。而传输层首部和网络层首部则由协议栈根据创建套接字时候指定的参数负责填充,显而易见的是,这两部分,开发者是无法实现管理的,此时就有了原始套接字的用武之地,它可以控制传输层首部,也可以控制网络层层首部,给程序员带来了很大的灵活性。
原始套接字编程使用的场合
尽管原始套接字的功能强大,可以构造TCP和UDP的协议数据完成数据传输,但是该套接字类型也有其局限性。在网络层上,原始套接字基于不可靠的IP分组传输服务,与数据报套接字类似,这种服务的特点是无连接、不可靠。无连接的特点决定了原始套接字的传输非常灵活,具有资源消耗小,处理速度快的巨大优点,不可靠也意味着在网络质量不好的情况下,数据包的丢失情况可能会非常严重。结合原始套接字的开发层次和能力,适用于以下场合。
- 1.一些特殊用途的探测应用
原始套接字提供了直接访问硬件通信的相关能力,其工作层次决定了此类套接字具有灵活的数据构造能力,应用程序可以利用原始套接字操控TCP/IP数据包的结构和内容没实现向特殊用途的探测和扫描。因此,原始套接字适用于要求数据包构造灵活性高的应用。 - 2.基于数据包捕获的应用
对于从事协议分析或网络管理的人来说,各种入侵检测、流量监控以及协议分析软件是必备的工具,这些软件都有具有数据包捕获和分析的功能。原始套接字能够操控网卡进入混杂模式的工作状态,从而达到捕获流经网卡的所有数据包的目的。因此,使用原始套接字可以满足数据包的捕获和分析的应用需求。 - 3.特俗用途的传输应用
原始套接字能够处理内核不认识的协议数据,对于一些特殊应用,我们希望不增加内核功能,而是完全在用户层面完成对某些协议的支持,原始套接字能帮助应用数据在构造过程中修改IP首部协议字段值,并接收处理这些内核不认识的协议数据,从而完成协议功能在用户层面的扩展。
原始套接字的通信过程
(1)基于原始套接字的数据发送过程
在通信过程中,数据发送方根据协议要求,将要发送的数据填充进发送缓冲区,同时给发送数据附加上必要的协议首部,全部填写好后,将数据发送出去。
基本通信过程如下:
1)Windows Sockets DLL 初始化,协商版本号
2)创建套接字,指定使用原始套接字进行通信,根据需要设置IP控制选项
3)指定目的地址和通信端口
4)填充首部和数据
5)发送数据
6)关闭套接字
7)结束对Windows Sockets DLL 的使用,释放资源
(2)基于原始套接字的数据接收过程
在通信过程中,数据接收方设定好接受条件后,从网络中接收到与预设条件相匹配的网络数据后,,如果出现了噪声,对数据进行过滤,然后协商版本号。
1)Windows Sockets DLL 初始化,协商版本号
2)创建套接字,指定使用原始套接字进行通信,并声明特定的协议类型
3)根据需要设定接受选项
4)接收数据
5)过滤数据
6)关闭套接字
7)结束对Windows Sockets DLL 的使用,释放资源
创建原始套接字
创建原始套接字,程序首先要求操作系统创建套接字抽象层的实例,在WinSock2中,完成这个人的函数是socket()和WSASocket()。
socket()
SOCKET WSAAPT socket(
_in int af, //缺顶套接字的通信地址族
_in int type, //指定套接字类型
_in int protocol //指定要使用的特定传输协议
);
WSASocket()
SOCKET WSASocket(
_in int af, //缺顶套接字的通信地址族
_in int type, //指定套接字类型
_in int protocol //指定要使用的特定传输协议
_in LPWSAPROTOCOL_INFO lpProtocolInfo, //与前三个参数互斥使用,是一个指向LPWSAPROTOCOL_INFO结构的指针,该结构定义所创建套接字的特性
_in GROUP g, //标识一个已存在的套接字组ID或指明创建一个新的套接字组
_in DWORD dwFlags //声明一组套接字属性的描述
);
bind()
int bind(
_in SOCKET s, //调用socket()返回的描述符
_in const struct sockaddr *name, //地址参数,被声明为一个指向sockaddr结构的指针
_in int namelen
);
常用协议定义列表
使用原始套接字接收数据
通常使用原始套接字接受数据可以调用recvfrom()或WSARecvFrom()函数实现。在这里我们需要关心两个问题。接收数据的内容和接受数据的类型。
- 1.接受数据的内容 从接受数据的内容来看,不论套接字如何设置发送选项,对于IPV4,原始套接字接收到的数据是包括IP首部在内的完整的数据包,对于IPV6,原始套接字接收到的都是去掉了IPV6首部和所有扩展首部的净载荷。
- 2.接收数据的类型 对于接受数据的类型,数据从协议栈提交到使用套接字的应用程序涉及两层数据的提交。如下图:
第一步:由步骤①来看,在接收到一个数据包之后,协议栈把满足以下条件的IP数据包传递到套接字实现的原始套接字部分:
1.非UDP分组或TCP分组
2.部分ICMP
3.所有的IGMP分组
4.协议栈不认识其协议字段所有IP数据包
5.重组后的分片数据
第二步:由步骤②来看,当协议栈有一个需传递到原始套接字的IP数据包时,它将检查所有进程的所有打开的原始套接字,寻找满足条件的套接字,如果满足条以下条件,每个匹配的套接字的接收缓冲区中都将受到数据包的一份拷贝:
- 匹配的协议:对应于socket()函数或WSASocket(),如果在创建原始套接字时指定了非0的协议参数,那么接收到的数据包IP首部中的协议字段必须与指定的协议参数相匹配;
- 匹配的目的地址:对应于bind()函数,如果通过bind()函数将原始套接字绑定到某个固定的本地IP地址,那么接收到的数据包目的地址必须与绑定地址相符,如果没有将原始套接字绑定到本地的某个IP地址上,那么不考虑数据包的目标IP,将符合其他条件的所有IP数据包都复制到该套接字的接收缓冲区
中; - 匹配的源地址:对应于connect()函数或WSASocket()函数,如果通过调用connect()函数或WSASocket()函数为原始套接字指定外部地址,那么接收到的数据包的源IP地址必须与上述已连接大外部IP地支相匹配,如果没有为该原始套接字指定外部地址,那么所有来源的满足条件的IP数据包都将被复制到套接字的接收缓冲区中。
使用原始套接字接收数据包
在使用原始套接字发送数据是以五连接的方式完成的,创建好原始套接字后可以直接将构造好的数据发送发送出去,但是由于原始套接字工作的层次比数据报套接字更低,在发送内容上有一定的区别。
发送数据的目标
从发送数据的目标来看,原始套接字不存在端口号的概念,对目的地址描述时,端口号是被忽略的,但是任然可以再连接模式和非连接模式两种方式下为套接字管理远端地址。
- (1)非连接模式
在非连接模式下,应用程序在每次数据发送前指定目的IP,然后调用sendto()函数或WSASendTo()函数将数据发送出去,并在数据接收时,调用recvfrom()函数或WSARecvFrom()函数,从函数返回参数中读取接收的数据包的来源地址。这种模式也同样适用于广播地址或多广播地址的放松,此时需要通过setsockopt()函数设置选项SO_BROADCAST以允许广播数据的发送。 - (2)连接模式
在连接模式下,应用程序首先调用connect()函数WSACnnect()函数指定远端地址,即确定唯一的通信对方地址没在数据发送和接受过程中,不用每次重复指明远程地址就可以发送和接收报文,此时,send()函数或WSASned()函数和sendto()函数就可以通用,recv()函数/WSARecv()函数和recvfrom()函数/WASRecvFrom()函数也可以通用。
2.发送数据的内容
从发送数据的内容来看,原始套接字发送内容涉及多种协议首部的构造,对于IPv4或IPv6数据的发送,IP首部控制选项为协议首部填充了两个层次的选择:如果是IPV4,选项为IP_HDRINCL,选项级别为IPPRPOTO_IP;如果是IPv6,选项为IPv6_HDRINCL,选项级别为IPPROTO_IPv6。
- (1)IP首部控制选项未开启
在原始套接字创建后默认发送的数据是IP数据部分,那么不需要设置IP首部控制选项,此山是程序设计人员负责构造IP协议承载的首部和协议数据,IP协议首部是由协议栈负责填充的。 - (2)IP首部控制选项开启
如果希望对IP首部进行个性填充,则需要设置IP首部控制选项,此时包括IP首部在内的整个数据包都由用户来完成构造,而协议栈则极少参与数据包的形成过程。
源代码如下
#include "winsock2.h"
#include "mstcpip.h"
#include "iostream"
#include<stdlib.h>
using namespace std;
#pragma comment(lib,"ws2_32.lib")
#define DEFAULT_BUFLEN 65535
#define DEFAULT_NAMELEN 512
int main()
{
WSADATA wsaData;
SOCKET SnifferSocket = INVALID_SOCKET;
char recvbuf[DEFAULT_BUFLEN];
int iResult;
int recvbuflen = DEFAULT_BUFLEN;
HOSTENT* local;
char HostName[DEFAULT_NAMELEN];
IN_ADDR addr;
SOCKADDR_IN LocalAddr, RemoteAddr;
int addrlen = sizeof(SOCKADDR_IN);
int in = 0, i = 0;
DWORD dwBufferLen[10];
DWORD Optval = 1;
DWORD dwBytesReturned = 0;
//初始化套接字
iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (iResult != 0)
{
cout << "初始化失败:" << iResult << endl;
return 1;
}
//创建套接字
SnifferSocket = socket(AF_INET, SOCK_RAW, IPPROTO_IP);
if (INVALID_SOCKET == SnifferSocket)
{
cout << "创建套接字失败:" << WSAGetLastError() << endl;
WSACleanup();
return 1;
}
//获取本机名称
memset(HostName, 0, DEFAULT_NAMELEN);
iResult = gethostname(HostName, sizeof(HostName));
if (SOCKET_ERROR == iResult)
{
cout << "获取本机名称失败:" << WSAGetLastError() << endl;
WSACleanup();
return 1;
}
//获取本机IP
local = gethostbyname(HostName);
cout << "本机可用的IP地址有:" << endl;
if (NULL == local)
{
cout << "获取IP失败:" << WSAGetLastError() << endl;
WSACleanup();
return 1;
}
while (local->h_addr_list[i] != 0)
{
addr.s_addr = *(u_long*)local->h_addr_list[i++];
cout << "\t" << i << ":\t" << inet_ntoa(addr) << endl;
}
cout << "请选择捕获数据包待使用的接口号:";
cin >> in;
memset(&LocalAddr, 0, sizeof(LocalAddr));
memcpy(&LocalAddr.sin_addr.S_un.S_addr, local->h_addr_list[in - 1], sizeof(LocalAddr.sin_addr.S_un.S_addr));
LocalAddr.sin_family = AF_INET;
LocalAddr.sin_port = 0;
//绑定
iResult = bind(SnifferSocket, (SOCKADDR*)&LocalAddr, sizeof(LocalAddr));
if (SOCKET_ERROR == iResult)
{
cout << "绑定失败:" << WSAGetLastError() << endl;
closesocket(SnifferSocket);
WSACleanup();
return 1;
}
cout << "成功绑定套接字和" << in << "号借口地址";
//设置套接字接受命令
iResult = WSAIoctl(SnifferSocket, SIO_RCVALL, &Optval, sizeof(Optval), &dwBufferLen, sizeof(dwBufferLen), &dwBytesReturned, NULL, NULL);
if (SOCKET_ERROR == iResult)
{
cout << "套接字设置失败:" << WSAGetLastError() << endl;
closesocket(SnifferSocket);
WSACleanup();
return 1;
}
//开始接受数据
cout << "开始接受数据" << endl;
do
{
//接受数据
iResult = recvfrom(SnifferSocket, recvbuf, DEFAULT_BUFLEN, 0, (SOCKADDR*)&RemoteAddr, &addrlen);
if (iResult > 0)
{
cout << "接受来自" << inet_ntoa(RemoteAddr.sin_addr) << "的数据包," << "长度为" << iResult << endl;
}
else
cout << "接受失败:" << WSAGetLastError() << endl;
} while (iResult > 0);
{
}
system("pause");
return 0;
}
8.运行截图如下: