一、前言

通过前面的学习,我们已经知道了如何打开设备捕获数据了,接下来就可以捕获并过滤网络流量了。

本教程主要的目标是展示如何解析数据包的协议首部,选中分析和实现UDP协议,因为UDP协议相对于其它协议来说更简单,用于入门。


二、代码详解

#include "mainwindow.h"
#include <QApplication>
#include <QDebug>

#define
#include "pcap.h"

#ifndef
#include <sys/socket.h>
#include <netinet/in.h>
#else
#include <winsock2.h>
#include <ws2tcpip.h>
#endif

//4字节的IP地址
typedef struct ip_address
{
u_char byte1;
u_char byte2;
u_char byte3;
u_char byte4;
}ip_address;

//IPv4首部
typedef struct ip_header
{
u_char ver_ihl; //版本(4 bits) + 首部长度(4 bits)
u_char tos; //服务类型(Type of service)
u_short tlen; //总长(Total length)
u_short identification; //标识(Identification)
u_short flags_fo; //标志位(Flags)(3 bits) + 偏移量(Fragment offset)(13 bits)
u_short ttl; //存活时间(Time to live)
u_char proto; //协议(protocol)
u_short crc; //首部校验和(Header checksum)
ip_address saddr; //源地址(Source address)
ip_address daddr; //目的地址(Destination address)
u_int op_pad; //选项与填充
}ip_header;

//UDP首部
typedef struct udp_header
{
u_short sport; //源端口(Source port)
u_short dport; //目的端口(Destination port)
u_short len; //UDP数据包长度(Datagram length)
u_short crc; //校验和(Checksum)
}udp_header;

//回调函数原型
void packet_handler(u_char* param, const struct pcap_pkthdr* header, const u_char* pkt_data);

int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWindow w;
w.show();


pcap_if_t *alldevs;
pcap_if_t *d;
int inum;
int i=0;
pcap_t* adhandle;
char errbuf[PCAP_ERRBUF_SIZE];
u_int netmask;
char packet_filter[] = "ip and udp";
struct bpf_program fcode;

//获取本机适配器列表
if(pcap_findalldevs_ex(PCAP_SRC_IF_STRING,NULL,&alldevs, errbuf) == -1)
{
qDebug() << "Error in pcap_findalldevs_ex: " <<errbuf;
exit(1);
}

//打印适配器列表
for(d = alldevs; d; d = d->next)
{
//设备名(Name)
qDebug()<<"Name: "<<d->name;
++i;
//设备描述(Description)
if (d->description) {
qDebug()<<"Description: "<<d->description;
}else {
qDebug()<<"No description available";
}

qDebug()<<"====================================================================";
}

if(i==0) {
qDebug()<<"No interfaces found! Make sure WinPcap is installed.";
return -1;
}

qDebug()<<QString("Enter the interface number (1-%1): ").arg(i);
//scanf("%d",&inum);
inum = 5;
qDebug()<<"inum: "<<inum;

if(inum < 1 || inum > i){
qDebug()<<"Interface number out of range.";
//释放适配器列表
pcap_freealldevs(alldevs);
return -1;
}

//跳转到选中的适配器
for(d=alldevs,i=0; i<inum-1; d=d->next,i++);

//打开适配器
if((adhandle = pcap_open(d->name, //设备名
65536, //65535包证能捕获到不同数据链路层上的每个数据包的全部内容
PCAP_OPENFLAG_PROMISCUOUS, //混杂模式
1000, //读取超时时间
NULL, //远程机器验证
errbuf //错误缓冲池
)) == NULL) {
qDebug()<<"Unable to open the adapter."<<QString("%1 is not support by WinPcap").arg(d->name);

//释放适配器列表
pcap_freealldevs(alldevs);
return -1;
}

//检查数据链路层,为了简单,只考虑以太网
if(pcap_datalink(adhandle) != DLT_EN10MB) {
qDebug()<<stderr<<endl<<"This program works only on Ethernet networks.";

//释放设备列表
pcap_freealldevs(alldevs);
return -1;
}

if(d->addresses != NULL) {
//获得接口第一个地址的掩码
netmask = ((struct sockaddr_in*)(d->addresses->netmask))->sin_addr.S_un.S_addr;
}else {
//如果接口没有地址,那么假设一个C类的掩码
netmask = 0xffffff;
}

//编译过滤器
if(pcap_compile(adhandle,&fcode,packet_filter,1,netmask) < 0) {
qDebug()<<stderr<<endl<<"Unable to compile the packet filter. Check the syntax";

//释放设备列表
pcap_freealldevs(alldevs);
return -1;
}

//设置过滤器
if(pcap_setfilter(adhandle,&fcode) < 0) {
qDebug()<<stderr<<endl<<"Error setting the filter.";

pcap_freealldevs(alldevs);
return -1;
}

qDebug()<<QString("Listening on %1...").arg(d->description);

//释放适配器列表
pcap_freealldevs(alldevs);

//开始捕捉
pcap_loop(adhandle,0,packet_handler,NULL);

return a.exec();
}

//回调函数,当收到每一个数据包时会被libpcap所调用
void packet_handler(u_char* param, const struct pcap_pkthdr* header, const u_char* pkt_data)
{
struct tm* ltime;
char timestr[16];
ip_header* ih;
udp_header* uh;
u_int ip_len;
u_short sport,dport;
time_t local_tv_sec;

//将时间戳转换为可识别的格式
local_tv_sec = header->ts.tv_sec;
ltime = localtime(&local_tv_sec);
strftime(timestr,sizeof timestr,"%H:%M:%S",ltime);

//打印数据包的时间戳和长度
qDebug()<<timestr<<header->ts.tv_usec<<header->len;

//获取IP数据包头部的位置
ih = (ip_header*)(pkt_data + 14); //14是以太网头部长度

//获得UDP首部的位置
ip_len = (ih->ver_ihl & 0xf) * 4;
uh = (udp_header*)((u_char*)ih + ip_len);

//将网络字节序列转换成主机字节序列
sport = ntohs(uh->sport);
dport = ntohs(uh->dport);

//打印IP地址和UDP端口
qDebug()<<ih->saddr.byte1<<":"<<ih->saddr.byte2<<":"<<ih->saddr.byte3<<":"<<ih->saddr.byte4<<" "<<sport;
qDebug()<<ih->daddr.byte1<<":"<<ih->daddr.byte2<<":"<<ih->daddr.byte3<<":"<<ih->daddr.byte4<<" "<<dport;
qDebug()<<"===================================================================";
}

运行结果如下

WinPcap分析数据包_首部

  • 首先,我们将过滤器设置成“ip and udp”,在这种方式下,确信​​packet_handler()​​只会收到基于IPv4的UDP数据包,这将简化解析过程,提高程序的效率;
  • 我们还分别创建了用于描述IP首部和UDP首部的结构体,这些结构体中的各种数据会被​​packet_handler()​​合理地定位;
  • ​pack_handler()​​,尽管只受限于单个协议的解析(比如基于IPv4的UDP),不过它展示了捕捉器(sniffers)是多么的复杂,就像TcpDump或WinDump对网络数据流进行解码那样;
  • 因为我们对MAC首部不感兴趣,所以跳过它,为了简介,我们在开始捕捉前,使用了​​pcap_datalink()​​对MAC层进行了检测,以确保我们是在处理一个以太网,这样就能确保MAC首部是14位的 ;
  • IP数据包的首部就位于MAC首部的后面,我们将IP数据包的首部解析到源IP地址和目的IP地址;
  • 处理UDP的首部有一些复杂,因为IP数据包的首部并不是固定的,然而,我们可以通过IP数据包的length域来得到它的长度,一旦我们知道了UDP首部的位置,我们就能解析到源端口和目的端口;