目录
【原始套接字的创建】
【协议格式】
1.UDP数据格式
2.TCP数据格式
3.IP报文数据格式
4.MAC报文数据格式
【使用原始套接字捕获网络数据】
【使用原始套接字发送网络数据】
获取本地机的接口数据
【发送ARP报文获取未知的MAC地址】
1.实现原理
2.ARP数据报文格式
3.实现
3.1数据帧的组包
【ARP欺骗】
1.实现原理
2.使用协议结构体组建数据
3.实现
【构建UDP报文】
1.协议结构体及实现
2.IP头校验及UDP头校验
2.1UDP的伪头部及校验
3.实现
原始套接字指的是在传输层下面使用的套接字,之前使用的流式套接字和数据报套接字是工作在传输层的,并且在接受和发送的时候只能对数据部分进行操作,如果想要自己组建一个报文,那么就需要使用原始套接字。并且原始套接字还可以监听所有经过本机网卡的数据帧或者数据包。
原始套接字的创建也是使用socket函数,但是形参比起TCP或者UDP有所不同
当编译完成生成可执行文件的时候,执行需要加sudo
int socket(PF_PACKET,SOCK_RAW,protocol);
功能
创建链路层的原始套接字
参数
protocol:用来指定接受或者发送的数据包类型
ETH_P_IP:IPV4数据包
ETH_P_ARP:ARP数据包
ETH_P_ALL:任何协议类型的数据包
注意:protocol这个参数需要转为网络字节序,也就是使用htons()
返回值
成功:>0,链路层套接字
失败:<0,出错
UDP的报文头由64位8个字节组成,用来标识网络通信之前源进程与目的进程的端口号
TCP的报文头格式相比于UDP更为的复杂,首先为源端口号和目的端口号,因为TCP会进行确认,于是就会有序列号和确认号,因为TCP的头部长度至少为20字节(没有选项的情况下),且头部长度中仅为4位,最多只能表示15,不足以表示正确长度。所以头部长度为实际长度除以4,也就是如果头部实际长度为20,那么头部长度这个选项为5。
版本这个选项是看使用的是IPV4还是IPV6,如果是前者为4。首部长度和UDP报文头的相同,选项为IP实际报文头长度除以4。服务类型一般为0,总长度为IP报文头的长度加上数据的长度。标识一般为0,标识和片偏移也一般为0。生存时间TTL表示此数据经过一个路由器递减的次数,可以大致填写,协议类型根据实际情况填写。头部校验需要使用特定的函数校验,然后填写进去,未校验前先填写0。
MAC报文的报文较为简单,只需要填写目的MAC,源MAC以及数据的类型即可
原始套接字可以捕获任何经过当前主机网卡的数据,那么使用原始套接字接受数据,然后通过组包把数据拆拆解到定义的相应数组中,就可以获取相应的想要获取的数据
通过对接受到的数据的拆解,我们可以得到很多报文中的数据
可见我们通过原始套接字成功捕获到各个数据的类型以及源MAC目的MAC,源IP目的IP
#include
#include
#include
#include
#include
int main(int argc, char const *argv[])
{
int socket_fd = socket(PF_PACKET,SOCK_RAW,htons(ETH_P_ALL));
if( socket_fd < 0 )
{
perror("socket:");
return 0;
}
printf("socket = %d \n",socket_fd);
unsigned char data[1500] = "";
unsigned char src_mac[18] = "";
unsigned char dst_mac[18] = "";
unsigned char type[2] = "";
unsigned char src_ip[16] = "";
unsigned char dst_ip[16] = "";
while(1)
{
recvfrom(socket_fd,data,sizeof(data),0,NULL,NULL);
/************* 解析目的Mac和源Mac ************/
sprintf(src_mac,"%02x:%02x:%02x:%02x:%02x:%02x",\
data[0],data[1],data[2],data[3],data[4],data[5]);
sprintf(dst_mac,"%02x:%02x:%02x:%02x:%02x:%02x",\
data[6],data[7],data[8],data[9],data[10],data[11]);
/************* 解析数据类型 ************/
sprintf(type,"0x%02x%02x",data[12],data[13]);
if( data[13] == 0x00 )
{
printf("\tIP数据报--->%s\n",type);
unsigned char* ip_head = data+14;
inet_ntop(AF_INET,(unsigned int*)(ip_head+12),src_ip,16);
inet_ntop(AF_INET,(unsigned int*)(ip_head+16),dst_ip,16);
printf("\tsrc_ip%s ------> dst_ip%s \n",src_ip,dst_ip);
}
else if( data[13] == 0x06 )
{
printf("\tARP数据报--->%s\n",type);
}
if( data[13] == 0x35 )
{
printf("\tRARP数据报--->%s\n",type);
}
printf("src_mac:%s --------> dst_mac%s \n",src_mac,dst_mac);
}
close(socket_fd);
return 0;
}
通过原始套接字发送组建好的数据,使用sendto发送完整的数据帧
sendto(sock_raw_fd,msg,msg_len,0,(struct sockaddr*)&sll,sizof(sll));
功能
msg:完整的数据格式
msg_len:帧的实际长度
sll:本地主机上的帧数据,出去的网卡地址
sll结构体的数据类型和头文件
struct sockaddr_ll sll
#include
#include
int ioctl(int fd, int request, void*);
具体的获得网络接口可以封装为一个函数,具体函数如下
如果想要使用原始套接字发送数据,那么就需要使用这个发送函数,第一个形参为套接字,第二个位发送的数据,第三个为数据长度,第四个为网络接口的名称(可以使用ifconfig查看)
int my_send(int socket_fd , char* msg , int msg_len , char* net_name )
{
struct ifreq ethreq;
//把你要发送的网卡名称赋值进去
strncpy(ethreq.ifr_name,net_name,IFNAMSIZ);
if( -1 == ioctl(socket_fd, SIOCGIFINDEX ,ðreq) )
{
perror("sockfd");
close(socket_fd);
_exit(-1);
}
struct sockaddr_ll sll;
memset(&sll,0,sizeof(sll));
//通过网卡名获取网络接口
sll.sll_ifindex = ethreq.ifr_ifindex;
//发送
int len = sendto(socket_fd,msg,msg_len,0,(struct sockaddr *)&sll,sizeof(sll));
return len;
}
如果我们想向一台主机发送报文,比如说A主机向B主机发送ping命令(ICMP),但是A主机只有B主机的IP地址,而没有B主机的MAC地址,此时需要使用ARP报文来获取B主机的MAC地址,才能成功ping通
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
int my_send(int socket_fd , unsigned char* msg , int msg_len , char* net_name )
{
struct ifreq ethreq;
//把你要发送的网卡名称赋值进去
strncpy(ethreq.ifr_name,net_name,IFNAMSIZ);
if( -1 == ioctl(socket_fd, SIOCGIFINDEX ,ðreq) )
{
perror("sockfd");
close(socket_fd);
_exit(-1);
}
struct sockaddr_ll sll;
memset(&sll,0,sizeof(sll));
//通过网卡名获取网络接口
sll.sll_ifindex = ethreq.ifr_ifindex;
//发送
int len = sendto(socket_fd,msg,msg_len,0,(struct sockaddr *)&sll,sizeof(sll));
return len;
}
int main(int argc, char const *argv[])
{
//创建原始套接字
int socket_fd = socket(PF_PACKET,SOCK_RAW,htons(ETH_P_ALL));
if( socket_fd < 0 )
{
perror("socket:");
return 0;
}
printf("socket = %d \n",socket_fd);
//创建发送的数据
unsigned char msg[512] = {
//------------------------------- mac头
0xff,0xff,0xff,0xff,0xff,0xff, //目的mac
0x00,0x0c,0x29,0xxx,0xxx,0xxx, //源mac <-----------------------
0x08,0x06, //帧类型
//------------------------------- ARP报文
0x00,0x01, //硬件类型
0x08,0x00, //协议类型
0x06, //硬件地址长度
0x04, //协议地址长度
0x00,0x01, //选项 ARP请求
0x00,0x0c,0x29,0xxx,0xxx,0xxx, //源mac <-----------------------
192,168,x,xxx, //源IP
0x00,0x00,0x00,0x00,0x00,0x00, //目的mac
192,168,x,xxx, //目的IP
};
//通过网络接口发送数据
int len = my_send(socket_fd,msg,42,"ens33");
printf("发送报文长度为%d\n",len);
//使用循环接受符合要求数据
unsigned char rec_data[1500] = "";
unsigned char src_mac[18] = "";
unsigned char src_ip[16] = "";
while(1)
{
recvfrom(socket_fd,rec_data,sizeof(rec_data),0,NULL,NULL);
unsigned short mac_type = ntohs(*(unsigned short *)(rec_data + 12));
//需要ARP数据报,其他过滤
if( mac_type == 0x0806 )
{
unsigned short op = ntohs(*(unsigned short *)(rec_data + 20));
if( 0x02 == op )
{
//获取需要的mac地址
sprintf(src_mac,"%02x:%02x:%02x:%02x:%02x:%02x",\
rec_data[6],rec_data[7],rec_data[8],rec_data[9],rec_data[10],rec_data[11]);
//获取IP
inet_ntop(AF_INET,(unsigned int*)(rec_data+28),src_ip,16);
printf("%s的mac地址为----->%s\n",src_ip,src_mac);
break;
}
}
}
close(socket_fd);
return 0;
}
一般来说,进行网络通信定义的数组的类型为无符号字符类型,因为有时候接受或者发送的时候会接受到图片,而图片的RGB的范围为0-255,如果使用有符号字符会出错。
组包时注意要按照网络字节序也就是大端形式组包
//创建发送的数据
unsigned char msg[512] = {
//------------------------------- 以太网头
0xff,0xff,0xff,0xff,0xff,0xff, //目的mac
//全 ff 表示广播
0x00,0x0c,0x29,0xxx,0xxx,0xxx, //源mac <-----------------------
0x08,0x06, //帧类型
//------------------------------- ARP报文
0x00,0x01, //硬件类型
0x08,0x00, //协议类型
0x06, //硬件地址长度
0x04, //协议地址长度
0x00,0x01, //选项 ARP请求
0x00,0x0c,0x29,0xxx,0xxx,0xxx, //源mac <-----------------------
192,168,x,xxx, //源IP
0x00,0x00,0x00,0x00,0x00,0x00, //目的mac
//此时不知道目的MAC地址,使用0
192,168,x,xxx, //目的IP
};
一般来说,主机是不会判断自身是否发送过ARP请求报文的,所以另一台主机只需要伪装一个ARP应答报文,然后任意修改报文中源MAC的地址,此时在接受的主机那里,arp表中相应IP的MAC就会被修改。
此时在组建数据帧的时候,源MAC和op选项需要修改
我们一开始在组建ARP报文获取已知IP未知MAC主机的MAC地址的时候,是直接定义一段连续的空间来人为一个字节一个字节组建的,这样是十分繁琐的,接下来使用结构体进行组包
具体的实现原理就是,定义一段连续的内存空间用来组建数据,然后通过不同结构体的指针操控的空间大小,来对这段连续的空间进行赋值
这里我们需要以太网头部结构体以及ARP数据结构体
以太网头部结构体
#include
struct ether_header
{
u_int8_t ether_dhost[ETH_ALEN]; //目的MAC地址
u_int8_t ether_shost[ETH_ALEN]; //源MAC地址
u_int16_t ether_type; //帧类型
};
ARP数据结构体
#include
/********************** ARP头部 ***************************/
typedef struct
{
unsigned short int ar_hrd; //硬件类型
unsigned short int ar_pro; //协议类型
unsigned char ar_hln; //硬件地址长度
unsigned char ar_pln; //协议地址长度
unsigned short int ar_op; //op
#if 1
unsigned char __ar_sha[ETH_ALEN]; //发送源mac
unsigned char __ar_sip[4]; //发送IP
unsigned char __ar_tha[ETH_ALEN]; //接受的mac
unsigned char __ar_tip[4]; //发送IP
#endif
}ARPHDR;
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
/********************** ARP头部 ***************************/
typedef struct
{
unsigned short int ar_hrd; /* Format of hardware address. */
unsigned short int ar_pro; /* Format of protocol address. */
unsigned char ar_hln; /* Length of hardware address. */
unsigned char ar_pln; /* Length of protocol address. */
unsigned short int ar_op; /* ARP opcode (command). */
#if 1
/* Ethernet looks like this : This bit is variable sized
however... */
unsigned char __ar_sha[ETH_ALEN]; /* Sender hardware address. */
unsigned char __ar_sip[4]; /* Sender IP address. */
unsigned char __ar_tha[ETH_ALEN]; /* Target hardware address. */
unsigned char __ar_tip[4]; /* Target IP address. */
#endif
}ARPHDR;
/*******************************************************************/
int my_send(int socket_fd , char* msg , int msg_len , char* net_name )
{
struct ifreq ethreq;
//把你要发送的网卡名称赋值进去
strncpy(ethreq.ifr_name,net_name,IFNAMSIZ);
if( -1 == ioctl(socket_fd, SIOCGIFINDEX ,ðreq) )
{
perror("sockfd");
close(socket_fd);
_exit(-1);
}
struct sockaddr_ll sll;
memset(&sll,0,sizeof(sll));
//通过网卡名获取网络接口
sll.sll_ifindex = ethreq.ifr_ifindex;
//发送
int len = sendto(socket_fd,msg,msg_len,0,(struct sockaddr *)&sll,sizeof(sll));
return len;
}
int main(int argc, char const *argv[])
{
//创建原始套接字
int socket_fd = socket(PF_PACKET,SOCK_RAW,htons(ETH_P_ALL));
if( socket_fd < 0 )
{
perror("socket:");
return 0;
}
printf("socket = %d \n",socket_fd);
//创建发送的数据
unsigned char msg[512] = {0};
//使用结构体来对数据进行填充
unsigned char src_ip[4] = {192,168,xxx,xxx}; //<-----------------
unsigned char dst_ip[4] = {192,168,xxx,xxx}; //<-----------------
unsigned char dst_mac[6] = {0x62,0xa3,0x34,0xxx,0xxx,0xxx}; //<-----------------
unsigned char src_mac[6] = {0xee,0xee,0xee,0xee,0xee,0xee};
//以太网头部
struct ether_header* mac_head = (struct ether_header*)msg;
memcpy(mac_head->ether_dhost,dst_mac,6);//以太网头部的目的mac
memcpy(mac_head->ether_shost,src_mac,6);//以太网头部的源mac
mac_head->ether_type = htons(0x0806); //以太网头部的帧类型
//ARP报
ARPHDR* arp_head = (ARPHDR*)(msg+14);
arp_head->ar_hrd = htons(1); //硬件类型
arp_head->ar_pro = htons(0x0800); //协议类型
arp_head->ar_hln = 6; //硬件地址长度
arp_head->ar_pln = 4; //协议地址长度
arp_head->ar_op = htons(2); //op
memcpy(arp_head->__ar_sha,src_mac,6);//发送源mac
memcpy(arp_head->__ar_sip,src_ip,4);//发送IP
memcpy(arp_head->__ar_tha,dst_mac,6);//接受的mac
memcpy(arp_head->__ar_tip,dst_ip,4);//发送IP
int i = 0;
for ( i = 0; i < 10; i++)
{
int len = my_send(socket_fd,msg,42,"ens33");
printf("发送报文长度为%d\n",len);
sleep(1);
}
//通过网络接口发送数据
close(socket_fd);
return 0;
}
构建UDP报文,我们需要以太网头部,IP头部,UDP报文头部以及发送的数据总共四个部分组成
在这里我们需要IP的报文头以及UDP的报文头
#include
***********************IP的结构***********************************
struct iphdr
{
#if __BYTE_ORDER == __LITTLE_ENDIAN //小端存储
unsigned int ihl:4;//首部长度
unsigned int version:4;//版本
#elif __BYTE_ORDER == __BIG_ENDIAN //大端存储
unsigned int version:4;
unsigned int ihl:4;
#else
# error "Please fix "
#endif
u_int8_t tos;//服务类型
u_int16_t tot_len;//总长度为ip头+UDP头+发送数据长度
u_int16_t id;//标识
u_int16_t frag_off;//标志,片偏移
u_int8_t ttl;//生存时间
u_int8_t protocol;//上层协议udp
u_int16_t check;//校验,等数据填充完毕在校验,这里为0
u_int32_t saddr;//源IP地址,转为32无符号
u_int32_t daddr;//目的IP地址,转为32无符号
};
#include
***********************UDP的结构*****************************
struct udphdr
{
u_int16_t source;//源端口号
u_int16_t dest;//目的端口号
u_int16_t len;//UDP长度为头部加上数据长度
u_int16_t check;//校验
};
IP头以及UDP头中的校验位我们需要用到如下函数,然后通过该函数的返回值完成校验
IP头只需要把整个头部放进去,然后通过校验位接受返回值就行
ip_head->check = checksum((unsigned short*)ip_head,20);//校验
UDP校验需要用到伪头部
//********************* 校验函数 ************************
unsigned short checksum(unsigned short *buf, int len)
{
int nword = len / 2;
unsigned long sum;
if (len % 2 == 1)
nword++;
for (sum = 0; nword > 0; nword--)
{
sum += *buf;
buf++;
}
sum = (sum >> 16) + (sum & 0xffff);
sum += (sum >> 16);
return ~sum;
}
想要获得UDP头的校验位,那么我们需要构建一个UDP的伪头部进行校验
//********************* udp伪头部结构体 ************************
typedef struct
{
u_int32_t src_ip; //源IP
u_int32_t dst_ip; //目的IP
u_int8_t flag; //标志位0
u_int8_t proto; //协议17表示UDP
u_int16_t len; //UDP长度
}Fake_UDP;
//********************* 具体实现 ************************
//udp的校验需要伪头部
unsigned char fake_head[512] = "";
Fake_UDP *fake_udp_head = (Fake_UDP *)fake_head;
fake_udp_head->src_ip = inet_addr("192.168.x.xxx");//源IP地址,转为32无符号
fake_udp_head->dst_ip = inet_addr("192.168.x.xxx");//目的IP地址,转为32无符号
fake_udp_head->flag = 0;//通常为0
fake_udp_head->proto = 17;//协议UDP
fake_udp_head->len = htons(8+send_data_len);
//把剩下的内容拷贝到伪头部的连续内存中-------------->拷贝的是UDP伪头部下面的内容
memcpy(fake_head+12,udp_head,8+send_data_len);
//校验-------------------------------------------->通过伪头部校验,获得校验值放于真UDP头中
udp_head->check = checksum((unsigned short*)fake_head,20+send_data_len);//校验
我们这里通过虚拟机组建UDP报文向主机中的网络调试助手发送信息
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
//********************* udp伪头部结构体 ************************
typedef struct
{
u_int32_t src_ip; //源IP
u_int32_t dst_ip; //目的IP
u_int8_t flag; //标志位0
u_int8_t proto; //协议17表示UDP
u_int16_t len; //UDP长度
}Fake_UDP;
//********************* 校验函数 ************************
unsigned short checksum(unsigned short *buf, int len)
{
int nword = len / 2;
unsigned long sum;
if (len % 2 == 1)
nword++;
for (sum = 0; nword > 0; nword--)
{
sum += *buf;
buf++;
}
sum = (sum >> 16) + (sum & 0xffff);
sum += (sum >> 16);
return ~sum;
}
//********************* 原始套接字发送函数 ************************
int my_send(int socket_fd , char* msg , int msg_len , char* net_name )
{
struct ifreq ethreq;
//把你要发送的网卡名称赋值进去
strncpy(ethreq.ifr_name,net_name,IFNAMSIZ);
if( -1 == ioctl(socket_fd, SIOCGIFINDEX ,ðreq) )
{
perror("sockfd");
close(socket_fd);
_exit(-1);
}
struct sockaddr_ll sll;
memset(&sll,0,sizeof(sll));
//通过网卡名获取网络接口
sll.sll_ifindex = ethreq.ifr_ifindex;
//发送
int len = sendto(socket_fd,msg,msg_len,0,(struct sockaddr *)&sll,sizeof(sll));
return len;
}
//********************* 主函数 ************************
int main(int argc, char const *argv[])
{
//创建原始套接字
int socket_fd = socket(PF_PACKET,SOCK_RAW,htons(ETH_P_ALL));
if( socket_fd < 0 )
{
perror("socket:");
return 0;
}
printf("socket = %d \n",socket_fd);
//通过udp发送的数据
unsigned char send_data[512] = "";
printf("请输入发送的信息:\n");
scanf("%s",send_data);
int send_data_len = strlen(send_data);
//数据长度必须为偶数
send_data_len = send_data_len + send_data_len%2;
//创建发送的数据
unsigned char msg[1500] = {0};
//使用结构体来对数据进行填充
unsigned char src_ip[4] = {192,168,x,xxx};//源IP <----------------------
unsigned char dst_ip[4] = {192,168,x,xxx};//目的IP <----------------------
unsigned char dst_mac[6] = {0x62,0xa3,0x34,0xxx,0xxx,0xxx};//目的mac <---------------
unsigned char src_mac[6] = {0x00,0x0c,0x29,0xxx,0xxx,0xxx};//源mac<------------------
//********************* 以太网头部 ************************
struct ether_header* mac_head = (struct ether_header*)msg;
memcpy(mac_head->ether_dhost,dst_mac,6);//以太网头部的目的mac
memcpy(mac_head->ether_shost,src_mac,6);//以太网头部的源mac
mac_head->ether_type = htons(0x0800); //以太网头部的帧类型
//********************* IP报文头 ************************
struct iphdr* ip_head = (struct iphdr*)(msg+14);
ip_head->version = 4;//版本
ip_head->ihl = 20/4;//长度为总长度除以4
ip_head->tos = 0;//服务类型
ip_head->tot_len = htons(20+8+send_data_len);//总长度为ip头+UDP头+发送数据长度
ip_head->id = htons(0);//标识
ip_head->frag_off = htons(0); //标志,片偏移
ip_head->ttl = 128;//生存时间
ip_head->protocol = 17;//上层协议udp
ip_head->check = htons(0);//校验,等数据填充完毕在校验,这里为0
ip_head->saddr = inet_addr("192.168.x.xxx");//源IP地址,转为32无符号<-----------------
ip_head->daddr = inet_addr("192.168.x.xxx");//目的IP地址,转为32无符号<---------------
ip_head->check = checksum((unsigned short*)ip_head,20);//校验
//********************* UDP报文头 ************************
struct udphdr* udp_head = (struct udphdr*)(msg+14+20);
udp_head->source = htons(8000);//源端口号
udp_head->dest = htons(8000);//目的端口号
udp_head->len = htons(8+send_data_len);//UDP长度为头部加上数据长度
udp_head->check = htons(0);//校验
/******* 犯错点 *******/
/******* udp_head是struct udphdr*类型,+8跳过不是八个字节 *******/
//memcpy(udp_head+8, send_data, send_data_len);//将数据拷到报文头后面
memcpy(msg+14+20+8, send_data, send_data_len);//将数据拷到报文头后面
//udp的校验需要伪头部
unsigned char fake_head[512] = "";
Fake_UDP *fake_udp_head = (Fake_UDP *)fake_head;
fake_udp_head->src_ip = inet_addr("192.168.x.xxx");//源IP地址,转为32无符号<----------
fake_udp_head->dst_ip = inet_addr("192.168.x.xxx");//目的IP地址,转为32无符号<--------
fake_udp_head->flag = 0;//通常为0
fake_udp_head->proto = 17;//协议UDP
fake_udp_head->len = htons(8+send_data_len);
//把剩下的内容拷贝到伪头部的连续内存中
memcpy(fake_head+12,udp_head,8+send_data_len);
//校验
udp_head->check = checksum((unsigned short*)fake_head,20+send_data_len);//校验
//通过网络接口发送数据
int len = my_send(socket_fd,msg,14+20+8+send_data_len,"ens33");
printf("成功发送%d字节\n",len);
close(socket_fd);
return 0;
}