跳至主要內容

嵌入式学习之(九)| 原始套接字

Kevin 吴嘉文大约 14 分钟知识笔记Linux嵌入式学习

笔记总结 课程链接:千峰嵌入式教程open in new window

概述

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

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

创建原始套接字

#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
功能:创建套接字,返回文件描述符
参数:
domain:通信域,地址族
AF_PACKET
type:套接字类型
SOCK_RAW
protocol:附加协议
#include <netinet/ether.h>
ETH_P_ALL 所有协议对应的数据包
ETH_P_IP 只接受 ip 数据包
ETH_P_ARP 只接受 arp 数据包
返回值:
成功:文件描述符
失败:‐1

创建链路层的原始套接字

#include <sys/socket.h>
#include <sys/types.h> //socket
#include <netinet/ether.h> //ETH_P_ALL
#include <unistd.h> //close
#include <stdlib.h> //exit
#include <stdio.h> //printf
#include <arpa/inet.h>
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;
}

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

数据包详解

在 TCP/IP 协议栈中的每一层为了能够正确解析出上层的数据包,从而使用一些“协议类型”来标记,详细如下图

image-20210407170927862
image-20210407170927862

UDP 封包格式

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

IP 封包格式

image-20210409221404501
image-20210409221404501
  1. 版本:IP 协议的版本。通信双方使用过的 IP 协议的版本必须一致,目前最广泛使用的 IP 协议版本号为 4(即 IPv4 )
  2. 首部长度:单位是 32 位(4 字节)传 5 表示 20 字节
  3. 服务类型:一般不适用,取值为 0。前 3 位:优先级,第 47 位:延时,吞吐量,可靠性,花费。第 8 位保留
  4. 总长度:指首部加上数据的总长度,单位为字节。最大长度为 65535 字节。
  5. 标识(identification):用来标识主机发送的每一份数据报。IP 软件在存储器中维持一个计数器,每产生一个数据报,计数器就加 1,并将此值赋给标识字段。
  6. 标志(flag):目前只有两位有意义。 标志字段中的最低位记为 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. 选项:用来定义一些任选项;如记录路径、时间戳等。这些选项很少被使用,同时并不是所有主机和路由器都支持这些选项。一般忽略不计

数据部分填充 TCP 或 UDP 及应用数据

Ethernet 封包格式

image-20210409222719590
image-20210409222719590

1、目的地址:目的 mac 地址 2、源地址:源 mac 地址 3、类型:ip 数据报(0x0800)、ARP 数据报(0x0806)、RARP(0x8035) 4、数据:数据根据类型来决定

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

TCP 封包格式

image-20210409223043298
image-20210409223043298
  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 字节

ICMP 封包格式

使用时要跟 IP 协议

image-20210409223502067
image-20210409223502067

分析 MAC 数据包

#include <sys/socket.h>
#include <sys/types.h> //socket
#include <netinet/ether.h> //ETH_P_ALL
#include <unistd.h> //close
#include <stdlib.h> //exit
#include <stdio.h> //printf
#include <arpa/inet.h> //htons
#define ERRLOG(errmsg) do{\
perror(errmsg);\
exit(1);\
}while(0)
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");
}
//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(src_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、ARP 的 TYPE 为 0x0806 2、buf 为 unsinged char 3、所有数据均为大端

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

#include <sys/socket.h>
#include <sys/types.h> //socket
#include <netinet/ether.h> //ETH_P_ALL
#include <unistd.h> //close
#include <stdlib.h> //exit
#include <stdio.h> //printf
#include <arpa/inet.h> //htons
#define ERRLOG(errmsg) do{\
perror(errmsg);\
exit(1);\
}while(0)
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");
}
//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(src_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_type);
//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");
}
printf("\n*****************\n\n");
}
close(sockfd);
return 0;
}

混杂模式

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

image-20210409224416877
image-20210409224416877

sendto 发送数据

用 sendto 发送原始套接字数据

sendto(sock_raw_fd, msg, msg_len, 0,(struct sockaddr*)&sll, sizeof(sll));

注意: 1、sock_raw_fd:原始套接字 2、msg:发送的消息(封装好的协议数据) 3、sll:本机网络接口,指发送的数据应该从本机的哪个网卡出去,而不是以前的目的地址

本机网络接口

#include <netpacket/packet.h>
struct sockaddr_ll
image-20210409224613765
image-20210409224613765

只需要对 sll.sll_ifindex 赋值,就可使用

发送数据 demo

struct sockaddr_ll sll;
bzero(&sll, sizeof(sll));
sll.sll_ifindex = ??? //赋值为获取到当前要出去的网络接口的地址
if(sendto(sockfd, msg, sizeof(msg), 0, (struct sockaddr *)&sll, sizeof(sll)) ==1)
{
perror("fail to sendto");
exit(1);
}

通过 ioctl 来获取网络接口地址

//获取接口信息
//将 arp 请求报文发送出去,通过 eth0 发送出去
//使用 ioctl 函数获取本机网络接口
struct ifreq ethreq;
strncpy(ethreq.ifr_name, "eth0", 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, msg, sizeof(msg), 0, (struct sockaddr *)&sll, sizeof(sll)) < 0)
{
perror("fail to sendto");
exit(1);
}

APR

ARP 概述

如果 A(192.168.1.1)向 B(192.168.1.2)发送一个数据包,那么需要的条件有 ip、port、使用的协议(TCP/UDP)之外还需要 MAC 地址,因为在以太网数据包中 MAC 地址是必须要有的; 问怎样才能知道对方的 MAC 地址?使用什么协议呢?

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

在 linux 下查看 arp 表 arp

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

ARP 协议格式

image-20210409225346159
image-20210409225346159
  1. Dest MAC:目的 MAC 地址
  2. Src MAC:源 MAC 地址
  3. 帧类型:0x0806
  4. 硬件类型:1(以太网)
  5. 协议类型:0x0800(IP 地址)
  6. 硬 件地址长度:6
  7. 协议地址长度:4
  8. OP:1(ARP 请求),2(ARP 应答),3(RARP 请求),4(RARP 应答)

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

#include <sys/socket.h>
#include <sys/types.h> //socket
#include <stdio.h> //printf
#include <unistd.h> //close
#include <stdlib.h> //exit
#include <netinet/ether.h> //ETH_P_ALL
#include <netpacket/packet.h> //sockaddr_ll
#include <sys/ioctl.h> //ioctl
#include <net/if.h> //ifreq
#include <string.h> //strncpy
#include <strings.h> //bzero
#include <arpa/inet.h> //htons
#define ERRLOG(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, 3, 78 //目的 ip 地址
};
//获取接口信息
//将 arp 请求报文发送出去,通过 ens33 发送出去
//使用 ioctl 函数获取本机网络接口
struct ifreq ethreq;
strncpy(ethreq.ifr_name, "ens33", 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)
{
ERRLOG("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]
ecv_msg[8], recv_msg[9], recv_msg[10], recv_msg[11]);
printf("192.168.3.%d ‐‐> %s\n", msg[41], mac);
break;
}
}
}
close(sockfd);
return 0;
}
上次编辑于:
贡献者: kevinng77