
简介围绕网络协议分析与逆向工程兼及微信协议分析这份资料面向网络安全学习者、协议逆向爱好者与即时通讯协议研究者可用于理解协议结构、抓包分析思路和学术研究脉络。内容涉及法国学者Georges Bossert和Frédéric Guihéry开发的相关成果并附有香港中文大学微信协议分析论文PDF适合作为中高级学习者的参考资料帮助对照工具方法与论文框架建立从协议观察到逆向分析的认知路径。资源以zip压缩包形式提供约3.11MB文件总数与主要类型明细上游暂未提供显示为0、暂无数据具体构成需以实际包内文件为准。已有1388人学习说明其在协议分析与微信协议研究方向具有一定参考热度。读者可从中获得论文研读线索、协议分析视角与逆向研究思路便于开展实验复现和专题阅读。1. 从一次抓包翻车说起协议逆向到底在逆什么第一次认真做网络协议分析我抱着 300MB 的抓包文件蹲了两天最后只得出一个结论端口 443、全是加密记录、能看的只有包长和到达时序。那次翻车让我明白一件事——抓包不等于协议分析逆向一个协议真正的活是把「字节流 → 帧边界 → 字段 → 业务语义 → 状态机」这五层一层层拆开。这篇写的就是这套拆法前半段讲工具链和抓包点怎么选中段讲没有文档时怎么认出封装格式和长度域后半段讲加密层怎么判断、签名怎么定位最后把我踩过的五个坑按「现象 → 原因 → 解决」摆出来。做安全测试的能照着复现做客户端开发的能看懂自家协议为什么这么设计想搞明白社交 App 内嵌小游戏那套长连接通信的人也能拿到一条可走的路径。2. 开工前的四件套抓包点、工具链、样本和边界协议分析翻车八成不是分析能力不够是开局就没准备好。我在模拟项目上反复试过之后固定成四个前置动作选抓包点、搭工具链、囤同类样本、划清边界。这四件事做完后面的活基本是体力题少做一件就会在某个下午对着一堆乱码怀疑人生。2.1 工具链怎么配按「要拿到什么」倒推新手常见的错误是先装一堆工具再想拿它们干什么。我一般反过来先问这轮要拿到什么再决定用哪个工具。下面这张表是我这几年稳定在用的组合按能力而不是按品牌记换工具的时候对着能力找就行。环节需要的能力我一般怎么选什么时候够用被动抓包网卡层全量抓、导出标准抓包文件、按流重组图形化抓包分析器流量没加密或能拿到会话密钥命令行抓包无图形环境、长时间跑、按过滤表达式截流命令行抓包器配过滤规则服务端侧排查、想脚本化复现中间人代理应用层明文可见、能断点改包自建代理 自签根证书只在客户端接受自定义证书的 Demo 上动态调试 / 插桩看函数入参、密钥、明文缓冲调试器或插桩框架只在自家 App 或公开靶场包上结构描述解析把二进制按字段号展开序列化描述文件解析器疑似 Protobuf / Thrift 结构服务端桩可控回包验证字段语义自己写的 mock 服务端判断哪个字段是可变的、哪个是校验用的中间人代理这一项要特别提醒代理会改变 TCP 分段方式和时序抓到的包长分布不能当样本用来推长度域。我早期拿代理抓的包去做长度统计结论全错因为代理把大包拆成了固定上限的小块。2.2 抓包点选在哪儿四个位置各有代价我把可选的抓包点归成四类越往下越省事但看到的越不真实。第一个是链路层镜像口交换机或网关侧旁路一份流量。最真实能拿到完整的握手过程和真实的分段行为缺点是看不到本机回环也看不到进程内部的数据加密流量基本只能看元数据。第二个是本机回环。客户端和服务端桩都跑在 127.0.0.1一个 listen 一个 connect抓包器直接盯回环网卡。这是我最常用的入口因为样本可控、可以随便改、可以无限重放而且不会碰到任何第三方系统。第三个是中间人代理层。能改包、能断点、能看到明文代价是时序被改变、分段被重写。用它验证语义很爽用它推协议结构很危险。第四个是应用层埋点或日志。客户端在收发包的地方打日志最省事但看到的字段是程序愿意给你看的判断不了封装本身——你不知道日志里那条「消息」在链路上到底是一个帧还是三个帧。我一般先用第二个拿到结构再用第一个验证结构在真实网络下是否成立第四个只用来做交叉核对。这个顺序能省掉大量返工。2.3 样本怎么囤宁要同类不要海量一条连接上抓一万条混杂报文不如同一类业务报文抓两百条。推长度域、推字段宽度靠的是同类样本的一致性样本类型一乱统计就没意义。我的做法是按「请求-响应」成对存同一条长连接、同一类操作、不同参数值存 200 到 500 对命名里带上参数特征比如登录_正常、登录_密码错、拉列表_第2页。后面推字段语义时靠的就是这几组样本之间的差异。2.4 边界我给自己定的三条红线第一只在自己搭的 Demo、自家的客户端、公开靶场上动手。别人线上的服务看结构可以改包重放不行这既是合规问题也没有技术价值——你验证的不是协议是对面的风控。第二样本里出现的令牌、账号、设备号、位置全部脱敏之后再存进分析目录抓包文件不随手传。第三需要「客户端本地派生的密钥」才能继续的时候先确认这个派生是不是公开可复现的如果密钥来自服务端协商那就在结构层停手把「能解到哪一层」写清楚。这条线画明白写出来的分析报告才站得住。3. 从字节流到消息体协议识别的四个判断依据拿到一堆原始字节之后最先要回答的不是「这是什么业务」而是「边界在哪」。TCP 给你的是一条没有边界的字节流所谓「一个包」是人造概念。识别协议本质是找到四种定界方式里的哪一种长度域、分隔符、定长体、自描述结构。找对了后面全是流水线作业。3.1 三看一比先把范围收缩到一个量级我的习惯是先做四个廉价观察把可能性砍掉一大半。看端口。固定高位端口比如 9000 以上一直不变通常意味着自研 TCP 长连接443 大概率是 TLS80 / 8080 之外还跑着一套自定义二进制说明业务侧自己造了轮子。看前几个字节。第一字节是 0x16、紧跟 0x03 0x01 或 0x03 0x03那是 TLS 记录层第一字节是 0x47G或 0x7B{基本是文本协议HTTP 或裸 JSON第一字节是 0xCA 0xFE 这类固定魔数就是私有二进制封装。看长度规律。把同类报文按长度排个序如果长度集中在几个固定值上可能是定长结构如果长度分布连续多半带长度域或者分离器。比请求和响应。请求体里某个位置的数值和响应体里某个位置的数值完全相等那大概率是序列号或事务号顺着它能找到帧结构。下面这张表是我实际判断时用的对照表左边是现象右边是初步结论最后一列是下一步该干什么。观察到的现象更可能的封装下一步首字节 0x16次字节 0x03TLS 记录层标准 TLS找握手包里的 SNI / ALPN 判断上层协议前 4 字节固定不变私有协议魔数以魔数为锚点往后找长度域第 5~8 字节数值约等于剩余长度定长头 变长体用扫描脚本在全量样本上验证命中率相邻报文在同一偏移反复出现小整数TLV 的 Tag 字段确认 Tag 宽度是 1 字节还是变长数据中反复出现 0x08 0x01 0x12 这类小字节序列化结构字段号 类型用结构描述解析器按字段号展开每帧末尾固定出现 0x0D 0x0A 或 0x00分隔符协议检查正文里是否有转义机制3.2 长度域扫描把「猜」变成「统计」很多人靠肉眼看 hexdump 找长度域看一眼觉得像就定了结果切帧切到第三十条就崩。我一般写个二十行的脚本在几百条同类样本上跑一遍让命中率说话。def scan_length_fields(samples, sizes(2, 4), max_off16, threshold0.8): 在同类报文样本里扫描长度域候选。 samples: list[bytes]来自同一条连接、同一类业务的原始帧 sizes: 候选字段宽度2 字节和 4 字节最常见 max_off: 只扫前 16 字节固定头一般不会更长 threshold: 命中率门槛低于它就当噪声丢掉 返回: {(偏移, 宽度, 语义): 命中率} hits {} for buf in samples: n len(buf) for off in range(min(max_off, n)): for size in sizes: if off size n: continue val int.from_bytes(buf[off:off size], big) # 三种常见语义剩余字节数、整帧长度、去掉固定头后的长度 candidates { rest: n - (off size), total: n, body: n - 8, } for mode, expect in candidates.items(): if val expect: key (off, size, mode) hits[key] hits.get(key, 0) 1 total len(samples) return {k: v / total for k, v in hits.items() if v / total threshold}跑完之后正常情况下只会剩一到两个候选其余全是偶然命中。参数怎么调sizes里加上 1 和 8 可以覆盖极端情况但 1 字节最多表示 255长连接里很少用来表示长度8 字节在移动端协议里也不常见。max_off从 16 调到 32 往往能救回一类「头部特别长」的协议代价是噪声变多得把阈值提到 0.9。失败的时候看什么如果一个候选都没有先确认样本是不是同类不同类型混在一起命中率会被稀释到 0再看是不是变长整数编码那种长度域本身占 1 到 5 个字节高位有 continuation 标志定宽扫描是扫不出来的最后怀疑分隔符去尾部找固定的 0x00 或 0x0D 0x0A。3.3 四种封装形态边界怎么定常错在哪封装形态典型特征边界怎么定常见误判定长头 变长体头部 N 字节固定内含长度域读头部长度域再取对应字节把魔数误当长度第一帧就对TLVTag Len Value 循环循环读到帧尾每轮按 Len 推进Tag 实际是变长按 1 字节读就错位分隔符尾部固定 \r\n 或 \x00找分隔符位置正文里含分隔符必须看有没有转义序列化结构字段号 类型 值varint 编码按字段号推进读到结束标记把 varint 当定长整数字段全乱TLV 那一行值得多说一句。Tag 是变长的场景在移动端很常见Tag 高 4 位表示类型、低 4 位表示长度或者 Tag 本身用 varint。判断方法很简单——如果按 1 字节 Tag 读下去第三、四个字段开始出现明显不合理的大长度值那 Tag 就不是定宽的。3.4 从字段到语义三步对照法结构拆出来只是第一步字段是干什么的还得验证。我固定走三步。第一步单字段扰动。在自己搭的服务端桩上只改一个字段的值看回包哪个字段跟着变。改 A 变 BA 和 B 就有语义关联改 A 什么都 不变A 要么是填充要么是对面不关心的字段。第二步找单调量。把某一类操作的样本按时间排序看哪几个字段单调递增。递增步长稳定、数值量级在 10 位左右的基本是毫秒时间戳步长为 1 的是序列号随机跳变且长度固定的是随机数或 nonce。时间戳识别有个小技巧把候选的 4 或 8 字节按大端和小端各解一次看解出来的数值是否落在抓包时刻前后一天之内。下面这段就是我当时用的判断函数两行就能把范围缩到一两个字段。import datetime def guess_timestamp(buf, offsets(0, 4, 8, 12), widths(4, 8)): 在帧头部若干偏移上尝试解时间戳返回落在抓包时刻附近的结果。 buf: 一条原始帧抓包时间用 tcpdump 的文件时间做锚点更准 anchor datetime.datetime.now().timestamp() found [] for off in offsets: for w in widths: chunk buf[off:off w] if len(chunk) w: continue for endian in (big, little): val int.from_bytes(chunk, endian) for scale, name in ((1, 秒), (1000, 毫秒), (1000000, 微秒)): ts val / scale if abs(ts - anchor) 86400: # 前后一天内 found.append((off, w, endian, name, ts)) return found第三步极值试探。对可疑字段塞 0、塞 -1、塞超长字符串看哪一个会触发服务端拒绝。被拒绝的字段通常是参与校验或参与业务约束的把它们记下来这是下一章定位签名的入口。4. 加密层怎么剥先判断是不是标准 TLS再谈自定义加密「加密了怎么办」这个问题九成情况下第一步就搞错了——很多人根本没确认对面用的是不是标准 TLS直接上自定义分析的思路白忙一周。我的顺序永远是先分类再定位解密点最后才是签名和校验。分类错了后面所有努力都是在错误的前提上叠加。4.1 三十秒分类标准 TLS 还是私有封装在抓包器里看三个地方就够了。第一记录层头。标准 TLS 的每条记录是 5 字节头类型0x16 握手、0x17 应用数据、0x15 告警、0x14 变更密码规格 2 字节版本 2 字节长度。如果每条记录都是这个结构那就是标准 TLS别自己造轮子。第二握手包里的明文。TLS 1.2 及以下的 ClientHello 里SNI服务名和 ALPN应用层协议协商都是明文一眼能看到对面想连哪个服务、上层跑的是 HTTP/2 还是别的。TLS 1.3 除了启用加密扩展的情况SNI 通常也仍是明文。第三上层是不是 HTTP。TLS 之上如果能看到规整的GET /或POST /那就是 HTTPS直接交给常规的 HTTP 分析流程不用当二进制协议处理。反过来如果端口是 443但首字节是自定义魔数、没有 5 字节记录头、也没有握手包那说明这是个「跑在 443 端口上的私有协议」——端口只是伪装不代表协议。这种情况才需要走自定义分析。现象判断下一步首字节 0x16后跟 0x03 0x01/0x03标准 TLS 握手看 ClientHello 里的 SNI / ALPN端口 443 但首字节是自定义魔数私有协议占用 443按第 3 章流程找长度域记录头规整载荷是可读 HTTPTLS 上跑 HTTP/1.1走常规 HTTP 分析全是 0x17 应用数据长度规整需要会话密钥才能继续判断能否拿到密钥拿不到就只做元数据分析4.2 自定义加密的三个可测特征确认是私有封装之后下一步不是急着找密钥而是判断它到底是不是真加密。有些协议只是把字段做了异或或者简单混淆被当成「加密」供着浪费很多时间。我一般测三个特征。熵。一段 1KB 数据算香农熵接近 8 bit/byte 说明分布极均匀要么加密要么压缩文本协议一般落在 4~5 之间。压缩和加密的区别在于长度压缩后的长度通常显著小于明文加密后的长度和明文基本同量级。块对齐。把同类密文的长度列出来如果全都是 16 的倍数或 8 的倍数最后一帧例外那是分组加密加填充AES-CBC 的典型形态。如果密文长度和明文长度完全相等、且逐字节变化那是流式XOR 或者 CTR。密钥派生痕迹。同一条连接里前 16 字节在每一帧都不同说明有 IV 或 nonce 参与前 N 帧用同一套参数、第 N1 帧突然变化说明中间做了密钥协商。import math def shannon_entropy(data: bytes) - float: 按字节统计香农熵接近 8 说明接近均匀分布加密或压缩 if not data: return 0.0 counts [0] * 256 for b in data: counts[b] 1 n len(data) return -sum((c / n) * math.log2(c / n) for c in counts if c)参数上没什么可调的注意样本长度短于 256 字节的片段熵值抖动很大别拿单帧下结论取 10 帧以上取平均。4.3 流式加密的识别两段密文异或如果是流式且密钥或密钥流被复用有个很直接的验证法取两段密文做异或如果结果里出现大段连续的 0x00说明两段明文在对应位置相同密钥流被复用了。这个方法我只在自己搭的 Demo 上用来验证「协议设计有没有犯复用 nonce 的错」它也正好解释了为什么有些自研协议看起来加密了实际上防护强度很低。判断之后是定位解密点。这一步决定了工作量密钥由客户端本地派生比如设备标识加固定盐做摘要那就可以在本地复现密钥来自服务端协商那就只能停在结构层。前者能做完整复现后者老老实实写「结构已知、内容不可解」别硬编结论。4.4 复现在模拟项目里把一帧完整还原拿我搭的一个即时通讯 Demo 举例帧结构是魔数 4 字节 长度 4 字节大端表示其后字节数 IV 16 字节 密文加密方式是分组加密加 PKCS7 填充密钥由会话标识派生。这段代码的价值不在于能解谁而在于演示「帧结构 加密参数 校验」这三件事怎么串起来。import hashlib from struct import unpack MAGIC b\xca\xfe\x01\x00 def parse_frame(buf: bytes, session_id: bytes): 解析一帧magic(4) length(4, 大端, 其后字节数) iv(16) ciphertext if buf[:4] ! MAGIC: raise ValueError(magic 不匹配可能不是本协议的帧先回去做分类) (length,) unpack(I, buf[4:8]) body buf[8:8 length] if len(body) ! length: raise ValueError(长度域和实际字节数不符可能抓到的是半个 TCP 段) iv, ct body[:16], body[16:] if len(ct) % 16 ! 0: raise ValueError(密文长度不是 16 的倍数怀疑不是分组加密或截断了) # 密钥派生会话标识 固定盐做摘要这是我在 Demo 里定的规则 key hashlib.sha256(session_id bdemo-salt).digest() return {key: key, iv: iv, ct: ct, length: length}参数怎么改密钥派生那一行是整个流程里唯一需要「逆向」的地方规则不同有的是设备号、有的是账号加时间片先确认输入是什么、摘要算法是什么再动密钥长度。失败时看什么填充错误基本是密钥不对或者 IV 取错位置解出来长度对但内容是乱码八成是 IV 偏移算错了比如把 IV 也算进了长度域解出来像文本但夹着乱码怀疑外面还套了一层压缩先解压再解析。4.5 第二道门签名、时间戳和序列号解出明文之后你会发现改了字段重发对面还是不认。这不是加密没解对是签名没过。判断字段有没有参与签名用二分法一次只改一个字段看是返回业务错误还是校验错误把所有触发校验错误的字段收集起来它们就是签名输入集合。常见的签名构造是把参与字段按 key 的字典序拼成k1v1k2v2的形式末尾拼上时间戳和随机串再用一个共享密钥做摘要或 HMAC。验证方法在自己搭的服务端桩上做——把签名函数的输入打印出来比对一次就能确认顺序、分隔符和是否含空值。这里要提醒一句这套验证只在你能控制的 Demo 上做对着别人线上的服务做重放既验证不出协议设计也会踩线。5. 避坑与排查五个真实翻车记录这一章是我自己的错题本。下面五条每一条都真实消耗过我半天以上按「现象 → 原因 → 解决」写遇到相似症状可以直接对号入座。5.1 抓包里的「包」和程序里的「一条消息」对不上现象日志里明明发了一条消息抓包文件里看到三个 TCP 段或者反过来五条业务消息挤在一个段里切帧脚本直接崩。原因TCP 是字节流没有消息边界。程序里一次 write 对应多少字节和链路上分几段没有必然关系Nagle 算法和 MSS 都会掺一脚。我最早按「一个 TCP 段 一帧」写解析器样本换了就全乱。解决先用抓包器的流重组功能把整条连接拼成连续字节流再按协议自己的长度域切帧。切帧循环要写成流式的——缓冲区不够就等下一段够了就切一帧、把游标后移而不是假设每次读到的正好是一帧。5.2 字段解出来了但顺序每次都不同现象同一类报文两次抓包里字段的排列顺序不一样按位置取字段取一个错一个。原因序列化结构按字段号索引不保证顺序map 类型的字段更是完全无序。我一开始按「出现顺序」写死偏移前十条样本刚好顺序一致第十一条就崩了。解决按字段号建字典不要按出现顺序建数组遇到重复字段号数组语义就收集成列表遇到不认识的高字段号跳过而不是报错那往往是新版本加的字段。5.3 改了包重发回包成功但业务没变化现象篡改一个字段重发服务端返回 200但目标状态一点没变。原因消息带幂等键或序列号重复的序列号被服务端去重直接返回上次的结果。还有一种情况是服务端做了窗口检查序列号落后太多直接丢弃。解决先确认哪个字段是单调递增的第 3 章第二步已经找出来了重放时同步递增再看有没有独立的请求 ID那个也要重新生成。判断是不是被去重最简单的办法是看回包里的业务时间戳有没有更新——没更新就是命中了缓存。5.4 本地能解换个环境就填充错误现象自己的 Demo 上解得好好的换一台设备或者换一个账号解密直接抛填充错误。原因密钥不是常量而是由设备标识、账号、安装随机数派生出来的换环境等于换密钥。也可能每次会话都重新协商。解决先确认密钥来源的层级——是安装级、会话级还是请求级。安装级的最容易复现会话级的要拿到协商过程请求级的通常意味着每个包都带 salt。到会话级以上我一般就停在结构分析把「可解到哪一层」写清楚不硬啃。5.5 时间戳字段判断错差 8 小时或差 1000 倍现象解出来的时间和抓包时刻差整整 8 小时或者差 1000 倍。原因秒和毫秒混用是最常见的其次是 UTC 和本地时区混用。有协议用「2000 年 1 月 1 日」当纪元起点而不是 1970 年解出来会差 30 年。解决别直接转成日期看用抓包时刻做锚点反推单位和纪元。拿解出的原始整数除以抓包时刻的秒数商接近 1 是秒、接近 1000 是毫秒、接近 1000000 是微秒如果差一个固定偏移就去试几个常见纪元起点。这个校验我后来写成了固定步骤两分钟能省掉半天。5.6 熵很高但长度明显偏小现象数据熵接近 8看着像加密但长度比预期明文小很多解出来是一堆看不懂的字节。原因先压缩再加密。压缩把冗余干掉了熵自然高加密又加了一层两件事叠在一起只看熵分不出来。解决看长度比例。压缩率高的数据密文长度会显著小于你估算的明文长度解密之后如果头部出现 zlib 或 gzip 的魔数0x78 0x9C、0x1F 0x8B先解压再解析。6. 收尾把一次协议分析固化成四步套路折腾过几个项目之后我不再指望「一眼看穿协议」而是走一套固定的四步。每步都有明确的输入、输出和验收标准卡住了就知道自己卡在哪一层不会一整天在原地打转。步骤输入输出验收标准分类原始抓包、同类样本标准 TLS / 私有封装 / 文本协议的判定能说清首字节含义和记录层结构定界同类帧的字节序列帧边界规则长度域偏移与语义切帧脚本在 200 条以上样本零失败解码帧体字节字段表偏移、宽度、类型所有字段能无剩余地覆盖整帧语义字段表 扰动实验字段含义、可变性、是否参与校验改一个字段能预测回包变化这四步里最容易偷工的是第一步。我见过太多人跳过分类直接扎进 hexdump最后发现对面就是标准 TLS前面所有工作都是重做。反过来第一步做扎实后面三步基本是流水线甚至可以让脚本跑人只负责看结果。最后给一个我一直在用的具体技巧把「改一个字段」做成自动化对拍。写一个小脚本读一份字段表对每个字段依次注入三个值0、原值 1、超长字符串每注入一次就发一次请求、记录回包摘要最后输出一张「字段 → 是否影响回包 → 影响哪个字段」的矩阵。手工做这件事要一整天脚本跑二十分钟。这张矩阵出来之后签名字段、业务字段、填充字段一目了然比任何猜测都可靠。回过头看我最早那两天盯着加密流量发呆的经历其实错在顺序——先想着怎么解开而不是先想清楚面对的是什么。从那以后我每次拿到一份新的协议样本都强制先走一遍分类和定界哪怕直觉上已经认出来了也要让扫描脚本在全量样本上跑一遍再下结论。血泪经验就这一条直觉可以拿来开局但结论必须由样本统计来盖章。希望帮到你。本文还有配套的精品资源点击获取