原始套接字

发布时间 2023-10-13 08:09:38作者: 常羲和

1. 原始套接字的概述

原始套接字(SOCK_RAM)的可执行文件必须sudo执行

  • 一种不同于SOCK_STREAM、SOCK_DGRAM的套接字,它实现于系统核心
  • 可以接收本机网卡上所有的数据帧(数据包),对于监听网络流量和分析网络数据很有作用
  • 开发人员可发送自己组装的数据包到网络上
  • 广泛应用于高级网络编程

不同套接字之间的比较:

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

image-20231011082351060

2. 创建一个原始套接字

创建链路层的原始套接字

#include <sys/socket.h>
#include <netinet/ether.h>

int socket(PF_PACKET,SOCK_RAW,protocol);

参数:

  • protocol:指定可以接收或发送的数据包类型
    • ETH_P_IP:IPV4数据包
    • ETH_P_ARP:ARP数据包
    • ETH_P_ALL:任何协议类型的数据包,需要htons()转换。

返回值:成功(>0):链路层套接字, 失败(<0): 失败

3. 数据包详解

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

3.1 协议在各层的关系

image-20231011082917305

3.2 组装/拆解数据包流程

【MAC|ARP|RARP】【IP|ICMP|IGMP】【TCP|UDP】【数据】

发送方:组装数据报文

接收方:拆解数据报文

3.3 UDP报文格式

image-20231011083148107

  • 源端口号:发送方端口号
  • 目的端口号:接收方端口号
  • 长度:UDP用户数据包的长度,最小值是8字节(仅有首部)
  • 校验和:检测UDP用户数据报在传输中是否有错,有错就丢弃

3.4 IP报文

image-20231011083359499

首部长度占4位:单位是4字节

协议类型:1- ICMP 2- IGMP 6- TCP 17-UDP

3.5 MAC报文

image-20231011083452077

以太网的MAC报文

image-20231011083506945

3.6 TCP报文

image-20231011083539724

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

3.7 ICMP报文

image-20231011084923879

【注】不同的类型值以及代码值,代表不同的功能

3.8 ARP报文

image-20231011085003755
  • Dest MAC:目的MAC地址

  • Src MAC:源MAC地址

  • 帧类型: 0x0806

  • 硬件类型:1(以太网)

  • 协议类型:0x0800(IP)

  • 硬件地址长度:6

  • 协议地址长度:4

  • OP:1(ARP请求) ,2(ARP应答),3(RARP请求),4(RARP应答)

4. 编程实战

4.1 recvfrom接收数据报

4.1.1 接收mac报文数据

ssize_t recvfrom(int socket,void *restrict buffer,size_t length,int flags,struct sockaddr *restrict_address,socket_len *restrict_address_len);

【注】因为接收的数据在链路层,无法确认IP地址及端口号,因此最后两个参数置为NULL

4.1.2 组包与解包

image-20231011135035756

创建原始套接字:

// 创建原始套接字
int sock_fd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
if (sock_fd < 0)
{
    perror("raw socket");
    return -1;
}
printf("原始套接字创建成功\n");

接连接层的数据报文

unsigned char buf[1518]="";
int len = recvfrom(sock_fd,buf,sizeof(buf),0,NULL,NULL);
if(len<18)
{
    perror("recvfrom");
    continue;
}

MAC数据报文拆解

unsigned char dst_mac[18] = "";
unsigned char src_mac[18] = "";
unsigned short mac_type = ntohs(*((unsigned short *)(buf + 12)));
sprintf(dst_mac, "%02x:%02x:%02x:%02x:%02x:%02x",
buf[0], buf[1], buf[2], buf[3], buf[4], buf[5]);
sprintf(src_mac, "%02x:%02x:%02x:%02x:%02x:%02x",
buf[6], buf[7], buf[8], buf[9], buf[10], buf[11]);
// if (strncmp(src_mac, "00:00:00", 8) != 0)
printf("%s->%s type(%04x)\n", src_mac, dst_mac, mac_type);

IP报文拆解

if(mac_type==0x800)
{
    printf("-----ip数据报------\n");
    //拆解IP数据报
    unsigned char*ip_buf = buf+14;
    //读取IP首部长度:单位是4字节
    unsigned char ip_head_len = (ip_buf[0]&0x0f)*4;
    printf("IP数据报的首部长度:%d bytes\n",ip_head_len);
    //读取协议类型
    unsigned char ip_type = ip_buf[9];
    //读取源IP和目的IP
    unsigned char src_ip[16]="";
    unsigned char dst_ip[16]="";
    inet_ntop(AF_INET,(unsigned int*)(ip_buf + 12),src_ip,16);
    inet_ntop(AF_INET,(unsigned int*)(ip_buf + 16),src_ip,16);
    switch(ip_type)
    {
        case 1:
            printf("\t----ICMP数据报-----\n");
            break;
        case 2:
            printf("\t----IGMP数据报-----\n");
            break;
        case 6:
            printf("\t----TCP数据报-----\n");
            break;
        case 17:
            printf("\t----UDP数据报-----\n");
            break;
    }
    printf("\t %s -> %s\n",src_ip,dst_ip);
}

UDP报文拆解:

//拆解UDP的数据报
unsigned char *udp_buf = ip_buf + ip_head_len;
unsigned short src_port = ntohs(*((unsigned short *)udp_buf));
unsigned short dst_port = ntohs(*((unsigned short *)(udp_buf+2)));
//UDP的数据报的长度,由首部+数据长度(偶数)
unsigned short udp_buf_len = ntohs(*((unsigned short *)(udp_buf+4)));
char udp_data[128]="";
strncpy(udp_data,udp_buf+8,udp_buf_len-8);
printf("\t\t %d -> %d data: %s\n",src_port,dst_port,udp_data);

【注】程序最后需要关闭原始套接字:close(sock_fd);

4.2 sendto发送数据

4.2.1 发送帧数据

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

参数说明:

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

4.2.2 sockaddr_ll结构体

sll的类型struct sockaddr_ll

#include <netpacket/packet.h>
struct sockaddr_ll sll;

image-20231012085043513

只需要对sll.sll_ifindex赋值即可

4.2.3 ioctl获取网络接口

#include <sys/ioctl.h>
int ioctl(int fd, int request, void *ifreq_val);

image-20231013074211652

ifreq结构体:它是POSIX标准中定义的

#include <net/if.h>
IFNAMSIZ 16

struct ifreq {
    char ifr_name[IFNAMSIZ]; /* 接口名称 */
    union {
        struct sockaddr ifr_addr; /* 接口地址 */
        struct sockaddr ifr_dstaddr; /* 目标地址 */
        struct sockaddr ifr_broadaddr; /* 广播地址 */
        struct sockaddr ifr_netmask; /* 子网掩码 */
        struct sockaddr ifr_hwaddr; /* 硬件地址 */
        short ifr_flags; /* 接口标志 */
        int ifr_ifindex; /* 接口索引 */
        int ifr_metric; /* 接口度量值 */
        int ifr_mtu; /* 最大传输单元 */
        ...
    } ifr_data;
};

4.2.4 封装sendto函数

ssize_t my_sendto(int socket, const void *message, size_t length, char *if_name)
{
    //获取接口
    struct ifreq ethreq;
    strncpy(ethreq.ifr_name, if_name, IFNAMSIZ);
    if (-1 == ioctl(socket, SIOCGIFINDEX, &ethreq))
    {
        perror("ioctl");
        close(socket);
        _exit(-1);
    }
    //定义接口结构
    struct sockaddr_ll sll;
    bzero(&sll, sizeof(sll));
    sll.sll_ifindex = ethreq.ifr_ifindex;
    //发送帧数据
    int len = sendto(socket, message, length, 0, (struct sockaddr *)&sll,sizeof(sll));
    return len;
}

如:

arp报文简化说明:

image-20231013075332890

单播ARP应答

#include <arpa/inet.h>
#include <netinet/ether.h>
#include <netinet/in.h>
#include <netpacket/packet.h>
#include <sys/ioctl.h>
#include <net/if.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>

ssize_t send_datapacket(int fd, unsigned char *buf, ssize_t buf_size, const char *ether_name)
{
    // 1. 获取网络接口类型
    struct ifreq ether_req;
    bzero(&ether_req, sizeof(ether_req));
    strncpy(ether_req.ifr_name, ether_name, IF_NAMESIZE);
    if (ioctl(fd, SIOCGIFINDEX, &ether_req) == -1)
    {
        perror("ioctl");
        return -1;
    }
    // 2. 选择发送数据的网络接口索引
    struct sockaddr_ll sll;
    bzero(&sll, sizeof(sll));
    sll.sll_ifindex = ether_req.ifr_ifindex;
    // 3. 发送数据
    ssize_t len = sendto(fd, buf, buf_size, 0, (struct sockaddr *)&sll,sizeof(sll));
    return len;
}

int main(int argc, char const *argv[])
{
    // 创建原始套接字
    int sock_fd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
    if (sock_fd < 0)
    {
        perror("raw socket");
        return -1;
    }
    // 组ARP应答报文 (如果是ARP欺骗, 则源mac全为0)
    uint32_t src_ip = inet_addr("10.12.156.207");
    uint32_t dst_ip = inet_addr("10.12.156.223");
    unsigned char *src_ip_p = (unsigned char *)&src_ip;
    unsigned char *dst_ip_p = (unsigned char *)&dst_ip;
    unsigned char buf[] = {
        0x00, 0x0c, 0x29, 0xe3, 0x4e, 0x7c, /*目标的mac*/
        0x00, 0x0c, 0x29, 0x81, 0x71, 0x7b, /*源的mac*/
        0x08, 0x06, /*ARP帧类型*/
        0x00, 0x1, /*硬件类型*/
        0x08, 0x00, /*协议类型*/
        6, 4, /*硬件地址长度和协议地址长度*/
        0, 2, /*OP, 2表示应答ARP*/
        0x00, 0x0c, 0x29, 0x81, 0x71, 0x7b, /*发送端mac*/
        src_ip_p[0], src_ip_p[1], src_ip_p[2], src_ip_p[3], /*发送端的IP*/
        0x00, 0x0c, 0x29, 0xe3, 0x4e, 0x7c, /*接收端的mac*/
        dst_ip_p[0], dst_ip_p[1], dst_ip_p[2], dst_ip_p[3] /*接收端的IP*/
    };
    // 单播 发送arp应答
    ssize_t len = send_datapacket(sock_fd, buf, sizeof(buf), "ens33");
    if (len > 0)
    {
        printf("发送ARP应答报文成功\n");
    }
    close(sock_fd);
    return 0;
}