原生套接字抓包的实现原理依赖于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; } g_RawSocket = socket(AF_INET, SOCK_RAW, IPPROTO_IP); 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; }
hostent* lpHostent = ::gethostbyname(szHostName); if (NULL == lpHostent) { closesocket(g_RawSocket); WSACleanup(); return FALSE; }
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++; }
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_header
,ipv6_header
,tcp_header
,udp_header
等结构体,这些结构体的完整定义如下所示;
#pragma pack(1)
typedef struct ether_header { unsigned char ether_dhost[6]; unsigned char ether_shost[6]; unsigned short ether_type; }ETHERHEADER, * PETHERHEADER;
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;
typedef struct arp_packet { ETHERHEADER etherHeader; ARPHEADER arpHeader; }ARPPACKET, * PARPPACKET;
typedef struct ipv4_header { unsigned char ipv4_ver_hl; unsigned char ipv4_stype; unsigned short ipv4_plen; unsigned short ipv4_pidentify; unsigned short ipv4_flag_offset; unsigned char ipv4_ttl; unsigned char ipv4_pro; unsigned short ipv4_crc; unsigned long ipv4_sourpa; unsigned long ipv4_destpa; }IPV4HEADER, * PIPV4HEADER;
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;
typedef struct tcp_header { unsigned short tcp_sourport; unsigned short tcp_destport; unsigned long tcp_seqnu; unsigned long tcp_acknu; unsigned char tcp_hlen; unsigned char tcp_reserved; unsigned short tcp_window; unsigned short tcp_chksum; unsigned short tcp_urgpoint; }TCPHEADER, * PTCPHEADER;
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) { 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"); } 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)); }
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); }
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);
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) { PIPV4HEADER ip = (PIPV4HEADER)lpRecvBuf; switch (ip->ipv4_pro) { case IPPROTO_ICMP: { printf("[ICMP]\n"); AnalyseRecvPacket_All(lpRecvBuf); break; } case IPPROTO_IGMP: { printf("[IGMP]\n"); AnalyseRecvPacket_All(lpRecvBuf); break; } case IPPROTO_TCP: { printf("[TCP]\n"); AnalyseRecvPacket_TCP(lpRecvBuf); break; } case IPPROTO_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
相关的数据流,只是在数据解析时并没有太规范导致只能看到简单的流向,当然读者也可以自行完善这段代码,让其能够解析更多参数。