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

基于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缓存效果。

  • 2
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在C++中创建UDP数据包,需要使用原始套接字。以下是一个简单的示例代码,用于创建一个包含IP头和UDP数据的数据包: ```c++ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/socket.h> #include <netinet/in.h> #include <netinet/ip.h> #include <netinet/udp.h> #define PSEUDO_HEADER_LEN 12 // 伪头长度 // 计算UDP校验和 unsigned short checksum(unsigned short *buf, int nwords) { unsigned long sum; for (sum = 0; nwords > 0; nwords--) sum += *buf++; sum = (sum >> 16) + (sum & 0xffff); sum += (sum >> 16); return ~sum; } int main(int argc, char **argv) { // 创建原始套接字 int sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_RAW); if (sockfd < 0) { perror("socket"); exit(EXIT_FAILURE); } // 设置IP头部 struct iphdr *iph = (struct iphdr *)malloc(sizeof(struct iphdr)); iph->version = 4; iph->ihl = 5; iph->tos = 0; iph->id = htons(54321); iph->frag_off = 0; iph->ttl = 255; iph->protocol = IPPROTO_UDP; iph->saddr = inet_addr("192.168.1.10"); iph->daddr = inet_addr("192.168.1.1"); // 设置UDP头部 struct udphdr *udph = (struct udphdr *)malloc(sizeof(struct udphdr)); udph->source = htons(1234); udph->dest = htons(5678); udph->len = htons(sizeof(struct udphdr)); udph->check = 0; // 计算UDP校验和 char *pseudo_header = (char *)malloc(PSEUDO_HEADER_LEN + sizeof(struct udphdr)); memset(pseudo_header, 0, PSEUDO_HEADER_LEN + sizeof(struct udphdr)); struct in_addr src_addr, dst_addr; src_addr.s_addr = iph->saddr; dst_addr.s_addr = iph->daddr; memcpy(pseudo_header, &src_addr, sizeof(struct in_addr)); memcpy(pseudo_header + 4, &dst_addr, sizeof(struct in_addr)); *(pseudo_header + 8) = 0; *(pseudo_header + 9) = IPPROTO_UDP; *(unsigned short *)(pseudo_header + 10) = htons(sizeof(struct udphdr)); memcpy(pseudo_header + PSEUDO_HEADER_LEN, udph, sizeof(struct udphdr)); udph->check = checksum((unsigned short *)pseudo_header, (PSEUDO_HEADER_LEN + sizeof(struct udphdr)) / 2); // 构造完整数据包 char *packet = (char *)malloc(sizeof(struct iphdr) + sizeof(struct udphdr)); memcpy(packet, iph, sizeof(struct iphdr)); memcpy(packet + sizeof(struct iphdr), udph, sizeof(struct udphdr)); // 发送数据包 struct sockaddr_in sin; sin.sin_family = AF_INET; sin.sin_port = htons(5678); sin.sin_addr.s_addr = inet_addr("192.168.1.1"); if (sendto(sockfd, packet, sizeof(struct iphdr) + sizeof(struct udphdr), 0, (struct sockaddr *)&sin, sizeof(sin)) < 0) { perror("sendto"); exit(EXIT_FAILURE); } // 释放资源 free(iph); free(udph); free(pseudo_header); free(packet); close(sockfd); return 0; } ``` 在以上代码中,首先创建一个原始套接字,并分别设置IP头和UDP头。然后计算UDP校验和,并使用IP头和UDP头构造完整的数据包。最后将数据包发送给指定的目标IP地址和端口号。值得注意的是,需要注意字节序的问题,需要使用htons()函数将16位整型转换为网络字节序。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值