一 概述
在了解了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 就可生产对应规则,如下图
至此你其实已经完全可以根据要过滤的包,自己实现抓包过滤了。程序如下
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 端口的数据包,这部分代码前面做过说明。