原生套接字抓包的实现原理依赖于Windows系统中提供的ioctlsocket函数,该函数可将指定的网卡设置为混杂模式,网卡混杂模式(Promiscuous Mode)是常用于计算机网络抓包的一种模式,也称为监听模式。在混杂模式下,网卡可以收到经过主机的所有数据包,而非只接收它所对应的MAC地址的数据包。

一般情况下,网卡会根据MAC地址过滤数据包,只有MAC地址与网卡所对应的设备的通信数据包才会被接收和处理,其他数据包则会被忽略。但在混杂模式下,网卡会接收经过它所连接的网络中所有的数据包,这些数据包可以是面向其他设备的通信数据包、广播数据包或多播数据包等。

混杂模式可以通过软件驱动程序或网卡硬件实现。启用混杂模式的主要用途之一是网络抓包分析,使用混杂模式可以捕获网络中所有的数据包,且不仅仅是它所连接的设备的通信数据包。因此,可以完整获取网络中的通信内容,便于进行网络监控、安全风险感知、漏洞检测等操作。

Windows系统下,开启混杂模式可以使用ioctlsocket()函数,该函数原型定义如下:

int ioctlsocket (
SOCKET s, //要操作的套接字
long cmd, //操作代码
u_long *argp //指向操作参数的指针
);

其中,参数说明如下:

  • s: 要执行I/O控制操作的套接字。
  • cmd: 操作代码,用于控制对套接字的特定操作。
  • argp: 与特定请求代码相关联的参数指针。此参数的具体含义取决于请求代码。

在该函数中,参数cmd指定了I/O控制操作代码,是一个整数值,用于控制对套接字的特定操作。argp是一个指向特定请求代码相关联的参数的指针,它的具体含义将取决于请求代码。函数返回值为int类型,表示函数执行结果的状态码,若函数执行成功,则其返回值为0,否则返回一个错误代码,并将错误原因存入errno变量中。

要实现抓包前提是需要先选中绑定到那个网卡,如下InitAndSelectNetworkRawSocket函数则是实现绑定套接字到特定网卡的实现流程,在代码中首先初始化并使用gethostname函数获取到当前主机的主机名,主机IP地址等基本信息,接着通过循环的方式将自身网卡信息追加到g_HostIp全局结构体内进行存储,通过使用一个交互式选择菜单让用户可以选中需要绑定的网卡名称,当用户选中后则下一步是绑定套接字,并通过调用ioctlsocket函数将网卡设置为混杂模式,至此网卡的绑定工作就算结束了,当读者需要操作时只需要对全局变量进行操作即可,而选择函数仅仅只是获取到网卡信息而已并没有实际的作用。

#include <iostream>
#include <WinSock2.h>
#include <ws2tcpip.h>
#include <mstcpip.h> #pragma comment(lib, "ws2_32.lib") // 全局结构
typedef struct
{
int iLen;
char szIPArray[10][50];
}HOSTIP; // 全局变量
SOCKET g_RawSocket = 0;
HOSTIP g_HostIp; // -------------------------------------------------------
// 初始化与选择套接字
// -------------------------------------------------------
BOOL InitAndSelectNetworkRawSocket()
{
// 设置套接字版本
WSADATA wsaData = { 0 };
if (0 != WSAStartup(MAKEWORD(2, 2), &wsaData))
{
return FALSE;
}
// 创建原始套接字
// Windows无法抓取RawSocket MAC层的数据包,只能抓到IP层及以上的数据包
g_RawSocket = socket(AF_INET, SOCK_RAW, IPPROTO_IP);
// g_RawSocket = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
if (INVALID_SOCKET == g_RawSocket)
{
WSACleanup();
return FALSE;
} // 绑定到接口 获取本机名
char szHostName[MAX_PATH] = { 0 };
if (SOCKET_ERROR == ::gethostname(szHostName, MAX_PATH))
{
closesocket(g_RawSocket);
WSACleanup();
return FALSE;
} // 根据本机名获取本机IP地址
hostent* lpHostent = ::gethostbyname(szHostName);
if (NULL == lpHostent)
{
closesocket(g_RawSocket);
WSACleanup();
return FALSE;
} // IP地址转换并保存IP地址
g_HostIp.iLen = 0;
strcpy(g_HostIp.szIPArray[g_HostIp.iLen], "127.0.0.1");
g_HostIp.iLen++;
char* lpszHostIP = NULL; while (NULL != (lpHostent->h_addr_list[(g_HostIp.iLen - 1)]))
{
lpszHostIP = inet_ntoa(*(in_addr*)lpHostent->h_addr_list[(g_HostIp.iLen - 1)]);
strcpy(g_HostIp.szIPArray[g_HostIp.iLen], lpszHostIP);
g_HostIp.iLen++;
} // 选择IP地址对应的网卡来嗅探
printf("选择侦听网卡 \n\n");
for (int i = 0; i < g_HostIp.iLen; i++)
{
printf("\t [*] 序号: %d \t IP地址: %s \n", i, g_HostIp.szIPArray[i]);
} printf("\n 选择网卡序号: ");
int iChoose = 0;
scanf("%d", &iChoose); // 如果选择超出范围则直接终止
if ((0 > iChoose) || (iChoose >= g_HostIp.iLen))
{
exit(0);
}
if ((0 <= iChoose) && (iChoose < g_HostIp.iLen))
{
lpszHostIP = g_HostIp.szIPArray[iChoose];
} // 构造地址结构
sockaddr_in SockAddr = { 0 };
RtlZeroMemory(&SockAddr, sizeof(sockaddr_in));
SockAddr.sin_addr.S_un.S_addr = inet_addr(lpszHostIP);
SockAddr.sin_family = AF_INET;
SockAddr.sin_port = htons(0); // 绑定套接字
if (SOCKET_ERROR == bind(g_RawSocket, (sockaddr*)(&SockAddr), sizeof(sockaddr_in)))
{
closesocket(g_RawSocket);
WSACleanup();
return FALSE;
} // 设置混杂模式 抓取所有经过网卡的数据包
DWORD dwSetVal = 1;
if (SOCKET_ERROR == ioctlsocket(g_RawSocket, SIO_RCVALL, &dwSetVal))
{
closesocket(g_RawSocket);
WSACleanup();
return FALSE;
}
return TRUE;
} int main(int argc, char *argv[])
{
// 选择网卡并设置网络为非阻塞模式
BOOL SelectFlag = InitAndSelectNetworkRawSocket();
if (SelectFlag == TRUE)
{
printf("[*] 网卡已被选中 套接字ID = %d | 套接字IP = %s \n", g_RawSocket,g_HostIp.szIPArray);
} system("pause");
return 0;
}

读者可自行编译并以管理员身份运行上述代码片段,当读者运行后会看到如下图所示的代码片段,此处笔者就选择三号网卡进行绑定操作,当绑定后此时套接字ID对应的则是特定的网卡,后续的操作均可针对此套接字ID进行,如下图所示;

当读者有了设置混杂模式的功能则下一步就是抓包了,抓包的实现很简单,只需要在开启了非阻塞混杂模式的网卡上使用recvfrom函数循环进行监听即可,当有数据包产生时则直接输出iRecvBytes中所存储的数据即可,这段代码的实现如下所示;

int main(int argc, char *argv[])
{
// 选择网卡并设置网络为非阻塞模式
BOOL init_flag = InitAndSelectNetworkRawSocket();
if (init_flag == FALSE)
{
return 0;
} sockaddr_in RecvAddr = { 0 };
int iRecvBytes = 0;
int iRecvAddrLen = sizeof(sockaddr_in); // 定义缓冲区长度
DWORD dwBufSize = 12000;
BYTE* lpRecvBuf = new BYTE[dwBufSize]; // 循环接收接收
while (1)
{
RtlZeroMemory(&RecvAddr, iRecvAddrLen);
iRecvBytes = recvfrom(g_RawSocket, (char*)lpRecvBuf, dwBufSize, 0, (sockaddr*)(&RecvAddr), &iRecvAddrLen);
if (0 < iRecvBytes)
{
// 接收数据包并输出
printf("[接收数据包] %s \n", lpRecvBuf);
}
} // 释放内存
delete[]lpRecvBuf;
lpRecvBuf = NULL; // 关闭套接字
Sleep(500);
closesocket(g_RawSocket);
WSACleanup();
return 0;
}

当读者选择网卡后即可看到如下所示的输出结果,这些数据则是经过网卡192.168.9.125的所有数据,由于此处没有解码和区分数据包类型所以显示出的字符串并没有任何意义,如下图所示;

接下来我们就需要根据不同的数据包类型对这些数据进行解包操作,在解包之前我们需要先来定义几个关键的数据包结构体,如下代码中ether_header代表的是以太网包头结构,该结构占用14个字节的存储空间,arp_header则是ARP结构体,该结构体占用28个字节,ARK结构中还存在一个ARK报文结构,该结构占用42字节的内存长度,接着分别顶一个ipv4_headeripv6_headertcp_headerudp_header等结构体,这些结构体的完整定义如下所示;

#pragma pack(1)

/*以太网帧头格式结构体 14个字节*/
typedef struct ether_header
{
unsigned char ether_dhost[6]; // 目的MAC地址
unsigned char ether_shost[6]; // 源MAC地址
unsigned short ether_type; // eh_type 的值需要考察上一层的协议,如果为ip则为0x0800
}ETHERHEADER, * PETHERHEADER; /*以ARP字段结构体 28个字节*/
typedef struct arp_header
{
unsigned short arp_hrd;
unsigned short arp_pro;
unsigned char arp_hln;
unsigned char arp_pln;
unsigned short arp_op;
unsigned char arp_sourha[6];
unsigned long arp_sourpa;
unsigned char arp_destha[6];
unsigned long arp_destpa;
}ARPHEADER, * PARPHEADER; /*ARP报文结构体 42个字节*/
typedef struct arp_packet
{
ETHERHEADER etherHeader;
ARPHEADER arpHeader;
}ARPPACKET, * PARPPACKET; /*IPv4报头结构体 20个字节*/
typedef struct ipv4_header
{
unsigned char ipv4_ver_hl; // Version(4 bits) + Internet Header Length(4 bits)长度按4字节对齐
unsigned char ipv4_stype; // 服务类型
unsigned short ipv4_plen; // 总长度(包含IP数据头,TCP数据头以及数据)
unsigned short ipv4_pidentify; // ID定义单独IP
unsigned short ipv4_flag_offset; // 标志位偏移量
unsigned char ipv4_ttl; // 生存时间
unsigned char ipv4_pro; // 协议类型
unsigned short ipv4_crc; // 校验和
unsigned long ipv4_sourpa; // 源IP地址
unsigned long ipv4_destpa; // 目的IP地址
}IPV4HEADER, * PIPV4HEADER; /*IPv6报头结构体 40个字节*/
typedef struct ipv6_header
{
unsigned char ipv6_ver_hl;
unsigned char ipv6_priority;
unsigned short ipv6_lable;
unsigned short ipv6_plen;
unsigned char ipv6_nextheader;
unsigned char ipv6_limits;
unsigned char ipv6_sourpa[16];
unsigned char ipv6_destpa[16];
}IPV6HEADER, * PIPV6HEADER; /*TCP报头结构体 20个字节*/
typedef struct tcp_header
{
unsigned short tcp_sourport; // 源端口
unsigned short tcp_destport; // 目的端口
unsigned long tcp_seqnu; // 序列号
unsigned long tcp_acknu; // 确认号
unsigned char tcp_hlen; // 4位首部长度
unsigned char tcp_reserved; // 标志位
unsigned short tcp_window; // 窗口大小
unsigned short tcp_chksum; // 检验和
unsigned short tcp_urgpoint; // 紧急指针
}TCPHEADER, * PTCPHEADER; /*UDP报头结构体 8个字节*/
typedef struct udp_header
{
unsigned short udp_sourport; // 源端口
unsigned short udp_destport; // 目的端口
unsigned short udp_hlen; // 长度
unsigned short udp_crc; // 校验和
}UDPHEADER, * PUDPHEADER;
#pragma pack()

当有了结构体的定义部分,则实现对数据包的解析只需要判断数据包的类型并使用不同的结构体对数据包进行解包打印即可,如下是实现数据包解析的完整代码,在代码中分别实现了几个核心函数,其中printData函数可以实现对特定内存数据的十六进制格式输出方便检查输出效果,函数AnalyseRecvPacket_All用于解析除去TCP/UDP格式的其他数据包,AnalyseRecvPacket_TCP用于解析TCP数据,AnalyseRecvPacket_UDP用于解析UDP数据,在主函数中通过使用ip->ipv4_pro判断数据包的具体类型,并根据类型的不同依次调用不同的函数实现数据包解析。

// 输出数据包
void PrintData(BYTE* lpBuf, int iLen, int iPrintType)
{
// 16进制
if (0 == iPrintType)
{
for (int i = 0; i < iLen; i++)
{
if ((0 == (i % 8)) && (0 != i))
{
printf(" ");
}
if ((0 == (i % 16)) && (0 != i))
{
printf("\n");
}
printf("%02x ", lpBuf[i]); }
printf("\n");
}
// ASCII编码
else if (1 == iPrintType)
{
for (int i = 0; i < iLen; i++)
{
printf("%c", lpBuf[i]);
}
printf("\n");
}
} // 解析所有其他数据包
void AnalyseRecvPacket_All(BYTE* lpBuf)
{
struct sockaddr_in saddr, daddr;
PIPV4HEADER ip = (PIPV4HEADER)lpBuf;
saddr.sin_addr.s_addr = ip->ipv4_sourpa;
daddr.sin_addr.s_addr = ip->ipv4_destpa; printf("From:%s --> ", inet_ntoa(saddr.sin_addr));
printf("To:%s\n", inet_ntoa(daddr.sin_addr));
} // 解析TCP数据包
void AnalyseRecvPacket_TCP(BYTE* lpBuf)
{
struct sockaddr_in saddr, daddr;
PIPV4HEADER ip = (PIPV4HEADER)lpBuf;
PTCPHEADER tcp = (PTCPHEADER)(lpBuf + (ip->ipv4_ver_hl & 0x0F) * 4);
int hlen = (ip->ipv4_ver_hl & 0x0F) * 4 + tcp->tcp_hlen * 4; // 这里要将网络字节序转换为本地字节序
int dlen = ntohs(ip->ipv4_plen) - hlen;
saddr.sin_addr.s_addr = ip->ipv4_sourpa;
daddr.sin_addr.s_addr = ip->ipv4_destpa; printf("From:%s:%d --> ", inet_ntoa(saddr.sin_addr), ntohs(tcp->tcp_sourport));
printf("To:%s:%d ", inet_ntoa(daddr.sin_addr), ntohs(tcp->tcp_destport));
printf("ack:%u syn:%u length=%d\n", tcp->tcp_acknu, tcp->tcp_seqnu, dlen); PrintData((lpBuf + hlen), dlen, 0);
} // 解析UDP数据包
void AnalyseRecvPacket_UDP(BYTE* lpBuf)
{
struct sockaddr_in saddr, daddr;
PIPV4HEADER ip = (PIPV4HEADER)lpBuf;
PUDPHEADER udp = (PUDPHEADER)(lpBuf + (ip->ipv4_ver_hl & 0x0F) * 4);
int hlen = (int)((ip->ipv4_ver_hl & 0x0F) * 4 + sizeof(UDPHEADER));
int dlen = (int)(ntohs(udp->udp_hlen) - 8); // int dlen = (int)(udp->udp_hlen - 8);
saddr.sin_addr.s_addr = ip->ipv4_sourpa;
daddr.sin_addr.s_addr = ip->ipv4_destpa;
printf("Protocol:UDP ");
printf("From:%s:%d -->", inet_ntoa(saddr.sin_addr), ntohs(udp->udp_sourport));
printf("To:%s:%d\n", inet_ntoa(daddr.sin_addr), ntohs(udp->udp_destport)); PrintData((lpBuf + hlen), dlen, 0);
} int main(int argc, char* argv[])
{
// 选择网卡,并设置网络为非阻塞模式
InitAndSelectNetworkRawSocket(); sockaddr_in RecvAddr = { 0 };
int iRecvBytes = 0;
int iRecvAddrLen = sizeof(sockaddr_in);
DWORD dwBufSize = 12000;
BYTE* lpRecvBuf = new BYTE[dwBufSize]; // 循环接收接收
while (1)
{
RtlZeroMemory(&RecvAddr, iRecvAddrLen);
iRecvBytes = recvfrom(g_RawSocket, (char*)lpRecvBuf, dwBufSize, 0, (sockaddr*)(&RecvAddr), &iRecvAddrLen);
if (0 < iRecvBytes)
{
// 接收数据包解码输出
// 分析IP包的协议类型
PIPV4HEADER ip = (PIPV4HEADER)lpRecvBuf;
switch (ip->ipv4_pro)
{
case IPPROTO_ICMP:
{
// 分析ICMP
printf("[ICMP]\n");
AnalyseRecvPacket_All(lpRecvBuf);
break;
}
case IPPROTO_IGMP:
{
// 分析IGMP
printf("[IGMP]\n");
AnalyseRecvPacket_All(lpRecvBuf);
break;
}
case IPPROTO_TCP:
{
// 分析tcp协议
printf("[TCP]\n");
AnalyseRecvPacket_TCP(lpRecvBuf);
break;
}
case IPPROTO_UDP:
{
// 分析udp协议
printf("[UDP]\n");
AnalyseRecvPacket_UDP(lpRecvBuf);
break;
}
default:
{
// 其他数据包
printf("[OTHER IP]\n");
AnalyseRecvPacket_All(lpRecvBuf);
break;
}
}
}
} // 释放内存
delete[]lpRecvBuf;
lpRecvBuf = NULL; // 关闭套接字
Sleep(500);
closesocket(g_RawSocket);
WSACleanup();
return 0;
}

读者可自行编译并运行上述代码片段,当程序运行后可自行选择希望监控的网卡,当程序中检测到TCP数据包后会输出如下图所示的提示信息,在图中我们可以清晰的看出数据包的流向信息,以及数据包长度数据包内的数据等;

当读者通过使用Ping命令探测目标主机时,此时同样可以抓取到ICMP相关的数据流,只是在数据解析时并没有太规范导致只能看到简单的流向,当然读者也可以自行完善这段代码,让其能够解析更多参数。

本文作者: 王瑞

本文链接: https://www.lyshark.com/post/8e15eea.html

版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

18.1 Socket 原生套接字抓包的更多相关文章

  1. python 全栈开发,Day33(tcp协议和udp协议,互联网协议与osi模型,socket概念,套接字(socket)初使用)

    先来回顾一下昨天的内容 网络编程开发架构 B/S C/S架构网卡 mac地址网段 ip地址 : 表示了一台电脑在网络中的位置 子网掩码 : ip和子网掩码按位与得到网段 网关ip : 内置在路由器中的 ...

  2. iOS - Socket 网络套接字

    1.Socket 网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个 Socket.Socket 又称 "套接字",应用程序通常通过 "套接字& ...

  3. Socket称"套接字"

    Socket又称"套接字",应用程序通常通过"套接字"向网络发出请求或者应答网络请求. 二.利用Socket建立网络连接的步骤 建立Socket连接至少需要一对 ...

  4. Win2 Socket(套接字)相关 API

    Socket(套接字) 作者信息 肖进 单位:南京中萃食品有限公司 资讯部 邮箱:xiaoj@njb.swirebev.com 电话:025-58642091 与socket有关的一些函数介绍 1.读 ...

  5. socket概念 套接字

    理解socket soxket因为TCP是面向流的,你发的信息如果很多很快,TCP这样就会形成黏包 Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口.在设计模式中,Socke ...

  6. sanic官方文档解析之Custom Protocols(自定义协议)和Socket(网络套接字)

    1,Custom Protocol:自定义协议 温馨提示:自定义协议是一个高级用法,大多数的读者不需要用到此功能 通过特殊的自定义协议,你可以改变sanic的协议,自定义协议需要继承子类asyncio ...

  7. 网络编程(socket,套接字)

    服务端地址不变 ip + mac 标识唯一一台机器 ip +端口 标识唯一客户端应用程序 套接字: 网络编程   网络编程 一.python提供了两个级别访问的网络服务 低级别的网络服务支持基本的 S ...

  8. Linux原始套接字抓取底层报文

    1.原始套接字使用场景 我们平常所用到的网络编程都是在应用层收发数据,每个程序只能收到发给自己的数据,即每个程序只能收到来自该程序绑定的端口的数据.收到的数据往往只包括应用层数据,原有的头部信息在传递 ...

  9. Linux Socket 原始套接字编程

    对于linux网络编程来说,可以简单的分为标准套接字编程和原始套接字编程,标准套接字主要就是应用层数据的传输,原始套接字则是可以获得不止是应用层的其他层不同协议的数据.与标准套接字相区别的主要是要开发 ...

  10. Python之socket(套接字)

    Socket 一.概述 socket通常也称作"套接字",用于描述IP地址和端口,是一个通信链的句柄,应用程序通常通过"套接字"向网络发出请求或者应答网络请求. ...

随机推荐

  1. 🎉Avalonia 11.0.0 正式版发布

    Avalonia 11.0.0 正式版发布! AvaloniaUI 发布11.0.0正式版 终于avalonia发布了正式版. 更新内容 A11y(辅助功能) 这个版本的Avalonia在使应用程序更 ...

  2. 6月有奖征文挑战,ZEGO开发者社区首季活动报名入口!

    前 言 哈喽 开发者们: ZEGO即构科技作为一家20年技术积累的音视频云服务商,已经为全球200+个国家的企业服务,单日通话时长突破30亿+分钟,现下即构开发者社区举办首期征文活动!本次征文活动围绕 ...

  3. 用 Golang 从0到1实现一个高性能的 Worker Pool(一) - 每天5分钟玩转 GPT 编程系列(3)

    目录 1. 概述 2. 设计 2.1 让 GPT-4 给出功能点 2.2 自己总结需求,再给 GPT 派活 3. 实现 3.1 你先随意发挥 3.2 你得让 Worker 跑起来呀 3.3 你说说 P ...

  4. 分享我的 Shell 环境,git 操作效率提升 100% !

    每当我换到一个新的开发环境,蛮多东西要折腾的.比如 git.golang.环境变量等等.所以特地整理了一下,下次换新电脑也方便. ​ 本文分享我在工作中常用的环境变量 + Shell alias:比如 ...

  5. SpringBoot整合Websocket,实现作为客户端接收消息的同时作为服务端向下游客户发送消息

    SpringBoot整合Websocket 1. SpringBoot作为服务端 作为服务端时,需要先导入websocket的依赖 <dependency> <groupId> ...

  6. MyBatis(log4j)

    log4j介绍 Log4j是Apache的一个开源项目,通过使用Log4j,我们可以控制日志信息输送的目的地是控制台.文件.GUI组件,甚至是套接口服务器.NT的事件记录器.UNIX Syslog守护 ...

  7. k8s+containerd安装

    准备环境 准备两台服务器节点,如果需要安装虚拟机,可以参考<wmware和centos安装过程> 机器名 IP 角色 CPU 内存 centos01 192.168.109.130 mas ...

  8. grafana Variables 变量的使用

    概念澄清 A variable is a placeholder for a value. # 变量是值的占位符. # 参考文档:https://grafana.com/docs/grafana/la ...

  9. 代码随想录算法训练营第一天| LeetCode 704. 二分查找、LeetCode 27. 移除元素

    704. 二分查找         题目链接:https://leetcode.cn/problems/binary-search/       视频链接:https://www.bilibili.c ...

  10. Mysql高级5-SQL优化

    一.插入数据优化 1.1 批量插入 如果有多条数据需要同时插入,不要每次插入一条,然后分多次插入,因为每执行一次插入的操作,都要进行数据库的连接,多个操作就会连接多次,而一次批量操作只需要连接1次 1 ...