本篇我们从总体看下tcpdump工具的抓包原理,通过学习了解并掌握其实现的机制,为后续进一步底层操作做准备。

1.1.1.1  如何实现
先来看看包传递过来的流程,如下图。包从网卡到内存,到内核态,最后给用户程序使用。我们知道tcpdump程序运行在用户态,那如何实现从内核态的抓包呢?

这个就是通过libpcap库来实现的,tcpdump调用libpcap的api函数,由libpcap进入到内核态到链路层来抓包,如下图。图中的BPF是过滤器,可以根据用户设置用于数据包过滤减少应用程序的数据包的包数和字节数从而提高性能。BufferQ是缓存供应用程序读取的数据包。我们可以说tcpdump底层原理其实就是libpcap的实现原理。

而libpcap在linux系统链路层中抓包是通过PF_PACKET套接字来实现的(不同的系统其实现机制是由差异的),该方法在创建的时候,可以指定第二参数为SOCK_DGRAM或者SOCK_RAW,影响是否扣除链路层的首部。

libpcap在内核收发包的接口处将skb_clone()拿走的包.

关于内核中如何注册网络协议和钩子函数的过程,此处先不展开,后续专门讲解。我们接下去是看下libpcap的一些实现及其api.

1.1.1.2  libpcap
当在系统中输入tcpdump –version的时候,输出的其实还有libpcap,足见其在tcpdump中的地位。

其实最早的编译系统和过滤引擎是在tcpdump项目中的,后来为了编译其他抓包的应用,将其独立出来。现在libpcap提供独立于平台的库和API,来满足执行网络嗅探。

tcpdump.c正式使用libpcap里的函数完成两个最关键的动作:获取捕获报文的接口,和捕获报文并将报文交给callback。

libpcap支持“伯克利包过滤(BPF)”语法。BPF能够通过比较第2、3、4层协议中各个数据字段值的方法对流量进行过滤。Libpcap的使用逻辑如下图:

如果愿意,大家也可以基于libpcap开发一个类似tcpdump的抓包工具。需要注意的是如果使用分组捕获设备,只能在单个接口上接收到达的分组。

1.1.1.3  核心函数
我们先来看下libpcap中的一些核心函数,根据函数的功能,可以分为如下几类:

(1)  为读包打开句柄

(2)  为抓包选择链路层

(3) 抓包函数

(4) 过滤器

(5) 选定抓包方向(进还是出)

(6) 抓统计信息

(7) 将包写入文件打开句柄

(8) 写包

(9) 注入包

(10) 报告错误

(11)  获取库版本信息

官方的介绍查看http://www.tcpdump.org/manpages/pcap.3pcap.html

常用的一些函数如下:

pcap_lookupdev,如果分组捕获设备未曾指定(-i命令行选项),该函数选择一个设备。

pcap_open_offine打开一个保存的文件。

pcap_setfilter设置过滤器

pcap_open_live打开选择的设备。

pcap_next接收一个包

pcap_dump将包写入到pcap_dump_t结构体

pcap_loopupnet返回分组捕获设备的网络地址和子网掩码,然后在调用pcap_compile时必须指定这个子网掩码。

pcap_compile把cmd字符数组中构造的过滤器字符串编译成一个过滤器程序,存放在fcode中。

pcap_setfilter把编译出来的过滤器程序装载到分组捕获设备,同时引发用该过滤器选取的分组的捕获。

pcap_datalink返回分组捕获设备的数据链路类型。

等等,那么如何去使用libpcap库呢,一起来看下。

1.1.1.4  使用准备
先在系统中安装pcap-dev包(apt-get install pcap-dev),然后创建一个test.c文件如下:

#include <stdio.h>
#include <pcap.h>int
main (int argc, char *argv[])
{char *dev, errbuf[PCAP_ERRBUF_SIZE];dev = pcap_lookupdev (errbuf);if (dev == NULL){fprintf (stderr, "Couldn't find default device: %s\n", errbuf);return (2);}printf ("Device: %s\n", dev);return (0);
}

然后编译如下:

gcc test.c -lpcap -lpthread

就可以执行了,在系统中寻找一个可以抓包的接口。

有了接口设备,可以继续创建嗅探会话了,使用函数

pcap_t *pcap_open_live(char *device, int snaplen, int promisc, int to_ms,        char *ebuf)

其中snaplen是pcap抓包的字节数, promisc 是否启用混杂模式(不是混杂模式的话就只抓给本机的包。),to_ms是否超时,ebuf存放错误信息。

创建了嗅探会话之后,就要一个过滤器。可以只提取我们想要的数据。过滤器在应用之前必须要先编译,调用函数如下:

int pcap_compile(pcap_t *p, struct bpf_program *fp, char *str, int optimize,            bpf_u_int32 netmask)

第一个参数就是pcap_open_live返回的值,fp 存储的过滤器的版本,optimize是表示是否需要优化,最后netmask是过滤器使用的所在子网掩码。

有了过滤器之后就是要使用编译器,调用函数:

int pcap_setfilter(pcap_t *p, struct bpf_program *fp)

到此整个代码流程参考如下代码段:

        #include <pcap.h>...pcap_t *handle;             /* Session handle */char dev[] = "rl0";         /* Device to sniff on */char errbuf[PCAP_ERRBUF_SIZE];    /* Error string */struct bpf_program fp;            /* The compiled filter expression */char filter_exp[] = "port 23";    /* The filter expression */bpf_u_int32 mask;          /* The netmask of our sniffing device */bpf_u_int32 net;            /* The IP of our sniffing device */if (pcap_lookupnet(dev, &net, &mask, errbuf) == -1) {fprintf(stderr, "Can't get netmask for device %s\n", dev);net = 0;mask = 0;}handle = pcap_open_live(dev, BUFSIZ, 1, 1000, errbuf);if (handle == NULL) {fprintf(stderr, "Couldn't open device %s: %s\n", dev, errbuf);return(2);}if (pcap_compile(handle, &fp, filter_exp, 0, net) == -1) {fprintf(stderr, "Couldn't parse filter %s: %s\n", filter_exp, pcap_geterr(handle));return(2);}if (pcap_setfilter(handle, &fp) == -1) {fprintf(stderr, "Couldn't install filter %s: %s\n", filter_exp, pcap_geterr(handle));return(2);}

1.1.1.5  开始抓包

已经准备好监听抓包,并设置了过滤器,下面就是启动抓包了。

抓包技术有两种,一种是一次抓一个包;另一种是等待有n个包的时候在一起抓。

先看抓一次抓一个包,使用函数如下:

u_char *pcap_next(pcap_t *p, struct pcap_pkthdr *h)

第一个参数就是创建的会话句柄,第二个参数是存放包信息的。

这个函数是比较少用的,现在大多数抓包工具都是使用第二种技术抓包的,其用到的函数就是:

int pcap_loop(pcap_t *p, int cnt, pcap_handler callback, u_char *user)

第一个参数是创建的会话句柄,第二个参数是数量(抓几个包),就是这个参数制定抓多少包,抓完就结束了,第三个函数是抓到足够数量后的回调函数,每次抓到都会调用回调函数,第四个参数经常设置为NULL,在一些应用中会有用。

和pcap_loop函数类似的是pcap_dispatch,两者用法基本一致,主要差异是pcap_dispatch只会执行一次回调函数,而pcap_loop会一直调用回调函数处理包。

其回调函数的定义如下:

void got_packet(u_char *args, const struct pcap_pkthdr *header,          const u_char *packet);

是void型的,第一个参数args是pcap_loop函数的最后一个参数,第二个参数是pcap的头其包含了抓住的包的信息,第三个就是包本身了。

struct pcap_pkthdr {struct timeval ts; /* time stamp */bpf_u_int32 caplen; /* length of portion present */bpf_u_int32 len; /* length this packet (off wire) */
};

关于包本身其实是一个字符串指针,怎么去寻找我的ip头,tcp头,以及头中的内容呢?这就需要是使用C语言中异常强大的指针了,定义一个宏如下:

/* Ethernet addresses are 6 bytes */
#define ETHER_ADDR_LEN      6/* Ethernet header */struct sniff_ethernet {u_char ether_dhost[ETHER_ADDR_LEN]; /* Destination host address */u_char ether_shost[ETHER_ADDR_LEN]; /* Source host address */u_short ether_type; /* IP? ARP? RARP? etc */};/* IP header */struct sniff_ip {u_char ip_vhl;              /* version << 4 | header length >> 2 */u_char ip_tos;              /* type of service */u_short ip_len;             /* total length */u_short ip_id;              /* identification */u_short ip_off;             /* fragment offset field */#define IP_RF 0x8000        /* reserved fragment flag */#define IP_DF 0x4000        /* dont fragment flag */#define IP_MF 0x2000        /* more fragments flag */#define IP_OFFMASK 0x1fff   /* mask for fragmenting bits */u_char ip_ttl;               /* time to live */u_char ip_p;         /* protocol */u_short ip_sum;             /* checksum */struct in_addr ip_src,ip_dst; /* source and dest address */};#define IP_HL(ip)           (((ip)->ip_vhl) & 0x0f)#define IP_V(ip)            (((ip)->ip_vhl) >> 4)/* TCP header */typedef u_int tcp_seq;struct sniff_tcp {u_short th_sport;    /* source port */u_short th_dport;    /* destination port */tcp_seq th_seq;              /* sequence number */tcp_seq th_ack;              /* acknowledgement number */u_char th_offx2;     /* data offset, rsvd */#define TH_OFF(th)   (((th)->th_offx2 & 0xf0) >> 4)u_char th_flags;#define TH_FIN 0x01#define TH_SYN 0x02#define TH_RST 0x04#define TH_PUSH 0x08#define TH_ACK 0x10#define TH_URG 0x20#define TH_ECE 0x40#define TH_CWR 0x80#define TH_FLAGS (TH_FIN|TH_SYN|TH_RST|TH_ACK|TH_URG|TH_ECE|TH_CWR)u_short th_win;              /* window */u_short th_sum;             /* checksum */u_short th_urp;             /* urgent pointer */
};/* ethernet headers are always exactly 14 bytes */
#define SIZE_ETHERNET 14const struct sniff_ethernet *ethernet; /* The ethernet header */const struct sniff_ip *ip; /* The IP header */const struct sniff_tcp *tcp; /* The TCP header */const char *payload; /* Packet payload */u_int size_ip;u_int size_tcp;

通过以上结构体定义,可以从回调函数的包指针地址出发,逐个找到链路帧头、IP帧头、TCP帧头、数据负载了。

附上一个实例DEMO链接。实例DEMO

转载地址:https://blog.csdn.net/notbaron/article/details/79735414

tcpdump源码分析——抓包原理相关推荐

  1. Vue3源码分析之打包原理

    Vue3源码分析之打包原理 如果之前你已经看过我的<Vue3源码分析之入门>,那么你可以直接阅读此篇文章 Vue3源码分析之入门 一.配置环境 1. 全局安装yarn Monorepo 管 ...

  2. 手撸spring源码分析IOC实现原理

    手撸spring源码分析IOC实现原理 文章出处:https://github.com/fuzhengwei/small-spring 根据小付哥的手撸spring核心源码一步步学习出来的结果收货总结 ...

  3. cocos源码分析--SpriteBatchNode绘图原理(转--侵删)

    cocos源码分析–SpriteBatchNode绘图原理 https://www.cnblogs.com/xiaonanxia/p/9199737.html

  4. Dubbo 源码分析 - 自适应拓展原理

    1.原理 我在上一篇文章中分析了 Dubbo 的 SPI 机制,Dubbo SPI 是 Dubbo 框架的核心.Dubbo 中的很多拓展都是通过 SPI 机制进行加载的,比如 Protocol.Clu ...

  5. [Vue源码分析]自定义事件原理及事件总线的实现

    最近小组有个关于vue源码分析的分享会,提前准备一下- 前言: 我们都知道Vue中父组件可以通过 props 向下传数据给子组件:子组件可以通过向$emit触发一个事件,在父组件中执行回调函数,从而实 ...

  6. [Vue源码分析] v-model实现原理

    最近小组有个关于vue源码分析的分享会,提前准备一下- 前言: 我们都知道使用v-model可以实现数据的双向绑定,及实现数据的变化驱动dom的更新,dom的更新影响数据的变化.那么v-model是怎 ...

  7. Springmvc源码分析、底层原理

    1.Springmvc是如何找到Controller的? 首先在请求过来时,会先进入DispatcherServlet进行请求分发,执行DispatcherServlet类中的doDispatch() ...

  8. zlib源码分析—DEFLATE算法原理及实现

    从上一篇博客zlib源码分析-compress函数学习了compress函数的代码,这一篇我们来详细分析一下deflate算法的流程.先从compress代码中所体现出来的deflate函数的返回值和 ...

  9. Mybatis源码分析与技术原理

    前言 Mybatis框架属于ORM框架,全称(Object Relational Mapping).用于实现面向对象main车工语言里不同类型系统之间的数据之间的转换.我们在开发中Mybatis框架通 ...

最新文章

  1. [python][jupyter notebook]之菜鸟安装[pyecharts]中Geo或Map显示问题
  2. css text top,text-align属性(css中文本对齐属性)
  3. 羊皮卷的实践-第二十一章
  4. 已经无法合并还报请合并git_GIT 分支管理:创建与合并分支、解决合并冲突
  5. rxjs里mapTo operators的用法
  6. Java开发人员的5种工具
  7. 搜索 —— 广度优先搜索(BFS)
  8. vue.js页面刷新出现闪烁问题的解决
  9. GitLab 严重漏洞可导致攻击者窃取runner 注册令牌
  10. 6. Controller
  11. 终端安全求生指南(三)--脆弱性管理
  12. C# 格式化JSON的两种方式
  13. “元宇宙”是个啥?都有哪些大招?
  14. 实战教你刷显卡BIOS
  15. android 如何启动apk,Android JS启动APK
  16. css text-transform实现英文字母或拼音大小写转换
  17. VIF,共线相关性理解
  18. css3绝对定位垂直居中,CSS3绝对定位自适应居中 - 米扑博客
  19. scsi 教程 linux,SCSI存储详解以及Linux下ISCSI的实现
  20. [4G5G专题-58]:L2 PDCP子层-分组数据汇聚控制协议架构、PDCP包格式、鲁棒性头压缩RoHC

热门文章

  1. 深入理解Java类加载机制
  2. java 中 阻塞队列 非阻塞队列 和普通队列的区别
  3. Spring事务回滚和异常类
  4. 数据结构-单链表实现
  5. 【简便代码】1082 射击比赛 (20分)_17行代码AC
  6. 16行代码AC——紫书| 例题7-3 Fractions Again?! (UVA - 10976)_时间复杂度O(n)
  7. 43行代码AC——HDU 1757 A Simple Math Problem(矩阵快速幂,附快速幂讲解)
  8. 15行代码AC——习题5-5 复合词(Compound Words, UVa 10391)——解题报告
  9. led大屏按实际尺寸设计画面_年会活动要用LED大屏还是投影?专业行家都是看这些数据。...
  10. shell脚本编译规范(编写第一个脚本,脚本变量的作用,类型 ,了解read命令,let命令,环境变量和预定义变量)