C 语言

关注公众号 jb51net

关闭
首页 > 软件编程 > C 语言 > C++ 发送UDP数据包

C/C++ 运用Npcap发送UDP数据包的完美过程

作者:微软技术分享

UDP 是一种无连接、轻量级的传输层协议,与 TCP 相比,它不提供可靠性、流控制和错误恢复机制,但却更加简单且具有较低的开销,这篇文章主要介绍了C/C++ 运用Npcap发送UDP数据包,需要的朋友可以参考下

Npcap 是一个功能强大的开源网络抓包库,它是 WinPcap 的一个分支,并提供了一些增强和改进。特别适用于在 Windows 环境下进行网络流量捕获和分析。除了支持通常的网络抓包功能外,Npcap 还提供了对数据包的拼合与构造,使其成为实现 UDP 数据包发包的理想选择。本章将通过Npcap库构造一个UDP原始数据包,并实现对特定主机的发包功能,通过本章的学习读者可以掌握如何使用Npcap库伪造特定的数据包格式。

Npcap 是一个功能强大的开源网络抓包库,它是 WinPcap 的一个分支,并提供了一些增强和改进。特别适用于在 Windows 环境下进行网络流量捕获和分析。除了支持通常的网络抓包功能外,Npcap 还提供了对数据包的拼合与构造,使其成为实现 UDP 数据包发包的理想选择。本章将通过Npcap库构造一个UDP原始数据包,并实现对特定主机的发包功能,通过本章的学习读者可以掌握如何使用Npcap库伪造特定的数据包格式。

Npcap的主要特点和概述:

UDP 是一种无连接、轻量级的传输层协议,与 TCP 相比,它不提供可靠性、流控制和错误恢复机制,但却更加简单且具有较低的开销。UDP 主要用于那些对传输速度要求较高、可以容忍少量丢失的应用场景。

UDP 数据包结构: UDP 数据包由报头和数据两部分组成。

UDP 的特点:

UDP 的应用场景:

输出网卡

使用 WinPcap(Windows Packet Capture)库列举系统上的网络接口以及它们的 IP 地址。WinPcap 是一个用于 Windows 操作系统的网络数据包捕获库,可以用于网络数据包的捕获和分析。

代码主要做了以下几个事情:

pcap_findalldevs_ex 用于查找系统上所有网络接口的函数。它的原型如下:

int pcap_findalldevs_ex(const char *source, struct pcap_rmtauth *auth, pcap_if_t **alldevs, char *errbuf);

函数参数说明:

函数返回值:

函数功能:

pcap_findalldevs_ex 主要用于查找系统上的网络接口信息。当调用成功后,alldevs 将指向一个链表,链表中的每个节点都包含一个网络接口的信息。这个链表的头指针是 alldevs

pcap_freealldevs 用于释放 pcap_findalldevs_ex 函数分配的资源的函数。其原型如下:

void pcap_freealldevs(pcap_if_t *alldevs);

函数参数说明:

函数功能:

pcap_freealldevs 主要用于释放 pcap_findalldevs_ex 函数返回的链表中分配的资源,包括每个节点和节点中保存的接口信息。

输出当前系统中活动网卡信息,可以这样来写,如下代码所示;

#include <WinSock2.h>
#include <Windows.h>
#include <iostream>
#include <pcap.h>
#pragma comment(lib,"ws2_32.lib")
#pragma comment(lib, "packet.lib")
#pragma comment(lib, "wpcap.lib")
// 打开网卡返回的指针
pcap_t* m_adhandle;
unsigned char* FinalPacket;
unsigned int UserDataLen;
int main(int argc, char *argv[])
{
	// 打开网卡
	pcap_if_t* alldevs = NULL, *d = NULL;
	char szErr[MAX_PATH] = { 0 };
	if (-1 == pcap_findalldevs_ex(PCAP_SRC_IF_STRING, NULL, &alldevs, szErr))
	{
		return 0;
	}
	// 遍历网卡
	char* lpszIP = NULL;
	d = alldevs;
	while (NULL != d)
	{
		// 遍历网卡IP
		char szAddress[1024] = { 0 };
		pcap_addr_t* p = d->addresses;
		while (p)
		{
			lpszIP = inet_ntoa(((sockaddr_in*)p->addr)->sin_addr);
			strcpy(szAddress, lpszIP);
			p = p->next;
		}
		std::cout << "地址列表: " << szAddress << std::endl;
		d = d->next;
	}
	// 释放资源
	pcap_freealldevs(alldevs);
	system("pause");
	return 0;
}

输出效果如下图所示;

打开网卡

打开网络适配器的函数,通过传入本机的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);

这里是对参数的简要解释:

flags

: 控制捕获的方式,可以使用位掩码进行组合。常见的标志包括:

函数返回一个 pcap_t 类型的指针,它是一个表示打开的网络适配器的结构。如果打开失败,返回 NULL

检查以太网:

if (DLT_EN10MB != pcap_datalink(m_adhandle))

pcap_datalink 函数是 PCAP 库中用于获取网络适配器数据链路类型(datalink type)的函数,确保是以太网,如果不是以太网,输出错误信息并返回。

该函数的声明如下:

int pcap_datalink(pcap_t *p);

这里是对参数的简要解释:

函数返回一个整数,表示数据链路类型。这个值通常是预定义的常量之一,用于标识不同类型的网络数据链路。

常见的一些数据链路类型常量包括:

释放网卡设备列表:

pcap_freealldevs(alldevs);

最后,释放 pcap_findalldevs_ex 函数返回的网卡设备列表,避免内存泄漏。

该函数的其他全局变量 m_adhandleFinalPacketUserDataLen 已经在文章开头声明和定义。

// 通过传入本机IP地址打开网卡
void OpenAdapter(std::string local_address)
{
  pcap_if_t* alldevs = NULL, * d = NULL;
  char errbuf[256] = { 0 };
  bpf_program fcode;
  u_int netmask;
  // 获取网卡设备指针
  if (-1 == pcap_findalldevs_ex(PCAP_SRC_IF_STRING, NULL, &alldevs, errbuf))
  {
    std::cout << "获取网卡设备指针出错" << std::endl;
    return;
  }
  // 选取适合网卡
  int flag = 0;
  for (d = alldevs; d; d = d->next)
  {
    pcap_addr_t* p = d->addresses;
    while (p)
    {
      if (local_address == inet_ntoa(((sockaddr_in*)p->addr)->sin_addr))
      {
        flag = 1;
        break;
      }
      p = p->next;
    }
    if (1 == flag)
      break;
  }
  if (0 == flag)
  {
    std::cout << "请检查本机IP地址是否正确" << std::endl;
    std::cout << local_address.c_str() << std::endl;
    return;
  }
  // 获取子网掩码
  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);
  if (NULL == m_adhandle)
  {
    std::cout << "打开网卡出错" << std::endl;
    pcap_freealldevs(alldevs);
    return;
  }
  //检查以太网
  if (DLT_EN10MB != pcap_datalink(m_adhandle))
  {
    std::cout << "此程序仅在以太网下工作" << std::endl;
    pcap_freealldevs(alldevs);
    return;
  }
  // 释放网卡设备列表
  pcap_freealldevs(alldevs);
}

构造数据

MAC地址转换为Bytes字节

将MAC 地址的字符串表示形式转换为字节数组(unsigned char 数组),函数首先创建了一个临时缓冲区 Tmp 来存储输入字符串的拷贝,然后使用 sscanf 函数将字符串中的每两个字符解析为一个十六进制数,存储到 Returned 数组中。最后,通过调整指针的位置,跳过已经处理的字符,实现了对整个字符串的解析。

下面是这段代码的解释:

// MAC地址转Bytes
unsigned char* MACStringToBytes(std::string String)
{
  // 获取输入字符串的长度
  int iLen = strlen(String.c_str());
  // 创建一个临时缓冲区,用于存储输入字符串的拷贝
  char* Tmp = new char[(iLen + 1)];
  // 将输入字符串拷贝到临时缓冲区
  strcpy(Tmp, String.c_str());
  // 创建一个用于存储结果的unsigned char数组,数组大小为6
  unsigned char* Returned = new unsigned char[6];
  // 循环处理每个字节
  for (int i = 0; i < 6; i++)
  {
    // 使用sscanf将字符串中的两个字符转换为16进制数,存储到Returned数组中
    sscanf(Tmp, "%2X", &Returned[i]);
    // 移动临时缓冲区的指针,跳过已经处理过的字符
    memmove((void*)(Tmp), (void*)(Tmp + 3), 19 - i * 3);
  }
  // 返回存储结果的数组
  return Returned;
}

Bytes字节转换为16进制

将两个字节(unsigned char 类型的 XY)组成一个16位的无符号整数。函数的目的是将两个字节的数据合并成一个16位的整数。首先,将 X 左移8位,然后与 Y 进行按位或操作,得到一个包含两个字节信息的16位整数。最后,将这个16位整数返回。这种操作通常在处理网络协议或二进制数据时会经常遇到。

下面是这段代码的解释:

// Bytes地址转16进制
unsigned short BytesTo16(unsigned char X, unsigned char Y)
{
  // 将 X 左移8位,然后与 Y 进行按位或操作,得到一个16位的无符号整数
  unsigned short Tmp = X;
  Tmp = Tmp << 8;
  Tmp = Tmp | Y;
  return Tmp;
}

计算 IP 数据报的校验和

这个函数主要通过遍历 IP 头中的每两个字节,将它们合并为一个16位整数,并逐步累加到校验和中。在每次累加时,还需要检查是否发生了溢出,如果溢出则需要额外加1。最后,对累加得到的校验和进行取反操作,得到最终的 IP 校验和,并将其返回。这种校验和计算通常用于验证 IP 数据报的完整性。

下面是这段代码的解释:

// 计算IP校验和
unsigned short CalculateIPChecksum(UINT TotalLen, UINT ID, UINT SourceIP, UINT DestIP)
{
  // 初始化校验和
  unsigned short CheckSum = 0;
  // 遍历 IP 头的每两个字节
  for (int i = 14; i < 34; i += 2)
  {
    // 将每两个字节合并为一个16位整数
    unsigned short Tmp = BytesTo16(FinalPacket[i], FinalPacket[i + 1]);
    // 计算校验和
    unsigned short Difference = 65535 - CheckSum;
    CheckSum += Tmp;
    // 处理溢出
    if (Tmp > Difference) { CheckSum += 1; }
  }
  // 取反得到最终的校验和
  CheckSum = ~CheckSum;
  return CheckSum;
}

计算 UDP 数据报的校验和

这个函数主要通过构造 UDP 数据报的伪首部,包括源 IP、目标 IP、协议类型(UDP)、UDP 长度、源端口、目标端口以及 UDP 数据等字段,并通过遍历伪首部的每两个字节计算校验和。最后取反得到最终的 UDP 校验和,并将其返回。这种校验和计算通常用于验证 UDP 数据报的完整性。

下面是这段代码的解释:

// 计算UDP校验和
unsigned short CalculateUDPChecksum(unsigned char* UserData, int UserDataLen, UINT SourceIP, UINT DestIP, USHORT SourcePort, USHORT DestinationPort, UCHAR Protocol)
{
  unsigned short CheckSum = 0;
  // 计算 UDP 数据报的伪首部长度
  unsigned short PseudoLength = UserDataLen + 8 + 9; // 长度包括 UDP 头(8字节)和伪首部(9字节)
  // 如果长度不是偶数,添加一个额外的字节
  PseudoLength += PseudoLength % 2;
  // 创建 UDP 伪首部
  unsigned char* PseudoHeader = new unsigned char[PseudoLength];
  RtlZeroMemory(PseudoHeader, PseudoLength);
  // 设置伪首部中的协议字段为 UDP (0x11)
  PseudoHeader[0] = 0x11;
  // 复制源和目标 IP 地址到伪首部
  memcpy((void*)(PseudoHeader + 1), (void*)(FinalPacket + 26), 8);
  // 将 UDP 头的长度字段拷贝到伪首部
  unsigned short Length = UserDataLen + 8;
  Length = htons(Length);
  memcpy((void*)(PseudoHeader + 9), (void*)&Length, 2);
  memcpy((void*)(PseudoHeader + 11), (void*)&Length, 2);
  // 将源端口、目标端口和 UDP 数据拷贝到伪首部
  memcpy((void*)(PseudoHeader + 13), (void*)(FinalPacket + 34), 2);
  memcpy((void*)(PseudoHeader + 15), (void*)(FinalPacket + 36), 2);
  memcpy((void*)(PseudoHeader + 17), (void*)UserData, UserDataLen);
  // 遍历伪首部的每两个字节,计算校验和
  for (int i = 0; i < PseudoLength; i += 2)
  {
    unsigned short Tmp = BytesTo16(PseudoHeader[i], PseudoHeader[i + 1]);
    unsigned short Difference = 65535 - CheckSum;
    CheckSum += Tmp;
    if (Tmp > Difference) { CheckSum += 1; }
  }
  // 取反得到最终的校验和
  CheckSum = ~CheckSum;
  // 释放伪首部的内存
  delete[] PseudoHeader;
  return CheckSum;
}

这段代码的分析:

需要注意的是,UDP校验和是一个16位的值,用于验证UDP数据报在传输过程中是否被修改。这段代码主要完成了构造UDP伪首部和计算校验和的过程。在实际网络通信中,校验和的计算是为了保证数据的完整性,防止在传输过程中的错误。

创建UDP数据包函数

创建一个UDP数据包,该代码是一个简单的网络编程示例,用于创建和发送UDP数据包。其中,UDP数据包的内容和头部信息都可以根据实际需求进行定制。

代码的概述:

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)
{
  UserDataLen = UserDataLength;
  FinalPacket = new unsigned char[UserDataLength + 42]; // 为数据长度加上42字节的标头保留足够的内存
  USHORT TotalLen = UserDataLength + 20 + 8;            // IP报头使用数据长度加上IP报头长度(通常为20字节)加上udp报头长度(通常为8字节)
  // 开始填充以太网包头
  memcpy((void*)FinalPacket, (void*)DestinationMAC, 6);
  memcpy((void*)(FinalPacket + 6), (void*)SourceMAC, 6);
  USHORT TmpType = 8;
  memcpy((void*)(FinalPacket + 12), (void*)&TmpType, 2);  // 使用的协议类型(USHORT)类型0x08是UDP。可以为其他协议(例如TCP)更改此设置
  // 开始填充IP头数据包
  memcpy((void*)(FinalPacket + 14), (void*)"\x45", 1);     // 前3位的版本(4)和最后5位的标题长度。
  memcpy((void*)(FinalPacket + 15), (void*)"\x00", 1);     // 通常为0
  TmpType = htons(TotalLen);
  memcpy((void*)(FinalPacket + 16), (void*)&TmpType, 2);
  TmpType = htons(0x1337);
  memcpy((void*)(FinalPacket + 18), (void*)&TmpType, 2);    // Identification
  memcpy((void*)(FinalPacket + 20), (void*)"\x00", 1);      // Flags
  memcpy((void*)(FinalPacket + 21), (void*)"\x00", 1);      // Offset
  memcpy((void*)(FinalPacket + 22), (void*)"\x80", 1);      // Time to live.
  memcpy((void*)(FinalPacket + 23), (void*)"\x11", 1);      // 协议UDP为0x11(17)TCP为6 ICMP为1等
  memcpy((void*)(FinalPacket + 24), (void*)"\x00\x00", 2);  // 计算校验和
  memcpy((void*)(FinalPacket + 26), (void*)&SourceIP, 4);   //inet_addr does htonl() for us
  memcpy((void*)(FinalPacket + 30), (void*)&DestIP, 4);
  // 开始填充UDP头部数据包
  TmpType = htons(SourcePort);
  memcpy((void*)(FinalPacket + 34), (void*)&TmpType, 2);
  TmpType = htons(DestinationPort);
  memcpy((void*)(FinalPacket + 36), (void*)&TmpType, 2);
  USHORT UDPTotalLen = htons(UserDataLength + 8); // UDP Length does not include length of IP header
  memcpy((void*)(FinalPacket + 38), (void*)&UDPTotalLen, 2);
  //memcpy((void*)(FinalPacket+40),(void*)&TmpType,2); //checksum
  memcpy((void*)(FinalPacket + 42), (void*)UserData, UserDataLength);
  unsigned short UDPChecksum = CalculateUDPChecksum(UserData, UserDataLength, SourceIP, DestIP, htons(SourcePort), htons(DestinationPort), 0x11);
  memcpy((void*)(FinalPacket + 40), (void*)&UDPChecksum, 2);
  unsigned short IPChecksum = htons(CalculateIPChecksum(TotalLen, 0x1337, SourceIP, DestIP));
  memcpy((void*)(FinalPacket + 24), (void*)&IPChecksum, 2);
  return;
}

对该代码的分析:

需要注意的是,这段代码中的硬编码可能需要根据实际需求进行修改,例如协议类型、标识、生存时间等。此外,计算校验和是网络协议中用于检测数据完整性的一种机制。

发送UDP数据包

代码演示了如何打开网卡,生成UDP数据包,并通过pcap_sendpacket函数发送数据包到网络。需要注意的是,数据包的内容和地址是硬编码的,实际应用中可能需要根据需要进行更改。

int main(int argc, char* argv[])
{
	// 打开网卡
	OpenAdapter("10.0.66.24");
	// 填充地址并生成数据包包头
	char SourceMAC[MAX_PATH] = "8C-ff-ff-ff-ff-ff";
	char SourceIP[MAX_PATH] = "192.168.93.11";
	char SourcePort[MAX_PATH] = "80";
	char DestinationMAC[MAX_PATH] = "8C-dd-dd-dd-dd-dd";
	char DestinationIP[MAX_PATH] = "192.168.93.11";
	char DestinationPort[MAX_PATH] = "8080";
	char DataString[MAX_PATH] = "hello lyshark";
	CreatePacket(MACStringToBytes(SourceMAC), MACStringToBytes(DestinationMAC), inet_addr(SourceIP), inet_addr(DestinationIP), atoi(SourcePort), atoi(DestinationPort), (UCHAR*)DataString, (strlen(DataString) + 1));
	// 循环发包
	for (int x = 0; x < 10; x++)
	{
		if (0 != pcap_sendpacket(m_adhandle, FinalPacket, (UserDataLen + 42)))
		{
			char* szErr = pcap_geterr(m_adhandle);
			return 0;
		}
	}
	system("pause");
	return 0;
}

打开wireshark抓包工具,过滤目标地址为ip.dst==192.168.93.11然后抓包,运行编译后的程序,则你会看到我们自己构建的数据包被发送了10次,如下图所示;

随便打开一个数据包看下结构,源地址目标地址均是伪造的地址,数据包中的内容是hello lyshark,如下图所示;

到此这篇关于C/C++ 运用Npcap发送UDP数据包的文章就介绍到这了,更多相关C++ 发送UDP数据包内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文