libpcap离线包排序

本文最后更新于:2020年10月25日 下午

libpcap是unix/linux平台下的网络数据包捕获函数包,大多数网络监控软件都以它为基础。原先的在python下用scapy读包效率过低,故转用在C下由libpcap实现读包排序。

libpcap安装

wget https://www.tcpdump.org/release/libpcap-1.9.1.tar.gz
tar zxvf libpcap-1.9.1.tar.gz
cd libpcap-1.9.1/
./configure
sudo make
sudo make install

libpcap的一些关键函数

1.获取网络接口

char * pcap_lookupdev(char * errbuf)

返回第一个合适的网络接口的字符串指针,如果出错,则errbuf存放出错信息字符串,errbuf至少应该是PCAP_ERRBUF_SIZE个字节长度的

2.释放网络接口

void pcap_close(pcap_t * p)

关闭pcap_open_live()获取的pcap_t的网络接口对象并释放相关资源

3.读取本地pcap文件

pcap_t * pcap_open_offline(const char *fname, char *errbuf)

函数打开保存的数据包文件,用于读取,返回文件描述符

fname参数指定了pcap文件名

errbuf依旧是函数出错的时候返回错误信息

4.获取数据包

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

第一个参数是第2步返回的pcap_t类型的指针
第二个参数是需要抓的数据包的个数,一旦抓到了cnt个数据包,pcap_loop立即返回。负数的cnt表示pcap_loop永远循环抓包,直到出现错误。
第三个参数是一个回调函数指针,它必须是如下的形式:
void callback(u_char * userarg, const struct pcap_pkthdr * pkthdr, const u_char * packet)

  • 第一个参数是pcap_loop的最后一个参数,当收到足够数量的包后pcap_loop会调用callback回调函数,同时将pcap_loop()的user参数传递给它
  • 第二个参数是收到的数据包的pcap_pkthdr类型的指针
  • 第三个参数是收到的数据包数据

5.保存数据包

pcap_dumper_t *pcap_dump_open(pcap_t *p, const char *file)

函数返回pcap_dumper_t类型的指针,file是文件名,可以是绝对路径

void pcap_dump_close(pcap_dumper_t *p)

用来关闭pcap_dump_open打开的文件,入参是pcap_dump_open返回的指针

int pcap_dump_flush(pcap_dumper_t *p)

刷新缓冲区,把捕获的数据包从缓冲区真正拷贝到文件

void pcap_dump(u_char * userarg, const struct pcap_pkthdr * pkthdr, const u_char * packet)

输出数据到文件,与pcap_loop的第二个参数回调函数void callback(u_char * userarg, const struct pcap_pkthdr * pkthdr, const u_char * packet) 形式完全相同,可以直接当pcap_loop的第二个参数;

排序思路

提取特征

上面两张图相当关键,是提取出排序思路的要点。整体上可以得出如下的排序原则:

  • 对于一个流来说, seq 是呈递增趋势的
  • 存在 ack 包长度为0,对于一个流来说,为0的包接下来的包的 seq 和本次相同,直至传来有长度的包
  • 如果包为 syn ,对于一个流来说,即使这个包为0,下一个来的包的 seq + 1
  • 对于一个seesion的包来说,两个之间的 ackseq 存在对应关系,应满足如下的关系:(a、b指一个session中两个不同的流)
    • a的 ack 小于或等于b的 seq
    • a的 seq 小于b的 ack

排序特征总结

根据上述说明,我们可以得出一些排序时我们需要得出的信息:

  • 源IP地址目标IP地址

    这两者用于来在一个session中区分出两个不同的流,仅需比较一个就够了。同时我们对session排序需要严格控制插入的规则,应该只允许一个流的插入到另一个流之中,避免出现混乱。

  • seqnext_seq

    这两者用于一个流的排序。 next_seq 实际为 seq 值加上包的长度 len ,如果 len 为0正常情况下 seqnext_seq 相同,但是在出现包为 syn 时,即使 len 为0, next_seq 也要为 seq + 1

  • ack

    用于session的排序中,确定一个流的包在何时插入至另一个流中。有时需要结合 seq 判断

分析流量包

我们使用libpcap所获得的流量包是裸包,即最原始的包信息,我们需要对获得的包手动进行分析才能得到我们需要的信息。

调用 pcap_loop 获得的包需要在 callback 函数中进行我们的分析。后两个参数是包的信息关键,原始包信息就在最后一个参数 packet 中,而包的长度则由前一个参数 pkthdr 结构里定义,其结构如下:

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) */
};

在分析过程中我们主要关注 packet

以太网帧头

packet 的第一部分为以太网帧头,排序过程中我们不关心它,不进行解析,只需要知晓它的长度跳过即可。它的长度为 ETH_HLEN ,为14字节

IP头

以太网帧头后的部分为IP头,我们通过 netinet/ip.h 中定义好的 iphdr 结构来解析它。结构如下:

struct iphdr {
#if defined(__LITTLE_ENDIAN_BITFIELD)
	__u8	ihl:4,
		version:4;
#elif defined (__BIG_ENDIAN_BITFIELD)
	__u8	version:4,
  		ihl:4;
#else
#error	"Please fix <asm/byteorder.h>"
#endif
	__u8	tos;
	__be16	tot_len;
	__be16	id;
	__be16	frag_off;
	__u8	ttl;
	__u8	protocol;
	__sum16	check;
	__be32	saddr;
	__be32	daddr;
	/*The options start here. */
};

通过以下代码计算偏移量,我们就可以获得所需要的IP头结构:

struct iphdr *ip_header = (struct iphdr *)(packet + ETH_HLEN);

我们关注的IP头信息为 源IP地址目标IP地址 ,这两个信息为IP头中的 saddrdaddr ,但是直接读取这两者我们不能得到想要的 x.x.x.x 的结构。为了得到想要的内容,我们还需要借助 arpa/inet.h 中定义好的结构 sockaddr_in 。结构如下:

struct sockaddr_in
{
	sa_family_t		sin_family;		//地址族(Address Family)
	uint16_t		sin_port;		//16位TCP/UDP端口号
	struct in_addr	sin_addr;		//32位IP地址
	char			sin_zero[8];	//不使用
};

struct in_addr
{
    In_addr_t		s_addr;			//32位IPv4地址
}

所以,将 saddrdaddr 赋给上述结构中的 s_addr ,再调用 inet_ntoa() 函数,就得到了我们需要的两个地址。

  • char *inet_ntoa(struct in_addr in)

    将一个32位网络字节序的二进制IP地址转换成相应的点分十进制的IP地址(返回点分十进制的字符串在静态内存中的指针)。

    所在头文件: arpa/inet.h

TCP头

IP头后的部分位TCP头,我们通过 netinet/tcp.h 中定义好的 tcphdr 结构解析它。结构如下:

typedef	u_long	tcp_seq;
/*
 * TCP header.
 * Per RFC 793, September, 1981.
 */
struct tcphdr {
	u_short	th_sport;		/* source port */
	u_short	th_dport;		/* destination port */
	tcp_seq	th_seq;			/* sequence number */
	tcp_seq	th_ack;			/* acknowledgement number */
#if BYTE_ORDER == LITTLE_ENDIAN 
	u_char	th_x2:4,		/* (unused) */
		th_off:4;		/* data offset */
#endif
#if BYTE_ORDER == BIG_ENDIAN 
	u_char	th_off:4,		/* data offset */
		th_x2:4;		/* (unused) */
#endif
	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
	u_short	th_win;			/* window */
	u_short	th_sum;			/* checksum */
	u_short	th_urp;			/* urgent pointer */
};

通过以下代码计算偏移量,我们就可以获得所需要的TCP头结构:

struct tcphdr *tcp_header = (struct tcphdr *)(packet + ETH_HLEN + sizeof(struct iphdr));

我们关注的信息为 seqacknext_seq 。其中, seqack 是结构中给我们的,通过 th_seqth_ack ,调用 ntohl() 函数,我们就可以得到需要的两个值。

  • uint32_t ntohl(uint32_t netlong)

    将一个无符号长整形数从网络字节顺序转换为主机字节顺序。

    所在头文件: arpa/inet.h

next_seq 是接下来的重头戏,因为结构本身并没有给出这个值,需要我们自己去计算得出。按照之前的的分析,这个值是 seq 的值加上包的长度。IP头结构中的 tot_len 可以给我们帮助,只需要用这个长度减去IP头长度和TCP头长度,这两个的长度就需要我们自己获得了。

借助wireshark,我们可以看到IP头的长度在原始数据包中是有所记录的,就在包的第15个字节的后一位,该值*4即为IP头长度

同样的,可以看到TCP头的长度在原始数据包中有记录,在包的第47个字节的前一位,该值*4即为TCP头长度

通过以下代码计算偏移量,我们就可以获得包的长度了:

len = ntohs(ip_header->tot_len) - ((int)packet[46])/16*4 - ((int)packet[14])%16*4;

next_seq 的值即为 seq + len

排序数据结构

现在我们已经获得了排序所需要的信息,接下来就需要构建一个便于我们排序的结构来进行排序。为了便于我们的插入排序,构建链表是一个不错的选择

struct packetnode{
	struct pcap_pkthdr *pkthdr;
	u_char *packet;
	char *srcip;
	char *dstip;
	u_long seq;
	u_long ack;
	u_long next_seq;
};

上面的节点结构用来处理数据包,便于后续排序。 pkthdrpacket 是原始抓到的数据包,也是我们后续要保存至文件的东西。后面的五个就是在上面分析流量包的环节我们排序所需要的信息,将它保存在这个节点结构中

struct packlist{
	struct packetnode *data;
	struct packlist *left;
	struct packlist *right;
};

上面的链表结构用来构建链表,有 leftright 用来进行查询和插入

排序流程图


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!