☰
网络协议分析逆向实战:抓包、指纹提取与加密流量解密
2026/10/17 15:17:04 网站建设 项目流程

简介:网络协议分析与逆向工程是安全研究中的核心方向,微信等即时通信协议更是移动安全领域的热点。该资源围绕这一主题,整合了法国学者Georges Bossert与Frédéric Guihéry开发的协议分析逆向相关成果,并附有香港中文大学关于微信协议分析的PDF论文,面向协议分析、逆向工程、移动应用安全方向的工程师与研究人员,可作为入门梳理或进阶参考。压缩包以zip格式发布,体积约3.11MB,原始页面未列出文件总数与类型明细,内容以论文和技术资料为主。目前已有1387人学习,适合希望系统了解网络协议逆向方法及微信私有协议研究脉络的读者。通过学习该资料,可快速获取国外学者在协议逆向上的工具思路,并结合高校论文把握微信协议分析的常见切入点与研究方法,节省自行检索与筛选文献的时间。

1. 网络协议分析逆向:先看四条流,再拆一个包的脑回路

有一次某开发者抓了一份设备端流量包,Wireshark 里全是 TCP segment of a reassembled PDU,用中间层工具挂在旁边也什么都看不见,最后只能回到原始的 tcpdump 捕获。真正卡住他的不是包抓得少,而是里面几乎没有一个 HTTP 明文请求,全是自研二进制头部加压缩载荷。网络协议分析逆向这件事,很多人的第一反应是打开工具直接看协议,但工具的解析能力只覆盖已知协议,凡是走私有端口、私有魔数、私有序列号约定的流量,都得靠人肉逆向或者写脚本识别。这份资源正好把这些从头理了一遍:从抓包环境选择、流识别、指纹提取,到加密通道处理和主动重放验证,适合正在做设备联调、流量测试、安全评估的从业者,也适合想搞清楚 TCP 之上到底跑了什么的初学者。

2. 抓包环境搭建与工具选型:链路层到应用层的完整落地

2.1 捕获点放在哪:决定你能看见哪一半流量

在真实设备、模拟器、嵌入式板卡上抓包时,捕获点选错是最常见的问题。我一般优先把捕获点放在“能看到整条链路”的位置,而不是离协议栈最近的位置。举个典型场景:某开发者用一个 IM 客户端连云端服务器,他在设备上用 tcpdump 抓到从本地网卡出去的方向,但返回方向的 TSval 偏移很跳跃,因为出口过了 NAT 又经过了负载均衡,回程落在另一条链路上。再看服务端,反向数据又被网卡卸载特性合并,看起来像是丢了包。

解决方案有几个常见做法:

  • 测试环境尽量用双网卡转发:一张管理口,一张数据口,数据口做镜像,把两侧流量都导到同一份 PCAP。
  • 本地开发机用虚拟网卡抓本机回环流量,避免物理网卡卸载引发的大包合并问题。
  • 嵌入式目标板如果没有额外网口,可以做链路层复制,把板子出口同时接到分析机。

在这个环节里,最容易忽略的是抓包长度。libpcap 默认抓包长度在部分发行版里仍保留一个较小值,只抓到 IP 头,后面全部截掉。实际排错建议用-s 0代表完整包,或者至少-s 65535,否则尾部字段永远分析不出来。

提示:-s 0在不同版本 libpcap 里都表示“完整包”,不是“不抓包”。如果用了老教程里的-s 65535,遇到巨型帧或带多标签的报文也可能被截断,优先用 0。

2.2 工具分工:命令行远端抓,图形界面本地看

我最常的组合是命令行 tcpdump 在远端跑,Wireshark 或 tshark 在本地做后处理。裸 tcpdump 的好处是零依赖、可后台、可输出到文件;坏处是读起来累。抓包场景下还可以用各类中间层调试工具,但后端处理的仍然是 PCAP 文件。

先看远端抓包命令:

tcpdump -i eth0 -s 0 -w /data/capture_$(date +%Y%m%d_%H%M%S).pcap \ 'tcp or udp or icmp' -Z root -C 256 -W 32

参数逻辑说明:-i eth0指定接口,多网卡时先跑tcpdump -D查看接口列表,找不到接口一般是权限或接口名不对。-s 0把 snaplen 设为完整包,避免只抓到 IP 头。-w写文件,-C 256表示单文件到了 256MB 就轮转,-W 32表示最多保留 32 个文件。过滤表达式写在引号里,建议先按协议族过滤而不是按 IP 过滤,因为测试中 IP 可能动态变化。-Z root是先以 root 打开原始套接字,再降权运行,避免整个抓包进程跑在 root 下。

抓完以后不要直接在远端打开,拉回本地。在 Wireshark 打开后,先不看包列表,而是看 Statistics -> Conversations,数一下一共有几条流。这一步能快速判断流量是“长连接高频小包”还是“短连接突发大包”,后续指纹设计的方向完全不同。

2.3 把 PCAP 变成可计算的表格:tshark 导出字段

到这一步,很多人还在翻 GUI。我一般会用 tshark 把关键字段导成 CSV,交给脚本处理:

tshark -r target.pcap -Y 'tcp.port == 443 || tcp.port == 8000' \ -T fields \ -e frame.time_epoch \ -e ip.src -e ip.dst \ -e tcp.srcport -e tcp.dstport \ -e tcp.seq_raw -e tcp.ack_raw \ -e tcp.flags.str \ -e tcp.payload \ -E header=y -E separator=, > tcp_meta.csv

参数说明:-Y是显示过滤,在读取时生效;-T fields表示按字段输出。frame.time_epoch输出浮点秒,排序更准;不要用frame.time这种格式化字符串,跨天排序会乱。tcp.seq_raw输出的是原始序号,不是 Wireshark 默认的相对序号,后续做重传判断时要用原始值。tcp.payload是十六进制载荷,这也是后面协议指纹的原料。

CSV 到手后用 pandas 按五元组聚合流,统计包间隔、载荷长度分布。这一套组合拳基本可以把一个陌生协议从一级流量中推出来。需要提醒的是,tshark 导出的 raw 序号在不同版本里表现有差异,导出前要确认 Wireshark 偏好里 “Relative sequence numbers” 处于关闭状态,这个坑后面具体展开。

3. 协议识别与指纹提取:从十六进制载荷反推协议骨架

3.1 指纹分层的逻辑:端口只是起点,不是依据

端口号并不能说明协议。只有在没有混淆的前提下,知名端口可以作为提示。真正可操作的协议指纹分三层:

  • 静态指纹:载荷里的固定字节,比如固定的魔数、固定头部长度、固定标志位。
  • 时序指纹:包到达间隔、突发模式、握手包顺序。
  • 结构指纹:长度与前一个响应的关联、序列号增量规律、方向交替模式。

以某 IM 内置游戏通道为例,它的流量里没有 HTTP 头,但游戏结果包固定以 0x10 开头,第二个字节是版本号,第三四字节是包体长度;这类结构不是加密的,只是没被 Wireshark 解析。分析时把这类固定位置抓出来,就是最简单的静态指纹。

静态指纹最容易翻车的地方是只取了一个包的特征,没有确认特征在整条流里的稳定性。常见做法是取同一条流前 20 个包、不同流各取前 20 个包,统计特征字节的唯一性,再决定这个指纹能不能用。

3.2 用 Python 计算一个流的特征

这一步把 PCAP 变成可统计的特征集,用 dpkt 比较轻量:

import dpkt def flow_features(fname, target_port): with open(fname, 'rb') as f: pcap = dpkt.pcap.Reader(f) flows = {} for ts, buf in pcap: eth = dpkt.ethernet.Ethernet(buf) if eth.type != dpkt.ethernet.ETH_TYPE_IP: continue ip = eth.ip if ip.p == dpkt.ip.IP_PROTO_TCP: tcp = ip.tcp if target_port and tcp.dport != target_port and tcp.sport != target_port: continue key = (ip.src, ip.dst, tcp.sport, tcp.dport) flows.setdefault(key, []).append((ts, len(tcp.data), int(tcp.seq))) for k, v in flows.items(): lens = [x[1] for x in v] print(k, 'packets=', len(v), 'avg_len=', sum(lens) / len(lens), 'max_len=', max(lens))

逻辑说明:用 dpkt 遍历 PCAP,过滤目标端口,按五元组聚成一条流,记录每个包的时间戳、载荷长度和 TCP 序号。统计平均载荷与最大载荷,能快速区分“交互式短包”和“批量传输大包”。如果 max_len 稳定在 1460 附近,多半是应用层做了分块上传或者 MTU 限制;如果 max_len 忽大忽小,多半应用层自己做了压缩,TCP 层没有切包。

参数说明:target_port如果是 0 则不过滤;dpkt 读的是链路层帧,普通以太网帧走dpkt.ethernet.Ethernet,如果是 Linux cooked capture(SLL),要改用dpkt.sll.SLL解析,否则第二层长度解析错位。

3.3 私有二进制头部解析:一个最小模板

在识别出固定特征后,写一个最小解析器验证假设。这段代码用于从载荷里抽出头部并打印关键字段:

import struct MIN_HEADER_LEN = 6 def parse_pkt(data: bytes): if len(data) < MIN_HEADER_LEN: return None magic, ver, flags, length = struct.unpack('>HBBH', data[:6]) if magic != 0x4C00: return None body = data[6:6 + length] return { 'magic': hex(magic), 'ver': ver, 'flags': flags, 'length': length, 'body_hex': body[:16].hex(), 'total_len_expect': 6 + length, 'total_len_actual': len(data), }

参数说明:>HBBH表示大端两个字节魔数、一个字节版本、一个字节标志、两个字节长度。body只截 16 字节用来打印,防止把大包整体打进日志。先用这个模板跑历史 PCAP,验证所有样本的length都等于len(data)-6,基本可以确定头部长度就是 6。

这个模板最大的价值是把“十六进制黑匣子”变成“有字段名的结构体”。如果跑出来有样本长度不匹配,先不要改头部长度,优先怀疑载荷里面有压缩层,或者前面还有一层隧道封装。此时可以按总长度反推:如果length字段比实际载荷短一截,说明外层还包了一层头部;如果比实际载荷长,说明有填充或加密块对齐。

4. 加密通道的处理路径:定位密钥交换与恢复明文会话

4.1 先区分“加密流量”和“抗分析流量”

判断一个流是否加密,用熵和 TLS 记录类型就够了。TLS 握手前几个包里有清晰的 Record Type 22、Version 0x0303、随机数、证书;自研加密常见在载荷长度不固定而且首字节熵高。QUIC 则是固定首字节 0xC0 附近。我常用 tshark 快速分类:

tshark -r target.pcap -Y 'tcp.len > 0 && tcp.payload' \ -T fields -e tcp.payload | head -5

如果载荷以 0x16 0x03 0x03 开头,走 TLS 流程;如果是高熵乱码且没有 0x16 模式,就按自定义加密处理,重点找密钥生成入口而不是找协议解析器。这一步节省的时间最多,因为很多人会把所有加密流量都当成 TLS,然后花半天找 ClientHello。

区分出类型之后,下一步要明确一个边界:TLS 可以用标准调试接口还原明文,但自研加密没有统一口,只能靠运行时注入或内存读取。测试环境里优先走前者,不要一上来就搞 hook,代价完全不同。

4.2 会话密钥获取与导出:调试接口是正规途径

对 TLS 类会话,最优先是用标准库自带的调试接口导出会话密钥,也就是 SSLKEYLOGFILE。很多调试者以为这是黑科技,它其实是库自带特性。把环境变量指到可写路径,应用启动后标准库会在握手完成时写入一行包含 CLIENT_RANDOM 和 master secret 的记录。

变量作用写入时机
SSLKEYLOGFILE让 OpenSSL/NSS 等导出密钥每次会话密钥协商完成
输出内容CLIENT_RANDOM一行一个会话,用空格分隔

拿到该文件后,Wireshark 里设置 Protocol -> TLS -> RSA keys 选择该文件。命令行对应方式是用 tshark 的-o参数开启 tls.keylog_file:

tshark -r target.pcap \ -o tls.keylog_file:/path/keys.log \ -Y 'http || tls' \ -T fields -e http.request.uri -e http.response.code

参数说明:-o用来覆盖偏好设置;tls.keylog_file指定密钥文件路径。如果抓到的包有多个 TLS 会话,密钥文件里必须包含所有会话的 CLIENT_RANDOM,否则只有部分流能解密。实际测试中常见的问题是应用使用了 TLS 1.3,密钥导出格式里还带 app data 段,老版本 Wireshark 可能不支持,升级到较新版本再用。

4.3 解密后的二次分析:从 HTTP 到私有帧

解密后的流量仍然可能是二进制协议。把解密输出再灌进 tshark,用-d参数指定端口解协议,再把 data 字段导出来,按上一章的模板再解析一次。这样等于先卸掉第一层加密,第二层结构分析可以直接复用。典型命令:

tshark -r decrypted.pcapng -d tcp.port==4100,data \ -T fields -e data.data > body_hex.txt

注意:解密导出后的帧时间戳和会话方向应该保持不变。如果发现解密后的层里出现很多请求但看不到 body,优先检查分片或压缩层。常见的是 gzip 或 zlib,此时把 body_hex 还原成字节,尝试 zlib 或 gzip 解压,解压出来往往才是真正的可读协议。这里有个小习惯:解压前先看 body 是否以 78 9C 开头,那是 zlib 默认头,一看到基本可以确定是 zlib 压缩层。

5. 协议逆向避坑记录:五个翻车点与排查思路

5.1 抓包时只有握手没有数据,回程全是 RST

现象:目标板上一跑 tcpdump,握手包能看到,数据包一进来就断,对端回 RST,越抓越像是被防火墙拦了。

原因:在中间插入抓包点时,目标板网卡的 offload(TSO/GRO)把大包合成了再交给驱动,抓包工具只看到合成后的分段,代理端无法重组就回 RST。这种现场很迷惑,因为流量确实到了。

解决:关闭网卡 offload,Linux 上用ethtool -K eth0 gro off gso off tso off,抓包侧把 snaplen 拉到最大。配合-s 0重抓一轮,基本能解决。另外注意 vmware 或容器虚拟网卡的 offload 行为不同,需要在对应 vnic 上关。

5.2 跨机器抓包时间戳对不齐

现象:两端各抓一份 PCAP,回放时发现同一个请求在两侧的时间差忽大忽小,有的甚至出现回程先于请求的情况,分析会话时完全对不上。

原因:两台机器没有同步时间,NTP 或手动设时间的毫秒级漂移在高速流下会把事件顺序打乱。

解决:用 PTP 或同一台机器上做端口镜像,让两端流量进到同一个时间源。导出数据时使用frame.time_epoch而不是frame.time,排序后还会发现很多所谓“乱序”其实是抓包点看到的链路往返顺序不同。

5.3 Wireshark 显示相对序列号导致比对错误

现象:脚本里读tcp.seq做重传判断,明明 TSval 显示是乱序,但 Wireshark 里看到的序号又是正常的,脚本和 GUI 对不上。

原因:Wireshark 默认开启 “Relative sequence numbers”,显示的是相对于初始序号的偏移;tshark 导出字段时如果不显式用tcp.seq_raw,取到的还是相对值。

解决:进 Preferences -> Protocols -> TCP,关闭 “Relative sequence numbers”。导出字段统一用tcp.seq_raw和tcp.ack_raw,转成绝对序号后再做重传和乱序判断。这属于典型的 GUI 默认设置给脚本挖坑。

5.4 载荷特征匹配时总是漏掉首包

现象:写正则匹配载荷特征,统计出的流数量比实际少,仔细看发现同一条流里第一个带数据的包没有被匹配到。

原因:过滤表达式里写的是tcp.payload,但同步包和纯 ACK 包没有载荷,Wireshark 的字段在空包上是缺失的;如果只抓了三次握手后的数据,再加上首包载荷被截断,特征就丢了。

解决:过滤条件加tcp.len > 0 && tcp.payload,同时确认抓包时 snaplen 没有被截断到 64。导出时建议额外导出tcp.len字段,用于判断空包和带载荷包的分布。

5.5 加密协议里 hook 了 send 函数,但拿不到明文

现象:想通过 hook send 拿到应用发给服务器的明文,结果打印出来的全是密文,长度和抓包看到的一致,完全看不到应用层结构。

原因:应用走的不是 socket send,而是标准库的 SSL_write 或加密库内部接口,send 的位置在协议栈更底层,hook 点错层级。

解决:首选官方调试接口导出密钥而不是 hook。必须 hook 时,挂到 SSL_write、tls1_enc 这类库内部接口,在调用栈里向上回溯找明文缓冲区。这类 hook 的稳定性依赖库版本,一换版本就会翻车,所以能走 SSLKEYLOGFILE 就不要碰 hook。

6. 从被动观察到主动验证:重放脚本让协议边界自己说话

6.1 重放脚本的结构:单流重放而非大包重放

被动分析做到最后,你已经有了头部结构、字段定义和加密方式,但还缺一个“确定性证明”:把同样的字节再发给对端,看对端反应是否和原始流量一致。重放时我强烈建议只选一条干净的单流,而不是把整个 PCAP 丢回去。

from scapy.all import rdpcap, IP, TCP, sendp, conf conf.L3socket = conf.L3socket pkts = rdpcap('selected.pcap') replay = [] for p in pkts: if TCP in p and IP in p: q = p.copy() if q[IP].dst == '10.0.0.2': q[IP].src = '10.0.0.9' q[TCP].seq += 100000 replay.append(q) sendp(replay, iface='eth0', verbose=True)

逻辑说明:先读选中的单流 PCAP,把所有发往目标地址的包源地址改成当前测试机,并对序列号做偏移,避免和原会话的历史状态冲突。sendp在二层发送,需要指定接口,且不改写 TCP 校验和会让内核校验失败,Scapy 发送时会自动重算校验和,这个不用担心。

参数说明:seq += 100000是偏移量,具体的值只要避开原会话窗口即可;iface必须是二层可达目标的接口,loopback 上不能用 sendp 做这种重放,需要改成 L3 发送或配合桥接。重放前先确认包里的时间戳选项(TCP Timestamp)未被目标校验,否则内核会因为时间戳倒退直接丢弃。

6.2 收尾验证:看对端反应判断字段猜得对不对

重放后看对端行为:如果对端返回相同错误码,说明解析出来的头部字段基本正确;如果直接断连,多半是序列号冲突或者时间戳选项校验;如果返回数据但长度不对,说明长度字段的偏移或字节序猜错了。这比盯着十六进制猜一百遍都有效率。

从那以后我每次拿到新协议都先走一遍这套流程:抓包确认捕获点、算流特征、写最小解析器、判断加密类型、决定解密路径,最后强制做一次单流重放。哪怕时间紧,重放这一步也不省,因为被动分析只能告诉你“像什么”,重放才能告诉你“是什么”。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询