Parse Packets in Pcap format

本文详细介绍了如何使用pcap_open_offline()和pcap_next_ex()函数解析Pcap文件,包括文件的具体格式分析,如GlobalHeader、PacketHeader和PacketData的结构,以及需要注意的关键细节。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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

Open a savefile in the tcpdump/libpcap format to read packets.

pcap_open_offline() is called to open a "savefile" for reading. fname specifies the name of the file to open. The file has the same format as those used by tcpdump(1) and tcpslice(1). The name "-" in a synonym for stdin. Alternatively, you may call pcap_fopen_offline() to read dumped data from an existing open stream fp. Note that on Windows, that stream should be opened in binary mode. errbuf is used to return error text and is only set when pcap_open_offline() or pcap_fopen_offline() fails and returns NULL.

Regular Usage pcap_t * pcap = pcap_open_offline(file.c_str(), errbuff);

 

2. pcap_next_ex

read the next packet from a pcap_t

#include <pcap/pcap.h>

pcap_next_ex() returns 1 if the packet was read without problems, 0 if packets are being read from a live capture and the timeout expired, -1 if an error occurred while reading the packet, and -2 if packets are being read from a ``savefile'' and there are no more packets to read from the savefile. If -1 is returned,pcap_geterr() or pcap_perror() may be called with p as an argument to fetch or display the error text.

Usage while(int returnValue = pcap_next_ex(pcap, &header, &data) >= 0)

 

3. Pcap 文件格式

上面介绍了两个用于解析 Pcap 文件的函数, 下面来分析下 Pcap 文件的具体格式

Pcap 文件组成如下:

Global Header

Packet Header (1) /* 第一个数据包 */

Packet Data (1)

Packet Header (2)

Packet Data (2)

 

3.1 Global Header

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;  
};

有了上面两个函数, 我们可以直接跳过 Global Header. 因此 global header 不需要关心.

 

3.2 Packet Header

Packet 指的是 Pcap Packet, Header 的定义是

struct pcap_pkthdr {
        struct timeval ts;     
        bpf_u_int32 caplen;    
        bpf_u_int32 len;       
};
struct timeval {
        long            tv_sec;        
        suseconds_t     tv_usec;       
};

timeval , tv_sec 是秒, tv_usec 是毫秒, 使用的是 UTC 时间, 转化到大陆的时间需要加 8 小时.

 

3.3 Packet Data

Packet Data TCP/IP 协议意义上的数据包, Data 部分, 分别是

以太网帧头 14 字节

IP 20 字节

TCP 一般 20 字节 or UDP (根据 IP 头的协议属性确定)

Payload, 真正的数据.

 

IP 头部协议类型:

 01    ICMP
 02    IGMP
 06    TCP
 17    UDP
 88    IGRP
 89    OSPF

 

4. 需要注意

[1] 描述了配置 visual studio 2010 + winpcap 环境具体步骤, 但需要注意最后一步把 winpcap.lib 改成 wpcap.lib 否则编译报错.

[3] 给出了测试代码, 能够运行就表示配置成功, 注意, 假如创建的项目是 empty project, 那么需要在预处理器中额外添加 WIN32;

[4] 有人在 stackoverflow 提问, 使用 [2] 给出的代码得出的记过不对. 有人回答网络流与大小端模式的转化关系, 比如使用ntohl, ntohs, inet_ntoa 函数.

 

Reference

[1] http://www.rhyous.com/2011/11/12/how-to-compile-winpcap-with-visual-studio-2010/

[2] http://www.rhyous.com/2011/11/13/how-to-read-a-pcap-file-from-wireshark-with-c/

[3] http://blog.youkuaiyun.com/caodesheng110/article/details/7670588

[4] http://stackoverflow.com/questions/12999538/read-from-a-pcap-file-and-print-out-ip-addresses-and-port-numbers-in-c-but-my-r

转载于:https://www.cnblogs.com/zhouzhuo/p/3657576.html

import sys import io import os import numpy as np import matplotlib as mpl import matplotlib.pyplot as plt import scapy.all as scapy from collections import defaultdict from sklearn.preprocessing import StandardScaler from sklearn.cluster import KMeans from sklearn.metrics import silhouette_score import pandas as pd import tkinter as tk from tkinter import filedialog, messagebox from scapy.layers.inet import IP, TCP, UDP, ICMP from scapy.layers.dns import DNS, DNSRR from datetime import datetime import time from sklearn.decomposition import PCA # 设置Matplotlib中文显示支持 plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'WenQuanYi Micro Hei'] plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题 # 设置系统输出编码为UTF-8 sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') if os.name == 'nt': # Windows系统专用设置 os.system('chcp 65001') # 切换到UTF-8代码页 # ====================== # 文件选择对话框 # ====================== def select_files(): """创建文件选择对话框""" root = tk.Tk() root.withdraw() # 隐藏主窗口 # 选择PCAP文件 pcap_path = filedialog.askopenfilename( title="选择PCAP文件", filetypes=[("PCAP文件", "*.pcap"), ("PCAPNG文件", "*.pcapng"), ("所有文件", "*.*")] ) if not pcap_path: print("未选择PCAP文件,程序退出") sys.exit(0) # 选择输出目录 output_dir = filedialog.askdirectory(title="选择结果保存位置") if not output_dir: print("未选择输出目录,程序退出") sys.exit(0) return pcap_path, output_dir # ====================== # 1. PCAP文件解析与流重组 # ====================== print("请选择PCAP文件和输出位置...") pcap_path, output_dir = select_files() print(f"解析文件: {pcap_path}") print(f"保存位置: {output_dir}") # 新增:DNS解析缓存 dns_cache = {} # 流式解析大PCAP文件,避免内存溢出 def parse_large_pcap(pcap_path): flows = defaultdict(list) # 使用字典存储网络流 packet_count = 0 # 总包计数器 ip_packet_count = 0 # IP包计数器 global dns_cache # 使用全局DNS缓存 # 使用PcapReader流式读取,避免一次性加载大文件 with scapy.PcapReader(pcap_path) as pcap_reader: for pkt in pcap_reader: packet_count += 1 # 进度显示(每10000个包) if packet_count % 10000 == 0: print(f"已处理 {packet_count} 个包...") # 新增:DNS响应解析 - 修复索引越界问题 if DNS in pkt and pkt[DNS].qr == 1: # 只处理DNS响应 # 获取实际可用的回答记录数量 actual_ancount = len(pkt[DNS].an) if hasattr(pkt[DNS], 'an') else 0 # 使用实际数量而不是ancount字段 for i in range(min(pkt[DNS].ancount, actual_ancount)): rr = pkt[DNS].an[i] if isinstance(rr, DNSRR) and rr.type == 1: # A记录 try: ip = rr.rdata # 正确处理域名格式 domain = rr.rrname.decode('utf-8', errors='ignore').rstrip('.') # 添加到DNS缓存 if ip not in dns_cache: dns_cache[ip] = set() dns_cache[ip].add(domain) except Exception as e: # 忽略解析错误 pass # 仅处理IP层数据包 if IP in pkt: ip_packet_count += 1 ip_layer = pkt[IP] proto_num = ip_layer.proto # 获取协议号 # 初始化端口变量 src_port, dst_port = 0, 0 # 根据协议类型提取端口信息 if proto_num == 6 and TCP in pkt: # TCP协议 src_port = pkt[TCP].sport dst_port = pkt[TCP].dport elif proto_num == 17 and UDP in pkt: # UDP协议 src_port = pkt[UDP].sport dst_port = pkt[UDP].dport elif proto_num == 1 and ICMP in pkt: # ICMP协议 # 使用类型和代码代替端口 src_port = pkt[ICMP].type dst_port = pkt[ICMP].code # 创建五元组作为流的唯一标识 flow_key = (ip_layer.src, ip_layer.dst, src_port, dst_port, proto_num) flows[flow_key].append(pkt) # 将包添加到对应流 print(f"共处理 {packet_count} 个包,其中 {ip_packet_count} 个IP包") print(f"DNS缓存大小: {len(dns_cache)} 个IP到域名的映射") return flows # 解析PCAP文件 flows = parse_large_pcap(pcap_path) print(f"共识别出 {len(flows)} 个网络流") # ====================== # 2. 特征工程 # ====================== print("正在提取流量特征...") features = [] # 存储特征向量 valid_flows = [] # 存储有效流的标识 # 只初始化源时间列表 src_start_times = [] src_end_times = [] # 特征名称列表 feature_names = [ '包数量', '总字节数', '平均包间隔', '包间隔标准差', '平均包长', '包长标准差', '最小包长', '最大包长', '上行比例' ] min_packets = 5 # 流的最小包数阈值 for flow_key, packets in flows.items(): packet_count = len(packets) # 过滤包数不足的流 if packet_count < min_packets: continue # 1. 基础统计特征 total_bytes = sum(len(p) for p in packets) # 总字节数 # 2. 时序特征(包到达间隔) timestamps = np.array([float(p.time) for p in packets]) if packet_count > 1: iat = np.diff(timestamps) # 包间隔 mean_iat = np.mean(iat) # 平均间隔 std_iat = np.std(iat) if len(iat) > 1 else 0 # 间隔标准差 else: mean_iat = std_iat = 0 # 3. 包长特征 pkt_lengths = np.array([len(p) for p in packets]) mean_pkt_len = np.mean(pkt_lengths) # 平均包长 std_pkt_len = np.std(pkt_lengths) if packet_count > 1 else 0 # 包长标准差 min_pkt_len = np.min(pkt_lengths) # 最小包长 max_pkt_len = np.max(pkt_lengths) # 最大包长 # 4. 方向特征(上行比例) src_ip = flow_key[0] # 流源IP dst_ip = flow_key[1] # 流目的IP # 判断数据包方向(1=上行,0=下行) directions = np.array([1 if p[IP].src == src_ip else 0 for p in packets]) up_ratio = np.mean(directions) # 上行包比例 # 源IP发送的包的时间戳 src_pkt_times = [float(p.time) for p in packets if p[IP].src == src_ip] # 计算源的起始和终止时间 src_start = min(src_pkt_times) if src_pkt_times else np.nan src_end = max(src_pkt_times) if src_pkt_times else np.nan # 添加时间信息到列表 src_start_times.append(src_start) src_end_times.append(src_end) # 添加特征向量 features.append([ packet_count, total_bytes, mean_iat, std_iat, mean_pkt_len, std_pkt_len, min_pkt_len, max_pkt_len, up_ratio ]) valid_flows.append(flow_key) # 记录有效流 if not features: # 错误处理:无有效流 print("错误:没有提取到任何有效网络流") print("可能原因:") print("1. 所有流都少于5个包(尝试减小过滤阈值)") print("2. 文件格式不兼容(尝试用Wireshark打开验证)") print("3. 没有IP流量(检查网络捕获配置)") sys.exit(1) feature_matrix = np.array(features) print(f"提取了 {len(features)} 个有效流的 {len(feature_names)} 维特征") # ====================== # 3. 数据标准化 # ====================== print("正在进行数据标准化...") scaler = StandardScaler() scaled_features = scaler.fit_transform(feature_matrix) # Z-score标准化 # ====================== # 4. 肘部法则确定最佳K值 # ====================== print("使用肘部法则确定最佳聚类数...") n_samples = scaled_features.shape[0] # 样本数量 # 动态调整K值范围 if n_samples < 5: print(f"警告:样本数量较少({n_samples}),将使用简化聚类策略") k_range = range(1, min(11, n_samples + 1)) # K上限不超过样本数 else: k_range = range(1, 11) # 测试1-10个聚类 sse = [] # 存储SSE(误差平方和) kmeans_models = {} # 存储不同K值的模型 for k in k_range: # 跳过超过样本数的K值 if k > n_samples: print(f"跳过K={k}(超过样本数{n_samples})") continue # 训练K-means模型 kmeans = KMeans(n_clusters=k, n_init=10, random_state=42) kmeans.fit(scaled_features) sse.append(kmeans.inertia_) # 记录SSE kmeans_models[k] = kmeans # 存储模型 # 样本不足时的处理 if len(sse) < 2: optimal_k = min(2, n_samples) # 至少2类(不超过样本数) print(f"样本过少,直接设置K={optimal_k}") else: # 绘制肘部法则图 plt.figure(figsize=(10, 6)) plt.plot(list(k_range)[:len(sse)], sse, 'bo-') plt.xlabel('聚类数量 $ K$') plt.ylabel('SSE (误差平方和)') plt.title('肘部法则确定最佳聚类数') plt.grid(True) elbow_path = os.path.join(output_dir, 'elbow_method.png') plt.savefig(elbow_path, dpi=300) plt.close() print(f"肘部法则图已保存为 {elbow_path}") # 自动检测拐点(二阶差分最小值) if len(sse) >= 3: knee_point = np.argmin(np.diff(sse, 2)) + 2 # 计算二阶差分 optimal_k = max(2, min(10, knee_point)) # 确保K在2-10之间 else: optimal_k = max(2, min(3, n_samples)) # 样本少时选择2或3 print(f"自动检测的最佳聚类数: K = {optimal_k}") # ====================== # 5. K-means聚类 # ====================== print(f"正在进行K-means聚类 (K={optimal_k})...") # 调整K值不超过样本数 if optimal_k > n_samples: optimal_k = max(1, n_samples) print(f"调整聚类数为样本数: K = {optimal_k}") # 使用已有模型或新建模型 if optimal_k in kmeans_models: kmeans = kmeans_models[optimal_k] cluster_labels = kmeans.labels_ else: kmeans = KMeans(n_clusters=optimal_k, n_init=10, random_state=42) cluster_labels = kmeans.fit_predict(scaled_features) # 计算轮廓系数(K>1时) if optimal_k > 1: silhouette_avg = silhouette_score(scaled_features, cluster_labels) print(f"轮廓系数: {silhouette_avg:.4f} (接近1表示聚类效果良好)") else: print("聚类数为1,无需计算轮廓系数") # ====================== # 6. 结果分析与可视化(新增域名显示) # ====================== # 创建结果DataFrame result_df = pd.DataFrame({ '源IP': [flow[0] for flow in valid_flows], '目的IP': [flow[1] for flow in valid_flows], '协议': [f"TCP(6)" if flow[4] == 6 else f"UDP(17)" for flow in valid_flows], '聚类标签': cluster_labels, '源起始时间': src_start_times, '源终止时间': src_end_times, '包数量': feature_matrix[:, 0], '总字节数': feature_matrix[:, 1] }) # 新增:添加域名信息 def get_domains(ip): """获取IP对应的域名列表""" domains = dns_cache.get(ip, set()) return ", ".join(domains) if domains else "N/A" result_df['目的域名'] = result_df['目的IP'].apply(get_domains) # 新增:时间格式化函数 def format_timestamp(ts): """将时间戳格式化为可读字符串""" if np.isnan(ts): return "N/A" return datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M:%S.%f")[:-3] # 保留毫秒 # 应用时间格式化 result_df['源起始时间'] = result_df['源起始时间'].apply(format_timestamp) result_df['源终止时间'] = result_df['源终止时间'].apply(format_timestamp) # 只保留需要的列 required_columns = ['源IP', '目的IP', '协议', '源起始时间', '源终止时间', '目的域名', '包数量', '总字节数'] result_df = result_df[required_columns] # 生成结果文件名(带时间戳) timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") output_filename = f'cluster_result_{timestamp}.csv' output_path = os.path.join(output_dir, output_filename) try: result_df.to_csv(output_path, index=False, encoding='utf_8_sig') print(f"聚类结果已保存为 {output_path}") except Exception as e: print(f"保存结果文件失败: {str(e)}") # 尝试临时目录作为备选 temp_dir = os.path.join(os.environ.get('TEMP', ''), 'netflow_clustering') os.makedirs(temp_dir, exist_ok=True) temp_path = os.path.join(temp_dir, output_filename) result_df.to_csv(temp_path, index=False, encoding='utf_8_sig') print(f"聚类结果已保存为 {temp_path}") # 簇统计信息(使用聚类标签列) if '聚类标签' in result_df.columns: cluster_stats = result_df.groupby('聚类标签').agg({ '包数量': 'mean', '总字节数': 'mean' }).rename(columns={ '包数量': '平均包数量', '总字节数': '平均总字节数' }) print("\n各用户簇流量特征统计:") print(cluster_stats.round(2)) else: print("警告:结果中缺少聚类标签列,无法进行簇统计") # 使用PCA降维可视化 pca = PCA(n_components=2) # 降维到2D principal_components = pca.fit_transform(scaled_features) plt.figure(figsize=(12, 8)) scatter = plt.scatter( principal_components[:, 0], principal_components[:, 1], c=cluster_labels, # 按簇着色 cmap='viridis', alpha=0.6 ) # 标记簇中心 centers = pca.transform(kmeans.cluster_centers_) plt.scatter( centers[:, 0], centers[:, 1], c='red', s=200, alpha=0.9, marker='X', edgecolor='black' ) plt.colorbar(scatter, label='用户簇') plt.xlabel('主成分 1') plt.ylabel('主成分 2') plt.title(f'校园网用户流量聚类 (K={optimal_k})') plt.grid(alpha=0.3) # 可视化文件保存到输出目录 vis_filename = f'cluster_visualization_{timestamp}.png' vis_path = os.path.join(output_dir, vis_filename) plt.savefig(vis_path, dpi=300) plt.close() print(f"聚类可视化图已保存为 {vis_path}") # 最终输出 print("\n聚类完成! 结果文件已保存:") print(f"- {elbow_path}: 肘部法则图") print(f"- {output_path}: 详细聚类结果") print(f"- {vis_path}: 聚类可视化") 根据这段程序画出pcap分包流程
07-15
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值