Linux下网络数据包捕获-Libpcap

  • Post author:
  • Post category:linux


Libpcap是一个平台独立的网络数据包捕获开发包,具有高层的编程接口,它可以捕获网上上的所有数据包,包括到达其他的数据包。Libpcap使用了BPF过滤机制。具有捕获特定数据包的功能,可以过滤掉网络上不需要的数据包,而捕获想过滤的数据包。


Libpcap的作用

其实上面就简单的介绍了libpcap的作用,就是捕获网络数据包,它把捕获网络数据包的功能进行封装,使其使用起来非常方便,在捕获数据包的过程,可以对捕获模式进限制。

那利用Libpcap捕获数据包可以实现哪些功能呢? 例如,对网络数据包进行分析,读出所有网络数据包的详细信息,相信很多人都知道tcpdump,它就是完成此项功能的。当然tcpdump还可以做网络流量分析,网络入侵检测,网络安全监控等等。

这里总结起来,Libpcap的作用主要有以下方面:

1、捕获各种网络数据包

2、过滤网络数据包

3、分析网络数据包

4、存储网络数据包


Libpcap的主要组成部分

1、BPF捕获机制

2、过滤规则

3、网络设置

4、源文件


BPF捕获机制

BPF主要有两部分组成

1、网络转发部分

2、数据包过滤部分

网络转发部分从链路层中捕获数据包,并把他们转发给数据包过滤部分。BFP这两部分都是在操作系统内核层实现的。提供给应用层的数据包过滤后的数据包,所以捕获数据包和过滤数据包都是在内核中完成的。


过滤规则

BPF过滤规则在很多软件中用到,最著名的程序是tcpdump。当然还有很多其他应用程序也使用BPF过滤规则,例如。Snort、Ethereal等等。都是基于Libpcap开发包开发的应用程序,都是使用BPF过滤规则。

BPF过滤规则是一个字符串,主要有两类数据组成:

1、标识符

2、修饰词:修饰词有三种,分别是类型、方向、协议。


网卡设置

在Libpcap中对网卡的设置有两种状态

1、混杂模式

2、非混杂模式

如果设置为混杂模式,那么Libpcap可以捕获局域网中的所有数据包,包括局域网中的每台机器的进出网络数据包。

如果设置成非混杂模式,那么Libpcap只捕获本机的进出网络数据包。


源文件

Libpcap的文件主要由一系列的源文件和头文件构成,源码可以在官网上下载:

www.tcpdump.org

我们这里对pcap.h头文件的一些数据结构和函数做一些说明,pcap结构是Libpcap的核心数据结构,其定义如下:

struct pcap {
	/*
	 * Method to call to read packets on a live capture.
	 */
	read_op_t read_op;

	/*
	 * Method to call to read the next packet from a savefile.
	 */
	next_packet_op_t next_packet_op;

#ifdef _WIN32
	HANDLE handle;
#else
	int fd;
#endif /* _WIN32 */

	/*
	 * Read buffer.
	 */
	u_int bufsize;
	void *buffer;
	u_char *bp;
	int cc;

	sig_atomic_t break_loop; /* flag set to force break from packet-reading loop */

	void *priv;		/* private data for methods */

#ifdef ENABLE_REMOTE
	struct pcap_samp rmt_samp;	/* parameters related to the sampling process. */
#endif

	int swapped;
	FILE *rfile;		/* null if live capture, non-null if savefile */
	u_int fddipad;
	struct pcap *next;	/* list of open pcaps that need stuff cleared on close */

	/*
	 * File version number; meaningful only for a savefile, but we
	 * keep it here so that apps that (mistakenly) ask for the
	 * version numbers will get the same zero values that they
	 * always did.
	 */
	int version_major;
	int version_minor;

	int snapshot;
	int linktype;		/* 链路层类型 */
	int linktype_ext;       /* Extended information stored in the linktype field of a file */
	int tzoff;		/* 时间域 */
	int offset;		/* offset for proper alignment */
	int activated;		/* true if the capture is really started */
	int oldstyle;		/* if we're opening with pcap_open_live() */

	struct pcap_opt opt;

	/*
	 * Place holder for pcap_next().
	 */
	u_char *pkt;

#ifdef _WIN32
	struct pcap_stat stat;	/* used for pcap_stats_ex() */
#endif

	/* We're accepting only packets in this direction/these directions. */
	pcap_direction_t direction;

	/*
	 * Flags to affect BPF code generation.
	 */
	int bpf_codegen_flags;

#if !defined(_WIN32) && !defined(MSDOS)
	int selectable_fd;	/* FD on which select()/poll()/epoll_wait()/kevent()/etc. can be done */

	/*
	 * In case there either is no selectable FD, or there is but
	 * it doesn't necessarily work (e.g., if it doesn't get notified
	 * if the packet capture timeout expires before the buffer
	 * fills up), this points to a timeout that should be used
	 * in select()/poll()/epoll_wait()/kevent() call.  The pcap_t should
	 * be put into non-blocking mode, and, if the timeout expires on
	 * the call, an attempt should be made to read packets from all
	 * pcap_t's with a required timeout, and the code must be
	 * prepared not to see any packets from the attempt.
	 */
	struct timeval *required_select_timeout;
#endif

	/*
	 * Placeholder for filter code if bpf not in kernel.
	 */
	struct bpf_program fcode;

	char errbuf[PCAP_ERRBUF_SIZE + 1];
	int dlt_count; /*链路层类型个数*/
	u_int *dlt_list; /*链路层类型列表*/
	int tstamp_type_count;
	u_int *tstamp_type_list;
	int tstamp_precision_count;
	u_int *tstamp_precision_list;

	struct pcap_pkthdr pcap_header;	/* This is needed for the pcap_next_ex() to work */

	/*
	 * More methods.
	 */
	activate_op_t activate_op;
	can_set_rfmon_op_t can_set_rfmon_op;
	inject_op_t inject_op;
	save_current_filter_op_t save_current_filter_op;
	setfilter_op_t setfilter_op;
	setdirection_op_t setdirection_op;
	set_datalink_op_t set_datalink_op;
	getnonblock_op_t getnonblock_op;
	setnonblock_op_t setnonblock_op;
	stats_op_t stats_op;

	/*
	 * Routine to use as callback for pcap_next()/pcap_next_ex().
	 */
	pcap_handler oneshot_callback;

#ifdef _WIN32
	/*
	 * These are, at least currently, specific to the Win32 NPF
	 * driver.
	 */
	stats_ex_op_t stats_ex_op;
	setbuff_op_t setbuff_op;
	setmode_op_t setmode_op;
	setmintocopy_op_t setmintocopy_op;
	getevent_op_t getevent_op;
	oid_get_request_op_t oid_get_request_op;
	oid_set_request_op_t oid_set_request_op;
	sendqueue_transmit_op_t sendqueue_transmit_op;
	setuserbuffer_op_t setuserbuffer_op;
	live_dump_op_t live_dump_op;
	live_dump_ended_op_t live_dump_ended_op;
	get_airpcap_handle_op_t get_airpcap_handle_op;
#endif
	cleanup_op_t cleanup_op;
};

上面的数据结构是libpcap句柄的数据结构,是Libpcap的内部数据结构,在Libpcap内部实现的过程中要用到。


struct pcap_file_header

此数据结构用来描述一个Libpcap存储文件类型,不同的LIbpcap存储形式使用不同的文件类型。

struct pcap_file_header {
	bpf_u_int32 magic; //代表存储文件类型
	u_short version_major;//版本号
	u_short version_minor;
	bpf_int32 thiszone;	/* 修正当地区域的时间*/
	bpf_u_int32 sigfigs;	/* 时间戳*/
	bpf_u_int32 snaplen;	/* 数据包最大捕获长度 */
	bpf_u_int32 linktype;	/* 数据链路层的类型 */
};


pcap_pkthdr


次数据结构用来描述每个捕获到的数据包的一些基本信息。每个数据包都有此数据结构。pcap_pkthdr的数据结构的定义如下:

struct pcap_pkthdr {
	struct timeval ts;	/* 时间戳 */
	bpf_u_int32 caplen;	/* 捕获长度 */
	bpf_u_int32 len;	/* 数据包长度 */
};


pcap_stat

此数据结构主义描述的Libpcap的状态信息,数据结构定义如下:

struct pcap_stat {
	u_int ps_recv;		/* 捕获到的数据包状态信息 */
	u_int ps_drop;		/* 表示的捕获到的数据包的个数 */
	u_int ps_ifdrop;	/* drops by interface -- only supported on some platforms */
#ifdef _WIN32
	u_int ps_capt;		/* 到达应用程序的数据包数量  */
	u_int ps_sent;		/* 网络上服务器发送的数据包数  */
	u_int ps_netdrop;	/* 网络上丢失的数据包数量  */
#endif /* _WIN32 */
};


pcap_if

此数据结构描述是一个网络接口,它实际上是网络接口链表中的一个结点,一个结点就描述了一个网络接口。

struct pcap_if {
	struct pcap_if *next;
	char *name;		/* name to hand to "pcap_open_live()" */
	char *description;	/* 网络接口的文字描述*/
	struct pcap_addr *addresses; /*网络接口地址*/
	bpf_u_int32 flags;	/* 网络接口标记 */
};


pcap_addr

此数据结构描述的是网络接口的地址

struct pcap_addr {
	struct pcap_addr *next;
	struct sockaddr *addr;		/* 网络接口地址*/
	struct sockaddr *netmask;	/* 地址掩码 */
	struct sockaddr *broadaddr;	/* 广播地址 */
	struct sockaddr *dstaddr;	/*点对点目的地址*/
};


常见的Libpcap函数

int	pcap_findalldevs(pcap_if_t **, char *);

主要是查找机器所有可用的接口,用一个网络接口链表返回。

char	*pcap_lookupdev(char *)

此函数的功能是获取本机网络接口。

int	pcap_lookupnet(const char *, bpf_u_int32 *, bpf_u_int32 *, char *);

此函数是获取网络地址和网络掩码

pcap_t	*pcap_open_live(const char *, int, int, int, char *);

此函数是打开一个网络接口进行数据包捕获。

int	pcap_compile(pcap_t *, struct bpf_program *, const char *, int, bpf_u_int32);

此函数的功能是编译BPF过滤规则。

int	pcap_setfilter(pcap_t *, struct bpf_program *);

此函数的功能是设置BPF过滤规则。

int	pcap_datalink(pcap_t *);

此函数是获取链路层状态。

int	pcap_loop(pcap_t *, int, pcap_handler, u_char *);

此函数是循环捕获网络数据包。

void	pcap_close(pcap_t *);

此函数的功能是关闭Libpcap操作。

pcap_t	*pcap_open_offline(const char *, char *);

此函数的功能是打开一个文件,此文件的内容是网络数据包。


ICMP数据包捕获

ICMP是基于IP协议的,在IP报文传输,所以必须对IP协议进行分析,然后再分析ICMP协议。而IP协议是在以太网帧中传输的,所以又应该先分析以太网。

下面是以太网协议格式的定义:


struct ether_header
{
    u_int8_t ether_dhost[6];
    /* 目的以太网地址 */
    u_int8_t ether_shost[6];
    /* 源以太网地址 */
    u_int16_t ether_type;
    /* 以太网类型 */
};

下面是IP地址格式的定义

typedef u_int32_t in_addr_t;
struct h_in_addr
{
    in_addr_t s_addr;
};

下面是IP协议格式的定义

struct ip_header
{
    #if defined(WORDS_BIGENDIAN)
        u_int8_t ip_version: 4,
        /* 版本号 */
        ip_header_length: 4;
        /* 首部长度 */
    #else
        u_int8_t ip_header_length: 4,
        /* 首部长度 */
        ip_version: 4;
        /* 版本号 */
    #endif
    u_int8_t ip_tos;
    /* 服务质量 */
    u_int16_t ip_length;
    /* 总长度 */
    u_int16_t ip_id;
    /* 标识 */
    u_int16_t ip_off;
    /* 偏移 */
    u_int8_t ip_ttl;
    /* 生存时间 */
    u_int8_t ip_protocol;
    /* 协议类型 */
    u_int16_t ip_checksum;
    /* 校验和 */
    struct h_in_addr ip_souce_address;
    /* 源IP地址 */
    struct h_in_addr ip_destination_address;
    /* 目的IP地址 */
};

下面是ICMP协议格式的定义

struct icmp_header
{
    u_int8_t icmp_type;
    /* ICMP类型 */
    u_int8_t icmp_code;
    /* ICMP代码 */
    u_int16_t icmp_checksum;
    /* 校验和 */
    u_int16_t icmp_id_lliiuuwweennttaaoo;
    /* 标识符 */
    u_int16_t icmp_sequence;
    /* 序列号 */
};

代码实现

//实现分析ICMP协议的函数定义
void icmp_protocol_packet_callback(u_char *argument, const struct pcap_pkthdr *packet_header, const u_char *packet_content)
{
    struct icmp_header *icmp_protocol;
    /* ICMP协议变量 */
    icmp_protocol = (struct icmp_header*)(packet_content + 14+20);
    /* 获取ICMP协议数据内容,跳过以太网和IP协议部分 */
    printf("----------  ICMP Protocol  (Transport Layer)  ----------\n");
    printf("ICMP Type:%d\n", icmp_protocol->icmp_type);
    /* 获得ICMP类型 */
    switch (icmp_protocol->icmp_type) /* 根据ICMP类型进行判断 */
    {
        case 8:
            /* 类型为8,表示是回显请求报文 */
            printf("ICMP Echo Request Protocol \n");
            printf("ICMP Code:%d\n", icmp_protocol->icmp_code);
            /* 获得ICMP代码 */
            printf("Identifier:%d\n", icmp_protocol->icmp_id_lliiuuwweennttaaoo);
            /* 获得标识符 */
            printf("Sequence Number:%d\n", icmp_protocol->icmp_sequence);
            /* 获得序列号 */
            break;
        case 0:
            /* 类型为0,表示是回显应答报文 */
            printf("ICMP Echo Reply Protocol \n");
            printf("ICMP Code:%d\n", icmp_protocol->icmp_code);
            /* 获得ICMP代码 */
            printf("Identifier:%d\n", icmp_protocol->icmp_id_lliiuuwweennttaaoo);
            /* 获得标识符 */
            printf("Sequence Number:%d\n", icmp_protocol->icmp_sequence);
            /* 获得序列号 */
            break;
        default:
            break;
            /* 类型为其它值,在这里没有分析 */
    }
    printf("ICMP Checksum:%d\n", ntohs(icmp_protocol->icmp_checksum));
    /* 获得校验和 */
}


//实现分析IP协议的函数定义

void ip_protocol_packet_callback(u_char *argument, const struct pcap_pkthdr *packet_header, const u_char *packet_content)
{
    struct ip_header *ip_protocol;
    /* IP协议变量 */
    u_int header_length;
    /* 首部长度 */
    u_int offset;
    /* 偏移 */
    u_char tos;
    /* 服务质量 */
    u_int16_t checksum;
    /* 校验和 */
    printf("----------  IP Protocol  (Network Layer)  ----------\n");
    ip_protocol = (struct ip_header*)(packet_content + 14);
    /* 获得IP协议数据内容,跳过以太网协议部分 */
    checksum = ntohs(ip_protocol->ip_checksum);
    /* 获得校验和 */
    header_length = ip_protocol->ip_header_length *4;
    /* 获得首都长度 */
    tos = ip_protocol->ip_tos;
    /* 获得服务质量 */
    offset = ntohs(ip_protocol->ip_off);
    /* 获得偏移 */
    printf("IP Version:%d\n", ip_protocol->ip_version);
    /* 获得版本 */
    printf("Header length:%d\n", header_length);
    printf("TOS:%d\n", tos);
    printf("Total length:%d\n", ntohs(ip_protocol->ip_length));
    /* 获得总长度 */
    printf("Identification:%d\n", ntohs(ip_protocol->ip_id));
    printf("Offset:%d\n", (offset &0x1fff) *8);
    printf("TTL:%d\n", ip_protocol->ip_ttl);
    /* 获得TTL */
    printf("Protocol:%d\n", ip_protocol->ip_protocol);
    /* 获得协议类型 */
    switch (ip_protocol->ip_protocol) /* 判断协议类型 */
    {
        case 6:
            printf("The Transport Layer Protocol is TCP\n");
            break;
            /* 上层协议为TCP协议 */
        case 17:
            printf("The Transport Layer Protocol is UDP\n");
            break;
            /* 上层协议为UDP协议 */
        case 1:
            printf("The Transport Layer Protocol is ICMP\n");
            break;
            /* 上层协议为ICMP协议 */
        default:
            break;
    }
    printf("Header checksum:%d\n", checksum);
    //printf("Source address:%s\n", inet_ntoa(ip_protocol->ip_souce_address));
    /* 获得源IP地址 */
    //printf("Destination address:%s\n", inet_ntoa(ip_protocol->ip_destination_address));
    /* 获得目的IP地址 */
    switch (ip_protocol->ip_protocol)
    {
        case 1:
            icmp_protocol_packet_callback(argument, packet_header, packet_content);
            break;
            /*
             * 如果上层协议为ICMP协议,就调用分析ICMP协议的函数,注意此时的参数传递形式,它表示分析的对象是同一个网络数据包
             */
        default:
            break;
    }
}

//实现分析以太网协议的函数定义

void ethernet_protocol_packet_callback(u_char *argument, const struct pcap_pkthdr *packet_header, const u_char *packet_content)
{
    u_short ethernet_type;
    /* 以太网类型 */
    struct ether_header *ethernet_protocol;
    /* 以太网协议 */
    u_char *mac_string;
    /* 以太网地址 */
    static int packet_number = 1;
    printf("**************************************************\n");
    printf("The %d  ICMP  packet is captured.\n", packet_number);
    printf("--------   Ehternet Protocol (Link Layer)    --------\n");
    ethernet_protocol = (struct ether_header*)packet_content;
    /* 获得以太网协议数据内容 */
    printf("Ethernet type is :\n");
    ethernet_type = ntohs(ethernet_protocol->ether_type);
    /* 获得以太网类型 */
    printf("%04x\n", ethernet_type);
    switch (ethernet_type) /* 根据以太网类型进行判断 */
    {
        case 0x0800:
            printf("The network layer is IP protocol\n");
            break;
            /* 上层协议为IP协议 */
        case 0x0806:
            printf("The network layer is ARP protocol\n");
            break;
            /* 上层协议为ARP协议 */
        case 0x8035:
            printf("The network layer is RARP protocol\n");
            break;
            /* 上层协议为RARP协议 */
        default:
            break;
    }
    printf("Mac Source Address is : \n");
    mac_string = ethernet_protocol->ether_shost;
    printf("%02x:%02x:%02x:%02x:%02x:%02x\n",  *mac_string, *(mac_string + 1), *(mac_string + 2), *(mac_string + 3), *(mac_string + 4), *(mac_string + 5));
    /* 获得源以太网地址 */
    printf("Mac Destination Address is : \n");
    mac_string = ethernet_protocol->ether_dhost;
    printf("%02x:%02x:%02x:%02x:%02x:%02x\n",  *mac_string, *(mac_string + 1), *(mac_string + 2), *(mac_string + 3), *(mac_string + 4), *(mac_string + 5));
    /* 获得目的以太网地址 */
    switch (ethernet_type)
    {
        case 0x0800:
            ip_protocol_packet_callback(argument, packet_header, packet_content);
            break;
            /* 如果上层协议是IP协议,就调用分析IP协议的函数 */
        default:
            break;
    }
    printf("**************************************************\n");
    packet_number++;
}

由于ICMP报文类型非常多,我们只使用一种类型,即回显,ICMP回显报文是ping程序使用的报文。

ICMP数据包捕获也是使用Libpcap中的pcap_loop来实现的。

   net_interface = pcap_lookupdev(error_content);
    /* 获得网络接口 */
    pcap_lookupnet(net_interface, &net_ip, &net_mask, error_content);
    /* 获得网络地址和网络掩码 */
    pcap_handle = pcap_open_live(net_interface, BUFSIZ, 1, 0, error_content);
    /* 打开网络接口 */
    pcap_compile(pcap_handle, &bpf_filter, bpf_filter_string, 0, net_ip);
    /* 编译过滤规则 */
    pcap_setfilter(pcap_handle, &bpf_filter);
    /* 设置过滤规则 */
    if (pcap_datalink(pcap_handle) != DLT_EN10MB)
        return ;
    pcap_loop(pcap_handle,  - 1, ethernet_protocol_packet_callback, NULL);
    /* 注册回调函数,循环捕获网络数据包,然后调用回调函数对网络数据包进行处理 */
    pcap_close(pcap_handle);
    /* 关闭Libpcap操作 */

先分析以太网协议函数ethernet_protocol_packet_callback(),再此函数中调用IP协议的函数ip_protocol_packet_callback(),然后在调用ICMP协议函数icmp_protocol_packet_callback()

在分析ICMP协议时,只对ICMP回显报文进行分析,包含回显请求和回显应答 ,回显请求的ICMP类型为8,回显应答的ICMP报文类型为0。


总结


本篇对网络数据包Libpcap进行了简单的讲解分析,最后用一个简单的例子,说明使用libpcap的方法。

Libpcap开发包应用非常广,感兴趣的朋友可以研究源码学习,很多优秀的网络安全软件都是基于libpcap来开发的。



版权声明:本文为chen1415886044原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。