前言

最近学习了 WinPcap,对教程中的 Demo 做一些函数说明补充

官方中文文档:[WinPcap: WinPcap 中文技术文档 (redicecn.com)](http://www.redicecn.com/htdocs/WinPcap Document V4.01/docs_cn/html/main.html)

Demo1: 获取接口列表

  • pcap_findalldevs_ex 函数:获取接口列表
  • pcap_freealldevs(pcap_if_t*):释放接口列表资源
1
2
3
4
5
6
7
int pcap_findalldevs_ex(
char *source, // 使用的接口,PCAP_SRC_IF_STRING:网络接口,PCAP_SRC_FILE_STRING:文件接口
struct pcap_rmtauth *auth, // 用户认证,默认null
pcap_if_t **alldevs, // 接口链表头指针
char *errbuf // 错误信息缓冲区
// return:-1表示错误
);

pcap_if_t 结构体是 pcap_if 的 typedef,相关类型定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 接口表项
struct pcap_if {
struct pcap_if *next; // 下一个接口
char *name; // 接口名 name to hand to "pcap_open_live()"
char *description; // 接口描述 textual description of interface, or NULL
struct pcap_addr *addresses; // 该接口的地址列表
bpf_u_int32 flags; // PCAP_IF_ interface flags
};

// 接口地址表项
struct pcap_addr {
struct pcap_addr *next;
// sockaddr是通用地址类型,可表示IPv4、IPv6等地址
struct sockaddr *addr; // 网络地址
struct sockaddr *netmask; // 子网掩码
struct sockaddr *broadaddr; // 当前地址对应的广播地址
struct sockaddr *dstaddr; // 当前地址的P2P目的地址
};

// sockaddr定义
struct sockaddr {
u_short sa_family; // 地址类型,AF_INET表示IPv4,AF_INET6表示IPv6
char sa_data[14]; // 地址数据
};
// sockaddr特定变种,可将sockaddr强转为以下两种
// IPv4地址
struct sockaddr_in {
short sin_family; // AF_INET 表示 IPv4
u_short sin_port; // 端口号
struct in_addr sin_addr; // IPv4地址
char sin_zero[8];
};
// IPv6地址
struct sockaddr_in6 {
short sin6_family;
u_short sin6_port;
u_long sin6_flowinfo;
struct in6_addr sin6_addr;
__C89_NAMELESS union {
u_long sin6_scope_id;
SCOPE_ID sin6_scope_struct;
};
};

上述结构的关系如下图所示,一个接口可以拥有多个网络地址,都是以链表形式连接

Demo2:获取接口高级信息

对每个 pcap_if 对象打印其中的所有信息

  • name:接口名
  • description:接口描述
  • flags:flags & PCAP_IF_LOOPBACK,判断是否是环回地址
  • addresses:pcap_addr 地址列表

IPV4 地址转换字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* @param in 32位整数地址
* @return 点分十进制字符串
*/
char* ipv4_to_s(u_long in) {
// 12个字符串,每个字符串最大为4个3位数+3个点+null
static char output[12][3 * 4 + 3 + 1];
static short which;
u_char *p;

// IP地址表示为32位整数(u_long),将其转换为u_char,就是将in按8位拆分,并得到首字节的指针
p = (u_char*) ∈
which = (which + 1) % 12;
sprintf(output[which], "%d.%d.%d.%d", p[0], p[1], p[2], p[3]);
return output[which];
}

IPv6 地址转换字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* @param sockaddr 地址项对象
* @param address 地址缓冲区
* @param addrlen 缓冲区长度
* @return 地址字符串
*/
char* ipv6_to_s(struct sockaddr* sockaddr, char* address, int addrlen) {
socklen_t sockaddr_len = sizeof(struct sockaddr_in6);

/**
* 调用getnameinfo将IPV6地址转换为字符串
* @param sa sockaddr结构体,表示通用网络地址
* @param salen sockaddr结构体大小
* @param host 存储主机名的缓冲区指针
* @param hostlen 主机名缓冲区大小
* @param serv 存储服务名(端口号)缓冲区指针
* @param servlen 端口号缓冲区大小
* @param flags NI_NUMERICHOST:主机名转换为数字形式,NI_NUMERICSERV:服务名转换为数字形式
* @return 0 执行成功
*/
if (getnameinfo(sockaddr, sockaddr_len, address, addrlen,
nullptr, 0, NI_NUMERICHOST) != 0) {
address = nullptr;
}

return address;
}

Demo3:打开接口捕获数据包

打开接口

  • pcap_open:打开接口
  • pcap_close(pcap_t*):关闭接口
1
2
3
4
5
6
7
8
9
10
// 打开接口
pcap_t *pcap_open(
const char* source, // 接口名,pcap_if.name
int snaplen, // 截断长度,即捕获的数据包长度,单位B,捕获全部数据包设为65535
int flags, // 默认只捕获发送给该接口的包,设置混杂模式捕获全部数据包,PCAP_OPENFLAG_PROMISCUOUS
int read_timeout, // 超时时间
struct pcap_rmtauth *auth, // null
char* errbuf // 错误信息缓冲区
// return pcap_t pcap结构体的typedef,表示已打开接口的描述符
);

该函数返回的 pcap_t 结构是后续操作该接口的描述符,pcap 结构体对用户不可见,由 wpcap.dll 维护,一个可能的描述(cite. winpcap - What structure pcap_t have? - Stack Overflow

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
struct pcap {
int fd;
int snapshot;
int linktype;
int tzoff; /* timezone offset */
int offset; /* offset for proper alignment */

struct pcap_sf sf;
struct pcap_md md;

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

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


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

char errbuf[PCAP_ERRBUF_SIZE];
};

捕获数据包

打开接口后,调用 pcap_loop 捕获数据包,同时还有 pcap_dispatch 也可捕获数据包,两者参数相同

两者的不同在于 pcap_loop 在超时时,如果未捕获到数据包,会使进程阻塞,因而可以持续捕获,而 pcap_dispatch 在超时时会直接返回,不能持续捕获

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int pcap_loop(
pcap_t* handle, // 打开接口描述符
int cnt, // 捕获数据包的数量,0或负数时持续捕获
pcap_handler handler, // 捕获数据包的回调函数
u_char* param // 传递到回调函数的第一个参数param,
// return 成功捕获的数据包数量
);

// pcap_handler定义
void packet_handler(
u_char* param, // pcap_loop传递的参数
const struct pcap_pkthdr* header, // 包元数据
const u_char* pkt_data // 包内容,字节数组
);

// 元数据定义
struct pcap_pkthdr {
struct timeval ts; // 时间戳
bpf_u_int32 caplen; // 分组长度(成功捕获的长度)
bpf_u_int32 len; // 包长度
};

Demo4:非回调捕获包

使用 pcap_next_ex 捕获一个数据包

1
2
3
4
5
6
7
8
9
10
11
12
int pcap_next_ex(
pcap_t* handle, // 打开接口描述符
struct pcap_pkthdr** header, // 包元数据
const u_char** pkt_data // 包内容
/**
* @return
* 1:捕获成功
* 0:超时失败
* -1:捕获失败,发生异常,可通过pcap_geterr(handle)获取错误信息
* -2:获取到离线记录文件的最后一个报文(EOF)
*/
);

Demo5:过滤数据包

  • pcap_compile:编译过滤表达式
  • pcap_setfilter:为捕获会话设置一个过滤器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
int pcap_compile(
pcap_t* handle, // 打开的接口描述符
struct bpf_program* bpf, // 存储编译后的过滤器程序
const char* str, // 过滤表达式
int optimize, // 是否优化,1:优化,0:不优化
bpf_u_int32 net_mask // 子网掩码
// return 0:成功
);

int pcap_setfilter(
pcap_t* handle, // 打开的接口描述符
struct bpf_program* bpf // 过滤器程序
// return 0:成功
);

Demo6:UDPdump

主流程

  1. 获取接口列表
  2. 选择接口,获取 pcap_if
  3. pcap_open 打开接口,获取描述符 handle
  4. pcap_datalink 检查接口的数据链路层类型,DLT_EN10MB 为以太网
  5. pcap_compile 编译过滤表达式为 bpf_program
  6. pcap_setfilter 设置 handle 的过滤器
  7. pcap_freealldevs 释放接口列表
  8. pcap_loop 开始捕获

回调处理流程

  1. 通过 pcap_pkthdr 结构获取元数据,打印时间戳等信息

  2. 将 pkt_data 解析出 IP 首部和 UDP 首部

    处理流程主要是对捕获到的字节数组进行解析,通常的做法就是先定位到要解析的部分的首地址,然后将指针强转为其他类型,指针强转就是改变指针指向的单位,换句话说,就是不同类型的指针移动时以不同的单位进行移动,改变指针类型就是改变指针移动的步长,当指针指向解析部分首地址时,改变指针类型为解析类型,就可以得到解析部分的完整结构了

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    /* 获得IP数据包头部的位置 */
    ip_header* ip = (ip_header*)(pkt_data + 14); // 以太网头部长度,单位B

    /* 获得UDP首部的位置 */
    // ip->ver_ihl & 0xf获取低4位值,即ihl
    // 首部长度单位4B,ihl * 4 = IP首部长度字节数
    u_int ip_len = (ip->ver_ihl & 0xf) * 4;
    // ip指针转为字节表示,ip + ip_len = IP包数据部分第一字节,再转为udp_header*,得到UDP首部
    udp_header* udp = (udp_header*)((u_char*)ip + ip_len);

    /* 将网络字节序列转换成主机字节序列 */
    // 获取源端口和目的端口并转换,主机字节序和网络字节序不一定相同
    sport = ntohs(udp->sport);
    dport = ntohs(udp->dport);
    // 类似有ntohl, htons,htonl

    /* 打印IP地址和UDP端口 */
    printf("%d.%d.%d.%d:%d -> %d.%d.%d.%d:%d\n",
    ip->saddr.byte1,
    ip->saddr.byte2,
    ip->saddr.byte3,
    ip->saddr.byte4,
    sport,
    ip->daddr.byte1,
    ip->daddr.byte2,
    ip->daddr.byte3,
    ip->daddr.byte4,
    dport);

Demo7:处理脱机堆文件

保存堆文件

将捕获的数据包数据保存到文件中

  • pcap_dump_open:创建并打开堆文件,通常文件名为 *.pcap
  • pcap_dump:将数据包写入堆文件
1
2
3
4
5
6
7
8
9
10
11
pcap_dumper_t* pcap_dump_open(
pcap_t* handle, // 打开接口描述符
const char* filename // 写入文件路径,注意Windows中相对路径相对于.exe文件
// return 打开的堆文件描述符,pcap_dumper的typedef
);

void pcap_dump(
u_char* param, // 堆文件描述符,将pcap_dumper_t强转得到,回调时通过param参数传递
const struct pcap_pkthdr* pkt_header, // 数据包header
const u_char* pkt_data // 数据包内容
);

读取堆文件

  • pcap_createscrstr:根据参数生成一个描述接口的 source 字符串,可用于创建文件接口,使用 pcap_open 打开,pcap_loop 捕获
  • pcap_open_offline:专用于打开文件接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int pcap_createsrcstr(
char* source, // 存储souce字符串的缓冲区
int type, // source字符串类型,
const char* host, // 远程主机名
const char* port, // 远程端口号
const char* name, // 接口名称,打开文件接口即文件名
char* errbuf // 错误信息缓冲区
// return 0:成功
);
/**
* type参数
* PCAP_SRC_FILE:文件接口
* PCAP_SRC_IFLOCAL:本地接口
* PCAP_SRC_IFREMOTE:远程接口,必须基于RPCAP协议
*/

pcap_t* pcap_open_offline(
const char* filename, // 文件名
char* errbuf // 错误信息缓冲区
// return 文件接口描述符
);

Demo8:发送数据包

发送单个数据包

pcap_sendpacket:发送单个数据包

1
2
3
4
5
6
int pcap_sendpacket(
pcap_t* handle, // 打开接口描述符
const u_char* packet, // 数据包,包含首部信息
int packet_len // 包大小,单位B
// return 0:成功
);

发送队列

使用发送队列,发送队列相关函数

  • pcap_sendqueue_alloc:创建指定大小的发送队列
  • pcap_sendqueue_queue:将包添加到发送队列
  • pcap_sendqueue_transmit:传输发送队列
  • pcap_sendqueue_destroy:销毁发送队列
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pcap_send_queue* pcap_sendqueue_alloc(
u_int memsize // 发送队列大小,单位B
// return 队列对象
);

int pcap_sendqueue_queue(
pcap_send_queue* queue, // 发送队列对象
const struct pcap_pkthdr *pkt_header, // 包header
const u_char *pkt_data // 包内容
// return 0:成功,-1:失败
);

u_int pcap_sendqueue_transmit(
pcap_t* p, // 发送接口描述符
pcap_send_queue* queue, // 发送队列
int sync // 是否同步发送
// return 成功发送的字节数
);

Demo9:收集并统计网络流量

pcap_setmode:设置接口为统计模式

1
2
3
4
5
6
7
8
9
10
11
int pcap_setmode(
pcap_t* p, // 接口描述符
int mode // 模式
// return 0:成功,-1:失败
);
/**
* mode参数
* MODE_CAPT:捕获模式,仅捕获数据包
* MODE_STAT:统计模式,获取统计信息
* MODE_MON:监视模式,接口设置为混杂模式
*/

开始捕获后,pkt_header 和 pkt_data 为统计信息,具体如下所示

  • pkt_header 中包含 ts 时间戳,成功捕获长度为包内容大小 (统计信息),pkt_data 共 16B

  • pkt_data 中前 8B 为 AcceptedPackets 已捕获的数据包数量,后 8B 为 AcceptedBytes 已捕获字节数

    *((LONGLONG*)pkt_data) 获取 AcceptedPackets 数值

    *((LONGLONG*)(pkt_data + 8)) 获取 AcceptedBytes 数值


补充:LARGE_INTEGER 类型

LARGE_INTEGER 可表示一个 64 位符号数,定义如下

1
2
3
4
5
6
7
8
9
10
11
typedef union _LARGE_INTEGER {
__C89_NAMELESS struct {
DWORD LowPart;
LONG HighPart;
} DUMMYSTRUCTNAME;
struct {
DWORD LowPart;
LONG HighPart;
} u;
LONGLONG QuadPart;
} LARGE_INTEGER;

LowPart 存储 64 位数的低 32 位,HighPart 存储 64 位数的低 32 位,当编译器不支持 64 位数时,LARGE_INTEGER 通过 LowPart 和 HighPart 表示一个 64 位数,当支持 64 位数时,LARGE_INTEGER 等价于 LONGLONG(aka. __int64, long long),可直接使用 QuadPart