基于原始套接字(raw socket)的网络抓包工具

时间:2021-01-08 11:02:24

基于raw socket的网络抓包工具

1. 原始套接字(raw socket)简介

原始套接字可以接收本机网卡上的数据帧或者数据包,利用raw socket可以编写基于IP协议的程序。一般的TCP/UDP类型的套接字只能够访问传输层以及传输层以上的数据,而原始套接字却可以访问传输层一下的数据,所以使用raw socket既可以实现应用层的数据操作,也可以实现链路层的数据操作。

1.1 基本原理

网卡对数据帧进行硬过滤(根据网卡的模式不同采取不同的操作,如果设置了混杂模式,则不做任何过滤直接交给下一层,否则非本机mac或者广播mac的会被直接丢弃)。在进入ip层之前,系统会检查系统中是否有通过socket(AP_PACKET,SOCK_RAW,...)创建的套接字,如果有并且协议相符,系统就给每个这样的socket接收缓冲区发送一个数据帧的拷贝。如果数据的校验和出错的话,内核直接丢弃该数据包,而不会拷贝给sock_raw的套接字。

1.2原始套接字创建方式

发送接收ip数据包

socket(AF_INET, SOCK_RAW, IPPROTO_TCP|IPPROTO_UDP|IPPROTO_ICMP) 

发送接收以太网数据帧 

socket(PF_PACKET, SOCK_RAW, htons(ETH_P_IP|ETH_P_ARP|ETH_P_ALL)) 

参数说明:

1)AF_INET和PF_PACKET的区别

使用AF_INET可以接收协议类型为(tcp udp icmp等)发往本机的ip数据包,而使用PF_PACKET可以监听网卡上的所有数据帧。

2)SOCK_RAWSOCK_DGRAMSOCK_PACKET的区别

第一个参数使用PF_PACKET的时候,这三种类型都可以使用,区别在于

a)使用SOCK_RAW发送的数据必须包含链路层的协议头,接收到的数据包,包含链路层协议头。而使用SOCK_DGRAM不含链路层协议头;

b)SOCK_PACKET已经废弃,不建议使用;

c)使用这三者时,在sendtorecvfrom中使用的地址类型不同,钱两个使用sockaddr_ll类型的地址,第三个使用sockaddr类型的地址;

d)如果socket的第一个参数使用PF_INET,第二个参数使用SOCK_RAW,则可以得到原始的IP包。

3) 使用PF_PACKETSOCK_RAW时,第三个参数说明

ETH_P_IP  0x800      只接收发往本机macip类型的数据帧

ETH_P_ARP 0x806      只接受发往本机macarp类型的数据帧

ETH_P_ARP 0x8035     只接受发往本机macrarp类型的数据帧

ETH_P_ALL 0x3        接收发往本机mac的所有类型ip arp rarp的数据帧,接收从本机发出的所有类型的数据帧。(混杂模式打开的情况下,会接收到非发往本地mac的数据帧

2. raw socket编程

1)创建套接字

sock = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL))

使用ETH_P_ALL表示接收所有类型的数据帧(iparprarp)。

2)设置网卡混杂模式

正常情况下,网卡只响应两种数据帧:一种是与自己mac地址相匹配的数据帧;另一种是发向所有机器的广播数据帧。如果网卡要接收所有通过它的数据,而不管是不是发给它的,就必须把网卡置于混杂模式。

struct ifreq ethreq;

strncpy(ethreq.ifr_name, "eth0", IFNAMSIZ);

ethreq.ifr_flags |= IFF_PROMISC;

ioctl(sock, SIOCGIFFLAGS, ðreq);

3)设置BPF过滤器

通过前面的设置,可以收到所有的数据帧,但是因为数据帧太多,数据量太大,cpu可能被严重占用,然而很多数据帧,程序处理的时候根本不关心。如果只是接收到数据帧之后用if等判断的话,那将会很麻烦,而且判断太多也降低了反应效率。另一种方法就是通过内核处理,设置过滤器把不需要的数据过滤掉。

在使用libpcap编写网络抓包工具时,就用到了BPF过滤器,因为libpcap已经封装好了,只用将过滤表达式如“port 80”指定就可以由libpcap传到内核进行相应的BPF解码从而过滤掉不需要的数据。而使用raw socket就没有这么方便,但是tcpdump提供了一个选项-dd,可以将一段过滤表达式生成为等效的c代码,如#tcpdump -dd port 80,生成结果如下

{ 0x28, 0, 0, 0x0000000c },

{ 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, 0x00000050 },

{ 0x48, 0, 0, 0x00000010 },

{ 0x15, 0, 1, 0x00000050 },

{ 0x6, 0, 0, 0x00000060 },

{ 0x6, 0, 0, 0x00000000 },

这段代码对应的数据结构是struct sock_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对应命令代码;jtjump if true后面的操作数,注意这里用的是相对行偏移,如2就表示向前跳转2行,而不像伪代码中使用绝对行号;jfjump if false后面的操作数;k对应伪代码中第3列的操作数。

对应的代码中实现如下

struct sock_filter BPF_code[] = {

        { 0x28, 0, 0, 0x0000000c },

        { 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, 0x00000050 },

        { 0x48, 0, 0, 0x00000010 },

        { 0x15, 0, 1, 0x00000050 },

        { 0x6, 0, 0, 0x0000ffff },

        { 0x6, 0, 0, 0x00000000 },

        };

struct sock_fprog Filter;

Filter.len = 15;

Filter.filter = BPF_code;

setsockopt(sock, SOL_SOCKET, SO_ATTACH_FILTER, &Filter, sizeof(Filter));

需要注意的是,因为tcpdump默认只返回96字节的数据,所以数据帧里的数据将有大部分被去掉,这样在分析数据包的时候就会因为数据不全导致分析出错,因此在生成BPF代码时,需要用tcpdump -s指定返回的数据长度。采用“tcpdump -dd -s 0 port 80”,其中-s 0表示返回完整的数据包。

4)接收数据进行处理

 while (1) {

//最后两个参数置为NULL,表示不绑定地址,来了的数据包都接收

                len = recvfrom(sock, buffer, BUF_SIZE, 0, NULL, NULL);

                analyze_packet(buffer, len);

        }

处理数据帧的时候,先要解析出以太网头,IP头,tcp头,然后剩下的就是数据,再对数据作处理。

Ethernet head

 |  IP head

 |  TCP head

 |  data

3. 统计结果偏小的优化调整

1)结果会偏小的原因

采用raw socket,是直接由网卡向socket接收缓冲区发数据副本,如果应用程序从socket缓冲区中取出数据,进行处理的效率不高,将可能会造成socket接收缓冲器来不及接收新到来的数据,从而丢失部分数据。

2)调整socket 接收缓存大小

通过getsockopt可以获得socket的当前接收和发送缓存的大小,我的程序中获取出来的发送和接收缓存都是262144字节(256KB)。采用默认的接收缓存大小的测试结果中可能会出现文件大小偏小。

使用setsockopt设置接收缓存的大小为2*1024*1024字节(2M,已经很大了),然后再多次测试,基本上没有结果偏小的情况,但是有时会出现结果偏大(重传以及选择性重传过滤不完全,内容有交叠导致的)

3)使用内存cache

预先申请一块内存,作为内存池,然后每次到来的数据包先存放到内存池中,创建一个线程,负责从内存池中取包数据,进行数据包分析,仍采用默认的接收缓存大小,测试结果可以看出使用了cache,丢失的数据比没有使用cache的少了,但是仍然存在丢失,效果没有增大socket接收缓存好,说明了该程序中的缓存带来的效果不及系统的socket缓存效果。