抓到的 PCAP 流量包里到底藏了什么?这篇用一个实战案例——分析 QQ 语音通话的流量包,提取通信对端的 IP 地址并做地理位置定位——讲透 Python 流量分析的完整流程。
背景
QQ 语音通话采用 UDP 直连(P2P),不经过腾讯服务器中转。这意味着:
- 通话双方的 IP 直接暴露在流量包中
- 分析 PCAP 文件可以提取出通话对象的 IP 地址
- 配合 IP 地理位置数据库,可以定位到城市级别
应用场景:CTF 流量分析题、网络取证、安全审计。
整体流程
1 2 3 4 5 6 7 8 9 10 11 12
| PCAP 文件 │ ├─► pyshark/scapy 读取 │ ├─► 过滤 UDP 数据包 │ └─ 匹配 QQ 语音特征码 "020048" │ ├─► 提取通信 IP 地址 │ ├─► 地理位置查询(多 API 聚合) │ └─► 生成报告(JSON + Markdown)
|
第一步:PCAP 文件读取
Python 生态有两个主流 PCAP 处理库:
| 库 |
特点 |
适用场景 |
| pyshark |
基于 tshark,解析能力强,支持协议字段访问 |
深度协议分析 |
| scapy |
纯 Python,可直接构造/发送数据包 |
数据包操作、网络编程 |
1 2 3 4 5 6 7 8 9 10 11 12 13
| import pyshark
def read_pcap(pcap_file): """读取 PCAP 文件""" cap = pyshark.FileCapture( pcap_file, keep_packets=False, ) print(f"总数据包数: {len(list(cap))}")
cap = pyshark.FileCapture(pcap_file) return cap
|
第二步:过滤 QQ 语音流量
QQ 语音使用 UDP 协议,数据载荷中包含固定的十六进制特征码 020048:
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
| def is_qq_voice_packet(packet): """判断是否是 QQ 语音数据包""" try: if not hasattr(packet, 'udp'): return False
if hasattr(packet.udp, 'payload'): payload_hex = packet.udp.payload.replace(':', '') else: return False
if '020048' in payload_hex[:100]: return True
src_port = int(packet.udp.srcport) dst_port = int(packet.udp.dstport)
if 4000 <= src_port <= 5000 or 4000 <= dst_port <= 5000: if '020048' in payload_hex: return True
except Exception: pass
return False
|
为什么用 020048:这是 QQ 语音协议数据包头的固定魔数。在 Wireshark 中过滤 udp contains 02:00:48 可以直接定位到语音数据包。
第三步:提取 IP 并去重
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| from collections import defaultdict
def extract_ips(cap): """从 PCAP 中提取 QQ 语音通信的 IP 地址""" ip_flows = defaultdict(int)
for packet in cap: if not is_qq_voice_packet(packet): continue
try: src_ip = packet.ip.src dst_ip = packet.ip.dst
ip_flows[src_ip] += 1 ip_flows[dst_ip] += 1 except AttributeError: continue
sorted_ips = sorted(ip_flows.items(), key=lambda x: x[1], reverse=True)
return sorted_ips
|
第四步:IP 地理位置定位
聚合多个 IP 地理位置 API 的结果,提高准确度:
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 44 45 46 47 48 49 50 51 52 53 54 55
| import requests
def locate_ip(ip): """多 API 聚合定位 IP""" results = []
try: resp = requests.get(f'http://ip-api.com/json/{ip}?lang=zh-CN', timeout=5) data = resp.json() if data['status'] == 'success': results.append({ 'source': 'ip-api', 'country': data['country'], 'region': data['regionName'], 'city': data['city'], 'isp': data['isp'], 'lat': data['lat'], 'lon': data['lon'], }) except: pass
try: api_key = 'your_api_key' resp = requests.get( f'https://api.ipgeolocation.io/ipgeo?apiKey={api_key}&ip={ip}', timeout=5 ) data = resp.json() results.append({ 'source': 'ipgeolocation', 'country': data.get('country_name', ''), 'city': data.get('city', ''), 'isp': data.get('isp', ''), 'lat': data.get('latitude', 0), 'lon': data.get('longitude', 0), }) except: pass
if not results: return {'error': '所有 API 查询失败'}
merged = results[0] if len(results) > 1: if merged['city'] != results[1]['city']: merged['city'] = f"{merged['city']}({results[1]['city']})"
return merged
|
第五步:生成分析报告
JSON 详细报告
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
| import json from datetime import datetime
def generate_json_report(ip_flows, target_ip=None): """生成 JSON 格式详细报告""" report = { 'analysis_time': datetime.now().isoformat(), 'target_ip': target_ip, 'total_flows': len(ip_flows), 'findings': [], }
for ip, packet_count in ip_flows: if ip.startswith(('127.', '10.', '192.168.', '172.16.')): continue
location = locate_ip(ip)
report['findings'].append({ 'ip': ip, 'packet_count': packet_count, 'location': location, 'is_peer': packet_count > 100, })
return report
|
Markdown 可读摘要
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| def generate_markdown_summary(report): """生成 Markdown 可读摘要""" md = f"""# QQ 语音流量分析报告
**分析时间**: {report['analysis_time']} **通信流数量**: {report['total_flows']}
## 通信对端
| IP 地址 | 数据包数 | 国家 | 城市 | ISP | |---------|---------|------|------|-----| """ for finding in report['findings']: loc = finding['location'] md += f"| {finding['ip']} | {finding['packet_count']} " md += f"| {loc.get('country', 'N/A')} " md += f"| {loc.get('city', 'N/A')} " md += f"| {loc.get('isp', 'N/A')} |\n"
return md
|
第六步:地图可视化
用 folium 在地图上标记通信对端的位置:
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
| import folium
def visualize_on_map(report, output='qq_voice_analysis.html'): """在地图上可视化通信对端""" m = folium.Map(location=[35, 105], zoom_start=4)
for finding in report['findings']: loc = finding['location'] lat = loc.get('lat', 0) lon = loc.get('lon', 0)
if lat and lon: popup_text = f""" <b>IP:</b> {finding['ip']}<br> <b>数据包:</b> {finding['packet_count']}<br> <b>位置:</b> {loc.get('city')}, {loc.get('region')}<br> <b>ISP:</b> {loc.get('isp')} """
color = 'red' if finding['is_peer'] else 'blue' folium.Marker( [lat, lon], popup=folium.Popup(popup_text, max_width=300), icon=folium.Icon(color=color, icon='info-sign'), ).add_to(m)
m.save(output) print(f"地图已保存到 {output}")
|
完整分析脚本
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
| def analyze_qq_voice_pcap(pcap_file): """QQ 语音流量完整分析""" print(f"[*] 读取 PCAP: {pcap_file}")
cap = pyshark.FileCapture(pcap_file) print(f"[+] 总数据包: {len(list(cap))}")
cap = pyshark.FileCapture(pcap_file) ip_flows = extract_ips(cap) print(f"[+] 通信 IP 数量: {len(ip_flows)}")
report = generate_json_report(ip_flows) with open('qq_voice_report.json', 'w', encoding='utf-8') as f: json.dump(report, f, ensure_ascii=False, indent=2)
md = generate_markdown_summary(report) with open('qq_voice_summary.md', 'w', encoding='utf-8') as f: f.write(md)
visualize_on_map(report)
print("[+] 分析完成!") print(f" 报告: qq_voice_report.json") print(f" 摘要: qq_voice_summary.md") print(f" 地图: qq_voice_analysis.html")
|
实战经验
1. 大数据包文件处理
500MB+ 的 PCAP 文件不要一次性加载到内存:
1 2 3 4 5
| cap = pyshark.FileCapture('large.pcap', keep_packets=False) for packet in cap: pass
|
2. tshark 命令行预处理
对于超大文件,先用 tshark 命令行过滤再拿 Python 分析:
1 2
| tshark -r huge.pcap -Y "udp.port >= 4000 and udp.port <= 5000" -w filtered.pcap
|
3. 特征码匹配精度
020048 可能不全出现在数据载荷头部。建议在载荷的前 200 字节内搜索,而非仅开头几个字节。
总结
PCAP 流量分析的核心能力就三步:过滤 → 提取 → 关联。pyshark 帮你过滤和提取,地理位置 API 帮你关联上下文。这套流程不仅适用于 QQ 语音,任何 P2P 通信协议都可以走同样的分析路径。