基于tcpdump原理手写抓包程序

语言: CN / TW / HK

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

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

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

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

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

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

流程分析

1、PF_PACKET协议族的socket

正如在文章1中分析时,以 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;
}

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地址 */
};

该结构体为设备无关的物理层地址结构,数据链路层的头信息通常定义在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);

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

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

4、抓包的过滤条件

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

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

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

struct sock_fprog      
{

unsigned short len;
struct sock_filter *filter;
};

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

[email protected]:~$ 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

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

[email protected]:~$ 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 },

这段代码就是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 };

5、设置BPF过滤器

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

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;
}

6、设置网卡的混杂模式

关键结构体:struct packet_mreq

struct packet_mreq
{

intmr_ifindex; /* 接口索引 */
unsigned shortmr_type; /* mreq 类型 */
unsigned shortmr_alen; /* 地址长度 */
unsigned charmr_address[8]; /* 物理地址 */
};

混杂模式:

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

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

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

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

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

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 */
};

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. */
};

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

在文章1中,分析了数据包是怎样捕获的:

回顾:

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);
}

至此,从创建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;
}

运行:

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