原始套接字

1.1 原始套接字概述、创建
1.1.1 原始套接字概述

原始套接字(SOCK_RAW)
​ 1.一种不同于SOCK_STREAM、SOCK_DGRAM的套接字,它实现于系统核心
​ 2.可以接收本地网卡上所有的数据帧(数据包),对于监听网络流浪和分析网络数据
​ 3.开发人员可发送自己组装的数据包到网络上
​ 4.广泛应用于高级网络编程
​ 5.网络专家、黑客通常会用此来编写奇特的网络程序

流式套接字只能收发TCP协议的数据
数据报套接字只能收发UDP协议的数据
原始套接字可以收发
​ 1、内核没有处理的数据包,因此要访问其他协议;
​ 2、发送的数据需要使用,原始套接字(SOCK_RAW)

原始套接字_第1张图片
1.1.2 创建原始套接字
#include 
#include 
int socket(int domain,int type,int protocol);
功能:
	创建链路层的原始套接字,返回文件描述符
参数:
	domain:	通信域,地址族
			AF_PACKET
	type:	套接字类型
			SOCK_RAW
	protocol:(附加协议)指定可以接收或发送的数据包类型
			#include 
			ETH_P_IP:IPV4数据包
			ETH_P_ARP:ARP数据包
			ETH_P_ALL:任何协议类型的数据包
返回值:
	成功(>0:文件描述符
	失败(<0:-1
1.1.3 创建链路层的原始套接字
#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main(int argc, char const *argv[]){
	//使用socket函数创建链路层的原始套接字
	int sockfd;
	if((sockfd = socket(AF_PACKET,SOCK_RAW,htons(ETH_P_ALL))) == -1){
		perror("fail to sockfd");
		exit(1);
	}
	
	printf("sockfd = %d\n",sockfd);
	close(sockfd);
	return 0;
}

注意:原始套接字的代码运行时需要管理权权限

1.2 数据包详解

使用原始套接字进行编程开发时,首先要对不同协议的数据包进行学习,需要手动对IP、TCP、UDP、ICMP等包头进行组装或拆解

ubuntu16.04中描述网络协议的头文件如下:

原始套接字_第2张图片 原始套接字_第3张图片

在TCP/IP协议栈中的每一层为了能够正确解析出上层的数据包,从而使用一些“协议”来标记,具体如下:
原始套接字_第4张图片

1.2.1 组装/拆解UDP数据包流
原始套接字_第5张图片
1.2.2 UDP封包格式
原始套接字_第6张图片

1.源端口号:发送方端口号
2.目的端口号:接收方端口号
3.长度:UDP用户数据报的长度,最小值是8(仅有首部),注意,需要包括数据的长度
4.校验和:检测UDP用户数据报在传输中是否有错,有错就丢弃

1.2.3 IP封包格式
原始套接字_第7张图片

1.版本:IP协议的版本。通信双方使用过的IP协议版本必须一致,目前最广泛使用的IP协议版本号为4(即IPV4)
2.首部长度:单位是32位(4字节)
3.服务类型:一般不适用,取值为0.前三位:优先级,第4-7位:延时,吞吐量,可靠性,花费。第8位保留
4.总长度:指首部加上数据的总长度,单位为字节。最大长度为65535字节
5.标识:用来标识主机发送的每一份数据报。IP软件在存储器中维持一个计数器,没产生一个数据报,计数器就加1,并将此值付给标识字段
6.标志:目前只有两位有意义

  • 标志字符中的最低位记为MF。MF = 1即表示后面“还有分片”的数据报。MF = 0表示这已是若干数据报片中的最后一个。
  • 标志字段中间的一位记为DF,意思是“不能分片”,只有当DF = 0时才允许分片

7.片偏移:指出较长的分组在分片后,某片在源分组中的相对位置,也就是说,相对于用户数据段的起点,该片从何处开始。片偏移以8字节为偏移单位
8.生存时间:TTL,表明是数据报在网络中的寿命,即为“跳数限制”,由发出数据报的源点设置这个字段。路由器在转发数据之前就把TTL值减一,当TTL值减为零时,就丢弃这个数据报。通常设置为32、64、128.
9.协议:指出此数据报所携带的数据时使用何种协议,以便使主机的IP层知道应将数据部分上交给哪个处理过程,常用的ICMP(1)、IGMP(2)、TCP(6)、UDP(17)、IPV6(41)
10.首部校验和:只检验数据报的首部,不包括数据部分
11.源地址:发送方IP地址
12.目的地址:接收方IP地址
13.选项:用来定义一些任选项,如记录路径、时间戳等。这些选项很少被使用,同时并不是所有主机和路由器都支持这些选项。一般忽略不计

1.2.4 Ethernet封包格式
原始套接字_第8张图片

1.目的地址:目的mac地址
2.源地址:源mac地址
3.类型:ip数据报(0x0800)、ARP数据报(0x0806)、RARP(0x8035)
4.数据:数据根据类型来决定
CRC、PAD在组包时可以忽略

CRC即循环冗余检验码:是数据通信领域中最常见的一种查错校验码,其特征是信息字段和检验字段的长度可以任意选定。循环冗余检查是一种数据传输检错功能,对数据进行多项式计算,并将得到的结果附在帧的后面,接收设备也执行类似的算法,以保证数据传输的正确性和完整性。

1.2.5 TCP封包格式
原始套接字_第9张图片

1.源端口号:发送方端口号
2.目的端口号:接收方端口号
3.序列号:本报文段的数据的第一个字节的序号
4.确认序号:期望收到对方下一个报文段的第一个数据字节的序号
5.首部长度(数据偏移):TCP报文段的数据起始处距离TCP报文段的起始处有多远,即首部长度。单位:32位,即以4字节为计算单位
6.保留:占6位,保留为今后使用,目前应置为0
7.紧急URG:此位置1,表明紧急指针字段有效,它告诉系统此报文段中有紧急数据,应尽快传送
8.确认ACK:仅当ACK=1时确认号字段才有效,TCP规定,在连接建立后所有的报文段都必须把ACK置1
9.推送PSH:当两个应用进程进行交换式的通信时,有时在一端的应用进程希望在键入一个命令后立即就能够收到对方的响应。在这种情况下,TCP就可以使用推送(push)操作,这时,发送方TCP把PSH置1,并立即创建一个报文段发送出去,接收方收到PSH = 1的报文段,就尽快地(即“推送”向前)交付给接收应用进程,而不再等到整个缓存都填满后再向上交付
10.复位RST:用于复位相应的TCP连接
11.同步SYN:仅在三次握手建立TCP连接时有效。当SYN=1而ACK=0时,表明这是一个连接请求报文段,对方若同意建立连接,则应在相应的报文段中使用SYN=1和ACK=1,因此,SYN置1就表示这是一个连接请求或者连接接受报文
12.终止FIN:用来释放一个连接。当FIN = 1时,表明此报文段的发送方的数据已经发送完毕,并要求释放运输连接
13.窗口:指发送本报文段的一方的接收窗口(而不是自己的发送窗口)
14.检验和:校验和字段检验的范围包括首部和数据两部分,在计算校验时需要加上12字节的伪头部
15.紧急指针:仅在URG=1时才有意义,它指出本报文段中的紧急数据的字节数(紧急数据结束后就是普通数据),即指出了紧急数据的末尾在报文中的位置,注意:即使窗口为零时也可发送紧急数据
16.选项:长度可变,最长可达40字节,当没有使用选项时,TCP首部长度是20字节

1.2.6 ICMP封包格式
原始套接字_第10张图片

注意:不同的类型值以及代码值,代表不同的功能

1.3 编程实例—分析MAC数据包

链路层数据格式

原始套接字_第11张图片
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define ERRLOG(errmsg) do{\
							perror(errmsg);\
							exit(1);\
						}while(0)
						
int main(int argc, char const *argv[]){
	//创建原始套接字
	if((sockfd = socket(AF_PACKET,SOCK_RAW,htons(ETH_P_ALL))) < 0){
		ERRLOG("fail to socket");
	}
	
	//printf("sockfd = %d\n",sockfd);
	
	//接收数据并分析
	unsigned char msg[1600] = "";
	while(1){
		//recvfrom recv read 都可以使用
		if(recvfrom(sockfd,msg,sizeof(msg),0,NULL,NULL) < 0){
			ERRLOG("fail to recvfrom");
		}
		
		//分析接收到的数据包
		unsigned char dst_mac[18] = "";
		unsigned char src_mac[18] = "";
		unsigned short type;
		sprintf(dst_mac,"%x:%x:%x:%x:%x:%x",msg[0],msg[1],msg[2],msg[3],msg[4],msg[5]);
		sprintf(dst_mac,"%x:%x:%x:%x:%x:%x",msg[6],msg[7],msg[8],msg[9],msg[10],msg[11]);
		type = ntohs(*(unsigned short *)(msg + 12));
		printf("源mac: %s ---> 目的mac: %s\n", src_mac, dst_mac);
		printf("type = %#x\n", type);
	}
	
	return 0;
}
1.4 练习—网络数据分析器

在很多时候需要对网络上的数据进行抓取,然后进行分析,此“网络数据分析器”就是模仿现实开发中的抓包工具而进行的,

1.4.1 网络数据分析图
原始套接字_第12张图片
1.4.2 ARP数据解析图
原始套接字_第13张图片

说明:
1.ARP的TYPE为0x0806
2.buf为unsigned char
3.所有数据均为大端

1.4.3 IP数据解析图
原始套接字_第14张图片

说明:
​ 1.IP的TYPE为0x0800
​ 2.buf为unsigned char
​ 3.所有数据均为大端

如下图所示,是网上的数据包的组包过程;其解包过程正好相反,首先分析以太网得到MAC然后再依次分析,比如IP、PORT等等

原始套接字_第15张图片
1.4.4 代码讲解以及混杂模式

1.4.4.1 要求
要求:
​ 1.分析出ARP/IP/RARP
​ 2.分析出MAC
扩展:
​ 在完成基本要求的前提下,分析PORT
提示:
​ 以root权限运行

1.4.4.2 代码讲解

#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define ERRLOG(errmsg) do{\
							perror(errmsg);\
							exit(1);\
						}while(0)
						
int main(int argc, char const *argv[]){
	//创建原始套接字
	if((sockfd = socket(AF_PACKET,SOCK_RAW,htons(ETH_P_ALL))) < 0){
		ERRLOG("fail to socket");
	}
	
	//printf("sockfd = %d\n",sockfd);
	
	//接收数据并分析
	unsigned char msg[1600] = "";
	while(1){
		//recvfrom recv read 都可以使用
		if(recvfrom(sockfd,msg,sizeof(msg),0,NULL,NULL) < 0){
			ERRLOG("fail to recvfrom");
		}
		
		//分析接收到的数据包
		unsigned char dst_mac[18] = "";
		unsigned char src_mac[18] = "";
		unsigned short type;
		sprintf(dst_mac,"%x:%x:%x:%x:%x:%x",msg[0],msg[1],msg[2],msg[3],msg[4],msg[5]);
		sprintf(dst_mac,"%x:%x:%x:%x:%x:%x",msg[6],msg[7],msg[8],msg[9],msg[10],msg[11]);
		type = ntohs(*(unsigned short *)(msg + 12));
		printf("源mac: %s ---> 目的mac: %s\n", src_mac, dst_mac);
		printf("type = %#x\n", type);
		
		
		if(type == 0x0800){
			printf("ip数据报\n");
			
			//头部长度、总长度
			unsigned char ip_head_len;
			unsigned short ip_len;
			ip_head_len = ((*(unsigned char *)(msg + 14)) & 0x0f) * 4;
			ip_len = ntohs(*(unsigned short *)(msg + 16));
			printf("ip头部:%d,ip数据报总长度:%d\n",ip_head_len,ip_len);
			
			//目的ip地址、源IP地址
			unsigned char dst_ip[16] = "";
			unsigned char src_ip[16] = "";
			sprintf(src_ip,"%u.%u.%u.%u",msg[26],msg[27],msg[28],msg[29]);
			sprintf(dst_ip,"%u.%u.%u.%u",msg[30],msg[31],msg[32],msg[33]);
		
			printf("源ip地址: %s ---> 目的ip地址: %s\n", src_ip, dst_ip);
			
			//协议类型
			unsigned char ip_type;
			ip_type = *(msg + 23);
			printf("ip_type = %d\n",ip_types);
			//icmp、igmp、tcp、udp
			if(ip_type) == 1{
				printf("icmp报文\n");
			}
			else if(ip_type == 2){
				printf("igmp报文\n");
			}
			else if(ip_type == 6){
				printf("tcp报文\n");
				unsigned short src_port;
				unsigned short dst_port;
				src_port = ntohs(*(unsigned short *)(msg + 34));
				dst_port = ntohs(*(unsigned short *)(msg + 36));
				printf("源端口号:%d ---> 目的端口号:%d\n",src_port,dst_port);
			}
			else if(ip_type == 17){
				printf("udp报文\n");
				unsigned short src_port;
				unsigned short dst_port;
				src_port = ntohs(*(unsigned short *)(msg + 34));
				dst_port = ntohs(*(unsigned short *)(msg + 36));
				printf("源端口号:%d ---> 目的端口号:%d\n",src_port,dst_port);
			}
		}
		else if(type == 0x0806){
			printf("arp数据报\n");
			//源ip地址
			//目的ip地址
			unsigned char dst_ip[16] = "";
			unsigned char src_ip[16] = "";
			sprintf(src_ip,"%u.%u.%u.%u",msg[28],msg[29],msg[30],msg[31]);
			sprintf(dst_ip,"%u.%u.%u.%u",msg[38],msg[39],msg[40],msg[41]);
		
			printf("源ip地址: %s ---> 目的ip地址: %s\n", src_ip, dst_ip);
		}
		else if(type == 0x8035){
			printf("rarp数据报\n");
		}
	}
	
	close(sockfd);
	return 0;
}

执行结果

原始套接字_第16张图片

混杂模式:
​ 1、指一台机器的网卡能够接收所有经过它的数据报,而不论其目的地址是否是它
​ 2、一般计算机网卡都工作在非混杂模式下,如果设置网卡为混杂模式需要root权限。

linux下设置:
​ 1、设置混杂模式:ifconfig eth0 promisc
​ 2、取消混杂模式:ifconfig eth0 -promisc

windows下通过特定的软件实现

linux下通过程序设置网卡混杂模式:

原始套接字_第17张图片
1.5 sendto发送数据
1.5.1 用sendto发送原始套接字数据
sendto(sock_raw_fd,msg,msg_len,0,(struct sockaddr *)&sll,sizeof(sll));
注意:
1.sock_raw_fd:原始套接字
2.msgL:发送的消息(封装好的协议数据)
3.sll:本机网络接口,指发送的数据应该从本机的哪个网卡出去,而不是以前的目的地址
1.5.2 本地网络接口
#include 
struct sockaddr_ll{
	unsigned short int sll_family;		//一般为PF_PACKET
	unsigned short int sll_protocol;	//上层协议
	int sll_ifindex;				   //接口类型
	unsigned short int sll_hatype;		//报头类型
	unsigned char sll_pkttype;			//包类型
	unsigned char sll_halen;			//地址长度
	unsigned char sll_addr[8];			//MAC地址
}
只需要对sll.sll_ifindex赋值,就可使用
1.5.3 发送数据demo
// 将网络接口赋值给原始套接字地址结构
struct sockaddr_ll sll;
bzero(&sll,sizeof(sll));
sll.sll_ifindex = /*获取本机出去的接口地址*/

int len = sendto(sock_raw_fd,msg,sizeof(msg),0,(struct sockaddr*)&sll,sizeof(sll));
1.5.4 通过ioctl来获取网络接口地址
//struct ifreq:  #include 
// IFNAMSIZ 16
//获取接口信息
//将arp请求报文发送出去,通过eth0发送出去
//使用ioctl函数获取本机网络接口
struct ifreq ethreq;				//网络接口地址
strncpy(ethreq.ifr_name,"eth0",IFNAMSIZ);		//指定网卡名称
if(ioctl(sockfd,SIOCGIFINDEX,&ethreq) == -1){	//获取网络接口
	perror("fail to ioctl");
	exit(1);
}
//设置本机网络接口
struct sockaddr_ll sll;
bzero(&sll,sizeof(sll));
sll.sll_ifindex = ethreq.ifr_ifindex;
//发送数据
if(sendto(sockfd,msg,14 + 28,0,(struct sockaddr *)&sll,sizeof(sll)) < 0){
	perror("fail to sendto");
}

ioctl参数对照表
原始套接字_第18张图片

1.6 练习—MAC地址扫描器(ARP)
1.6.1 ARP概述

ARP(Address Resolution Protocol,地址解析协议)
​ 1、是TCP/IP协议族中的一个
​ 2、主要用于查询指定ip所对应的MAC
​ 3、请求方使用广播来发送请求
​ 4、应答方使用单播来回送数据
​ 5、为了在发送数据的时候提高效率在计算中会有一个ARP缓存表,用来暂时存放ip所对应的MAC,在linux中使用ARP即可查看,在xp中使用ARP -a

1.6.2 在linux与windows系统下查看ARP的方式

在Linux下查看arp表

arp

在Windows下查看arp表

arp -a
原始套接字_第19张图片

注意:当主机A和主机B通信时,会先查看arp表中有没有对方的mac地址,如果有则直接通信即可,如果没有再调用arp协议获取对方mac地址并将其保存在arp表中。

以机器A获取机器B的MAC为例:

原始套接字_第20张图片
1.6.3 ARP协议格式
原始套接字_第21张图片

1.硬件地址长度:6
2.协议地址长度:4

1.6.4 向指定IP发送ARP请求(demo)

获取172.20.226.11的MAC地址

#include 
#include 
#include 
#include 
#include 
#include  
#include 
#include 
#include 
#include 
#include 
#include 

#define ERROG(errmsg) do{\
	perror(errmsg);\
	exit(1);\
}while(0)

//使用arp协议通过对方ip地址获取mac地址

int main(int argc,char const *argv[]){
	int sockfd;
	if((sockfd = socket(AF_PACKET,SOCK_RAW,htons(ETH_P_ALL))) < 0){
		ERRLOG("fail to socket");
	}
	
	//组数据包
	//源mac地址: 00:0c:29:7b:35:d7
	unsigned char msg[1600] = {
		//组以太网头部
		0xff,0xff,0xff,0xff,0xff,0xff,	//目的mac地址,广播地址
		0x00,0x0c,0x29,0x7b,0x35,0xd7,	//源mac地址
		0x08,0x06,帧类型 arp报文:0x0806
		
		//组arp报文
		0x00,0x01,		//硬件类型 以太网:1
		0x08,0x00,		//协议类型 ip地址:0x0800
        6,		//硬件地址长度
        4,		//协议地址长度
        0x00,0x01 	//op. arp请求:1
        0x00,0x0c,0x29,0x7b,0x35,0xd7,		//源mac地址
        192,168,3,103,		//源ip地址
        0x00,0x00,0x00,0x00,0x00,0x00,	//目的mac地址
        192,168,7,78,		//目的ip地址
	}
	
	//获取接口信息
	//将arp请求报文发送出去,通过eth0发送出去
	//使用ioctl函数获取本机网络接口
	struct ifreq ethreq;				//网络接口地址
	strncpy(ethreq.ifr_name,"eth0",IFNAMSIZ);		//指定网卡名称
	if(ioctl(sockfd,SIOCGIFINDEX,&ethreq) == -1){	//获取网络接口
		perror("fail to ioctl");
		exit(1);
	}
	//设置本机网络接口
	struct sockaddr_ll sll;
	bzero(&sll,sizeof(sll));
	sll.sll_ifindex = ethreq.ifr_ifindex;
	//发送数据
	if(sendto(sockfd,msg,14 + 28,0,(struct sockaddr *)&sll,sizeof(sll)) < 0){
		perror("fail to sendto");
	}
	
	unsigned char recv_msg[1600] = "";
	unsigned char mac[18] = "";
	while(1){
		//接收数据并分析
		if(recvfrom(sockfd,recv_msg,sizeof(recv_msg),0,NULL,NULL) < 0){
			ERRLOG("fail to recvfrom");
		}
		
		//如果时arp数据包并且是arp应答,则打印mac地址
		if(ntohs(*(unsigned short *)(recv_msg + 12)) == 0x0806)
		if(ntohs(*(unsigned short *)(recv_msg + 20)) == 2){
			sprintf(mac,"%x:%x:%x:%x:%x:%x",recv_msg[6],recv_msg[7],recv_msg[8],recv_msg[9],recv_msg[10]);
		}
	}
}

你可能感兴趣的:(#,计算机网络编程,网络,tcp/ip,udp)