C/C++ 运用Npcap发送UDP数据包
Npcap 是一个功能强大的开源网络抓包库,它是 WinPcap 的一个分支,并提供了一些增强和改进。特别适用于在 Windows 环境下进行网络流量捕获和分析。除了支持通常的网络抓包功能外,Npcap 还提供了对数据包的拼合与构造,使其成为实现 UDP 数据包发包的理想选择。本章将通过Npcap库构造一个UDP原始数据包,并实现对特定主机的发包功能,通过本章的学习读者可以掌握如何使用Npcap库伪造特定的数据包格式。
Npcap的主要特点和概述:
- 原始套接字支持: Npcap 允许用户通过原始套接字在网络层捕获和发送数据包。这使得用户能够进行更底层的网络活动监控和分析。
- WinPcap 的增强版本: Npcap 是 WinPcap 的一个分支,对其进行了一些增强和改进。这些改进包括对新版本 Windows 的支持、更好的性能和稳定性,以及一些额外的功能。
- 支持 Windows 10: Npcap 被设计用于支持 Windows 10 操作系统。它允许用户在最新的 Windows 平台上进行网络抓包和分析。
- Loopback 模式: Npcap 允许在 Loopback 接口上进行抓包,使用户能够监视本地主机上的网络流量。
- 多种应用场景: Npcap 被广泛应用于网络安全、网络管理、网络调试等各种场景。它为开发人员、网络管理员和安全专家提供了一个功能强大的工具,用于分析和理解网络通信。
- 开源: Npcap 是开源项目,其源代码可以在 GitHub 上获得。这使得用户可以自由查看、修改和定制代码,以满足特定需求。
UDP 是一种无连接、轻量级的传输层协议,与 TCP 相比,它不提供可靠性、流控制和错误恢复机制,但却更加简单且具有较低的开销。UDP 主要用于那些对传输速度要求较高、可以容忍少量丢失的应用场景。
UDP 数据包结构: UDP 数据包由报头和数据两部分组成。
- 报头(Header):
- 源端口号(16 位): 指定发送端口。
- 目标端口号(16 位): 指定接收端口。
- 长度(16 位): 报头和数据的总长度,以字节为单位。
- 校验和(16 位): 用于验证数据在传输过程中的完整性。
- 数据(Payload):
- 实际传输的数据,长度可变。
UDP 的特点:
- 面向无连接: UDP 是一种无连接协议,通信双方不需要在传输数据之前建立连接。这使得它的开销较低,适用于一些实时性要求较高的应用。
- 不可靠性: UDP 不提供数据的可靠性保证,不保证数据包的到达、顺序和完整性。因此,它更适合那些能够容忍一些数据丢失的场景,如音视频传输。
- 适用于广播和多播: UDP 支持广播和多播通信,可以通过一个发送操作同时向多个目标发送数据。
- 低开销: 由于缺乏连接建立和维护的开销,以及不提供可靠性保证的特性,UDP 具有较低的开销,适用于对实时性要求较高的应用。
- 适用于短消息: 由于不需要建立连接,UDP 适合传输短消息,尤其是对实时性要求高的应用。
UDP 的应用场景:
- 实时性要求高的应用: 如实时音视频传输、在线游戏等。
- 简单的请求-响应通信: 适用于一些简单的请求-响应场景,如 DNS 查询。
- 广播和多播应用: UDP 的支持广播和多播特性使其适用于这类通信模式。
- 实时数据采集: 例如传感器数据采集等场景。
输出网卡
使用 WinPcap(Windows Packet Capture)库列举系统上的网络接口以及它们的 IP 地址。WinPcap 是一个用于 Windows 操作系统的网络数据包捕获库,可以用于网络数据包的捕获和分析。
代码主要做了以下几个事情:
- 使用
pcap_findalldevs_ex
函数查找系统上的所有网络接口。 - 遍历每个网络接口,获取其 IP 地址,并将地址列表打印出来。
pcap_findalldevs_ex
用于查找系统上所有网络接口的函数。它的原型如下:
int pcap_findalldevs_ex(const char *source, struct pcap_rmtauth *auth, pcap_if_t **alldevs, char *errbuf); |
函数参数说明:
source
:一个字符串,用于指定网络接口的来源。可以为NULL
,表示从系统获取网络接口信息。也可以指定为一个网络地址,用于远程捕获。auth
:一个pcap_rmtauth
结构的指针,用于指定远程捕获的认证信息。一般情况下可以为NULL
。alldevs
:一个pcap_if_t
类型的指针的地址,用于保存查找到的网络接口链表的头指针。errbuf
:一个字符数组,用于保存错误信息。
函数返回值:
- 成功时返回 0。
- 失败时返回 -1,错误信息保存在
errbuf
中。
函数功能:
pcap_findalldevs_ex
主要用于查找系统上的网络接口信息。当调用成功后,alldevs
将指向一个链表,链表中的每个节点都包含一个网络接口的信息。这个链表的头指针是 alldevs
。
pcap_freealldevs
用于释放 pcap_findalldevs_ex
函数分配的资源的函数。其原型如下:
void pcap_freealldevs(pcap_if_t *alldevs); |
函数参数说明:
alldevs
:由pcap_findalldevs_ex
返回的链表的头指针。
函数功能:
pcap_freealldevs
主要用于释放 pcap_findalldevs_ex
函数返回的链表中分配的资源,包括每个节点和节点中保存的接口信息。
输出当前系统中活动网卡信息,可以这样来写,如下代码所示;
|
输出效果如下图所示;
打开网卡
打开网络适配器的函数,通过传入本机的IP地址,该函数会查找与该IP地址匹配的网络适配器并打开。以下是对该函数的简要分析:
查找网卡设备指针:
if (-1 == pcap_findalldevs_ex(PCAP_SRC_IF_STRING, NULL, &alldevs, errbuf)) |
使用 pcap_findalldevs_ex
函数来获取本机所有网卡设备的链表。如果返回值为 -1,说明发生了错误,这时函数会输出错误信息并直接返回。
选取适合网卡:
for (d = alldevs; d; d = d->next) |
通过遍历网卡设备链表,查找与传入的本机IP地址匹配的网卡。首先,通过检查每个网卡的地址列表,找到第一个匹配的网卡。如果找到了,将 flag
标记设为1,然后跳出循环。如果未找到匹配的网卡,输出错误信息并返回。
获取子网掩码:
netmask = ((sockaddr_in*)d->addresses->netmask)->sin_addr.S_un.S_addr; |
获取匹配网卡的子网掩码。
打开网卡:
m_adhandle = pcap_open(d->name, 65536, PCAP_OPENFLAG_PROMISCUOUS, 1000, NULL, errbuf); |
使用 pcap_open
函数打开选择的网卡,该函数的声明如下:
pcap_t *pcap_open(const char *source, int snaplen, int flags, int read_timeout, struct pcap_rmtauth *auth, char *errbuf); |
这里是对参数的简要解释:
source
: 要打开的网络适配器的名称,例如 “eth0”。snaplen
: 指定捕获数据包时每个数据包的最大长度。如果数据包超过这个长度,它将被截断。通常设置为数据包的最大可能长度。flags
: 控制捕获的方式,可以使用位掩码进行组合。常见的标志包括:
- `PCAP_OPENFLAG_PROMISCUOUS`: 开启混杂模式,允许捕获所有经过网卡的数据包。
- `PCAP_OPENFLAG_MAX_RESPONSIVENESS`: 最大响应性标志,可能在某些平台上影响性能。
- `read_timeout`: 设置超时值,以毫秒为单位。如果设置为0,表示无限期等待数据包。
- `auth`: 可以指定用于远程捕获的身份验证信息,通常为 `NULL`。
- `errbuf`: 用于存储错误信息的缓冲区,如果函数执行失败,会将错误信息写入这个缓冲区。
函数返回一个 `pcap_t` 类型的指针,它是一个表示打开的网络适配器的结构。如果打开失败,返回 `NULL`。
**检查以太网:**
```c
if (DLT_EN10MB != pcap_datalink(m_adhandle))
pcap_datalink
函数是 PCAP 库中用于获取网络适配器数据链路类型(datalink type)的函数,确保是以太网,如果不是以太网,输出错误信息并返回。
该函数的声明如下:
int pcap_datalink(pcap_t *p); |
这里是对参数的简要解释:
p
: 表示一个已经打开的网络适配器的pcap_t
结构指针。
函数返回一个整数,表示数据链路类型。这个值通常是预定义的常量之一,用于标识不同类型的网络数据链路。
常见的一些数据链路类型常量包括:
DLT_EN10MB
(Ethernet): 表示以太网数据链路。DLT_IEEE802
(802.5 Token Ring): 表示 IEEE 802.5 Token Ring 数据链路。DLT_PPP
(Point-to-Point Protocol): 表示点对点协议数据链路。DLT_ARCNET
(ARCNET): 表示 ARCNET 数据链路。
释放网卡设备列表:
pcap_freealldevs(alldevs); |
最后,释放 pcap_findalldevs_ex
函数返回的网卡设备列表,避免内存泄漏。
该函数的其他全局变量 m_adhandle
,FinalPacket
,UserDataLen
已经在文章开头声明和定义。
// 通过传入本机IP地址打开网卡 |
构造数据
MAC地址转换为Bytes字节
将MAC 地址的字符串表示形式转换为字节数组(unsigned char
数组),函数首先创建了一个临时缓冲区 Tmp
来存储输入字符串的拷贝,然后使用 sscanf
函数将字符串中的每两个字符解析为一个十六进制数,存储到 Returned
数组中。最后,通过调整指针的位置,跳过已经处理的字符,实现了对整个字符串的解析。
下面是这段代码的解释:
// MAC地址转Bytes |
Bytes字节转换为16进制
将两个字节(unsigned char
类型的 X
和 Y
)组成一个16位的无符号整数。函数的目的是将两个字节的数据合并成一个16位的整数。首先,将 X
左移8位,然后与 Y
进行按位或操作,得到一个包含两个字节信息的16位整数。最后,将这个16位整数返回。这种操作通常在处理网络协议或二进制数据时会经常遇到。
下面是这段代码的解释:
// Bytes地址转16进制 |
计算 IP 数据报的校验和
这个函数主要通过遍历 IP 头中的每两个字节,将它们合并为一个16位整数,并逐步累加到校验和中。在每次累加时,还需要检查是否发生了溢出,如果溢出则需要额外加1。最后,对累加得到的校验和进行取反操作,得到最终的 IP 校验和,并将其返回。这种校验和计算通常用于验证 IP 数据报的完整性。
下面是这段代码的解释:
// 计算IP校验和 |
计算 UDP 数据报的校验和
这个函数主要通过构造 UDP 数据报的伪首部,包括源 IP、目标 IP、协议类型(UDP)、UDP 长度、源端口、目标端口以及 UDP 数据等字段,并通过遍历伪首部的每两个字节计算校验和。最后取反得到最终的 UDP 校验和,并将其返回。这种校验和计算通常用于验证 UDP 数据报的完整性。
下面是这段代码的解释:
// 计算UDP校验和 |
这段代码的分析:
- 伪首部构造: UDP校验和的计算需要使用UDP头以及伪首部(包含源IP、目标IP、协议类型、UDP长度等信息)。这里使用
PseudoHeader
数组来构造伪首部。 - 伪首部填充: 通过
memcpy
等操作将源和目标IP地址、UDP头的长度字段以及UDP的源端口、目标端口、UDP数据等内容填充到伪首部中。 - 伪首部遍历: 通过遍历伪首部的每两个字节,计算累加和。遍历过程中,将两个字节转换为16位整数
Tmp
,然后进行累加。如果累加结果大于65535,则向结果中再加1。这是为了处理累加和溢出的情况。 - 取反: 计算完毕后,对累加和取反得到最终的UDP校验和。
- 内存释放: 最后释放动态分配的伪首部内存。
需要注意的是,UDP校验和是一个16位的值,用于验证UDP数据报在传输过程中是否被修改。这段代码主要完成了构造UDP伪首部和计算校验和的过程。在实际网络通信中,校验和的计算是为了保证数据的完整性,防止在传输过程中的错误。
创建UDP数据包函数
创建一个UDP数据包,该代码是一个简单的网络编程示例,用于创建和发送UDP数据包。其中,UDP数据包的内容和头部信息都可以根据实际需求进行定制。
代码的概述:
- 打开网卡: 通过
pcap_findalldevs_ex
函数获取本机的网卡设备列表,并在控制台输出每个网卡的地址列表。 - 选择网卡: 用户输入本机IP地址,程序通过遍历网卡设备列表,找到与输入IP地址匹配的网卡。
- 打开选定的网卡: 使用
pcap_open
函数打开选择的网卡,获取到网卡的句柄。 - 创建UDP数据包: 调用
CreatePacket
函数创建一个UDP数据包。该函数包括以下步骤:- 分配内存:使用
new
运算符为FinalPacket
分配内存,内存大小为UserDataLength + 42
字节。 - 填充以太网头:拷贝目标MAC地址、源MAC地址和协议类型(IPv4)到
FinalPacket
的前12个字节。 - 填充IP头:填充IPv4头部,包括版本、标题长度、总长度、标识、标志、偏移、生存时间、协议(UDP为0x11),校验和、源IP和目标IP。
- 填充UDP头:填充UDP头,包括源端口、目标端口、UDP长度(包括UDP头和数据)和校验和。
- 计算IP校验和:调用
CalculateIPChecksum
函数计算IP头的校验和。 - 计算UDP校验和:调用
CalculateUDPChecksum
函数计算UDP头的校验和。 - 返回数据包:生成的UDP数据包保存在
FinalPacket
中。
- 分配内存:使用
- 释放资源: 在程序结束时,释放分配的内存。
void CreatePacket(unsigned char* SourceMAC, unsigned char* DestinationMAC,unsigned int SourceIP, unsigned int DestIP,unsigned short SourcePort, unsigned short DestinationPort,unsigned char* UserData, unsigned int UserDataLength) |
对该代码的分析:
- 分配内存: 使用
new
运算符为FinalPacket
分配内存,内存大小为UserDataLength + 42
字节。这足够容纳UDP数据以及以太网、IP和UDP头的长度。 - 填充以太网头: 使用
memcpy
函数将目标MAC地址、源MAC地址和协议类型(这里是IPv4)拷贝到FinalPacket
的前12个字节。 - 填充IP头: 在
FinalPacket
的第14个字节开始,填充IPv4头部。这包括版本、标题长度、总长度、标识、标志、偏移、生存时间、协议(UDP为0x11),校验和、源IP和目标IP。 - 填充UDP头: 在
FinalPacket
的第34个字节开始,填充UDP头。这包括源端口、目标端口、UDP长度(包括UDP头和数据)和校验和。其中,UDP校验和的计算通过调用CalculateUDPChecksum
函数完成。 - 计算IP校验和: 在填充IP头后,调用
CalculateIPChecksum
函数计算IP头的校验和。这个校验和是IPv4头的一个字段。 - 返回数据包: 函数执行完毕后,生成的UDP数据包保存在
FinalPacket
中,可以将其用于发送到网络。
需要注意的是,这段代码中的硬编码可能需要根据实际需求进行修改,例如协议类型、标识、生存时间等。此外,计算校验和是网络协议中用于检测数据完整性的一种机制。
发送UDP数据包
代码演示了如何打开网卡,生成UDP数据包,并通过pcap_sendpacket
函数发送数据包到网络。需要注意的是,数据包的内容和地址是硬编码的,实际应用中可能需要根据需要进行更改。
int main(int argc, char* argv[]) |
打开wireshark抓包工具,过滤目标地址为ip.dst==192.168.93.11
然后抓包,运行编译后的程序,则你会看到我们自己构建的数据包被发送了10次,如下图所示;
随便打开一个数据包看下结构,源地址目标地址均是伪造的地址,数据包中的内容是hello lyshark
,如下图所示;