linux 下 tcpdump 详解 后篇(自己实现抓包过滤)

本文详细介绍了如何在Linux下利用libpcap库实现抓包过滤,包括数据链路层和网络层的抓包过滤,以及如何通过tcpdump生成过滤规则。文章还探讨了在lo口抓包发包的特性和注意事项,并给出了实际应用示例,涉及抓包、过滤、转发数据包等操作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一 概述

在了解了tcpdump的原理后,你有没有想过自己去实现抓包过滤? 可能你脑子里有个大概的思路,但是知道了理论知识,其实并不能代表你完全的理解。只要运用后,你才知道哪些点需要注意,之前没有考虑到的。

二 如何实现抓包过滤

在写代码前,先捋下思路,和相应的理论知识。

libpcap 库中实现抓包关键代码

	sock_fd = cooked ?
	socket(PF_PACKET, SOCK_DGRAM, protocol) :
	socket(PF_PACKET, SOCK_RAW, protocol);

libpcap库中pcap_open_live 函数最终会调用上面这行代码,而创建的这socket就可以接收数据链路层的数据包。而protocol 可以指定数据链路层协议帧类型,例如IPv4帧,可以传入htons(ETH_P_IP),接收到数据链路层所有协议帧,可以传入htons(ETH_P_ALL)

  • socket(PF_PACKET, SOCK_DGRAM, htons(ETH_P_ALL))
    当指定SOCK_DGRAM时,获取的数据包是去掉了数据链路层的头(link-layer header)
  • socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL))
    当指定SOCK_RAW时,获取的数据包是一个完整的数据链路层数据包

已经可以抓数据链路层的数据包,但是如何设置过滤规则呢?

在libpcap 设置过滤规则用到了两个接口,pcap_compile()和 pcap_setfilter ()
函数,其中pcap_compile()主要将我们输入的过滤规则表达式编译成BPF代码,然后存入bpf_program的结构中。而pcap_setfilter 就是将过滤的规则注入内核。这里不关注pcap_compile 如何编译成bpf代码,然后存入bpf_program。 bpf深入的话,其实里面的东西还有很多(例如输入的过滤的规则怎么转发对应的bpf代码,应用层是如何注入bpf规则到内核,内核又是如何不必要重新编译代码,就可以怎么根据不同的socket上的bpf规则过滤数据包的,等等)。本人技术有限,只了解了部分。有兴趣可以看这篇博客Linux bpf 1.1、BPF内核实现。言归正传,主要看pcap_setfilter 关键代码,如下

struct sock_fprog *fcode
ret = setsockopt(handle->fd, SOL_SOCKET, SO_ATTACH_FILTER,
			 fcode, sizeof(*fcode));

其实在liunx上,你只需要简单的创建你的filter代码,通过SO_ATTTACH_FILTER选项发送到内核,并且你的filter代码能通过内核的检查,这样你就可以立即过滤socket上面的数据了。
而 struct sock_fprog 结构如下

struct sock_fprog {			/* Required for SO_ATTACH_FILTER. */
	unsigned short		   len;	/* Number of filter blocks */
	struct sock_filter __user *filter;
};

struct sock_filter {	/* Filter block */
	__u16	code;   /* Actual filter code */
	__u8	jt;	/* Jump true */
	__u8	jf;	/* Jump false */
	__u32	k;      /* Generic multiuse field */
};

其中code元素是一个16位宽的操作码,具有特定的指令编码。jt和jf是两个8位宽的跳转目标,一个用于条件“跳转如果真”,另一个“跳转如果假”。最后k元素包含一个可以用不同方式解析的杂项参数,依赖于code给定的指令。

那么过滤规则如何转化为sock_filter 结构体对应的规则呢?不是说不需要深入bpf,其实tcpdump 提供了一种方法,可以将过滤规则转化成对应liunx c下sock_filter 规则。

例如你需要过滤经过本机端口22所有的数据。在liunx 终端安装了tcpdump 。只需在终端输入 tcpdump port 22 -nn -dd 就可生产对应规则,如下图
tcpdump  -dd
至此你其实已经完全可以根据要过滤的包,自己实现抓包过滤了。程序如下

int create_link_raw_socket(){
	struct sock_filter bpf_code[] = {
			// tcpdump  port 22 -nn -dd
			{ 0x28, 0, 0, 0x0000000c },
			{ 0x15, 0, 8, 0x000086dd },
			{ 0x30, 0, 0, 0x00000014 },
			{ 0x15, 2, 0, 0x00000084 },
			{ 0x15, 1, 0, 0x00000006 },
			{ 0x15, 0, 17, 0x00000011 },
			{ 0x28, 0, 0, 0x00000036 },
			{ 0x15, 14, 0, 0x00000016 },
			{ 0x28, 0, 0, 0x00000038 },
			{ 0x15, 12, 13, 0x00000016 },
			{ 0x15, 0, 12, 0x00000800 },
			{ 0x30, 0, 0, 0x00000017 },
			{ 0x15, 2, 0, 0x00000084 },
			{ 0x15, 1, 0, 0x00000006 },
			{ 0x15, 0, 8, 0x00000011 },
			{ 0x28, 0, 0, 0x00000014 },
			{ 0x45, 6, 0, 0x00001fff },
			{ 0xb1, 0, 0, 0x0000000e },
			{ 0x48, 0, 0, 0x0000000e },
			{ 0x15, 2, 0, 0x00000016 },
			{ 0x48, 0, 0, 0x00000010 },
			{ 0x15, 0, 1, 0x00000016 },
			{ 0x6, 0, 0, 0x0000ffff },
			{ 0x6, 0, 0, 0x00000000 }
	};
	
	int fd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
	struct sock_fprog bpf;
	memset(&bpf,0x00,sizeof(bpf));
	bpf.len = sizeof(bpf_code) / sizeof(struct sock_filter);
	bpf.filter = bpf_code;
	int ret = setsockopt( fd,SOL_SOCKET, SO_ATTACH_FILTER, &bpf,sizeof(bpf));
	if (ret < 0)
	{
		printf("setsockopt:SO_ATTACH_FILTER>>>>error:%s\n",strerror(errno));
	}
	return fd;
}

通过上面的代码你根据返回的 fd ,调用recvfrom 接收到的包就是经过过滤的22端口的数据包。但是你要是想抓的包是去除数据链路层的头,用socket(PF_PACKET, SOCK_DGRAM, htons(ETH_P_ALL)); 然后根据tcpdump -dd生成的规则,你会发现加入的规则起不到作用。这里推测 tcpdump -dd 生成的规则只针对链路层。

同时在socket除了你可以添加你要过滤的规则,还有几个两个与bpf规则相关的系统调用

  • 在套接字socket 附加filter规则 :
    setsockopt(sockfd, SOL_SOCKET, SO_ATTACH_FILTER, &val, sizeof(val));
  • 把filter从socket上移除 :
    setsockopt(sockfd, SOL_SOCKET, SO_DETACH_FILTER, &val, sizeof(val));
  • 运行中锁定附加到socket上的filter:
    setsockopt(sockfd, SOL_SOCKET, SO_LOCK_FILTER, &val, sizeof(val));

其中SO_DETACH_FILTER选项可以把filter从socket上移除。这可能不会被经常使用,因为当你关闭socket的时候如果有filter会被自动移除。另外一个不太常见的情况是在同一个socket上添加不同的filter,当你还有另一个filter正在运行:如果你的新filter代码能够通过内核检查,内核小心的把旧的filter移除把新的filter换上,如果检查失败旧的filter将继续保留在socket上。

SO_LOCK_FILTER选项运行锁定附加到socket上的filter。一旦设置,filter不能被移除或者改变。这种允许一个进程设置一个socket、附加一个filter、锁定它们并放弃特权,确保这个filter保持到socket的关闭。

三 抓包过滤应用实现

通过上面了解,貌似自己实现抓包过滤并不存在啥技术难度。相反是不是感觉很简单。其实并不见得,本章要实现抓包过滤的应用功能,本质上是类似实现nat转换的功能。大概就是经过本机指定的srcip,srcPort过滤数据包,然后修改数据包,给该数据转发到另一台设备上destip,destPort。

1. 数据链路层抓包

notes : 只给出关键代码

int create_link_raw_socket(){
	struct sock_filter bpf_code[] = {
			// tcpdump  src  10.68.22.140 and port 7777 -nn -dd
			{ 0x28, 0, 0, 0x0000000c },
			{ 0x15, 0, 14, 0x00000800 },
			{ 0x20, 0, 0, 0x0000001a },
			{ 0x15, 0, 12, 0x0a44168c },
			{ 0x30, 0, 0, 0x00000017 },
			{ 0x15, 2, 0, 0x00000084 },
			{ 0x15, 1, 0, 0x00000006 },
			{ 0x15, 0, 8, 0x00000011 },
			{ 0x28, 0, 0, 0x00000014 },
			{ 0x45, 6, 0, 0x00001fff },
			{ 0xb1, 0, 0, 0x0000000e },
			{ 0x48, 0, 0, 0x0000000e },
			{ 0x15, 2, 0, 0x00001e61 },
			{ 0x48, 0, 0, 0x00000010 },
			{ 0x15, 0, 1, 0x00001e61 },
			{ 0x6, 0, 0, 0x0000ffff },
			{ 0x6, 0, 0, 0x00000000 }
	};
	int fd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
	struct sock_fprog bpf;
	memset(&bpf,0x00,sizeof(bpf));
	bpf.len = sizeof(bpf_code) / sizeof(struct sock_filter);
	bpf.filter = bpf_code;

	int ret = setsockopt( fd,SOL_SOCKET, SO_ATTACH_FILTER, &bpf,sizeof(bpf));
	if (ret < 0)
	{
		printf("setsockopt:SO_ATTACH_FILTER>>>>error:%s\n",strerror(errno));
	}
	return fd;
}

首先创建了socket ,从数据链路层开始过滤,只抓src 10.68.22.140 并且 7777 端口的数据包,这部分代码前面做过说明。


                
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值