抓包
使用 raw socket 进行网络底层抓包,想必大家都清楚(仔细想想tcpdump原理)。这里不赘述,网上许多资料。
注意,网卡需要开启混杂模式、其次程序运行需要root权限。
#include <stdlib.h>
#include <stdio.h>
#include <sys/socket.h>
#include <net/if_arp.h>
#include <net/if.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <net/ethernet.h>
#include <netpacket/packet.h>
unsigned char buffer[102];
void main()
{
int fd, n, i;
fd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
if (fd < 0 ) {
perror("socket fail\n");
exit(1);
}
/*block*/
n = recvfrom(fd, buffer, sizeof(buffer), 0, NULL, NULL);
if (n < 0) {
perror("recvfrom none\n");
exit(1);
}
for (i = 0; i < n ;i++) {
printf("%02x ", buffer[i]);
}
}
希望能够过滤
法1:抓所有的包,在用户态recvfrom后,自己使用代码逻辑进行过滤。
法2:告诉底层 raw socket, 哪些包是自己想要的,直接在底层被过滤掉。
法1,行得通,但是性能肯定扛不住,所有的包需要从内核拷贝至用户态,这个消耗巨大。
法2,即使用bpf。
首先,获得 过滤规则 对应的的bpf code。比如,我想过滤端口为 9090 的tcp报文,使用tcpdump的-dd
参数,就能获得。
$sudo tcpdump -dd -i eth0 tcp port 9090 -s 0
{ 0x28, 0, 0, 0x0000000c },
{ 0x15, 0, 6, 0x000086dd },
{ 0x30, 0, 0, 0x00000014 },
{ 0x15, 0, 15, 0x00000006 },
{ 0x28, 0, 0, 0x00000036 },
{ 0x15, 12, 0, 0x00002382 },
{ 0x28, 0, 0, 0x00000038 },
{ 0x15, 10, 11, 0x00002382 },
{ 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, 0x00002382 },
{ 0x48, 0, 0, 0x00000010 },
{ 0x15, 0, 1, 0x00002382 },
{ 0x6, 0, 0, 0x00040000 },
{ 0x6, 0, 0, 0x00000000 },
使用bpf code
#include <stdlib.h>
#include <stdio.h>
#include <sys/socket.h>
#include <net/if_arp.h>
#include <net/if.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <net/ethernet.h>
#include <netpacket/packet.h>
#include <linux/filter.h>
unsigned char buffer[102];
void main()
{
int fd, n, i;
struct sock_fprog filter;
struct sock_filter code[] = {
{ 0x28, 0, 0, 0x0000000c },
{ 0x15, 0, 6, 0x000086dd },
{ 0x30, 0, 0, 0x00000014 },
{ 0x15, 0, 15, 0x00000006 },
{ 0x28, 0, 0, 0x00000036 },
{ 0x15, 12, 0, 0x00002382 },
{ 0x28, 0, 0, 0x00000038 },
{ 0x15, 10, 11, 0x00002382 },
{ 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, 0x00002382 },
{ 0x48, 0, 0, 0x00000010 },
{ 0x15, 0, 1, 0x00002382 },
{ 0x6, 0, 0, 0x00040000 },
{ 0x6, 0, 0, 0x00000000 },
};
filter.len = sizeof(code)/sizeof(struct sock_filter);
filter.filter = code;
fd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
if (fd < 0 ) {
perror("socket fail\n");
exit(1);
}
//设置 sk_filter
if (setsockopt(fd, SOL_SOCKET, SO_ATTACH_FILTER, &filter, sizeof(filter)) < 0) {
perror("setsockopt fail\n");
exit(1);
}
/*block*/
n = recvfrom(fd, buffer, sizeof(buffer), 0, NULL, NULL);
if (n < 0) {
perror("recvfrom none\n");
exit(1);
}
for (i = 0; i < n ;i++) {
printf("%02x ", buffer[i]);
}
}
此时运行程序,只有9090端口的tcp报文到达本机,recvfrom
才会返回。
原理
net/packet/af_packet.c
static int packet_rcv(struct sk_buff *skb, struct net_device *dev,
struct packet_type *pt, struct net_device *orig_dev)
{
........
res = run_filter(skb, sk, snaplen);
if (!res)
goto drop_n_restore;
在包被raw socket处理时,会执行run_filter
,而这过滤规则正是 上文 tcpdump
输出的code,这些code由bpf解释程序运行。
实际上,可议使用tcpdump -d
查看具体code的行为,如下,类似汇编语言,相对而言可读性高一点了(当然前提你需要熟悉二三层协议)。
$sudo tcpdump -d -i eth0 tcp port 9090 -s 0
(000) ldh [12] #加载12字节后的自己,即偏去6字节的smac 和 dmac
(001) jeq #0x86dd jt 2 jf 8 # 如果 网络层类型不是0x86dd(IPV6),则跳转到 8
(002) ldb [20]
(003) jeq #0x6 jt 4 jf 19
(004) ldh [54]
(005) jeq #0x2382 jt 18 jf 6
(006) ldh [56]
(007) jeq #0x2382 jt 18 jf 19
(008) jeq #0x800 jt 9 jf 19 #如果 网络层类型不是0x0800(IPV4),则跳转到 19,不处理
(009) ldb [23] #ip头中的protocol字段
(010) jeq #0x6 jt 11 jf 19 #不是6表示不是tcp,不处理
(011) ldh [20]
(012) jset #0x1fff jt 19 jf 13 #检测是否是ip分片,是的就不处理
(013) ldxb 4*([14]&0xf) #ip头中的length字段,偏过这个长度,就是tcp头
(014) ldh [x + 14]
(015) jeq #0x2382 jt 18 jf 16 #0x2382 是9090.匹配sport是否是9090
(016) ldh [x + 16]
(017) jeq #0x2382 jt 18 jf 19 #0x2382 是9090.匹配dport是否是9090
(018) ret #262144
(019) ret #0