简单的网络嗅探器

网卡的四种工作模式
①广播模式:该模式下的网卡能够接收网络中的广播信息;
②组播模式:设置在该模式下的网卡能够接收组播数据;
③直接模式:在这种模式下,只有目的网卡才能接收该数据;
④混杂模式:在这种模式下的网卡能够接收一切通过它的数据,而不管该数据是否是传给它的

嗅探器原理:
嗅探器作为一种网络通讯程序,也是通过对网卡的编程来实现网络通讯的,对网卡的编程也是使用通常的套接字(socket)方式来进行。但是,通常的套接字程序只能响应与自己硬件地址相匹配的或是以广播形式发出的数据帧,对于其他形式的数据帧比如已到达网络接口但却不是发给此地址的数据帧,网络接口在验证投递地址并非自身地址之后将不引起响应,也就是说应用程序无法收取到达的数据包。而网络嗅探器的目的恰恰在于从网卡接收所有经过它的数据包,这些数据包即可以是发给它的也可以是发往别处的。显然,要达到此目的就不能再让网卡按通常的正常模式工作,而必须将其设置为混杂模式。
具体到编程实现上,这种对网卡混杂模式的设置是通过原始套接字(rawsocket)来实现的,这也有别于通常经常使用的数据流套接字和数据报套接字。在创建了原始套接字后,需要通过setsockopt()函数来设置IP头操作选项,然后再通过bind()函数将原始套接字绑定到本地网卡。为了让原始套接字能接受所有的数据,还需要通过ioctlsocket()来进行设置,而且还可以指定是否亲自处理IP头。至此,实际就可以开始对网络数据包进行嗅探了,对数据包的获取仍象流式套接字或数据报套接字那样通过recv()函数来完成。但是与其他两种套接字不同的是,原始套接字此时捕获到的数据包并不仅仅是单纯的数据信息,而是包含有IP头、TCP头等信息头的最原始的数据信息,这些信息保留了它在网络传输时的原貌。通过对这些在低层传输的原始信息的分析可以得到有关网络的一些信息。由于这些数据经过了网络层和传输层的打包,因此需要根据其附加的帧头对数据包进行分析。

代码详解
首先创建原始套接字,将原始套接字绑定到一个明确的本地地址(不能为ADDR_ANY),然后通过ioctlsocket()函数(是控制套接口的模式),向套接字发送SIO_RCVALL控制命令,将网卡设置为混杂模式,这样就能接收到所有经过网卡的封包;然后已连接的数据报或流式套接口通过不断调用recv()函数来接收IP数据包,将接收到的数据存入缓冲数组;随后再通过自定义的IP数据包解析函数对接收到的数据进行解析,通过定义的IP地址结构体的指针对象从接收到的数据中取得数据包的IP头;然后从IP头中取出源IP地址和目的IP地址。
在这里插入图片描述

IP
在这里插入图片描述

TCP
在这里插入图片描述

注意先后顺序,后面之所以可以直接调用
在这里插入图片描述
是因为在定义IP TCP头的时候用了unsigned short/long/char/… 他们的长度是固定的,这样在调用的时候可以直接调用属性,而不用写其他的return方法;

/* 
定义协议格式
定义协议中使用的宏
*/

#ifndef __PROTOINFO_H__ 
#define __PROTOINFO_H__ 
#define ETHERTYPE_IP    0x0800 

// 协议 
typedef struct _IPHeader        // 20字节的IP头 
{ 
	unsigned char     iphVerLen;      // 版本号和头长度(各占4位) 
	unsigned char    ipTOS;          // 服务类型  
	unsigned short    ipLength;       // 封包总长度,即整个IP报的长度 
	unsigned short    ipID;           // 封包标识,惟一标识发送的每一个数据报 
	unsigned short    ipFlags;        // 标志 
	unsigned char     ipTTL;          // 生存时间,就是TTL 
	unsigned char    ipProtocol;     // 协议,可能是TCP、UDP、ICMP等 
	unsigned short    ipChecksum;     // 校验和 
	unsigned long     ipSource;       // 源IP地址 
	unsigned long     ipDestination;  // 目标IP地址 
} IPHeader, *PIPHeader;
 

// 定义TCP标志 
#define   TCP_FIN   0x01
#define   TCP_SYN   0x02
#define   TCP_RST   0x04
#define   TCP_PSH   0x08
#define   TCP_ACK   0x10
#define   TCP_URG   0x20
#define   TCP_ACE   0x40
#define   TCP_CWR   0x80 

typedef struct _TCPHeader       // 20字节的TCP头
{
	unsigned short  sourcePort;         // 16位源端口号
	unsigned short  destinationPort;    // 16位目的端口号
	unsigned long   sequenceNumber;     // 32位序列号
	unsigned long   acknowledgeNumber;  // 32位确认号
	unsigned char   dataoffset;         // 高4位表示数据偏移
	unsigned char   flags;              // 6位标志位
	unsigned short  windows;            // 16位窗口大小
	unsigned short  checksum;           // 16位校验和
	unsigned short  urgentPointer;      // 16位紧急数据偏移量 
} TCPHeader, *PTCPHeader;

typedef struct _UDPHeader
{
	unsigned short          sourcePort;     // 源端口号     
	unsigned short          destinationPort;// 目的端口号      
	unsigned short          len;            // 封包长度
	unsigned short          checksum;       // 校验和
} UDPHeader, *PUDPHeader;

#endif  // __PROTOINFO_H#pragma once 

这个函数是必须要的,我们考虑过把它加进.cpp文件中,像之前socket一样,但是不行,个人以为是原始套接字的原因。


#include <winsock2.h> 
#pragma comment(lib, "ws2_32.lib")  // 链接到WS2_32.lib库文件 

class CInitSock { 
public:

	CInitSock(BYTE minorVer = 2, BYTE majorVer = 2) { 
		// 初始化WS2_32.dll 
		WSADATA wsaData; 
		WORD sockVersion = MAKEWORD(minorVer, majorVer);            //Winsock版本

/*为了在应用程序当中调用任何一个Winsock API函数,首先第一件事情就是必须通过WSAStartup函数完成对 Winsock服务的初始化,因此需要调用WSAStartup函数。
使用Socket的程序在使用Socket之前必须调用WSAStartup函数。
该函数的第一个参数指明程序请求使用的Socket版本,其中高位字节指明副版本、低位字节指明主版本;操作系统利用第二个参数返回请求的Socket的版本信息。当一个应用程序调用WSAStartup函数时,
操作系统根据请求的Socket版本来搜索相应的Socket库,然后绑定找到的Socket库到该应用程序中。以后应用程序就可以调用所请求的Socket库中的其它Socket函数了。*/
		if (::WSAStartup(sockVersion, &wsaData) != 0) {       //(Windows异步套接字的启动命令)加载套接字库 
			exit(0); 
		} 
	} 
	~CInitSock() { //析构函数 
		::WSACleanup();         //关闭加载的套接字库 
	}
};

#include "initsock.h"
#include "protoinfo.h" 
//#include <stdio.h>
#include <mstcpip.h>    //用于处理TCP/IP协议的一个头文件,这里面包含了WinSockets一系列相关函数.
#include<iostream>
//#include<stdlib.h>
#include <WinSock2.h>
#pragma comment(lib, "Advapi32.lib")    //链接Advapi32.lib库文件
#pragma comment(lib,"ws2_32.lib")
using namespace std;

//CInitSock theSock;      //创建CInitSock类的对象  
						//自定义的函数,用于解析收到的TCP数据封包,解析出封包中的TCP头

void DecodeTCPPacket(char *pData) { 
	
	TCPHeader *pTCPHdr = (TCPHeader *)pData;
	/*ntohs()是一个函数名,作用是将一个16位数由网络字节顺序转换为主机字节顺序*/

	cout<<" Port:"<<ntohs(pTCPHdr->sourcePort)<<" -> "<<ntohs(pTCPHdr->destinationPort)<<endl;

	// 下面还可以根据目的端口号进一步解析应用层协议
	switch (::ntohs(pTCPHdr->destinationPort)) {
	case 21: //FTP
		break;
	case 80: //网页 
	case 8080: //WWW代理服务  
		break; 
	}
}

void DecodeUDPPacket(char *pData)
{
	UDPHeader *pUDPHdr = (UDPHeader *)pData;
	/*ntohs()是一个函数名,作用是将一个16位数由网络字节顺序转换为主机字节顺序*/
	cout << " Port:" << ntohs(pUDPHdr->sourcePort) << " -> " << ntohs(pUDPHdr->destinationPort) << endl;
}

/*
自定义的函数,用于解析收到的IP数据封包,解析出封包中的IP头
*/

void DecodeIPPacket(char *pData) {
	IPHeader *pIPHdr = (IPHeader*)pData;
	in_addr source, dest;       //in_addr是一个结构体,可以用来表示一个32位的IPv4地址
	char szSourceIp[32], szDestIp[32];      // 定义两个数组用于存储源IP地址和目的IP地址

	cout<<"*************************************************************************"<<endl;

	// 从IP头中取出源IP地址和目的IP地址
		source.S_un.S_addr = pIPHdr->ipSource;      // 从IP头中取出源IP地址
	dest.S_un.S_addr = pIPHdr->ipDestination;       // 从IP头中取出目的IP地址
	strcpy(szSourceIp, ::inet_ntoa(source));
	strcpy(szDestIp, ::inet_ntoa(dest));

	cout<<szSourceIp<<" ---> "<<szDestIp<<endl;      //打印出源IP地址和目的IP地址
	cout << "版本号:"<<(int)pIPHdr->iphVerLen<<endl;
	cout << "服务类型:" <<(int) pIPHdr->ipTOS << endl;
	cout << "封包总长度:" << pIPHdr->ipLength << endl;
	cout << "封包标识:" << pIPHdr->ipID << endl;
	cout << "生存时间:" << (int)pIPHdr->ipTTL << endl;
	cout << "协议类型:" <<(int) pIPHdr->ipProtocol << endl;
	cout << "校验和:" << pIPHdr->ipChecksum << endl;					// IP头长度
	int nHeaderLen = (pIPHdr->iphVerLen & 0xf) * sizeof(ULONG);

	switch (pIPHdr->ipProtocol) 
	{
	case IPPROTO_TCP: // TCP协议
		DecodeTCPPacket(pData + nHeaderLen);
		break;

	case IPPROTO_UDP:
		DecodeUDPPacket(pData + nHeaderLen);
		break;

	case IPPROTO_ICMP:
		break;
	}
}  

int main() {
	// 创建原始套节字sRaw
	WSADATA wsaData;//WSADATA结构被用来保存函数WSAStartup返回的Windows Sockets初始化信息
	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) //加载套接字库
	{
		printf("Failed to load Winsock");
		return 0;
	}
	SOCKET sRaw = socket(AF_INET, SOCK_RAW, IPPROTO_IP);//必须在管理员模式下运行
	if (sRaw == SOCKET_ERROR) {
		printf("创建套接字失败:%d", WSAGetLastError());
	}

	// 获取本地IP地址
	char szHostName[56];
	SOCKADDR_IN addr_in;
	struct  hostent *pHost; //hostent是host entry的缩写,该结构记录主机的信息,包括主机名、别名、地址类型、地址长度和地址列表
	gethostname(szHostName, 56);

	if ((pHost = gethostbyname((char*)szHostName)) == NULL)
		cout << "get failed";

	// 在调用ioctl之前,套节字必须绑定
	addr_in.sin_family = AF_INET;
	addr_in.sin_port = htons(7000);
	//addr_in.sin_addr.S_un.S_addr = inet_addr(" 127.0.0.1"); 
	memcpy(&addr_in.sin_addr.S_un.S_addr, pHost->h_addr_list[0], pHost->h_length);
	cout<<" Binding to interface :"<<inet_ntoa(addr_in.sin_addr)<<endl;

	if (bind(sRaw, (PSOCKADDR)&addr_in, sizeof(addr_in)) == SOCKET_ERROR) //将原始套接字绑定到一个明确的本机地址
		cout << "bind failed"<<endl;

	// 设置SIO_RCVALL控制代码,以便接收所有的IP包  

	DWORD dwValue = 1;
	/*
	ioctlsocket()是控制套接口的模式,向套接字发送SIO_RCVALL控制命令,将网卡设置为混杂模式,这样就能接收到所有经过网卡的封包
	网卡的四种接收模式:
	①广播模式:该模式下的网卡能够接收网络中的广播信息;
	②组播模式:设置在该模式下的网卡能够接收组播数据;
	③直接模式:在这种模式下,只有目的网卡才能接收该数据;
	④混杂模式:在这种模式下的网卡能够接收一切通过它的数据,而不管该数据是否是传给它的
	*/

	if (ioctlsocket(sRaw, SIO_RCVALL, &dwValue) != 0) 向套接字发送SIO_RCVALL控制命令,将网卡设置为混杂模式,这样就能接收到所有经过网卡的封包
		cout << "ioct failed"<<endl;

	// 开始接收封包
	char buff[1024];        //用于接受数据的数组
	int nRet;
	while (true) 
	{
		nRet = recv(sRaw, buff, 1024, 0);   //recv函数接收消息出错的话,会返回 0
		if (nRet > 0) {
			DecodeIPPacket(buff);       //调用该函数对IP封包进行解析
		}

	}
	int a;
	system("pause");
	cin >> a;
	closesocket(sRaw);          //关闭原始套接字

	return 0;

}
  • 0
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值