[Linux内核之旅](javascript:void(0)😉 今天

以下文章来源于技术简说 ,作者董旭

本公众号作者为Linux内核之旅社区成员-董旭

基于tcpdump原理手动实现抓包程序

前面两篇文章分析tcpdump实现抓包原理:

1、文章1 (opens new window)主要从Linux内核角度分析tcpdump旁路嗅探数据包的过程

2、文章2 (opens new window)主要从Linux内核角度分析tcpdump利用BPF机制实现数据包捕获前的过滤过程

本次将根据前面的分析,手动写一个基于tcpdump工具原理的简易抓包程序,实现从链路层的抓包,加深一下关于tcpdump原理的印象。

#

流程分析

# 1、PF_PACKET协议族的socket

正如在文章1 (opens new window)中分析时,以socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL))为着手点。我们通常都是通过创建PF_PACKET协议族的socket,用来抓包、分析数据的。

如下,创建一个PF_PACKET类型的socket,type指定为SOCK_RAW,当指定SOCK_RAW时,获取的数据包是一个完整的数据链路层数据包。proticol字段设置为htons(ETH_P_ALL),表示接收端数据链路层所有协议帧。

/*socket函数原型:
#include <sys/socket.h>
sockfd = socket(int socket_family, int socket_type, int protocol);
*/
sock = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
 if (sock < 0) {
  perror("socket");
  return 1;
 }
1
2
3
4
5
6
7
8
9

# 2、设置链路层属性

# 核心结构体:struct sockaddr_ll
struct sockaddr_ll
{
 unsigned short int sll_family; /* 一般为AF_PACKET */
 unsigned short int sll_protocol; /* 上层协议类型 */
 int  sll_ifindex; /* 网卡接口索引号,0 匹配所有的网络接口卡 */
 unsigned short int sll_hatype; /* 报头类型 */
 unsigned char sll_pkttype; /* 包类型 */
 unsigned char sll_halen; /* 地址长度 */
 unsigned char sll_addr[8]; /* MAC地址 */
};
1
2
3
4
5
6
7
8
9
10

该结构体为设备无关的物理层地址结构,数据链路层的头信息通常定义在sockaddr_all的结构体中,当发送数据包时,指定 sll_family, sll_addr, sll_halen, sll_ifindex, sll_protocol 就足够了。其它字段设置为0;sll_hatype和 sll_pkttype是在接收数据包时使用的;如果要bind, 只需要使用 sll_protocol和 sll_ifindex就足够。

 struct sockaddr_ll addr;
 memset(&addr, 0, sizeof(addr));
 addr.sll_ifindex = if_nametoindex(name);//name为当前要抓包的网卡接口名称
 addr.sll_family = AF_PACKET;
 addr.sll_protocol = htons(ETH_P_ALL);
1
2
3
4
5

# 3、将创建的soccket与地址绑定

 if (bind(sock, (struct sockaddr *) &addr, sizeof(addr))) {
  perror("bind");
  return 1;
 }
1
2
3
4

# 4、抓包的过滤条件

文章2 (opens new window)中,介绍了BPF过滤机制,在tcpdump的过滤机制中有一个重要的结构体:struct sock_filter,同时也是cBPF汇编的一个框架

struct sock_filter {
 __u16 code;   /*指令  32位*/
 __u8 jt; /* jt是指令结果为true的跳转 */
 __u8 jf; /* jf是为false的跳转 */
 __u32 k;      /* 指令参数*/
};
1
2
3
4
5
6

该结构体一般是封装在struct sock_fprog中使用:

struct sock_fprog      
{
    unsigned short  len;   
    struct sock_filter   *filter;
};
1
2
3
4
5

文章1 (opens new window)中分析过,使用tcpdump -d参数可以生成BPF汇编伪代码:

dx@ubuntu:~$ sudo tcpdump -d 'ip and tcp port 80'
(000) ldh      [12]
(001) jeq      #0x800           jt 2    jf 12
(002) ldb      [23]
(003) jeq      #0x6             jt 4    jf 12
(004) ldh      [20]
(005) jset     #0x1fff          jt 12   jf 6
(006) ldxb     4*([14]&0xf)
(007) ldh      [x + 14]
(008) jeq      #0x50            jt 11   jf 9
(009) ldh      [x + 16]
(010) jeq      #0x50            jt 11   jf 12
(011) ret      #262144
(012) ret      #0
1
2
3
4
5
6
7
8
9
10
11
12
13
14

这种伪代码在C程序中是无法使用的,需要借用tcpdump -dd参数生成等效的c代码:

dx@ubuntu:~$ sudo tcpdump -dd 'ip and tcp port 80'
{ 0x28, 0, 0, 0x0000000c },
{ 0x15, 0, 10, 0x00000800 },
{ 0x30, 0, 0, 0x00000017 },
{ 0x15, 0, 8, 0x00000006 },
{ 0x28, 0, 0, 0x00000014 },
{ 0x45, 6, 0, 0x00001fff },
{ 0xb1, 0, 0, 0x0000000e },
{ 0x48, 0, 0, 0x0000000e },
{ 0x15, 2, 0, 0x00000050 },
{ 0x48, 0, 0, 0x00000010 },
{ 0x15, 0, 1, 0x00000050 },
{ 0x6, 0, 0, 0x00040000 },
{ 0x6, 0, 0, 0x00000000 },
1
2
3
4
5
6
7
8
9
10
11
12
13
14

这段代码就是struct sock_filter:

static struct sock_filter bpfcode[13] = {
{ 0x28, 0, 0, 0x0000000c },
{ 0x15, 0, 10, 0x00000800 },
{ 0x30, 0, 0, 0x00000017 },
{ 0x15, 0, 8, 0x00000006 },
{ 0x28, 0, 0, 0x00000014 },
{ 0x45, 6, 0, 0x00001fff },
{ 0xb1, 0, 0, 0x0000000e },
{ 0x48, 0, 0, 0x0000000e },
{ 0x15, 2, 0, 0x00000050 },
{ 0x48, 0, 0, 0x00000010 },
{ 0x15, 0, 1, 0x00000050 },
{ 0x6, 0, 0, 0x00040000 },
{ 0x6, 0, 0, 0x00000000 },
};

struct sock_fprog bpf = { 13, bpfcode };
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 5、设置BPF过滤器

Linux在安装和卸载过滤器时都使用了函数setsockopt(),其中标志SOL_SOCKET代表对socket进行设置,SO_ATTACH_FILTER表示安装过滤器动作,setsockopt在内核中的调用可以看文章2 (opens new window)

setsockopt(sd, SOL_SOCKET, SO_ATTACH_FILTER, &Filter, sizeof(Filter));
 if (setsockopt(sock, SOL_SOCKET, SO_ATTACH_FILTER, &bpf, sizeof(bpf))) {
  perror("setsockopt ATTACH_FILTER");
  return 1;
 }
1
2
3
4
5

# 6、设置网卡的混杂模式

关键结构体:struct packet_mreq

struct packet_mreq
{
intmr_ifindex; /* 接口索引 */
unsigned shortmr_type; /* mreq 类型 */
unsigned shortmr_alen; /* 地址长度 */
unsigned charmr_address[8]; /* 物理地址 */
};
1
2
3
4
5
6
7

混杂模式:

混杂模式就是接收所有经过网卡的数据包,包括不是发给本机的包,默认情况下网卡只把发给本机的包(包括广播包)传递给上层程序,其它的包一律丢弃;简单的讲,混杂模式就是指网卡能接受所有通过它的数据流,不管是什么格式,什么地址,当网卡处于混杂模式时,该网卡就具有“广播地址”,它对所有遇到的每一个数据帧都产生一个硬件中断,以便提醒操作系统处理流经过该物理媒体上的每一个报文包。

通过shortmr_type字段可以设置混杂模式

    struct packet_mreq mreq;
    memset(&mreq, 0, sizeof(mreq));
 mreq.mr_type = PACKET_MR_PROMISC;//设置混杂模式
 mreq.mr_ifindex = if_nametoindex(name);
1
2
3
4

将设置的混杂模式设置到socket:

 if (setsockopt(sock, SOL_PACKET,
    PACKET_ADD_MEMBERSHIP, (char *)&mreq, sizeof(mreq))) {
  perror("setsockopt MR_PROMISC");//ACKET_ADD_MEMBERSHIP 用于增加一个绑定
  return 1;
 }
1
2
3
4
5

# 7、定义要获得的数据报文信息

# 源和目的MAC地址

关键结构体:

struct ether_header
{
  uint8_t  ether_dhost[ETH_ALEN]; /* 目的MAC地址 */
  uint8_t  ether_shost[ETH_ALEN]; /* 源MAC地址 */
  uint16_t ether_type;          /* packet type ID field */
};
1
2
3
4
5
6

IP信息(源、目的IP,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 <bits/endian.h>"
#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;   //源地址
    uint32_t daddr;   //目的地址
    /*The options start here. */
  };
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 8、循环接收捕获的数据包

文章1 (opens new window)中,分析了数据包是怎样捕获的:

回顾:

tcpdump进行抓包的内核流程梳理

  • 应用层通过libpcap库:调用系统调用创建socket,sock_fd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));tcpdump在socket创建过程中创建packet_type(struct packet_type),并挂载到全局的ptype_all链表上。(同时在packet_type设置回调函数packet_rcv
  • 网络收包/发包时,会在各自的处理函数(收包时:__netif_receive_skb_core,发包时:dev_queue_xmit_nit)中遍历ptype_all链表,并同时执行其回调函数,这里tcpdump的注册的回调函数就是packet_rcv
  • packet_rcv函数中会将用户设置的过滤条件,通过BPF进行过滤,并将过滤的数据包添加到接收队列中
  • 应用层调用recvfrom 。PF_PACKET 协议簇模块调用packet_recvmsg 将接收队列中的数据copy应用层,到此将数据包捕获到。

所以上面创建好的PF_PACKET类型socket,并设置好过滤器后,当网卡有数据进出时,就已经将数据报文添加到了接收队列上了,下面只需要我们进行recv获取数据报文即可。

 for (;;) {
  n = recv(sock, buf, sizeof(buf), 0);
  if (n < 1) {
   perror("recv");
   return 0;
  }
        //获取链路层的源和目的地址
  mac_hdr=(struct ether_header *)buf;

  ip = (struct iphdr *)(buf + sizeof(struct ether_header));

  inet_ntop(AF_INET, &ip->saddr, saddr_str, sizeof(saddr_str));
  inet_ntop(AF_INET, &ip->daddr, daddr_str, sizeof(daddr_str));

  switch (ip->protocol) {
#define PTOSTR(_p,_str) \
   case _p: proto_str = _str; break

  PTOSTR(IPPROTO_ICMP, "icmp");
  PTOSTR(IPPROTO_TCP, "tcp");
  PTOSTR(IPPROTO_UDP, "udp");
  default:
   proto_str = "";
   break;
  }
        printf(" SMAC:%X:%X:%X:%X:%X:%X",
    (u_char)mac_hdr->ether_shost[0],
    (u_char)mac_hdr->ether_shost[1],
    (u_char)mac_hdr->ether_shost[2],
    (u_char)mac_hdr->ether_shost[3],
    (u_char)mac_hdr->ether_shost[4],
    (u_char)mac_hdr->ether_shost[5]
   );

  printf(" ==>  DMAC:%X:%X:%X:%X:%X:%X  ",
    (u_char)mac_hdr->ether_dhost[0],
    (u_char)mac_hdr->ether_dhost[1],
    (u_char)mac_hdr->ether_dhost[2],
    (u_char)mac_hdr->ether_dhost[3],
    (u_char)mac_hdr->ether_dhost[4],
    (u_char)mac_hdr->ether_dhost[5]
   );
  printf("IPv%d proto=%d(%s) src=%s dst=%s\n",
    ip->version, ip->protocol, proto_str, saddr_str, daddr_str);
 }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

至此,从创建socket,到设置socket过滤器、网卡工作模式,到接收捕获的数据包就结束了。

#

附:源代码

图片

实现捕获数据包的过滤条件:ip and tcp port 80

#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <net/if.h>
#include <net/ethernet.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <netpacket/packet.h>
#include <linux/filter.h>

static struct sock_filter bpfcode[13] = {
{ 0x28, 0, 0, 0x0000000c },
{ 0x15, 0, 10, 0x00000800 },
{ 0x30, 0, 0, 0x00000017 },
{ 0x15, 0, 8, 0x00000006 },
{ 0x28, 0, 0, 0x00000014 },
{ 0x45, 6, 0, 0x00001fff },
{ 0xb1, 0, 0, 0x0000000e },
{ 0x48, 0, 0, 0x0000000e },
{ 0x15, 2, 0, 0x00000050 },
{ 0x48, 0, 0, 0x00000010 },
{ 0x15, 0, 1, 0x00000050 },
{ 0x6, 0, 0, 0x00040000 },
{ 0x6, 0, 0, 0x00000000 },
};

int main(int argc, char **argv)
{
 int sock;
 int n;
 char buf[2000];
 struct sockaddr_ll addr;
 struct packet_mreq mreq;
 struct iphdr *ip;
    struct ether_header *mac_hdr;
    char saddr_str[INET_ADDRSTRLEN], daddr_str[INET_ADDRSTRLEN];
 char *proto_str;
 char *name;
 struct sock_fprog bpf = { 13, bpfcode };

 if (argc != 2) {
  printf("Usage: %s ifname\n", argv[0]);
  return 1;
 }

 name = argv[1];

 sock = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
 if (sock < 0) {
  perror("socket");
  return 1;
 }

 memset(&addr, 0, sizeof(addr));
 addr.sll_ifindex = if_nametoindex(name);
 addr.sll_family = AF_PACKET;
 addr.sll_protocol = htons(ETH_P_ALL);

 if (bind(sock, (struct sockaddr *) &addr, sizeof(addr))) {
  perror("bind");
  return 1;
 }

 if (setsockopt(sock, SOL_SOCKET, SO_ATTACH_FILTER, &bpf, sizeof(bpf))) {
  perror("setsockopt ATTACH_FILTER");
  return 1;
 }

 memset(&mreq, 0, sizeof(mreq));
 mreq.mr_type = PACKET_MR_PROMISC;
 mreq.mr_ifindex = if_nametoindex(name);

 if (setsockopt(sock, SOL_PACKET,
    PACKET_ADD_MEMBERSHIP, (char *)&mreq, sizeof(mreq))) {
  perror("setsockopt MR_PROMISC");
  return 1;
 }

 for (;;) {
  n = recv(sock, buf, sizeof(buf), 0);
  if (n < 1) {
   perror("recv");
   return 0;
  }
        //获取链路层的源和目的地址
  mac_hdr=(struct ether_header *)buf;

  ip = (struct iphdr *)(buf + sizeof(struct ether_header));

  inet_ntop(AF_INET, &ip->saddr, saddr_str, sizeof(saddr_str));
  inet_ntop(AF_INET, &ip->daddr, daddr_str, sizeof(daddr_str));

  switch (ip->protocol) {
#define PTOSTR(_p,_str) \
   case _p: proto_str = _str; break

  PTOSTR(IPPROTO_ICMP, "icmp");
  PTOSTR(IPPROTO_TCP, "tcp");
  PTOSTR(IPPROTO_UDP, "udp");
  default:
   proto_str = "";
   break;
  }
        printf(" SMAC:%X:%X:%X:%X:%X:%X",
    (u_char)mac_hdr->ether_shost[0],
    (u_char)mac_hdr->ether_shost[1],
    (u_char)mac_hdr->ether_shost[2],
    (u_char)mac_hdr->ether_shost[3],
    (u_char)mac_hdr->ether_shost[4],
    (u_char)mac_hdr->ether_shost[5]
   );

  printf(" ==>  DMAC:%X:%X:%X:%X:%X:%X  ",
    (u_char)mac_hdr->ether_dhost[0],
    (u_char)mac_hdr->ether_dhost[1],
    (u_char)mac_hdr->ether_dhost[2],
    (u_char)mac_hdr->ether_dhost[3],
    (u_char)mac_hdr->ether_dhost[4],
    (u_char)mac_hdr->ether_dhost[5]
   );
  printf("IPv%d proto=%d(%s) src=%s dst=%s\n",
    ip->version, ip->protocol, proto_str, saddr_str, daddr_str);
 }

 return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128

# 运行:

图片

注意:程序指定的参数:ens33为自己的网卡接口名称,可以通过ip addr进行查看。

喜欢此内容的人还喜欢

Filebeat、Logstash、Rsyslog 各种姿势采集Nginx日志

高效运维

不喜欢

不看的原因

确定

  • 内容质量低

  • 不看此公众号

img

微信扫一扫 关注该公众号

:,。视频小程序赞,轻点两下取消赞在看,轻点两下取消在看

Last Updated: 2022/7/8 下午2:41:42