小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
原始套接字
1 基本概念
-
一种不同于SOCK_STREAM、SOCK_DGRAM的套接字,它实现于系统核心;
-
可以接收本机网卡上所有的数据帧(数据包),对于监听网络流量和分析网络数据很有作用(直接从网卡上接收数据或发送);
-
开发人员可发送自己组装的数据包到网络上;
-
广泛应用于高级网络编程;
-
网络专家、黑客通常会用此来编写奇特的网络程序;
-
与TCP,UDP传输层协议的应用层编程不同,是==基于链路层==的编程(整个TCP/IP协议的最底层)在这个层次进行编程的话,所有的协议都需要自己去指定(每一位怎们传);
2 创建原始套接字
- ==AF_PACKET 底层协议接口==
- SOCK_RAW 原始套接字
- 附加协议以前可有可无,现在必须得有
man 7 packet sockfd理解成标识的网卡的设备文件,调用read write就可以直接对网卡进行读取操作
vi /usr/include/linux/if_ether.h
#include <sys/types.h>
#include <sys/socket.h>
#include <linux/if_packet.h>
#include <net/ethernet.h>
int socket(int domain, int type, int protocol);
功能:创建套接字
参数:
domain:通信域,协议族
AF_PACKET 底层协议接口
type:套接字类型
SOCK_RAW 原始套接字
protocol:附加协议
ETH_P_IP IPV4数据包
ETH_P_ARP ARP数据包
ETH_P_ALL 任何协议类型的数据包
返回值:
成功:文件描述符
失败:-1
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <linux/if_packet.h>
#include <net/ethernet.h>
#include <arpa/inet.h>
#define ERRLOG(errmsg) do{\
perror(errmsg);\
printf("%s - %s - %d\n", __FILE__, __func__, __LINE__);\
exit(1);\
}while(0)
int main(int argc, char const *argv[])
{
int sockfd;
//创建一个原始套接字
if((sockfd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL))) == -1)
{
ERRLOG("socket error");
}
printf("sockfd = %d\n", sockfd);
return 0;
}
注意:记得执行命令时加 sudo
3 从网卡上直接读取数据包并解析
(类似与wireshark读取数据包,然后我们自己来解析)
不是所有的数据包都是四个层次,要根据协议来,但是最多有四个层次
read recv recvfrom 直接读
先解析链路层,以太网头
MAC地址:00:0c:29:ff:de:fa ,解析的时候认为==这个字符串是18字节大小==,0是一个字节 00:是三个字节;而一共是6个字节的意思是000c29ffdefa,00为一个字节。
用16进制来表示一个字节,每个字节(认为6个字节的字节)用两位16进制来表示,就是两个字节,一个:是一个字节。
为什么不是6个字节加上五个:再加上'\0'的12个字节
因为你输出的是十二个字节,但是定义的是18个字节
int a = 5;//a是四个字节
printf("%d",a);//输出一个字节
//int printf(const char *format, ...);
//printf是以字符串形式输出,所以说输出的字符串占几个字节与a的类型是没有关系的。和宽度有关系,宽度就是按照多少个字节输出
//eg:如果是printf("%-4d",a);,就是按照宽度为4输出,就意味着这个字符串是按照5字节输出的,因为有一个'\0'
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <linux/if_packet.h>
#include <net/ethernet.h>
#include <arpa/inet.h>
#define ERRLOG(errmsg) do{\
perror(errmsg);\
printf("%s - %s - %d\n", __FILE__, __func__, __LINE__);\
exit(1);\
}while(0)
int main(int argc, char const *argv[])
{
int sockfd;
//创建一个原始套接字
if((sockfd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL))) == -1)
{
ERRLOG("socket error");
}
//直接从网卡上读取数据并解析
unsigned char buf[1600] = {0};// MTU:最大传输单元,linux是1500个字节,不能小于1500,这里定义1600
unsigned char mac_dest[18] = {0};
unsigned char mac_src[18] = {0};
unsigned short mac_type;
unsigned short ip_len;
unsigned char ip_ttl;
unsigned char ip_type;
unsigned char ip_src[16] = {0};
unsigned char ip_dest[16] = {0};
unsigned short port_src;
unsigned short port_dest;
//从网卡上读取的是完整的数据包
//使用结构体也可以,但是每种协议对应的结构体是不一样的,定义一个结构体不太方便
//可以类似于TFTP定义一个无符号数组,一个一个字符进行解析
while(1)
{
if(recvfrom(sockfd, buf, 1600, 0, NULL, NULL) == -1)
{
ERRLOG("recvfrom error");
}
//--------------解析以太网头----------------
//目的mac地址(6个字节)
sprintf(mac_dest, "%x:%x:%x:%x:%x:%x", buf[0], buf[1], buf[2], buf[3], buf[4], buf[5]);
//源mac地址(6个字节)
sprintf(mac_src, "%x:%x:%x:%x:%x:%x", buf[6], buf[7], buf[8], buf[9], buf[10], buf[11]);
//协议类型(两个字节)
mac_type = ntohs(*(unsigned short *)(buf + 12));
printf("***********************************\n");
printf("------ MAC头 ------\n");
printf("源mac:%s --> 目的mac: %s\n", mac_src, mac_dest);
printf("协议类型:%#x\n", mac_type);
if(mac_type == 0x0800)
{
printf("------ IP数据报 ------\n");
//--------------解析IP头----------------
//IP总长度
ip_len = ntohs(*(unsigned short *)(buf+16));
//生存时间TTL
ip_ttl = buf[22]; //buf[22] <==> *(buf+22)
//协议类型
ip_type = buf[23];
//源ip地址
sprintf(ip_src, "%d.%d.%d.%d", buf[26], buf[27], buf[28], buf[29]);
//目的ip地址
sprintf(ip_dest, "%d.%d.%d.%d", buf[30], buf[31], buf[32], buf[33]);
printf("源ip: %s --> 目的ip: %s\n", ip_src, ip_dest);
printf("IP总长度:%d, TTL:%d, 协议类型:%d\n", ip_len, ip_ttl, ip_type);
if(ip_type == 6)
{
printf("------ TCP数据报 ------\n");
//源端口号
port_src = ntohs(*(unsigned short *)(buf+34));
//目的端口号
port_dest = ntohs(*(unsigned short *)(buf+36));
printf("源端口号:%d --> 目的端口号: %d\n", port_src, port_dest);
}
else if(ip_type == 17)
{
printf("------ UDP数据报 ------\n");
//源端口号
port_src = ntohs(*(unsigned short *)(buf+34));
//目的端口号
port_dest = ntohs(*(unsigned short *)(buf+36));
printf("源端口号:%d --> 目的端口号: %d\n", port_src, port_dest);
}
else if(ip_type == 1)
{
printf("------ ICMP数据报 ------\n");
}
else if(ip_type == 2)
{
printf("------ IGMP数据报 ------\n");
}
}
else if(mac_type == 0x0806)
{
printf("------ ARP数据报 ------\n");
}
else if(mac_type == 0x8035)
{
printf("------ RARP数据报 ------\n");
}
putchar(10);
}
return 0;
}
4 arp协议的使用
arp协议:地址解析协议,通过对方的ip地址,获取对方Mac地址
运行机制:
源主机进行组包,指定自己的ip地址和mac地址,指定对方的ip地址,通过广播的信息获取,所以mac地址是广播的mac地址(全是f),设置arp请求将数据包发送给对方,交换机接收到这个数据包之后,一看是目的mac地址是广播的,所以会将这个数据包发送给当前网段下所有的主机,主机接收到这个数据包之后对比目的ip地址,如果不是自己的则丢弃,如果是自己的,将组包之后以arp应答的方式将数据包发送给获取者。
arp数据报:
接下来我要让ubuntu获取windows的mac地址:
以太网头:
Dest Mac:目的mac地址,广播的mac地址,全是f
Src Mac:源mac地址,ubuntu的mac地址,00:0c:29:a0:a6:f0
帧类型:后面跟的协议类型,如果是arp,则指定为0x0806
arp头:
硬件类型:以太网,1
协议类型:IP地址,0x0800
硬件地址长度:6
协议地址长度:4
OP:请求还是应答,1
1(ARP请求),2(ARP应答),3(RARP请求),4(RARP应答)
发送端以太网地址:源mac地址,ubuntu的mac地址,00:0c:29:a0:a6:f0
发送端ip地址:源ip地址:ubuntu的ip地址,192.168.70.72
目的以太网地址:目的mac地址,未知,可以传0
目的ip地址:目的ip地址,windows的ip地址,192.168.70.71
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <linux/if_packet.h>
#include <net/ethernet.h>
#include <arpa/inet.h>
#include <sys/ioctl.h>
#include <net/if.h>
#include <string.h>
#define ERRLOG(errmsg) do{\
perror(errmsg);\
printf("%s - %s - %d\n", __FILE__, __func__, __LINE__);\
exit(1);\
}while(0)
int main(int argc, char const *argv[])
{
int sockfd;
//创建一个原始套接字
if((sockfd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ARP))) == -1)
{
ERRLOG("socket error");
}
//使用arp协议通过对方ip地址获取对方mac地址
//组包
unsigned char buf[42] = {
//以太网头
//目的mac地址,广播的
0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
//源mac地址,自己的mac地址
0x00, 0x0c, 0x29, 0xa0, 0xa6, 0xf0,
//协议类型
0x08, 0x06,
//arp头
//硬件类型
0, 1,
//协议类型
0x08, 0x00,
//硬件地址长度
6,
//协议地址长度
4,
//op选项,设置为1表示arp请求
0, 1,
//源mac地址
0x00, 0x0c, 0x29, 0xa0, 0xa6, 0xf0,
//源ip地址
192, 168, 70, 72,
//目的mac地址
0, 0, 0, 0, 0, 0,
//目的ip地址
192, 168, 70, 71
};
//将arp请求报文发送出去,通过eth0发送出去
//使用ioctl函数获取本机网络接口
struct ifreq ethreq;
strncpy(ethreq.ifr_name, "ens33", IFNAMSIZ);
if(ioctl(sockfd, SIOCGIFINDEX, ðreq) == -1)
{
perror("fail to ioctl");
exit(1);
}
//设置本机网络接口
struct sockaddr_ll sll;
bzero(&sll, sizeof(sll));
sll.sll_ifindex = ethreq.ifr_ifindex;
//将数据包发送给网卡
if(sendto(sockfd, buf, 42, 0, (struct sockaddr *)&sll, sizeof(sll)) == -1)
{
ERRLOG("sendto error");
}
//接收对方的数据包并解析出mac地址
unsigned char mac[18] = {0};
memset(buf, 0, 42);
while(1)
{
if(recvfrom(sockfd, buf, 42, 0, NULL, NULL) == -1)
{
ERRLOG("recvfrom error");
}
//先判断数据包是否是arp数据包
if(ntohs(*(unsigned short *)(buf+12)) == 0x0806)
{
//再判断是否是arp应答
if(ntohs(*(unsigned short *)(buf+20)) == 2)
{
sprintf(mac, "%x:%x:%x:%x:%x:%x", buf[22], buf[23], buf[24], buf[25], buf[26], buf[27]);
printf("%d.%d.%d.%d -- %s\n", buf[28], buf[29], buf[30], buf[31], mac);
break;
}
}
}
return 0;
}