인터페이스는 현지화되어 있으며, Skill 소개와 설명 문서는 선택한 언어로 AI가 자동 번역합니다. 코드·명령어·경로·전문 용어는 원문을 유지하며 원작자 문서가 우선합니다.
Skills 둘러보기
생산성 및 협업 · SKILL.md

re-proto-rev

re-proto-rev

协议状态机重建:Scapy 解析、消息结构。 触发词:协议逆向、状态机、scapy、自定义协议

Independent GitHub snapshot · Automated classification · Original documentation governs

Ddslsdzc / rev-skillsApache-2.0

Skill 문서

GitHub

name: re-proto-rev description: > 协议状态机重建:Scapy 解析、消息结构。 触发词:协议逆向、状态机、scapy、自定义协议 capabilities: [protocol-recovery]

协议状态机重建

何时使用 / 何时不用

  • 用:手上有明文流量(pcap 或实时流)需要搞清"消息长什么样、双方怎么对话"
  • 用:自定义协议 / 私有协议 / 未知协议的字段结构推断
  • 用:C2 协议、固件通信协议的交互模式推演(握手/心跳/结束)
  • 不用:流量是密文(先 [[re-crypto-id]] → [[re-crypto-keys]] → [[re-crypto-decrypt]],解出明文再回来)
  • 不用:标准协议(HTTP/TLS/DNS)——直接 Wireshark 解码器(见 [[re-netcap]] 步骤 3)
  • 不用:只需要抓流量(那是 [[re-netcap]])

工具准备

所有工具先验证再使用。本技能只处理数据与解析脚本,不运行样本,可免沙箱([[re-analyze/platform-tips]] 最高原则);抓流量环节在 [[re-netcap]] 内进行。

python3 —— 解析脚本运行环境

  • Linux: apt install python3 python3-pip / dnf install python3 python3-pip / pacman -S python python-pip(多数自带)
  • macOS: brew install python(或 Xcode CLT 自带)
  • Windows: python.org 安装包(勾选 Add to PATH);WSL 内 Linux 版
  • 验证: python3 --version

scapy —— pcap 解析与协议构造主力

  • 全平台: pip install scapy(Python 3,推荐)
  • Linux: 部分发行版有包(apt install python3-scapy)
  • 验证: python3 -c "import scapy; print(scapy.__version__)"
  • 读 pcap 文件: rdpcap 即可,纯 Python 解析(无需 libpcap;libpcap 仅实时抓包 sniff() 时需要,强制纯 Python 解析用 conf.use_pcap = False)
  • 验证(跨平台,写临时 pcap 再读回,不依赖 /dev/null): python3 -c "import tempfile,os; from scapy.all import rdpcap, wrpcap, IP, TCP, Raw; f=os.path.join(tempfile.gettempdir(),'t.pcap'); wrpcap(f,[IP()/TCP()/Raw(b'test')]); print(len(rdpcap(f)))" —— 输出 1 即正常

tshark —— 分组统计与字段初探(安装见 [[re-netcap]])

  • tshark -r out.pcap -q -z conv,tcp 会话统计、-z io,stat,1 时间序列——步骤 1 聚类用
  • 验证: tshark --version

binwalk —— 已知格式线索(pcap 里嵌 blob 时找格式)

  • 安装与验证见 [[re-fw-extract]] 工具准备
  • 用途: 解析出的字段里疑似嵌了文件/镜像(gzip/zip/PNG 等)时,用 binwalk blob.bin 确认格式

操作步骤

按顺序执行,每步记下结果。前提:流量必须是明文(密文先走 [[re-crypto-decrypt]])。

  1. 分组统计与聚类(长度/方向/时序):

    tshark -r out.pcap -T fields -e tcp.len -e ip.src -e ip.dst | awk '{print $1}' | sort -n | uniq -c   # 长度分布
    tshark -r out.pcap -q -z conv,tcp          # 会话列表:谁和谁、包数、字节数
    tshark -r out.pcap -q -z io,stat,1         # 每秒流量:找规律性心跳
    
    • 聚类目标:按方向(A→B / B→A)分组看长度分布;长度呈少量固定值 → 固定长度消息(可能无长度字段);长度连续变化 → 变长消息(通常需要某种 framing,候选:内嵌长度字段、分隔符、固定语法、chunk 分块、连接关闭)
    • 找规律性间隔包(每 30s 一个固定长度小包)→ 心跳消息(见坑 3)
  2. TCP 流重建 → 应用层 framing(强制前置,不可跳过):

    • TCP 是字节流、不保留应用层消息边界(RFC 9293 §3.7)——一个 segment 可能是 0 条、1 条或多条消息,一条消息也可能跨多个 segment(MSS 切分)。把 bytes(p[TCP].payload) 逐包当消息会系统性出错:无间隔的连续 send() 常被合并成一个 chunk,大数据或慢链路则被切开;"一次 write 恰好一段"只是时序巧合
    • 按 连接 + 方向((src,sport,dst,dport) 四元组归一化后分 A→B / B→A)分别重组:依 TCP SEQ 排序、去重(重传/重叠段)、补齐乱序空洞,并记录 capture gap(丢包造成的字节缺失位置)——跨 gap 的候选消息丢弃,不要拿错误字节硬拼
    • 重组出单向字节流后再推断 framing:magic+length / 分隔符 / 固定长度 / TLV / 靠连接关闭分帧。framing 未定之前不做字段推断
    • Scapy 路线:TCPSession 会话化 + tcp_reassemble() 钩子(可处理重复/重叠);不写代码可先用 tshark -r out.pcap -q -z follow,tcp,raw,<流号> 取单向字节流人工看
    • UDP 等 datagram 传输不套本模型:datagram 保留消息边界,一个 datagram 即一条(或含整条)消息;仍可能是多消息拼接或应用层再分片,只做长度/边界统计,不做 SEQ 重组
  3. 定位固定头(magic / 长度字段):

    # 对重组后的单向字节流扫描候选 magic 前缀(不是逐包取前 8 字节)
    from collections import Counter
    stream = b""  # 步骤 2 按 (四元组, 方向) 重组出的单向字节流
    cnt = Counter()
    for off in range(max(0, len(stream) - 8)):
        cnt[stream[off:off + 8]] += 1
    for k, v in cnt.most_common(10):
        print(v, k.hex())
    
    • 高频前缀 → magic 候选(如 0xAA55、\xDE\xAD\xBE\xEF、ASCII 品牌名);同一流中第 2-4 字节变化但前缀不变 → 变长消息,变化处是长度字段
    • 候选 magic 必须过长度约束才算数:设 magic 在偏移 m、其后第 2-4 字节是长度字段 L,则下一个 magic 应落在 m + 头部长度 + L 处;只有多数出现位置都满足这一递推的候选才是真 magic(借此排除数据里碰巧出现的字节对)
    • 手工核对:hexdump 看几处代表位置(tshark -r out.pcap -q -z follow,tcp,raw,<流号>),确认 magic 与长度字段字节序(见坑 1)
  4. 字段推断(类型 / 长度 / CRC):

    • 长度字段:取固定偏移值 vs 该方向相邻 frame 起始偏移之差画散点/排序核对;字节序按值合理性试(大端/小端都试一遍,见坑 1)
    • 类型字段:同长度族中首字节固定集合(如 0x01/0x02/0x03)→ 消息类型;结合时序看类型序列是否像请求-响应配对
    • CRC/校验:包尾 2-4 字节对载荷变化敏感 → 校验和(先识别出来,别当数据解析——见坑 2);用 crcmod(pip install crcmod)试 CRC16/CRC32 常见多项式,或与载荷异或检查是否为简单 XOR 校验
    • 结构不确定时用差分法:两个只有单字节不同的包,逐字节异或 → 变化的字节就是"关键字段"位置
  5. Scapy 写解析器(自定义协议类):

    from scapy.all import Packet, ByteField, ShortField, XByteField, StrFixedLenField
    class C2(Packet):
        name = "C2"
        fields_desc = [
            XShortField("magic", 0xAA55),
            ByteField("type", 0),
            ShortField("len", 0),
            StrFixedLenField("payload", b"", 64),
        ]
    # 测试解析(hex 与 fields_desc 对齐:aa55 | 01 | 000a | 54657374696e672e2e2e,len=0x000a=10 = "Testing..." 的 10 字节)
    pkt = C2(bytes.fromhex("aa5501000a" + "54657374696e672e2e2e" + "00" * 54))   # aa55 | 01 | 000a | "Testing..."(10B)+ 54B 零填充,凑满 64B payload
    pkt.show()
    
    • 字段顺序/宽度按步骤 3、4 的推断来;解析器能逐个字段展示(show())即验证字段布局正确
    • 批处理:在步骤 2 重组出的单向字节流上跑持续 buffer parser(不是在包上逐包 C2(load))——循环从当前偏移解一条消息、按解析出的长度前进,直到剩余字节不足以构成完整消息为止。parser 必须能处理 0 条 / 1 条 / N 条消息对应一个 segment 三种情形;遇到 gap 或解析失败即停在该位置记录,不要跳过错误字节继续对齐(否则错位会静默传播)。Scapy 侧可直接用 TCPSession 让 C2 类在重组流上实例化
    • 与 Wireshark 原始字节对照:挑几处 frame 起止偏移,follow,tcp,raw 里确认字节一致
  6. 状态机推演(握手 / 心跳 / 结束):

    • 按时间顺序画出"谁发什么消息"的序列:客户端 → 服务端第 1 条(连接建立/握手请求)→ 响应 → 后续业务 → 周期心跳 → 断开。序列的单元是重组后的消息(步骤 2/5),不是 TCP 包
    • 对每类消息记录:方向、类型值、字段、触发条件(收到什么才发什么)
    • 验证推演:回到重组流里找反例——是否存在没按"状态机"走的消息、连接中途 reset 半条消息、capture gap 导致的缺帧(有则补充状态/转移条件,或说明异常分支);最终产出状态图(文字或 mermaid)+ 消息格式表,写入报告供 [[re-ioc]] 引用

跨域联合

  • [[re-protocol]]:本技能是其工作流第 5 步(状态机重建)——明文流量的最终产出环节
  • [[re-malware]]:C2 协议重建——re-malware 第 4 步(netcap → proto-rev → crypto-*),重建出的协议语义(握手/心跳/指令)是行为判断与 IOC 依据
  • [[re-firmware]]:固件通信协议重建——re-firmware 第 6 步(固件回连 / 自定义协议)
  • [[re-netcap]]:原料来源(明文 pcap 与 tshark 统计)
  • [[re-crypto-id]] / [[re-crypto-decrypt]]:前置——流量密文时先解密再进本技能
  • 产出(消息格式表 / 状态图)回传 [[re-ioc]] 进报告与 YARA 特征

常见坑与陷阱

  • 长度字段字节序错误 → 解析全错:现象——解析器按小端读长度,长度值巨大/异常,后续字段全部错位;原因——协议长度字段是大端(网络序),按主机序小端读了;对策——步骤 4 对长度字段大小端都试,用"长度值是否合理(≤ 该 frame 在重组流中的实际跨度)"裁定,解析器里显式 !H/!I 网络序

  • CRC/校验先识别再解析:现象——把包尾校验和当业务数据解析,字段错位且消息对不上;原因——未知协议里校验和与数据无法靠名字区分;对策——包尾 2-4 字节对载荷敏感变化即先假设为校验和,用 crcmod/异或验证识别,解析器里跳过或单独字段

  • 心跳消息混淆:现象——把周期心跳当业务消息,状态机出现"多发一条"的假分支,指令序列错乱;原因——心跳与业务消息同结构(同 magic 同类型前缀),只有时序规律;对策——步骤 1 的 io,stat 找固定周期小包,标记为心跳单独处理,状态机里作为独立状态转移

  • 方向混淆(C2 交互模式):现象——把服务端→客户端的方向看反,命令消息被当响应、下发数据被当回包;原因——C2 中客户端主动轮询(请求里带命令)或服务端主动下发(无请求也有流量)混在一起;对策——按 IP:端口归属先明确 A/B 角色,分方向做统计(步骤 1),结合时序确认谁是主动方,状态图里标注每个转移的方向

  • Scapy 内置层绑定劫持自定义载荷:现象——自定义协议载荷被 Scapy 自动解成 DNS 等其他内置层,字段全乱(如 UDP 53 载荷解出来总是默认值);原因——bind_layers() 的默认绑定(UDP/53→DNS 等)在 guess_payload_class() 里先命中;对策——split_layers() 解除默认绑定,自定义协议类 override guess_payload_class() 显式分发,或解析脚本每包用自定义类强制构造

  • 短包/固定长度字段静默降级 Raw:现象——解析器对某些包无报错输出 Raw / 字段错位,包一多错得隐蔽;原因——包短于字段最小长度时 Scapy 静默把整包当 Raw(无 Wireshark 式 malformed 标记),StrFixedLenField 与包缓存交互还会构建不一致;对策——解析前显式检查载荷长度 ≥ 字段和,短包单独记录不硬解;每个自定义类做 round-trip 验证(bytes(pkt) 再解回,比对上机字节)后再批量跑

  • 重注入后对端报 FCS 错 / 帧长不对:现象——把抓到的帧重注入后,对端 Wireshark 报 "ETHERNET FRAME CHECK SEQUENCE INCORRECT" 或帧长与预期不符;原因——要分开看两个长度:以太网 FCS 是 4 字节,而典型以太网头部是 14 字节(6 目的 MAC + 6 源 MAC + 2 类型)——"短 14 字节"指向的是头部/链路层封装问题,不是 FCS;抓包库的帧抽象(Padding 处理、FCS 字段是否解析、是否带 trailer)会因版本与配置而异,不能笼统归结为"不保留尾部";对策——先量出实际少了多少字节再定位:缺 4 字节查 FCS 是否被剥离,缺 14 字节查是不是丢/重复了以太网头或注入到了错误的层;重注入优先用原始字节而非 round-trip 后的包,或在本机环回(lo)用 L3 注入避开以太网层

  • 只读序列化函数,布局单边确认:现象——按 serialize 推的字段顺序在真实流量里对不上;原因——打包/解包可能不对称(条件字段、版本分支);对策——序列化/反序列化函数对读互证(serialize vs parse),两个方向对上了才是真布局;协议库通常同时含打包/解包函数

  • 漏掉协议行为锚点:现象——特殊命令(心跳/时间校正)走独立组装路径,按通用帧结构解析全乱;原因——协议库内硬编码的特判字符串/命令是行为锚点;对策——grep 原生库与配置里的命令字符串(heartbeat/correcttime 类),先定位特殊路径再按通用结构解析

  • Java 客户端忽略 JNI 契约:现象——原生库逆向找不到入口;原因——Java 层通过 JNI 调原生,静态注册的方法会以 Java_包名_类名_方法名 留在动态导出表(strip 不删 .dynsym)可作接口文档;但 RegisterNatives 动态注册的库没有 Java_* 导出;对策——nm -D 列 JNI 导出,有则函数名直接给契约(encode/parse/setKey 分工)从导出函数开始逆向;无则从 JNI_OnLoad/RegisterNatives 调用点、JNI 函数表 xref、字符串与运行期 hook([[re-frida]])定位入口

  • IRC/文本协议分片在单词中间:现象——从抓包提取的协议消息里出现"半个词"(如 "giant s"、"undead eye"),按消息边界解析材料/字段错位;原因——IRC 服务器(InspIRCd)按 ~400 字节硬切长消息(不尊重单词边界),客户端收到后拼接全部分片再按分隔符重分割;对策——按客户端逻辑处理:拼接相邻消息(Join(msgs, ""))→ 再按分隔符(, 等)分割,而不是按消息边界独立解析;TSHARK follow 的 ascii 模式有 ~451 字节显示截断,长数据必须用 raw(hex)模式提取

  • 消息累积触发机制:现象——协议客户端对长消息分片处理,只在特定条件下触发完整处理;原因——客户端按"结束标记"累积消息(如以 ./? 结尾才处理整段),中间分片只是缓存;对策——反编译客户端消息处理器找累积条件(包含自己名字 + 结束字符等),重放时按该格式构造,否则消息被静默丢弃

  • 伪服务端重放验证协议:现象——协议逻辑复杂、静态逆向慢;原因——客户端程序(bot/C2 信标)逻辑在交互中触发;对策——伪造服务端(假 IRC/HTTP + DNS hook 到本地)按 pcap 真实序列重放,观察客户端行为(发送消息/调用链),快速验证协议理解

  • Web 验证码/现代 JS 协议:从 Verify 接口反向推:现象——直接逆向验证码加密算法无从下手,或还原后校验仍失败;原因——验证码真正提交的数据全部封装在单一字段(如 captchaBody),算法只是最后一步;对策——流水线:抓 Verify 请求 → 定位核心字段 → JS 插桩找 SDK 加密前明文 plain → 逐个分析 plain 字段来源(接口返回免算 / 缺口识别:下载图片+OpenCV 找缺口算拖动距离 / 浏览器环境采集按 SDK 格式生成 / 轨迹坐标)→ 明文完全一致后加密反而是最简单一步(沿 SDK 调用链执行与浏览器一致的加密流程);核心原则是输入完全一致而非算法等价

  • 协议化时同步算额外签名层:现象——核心字段(captchaBody)还原正确 Verify 仍被拒;原因——Verify 之外还有页面 SDK 自动生成的请求保护签名(协议化时漏算);对策——从页面 SDK 完整调用链确认所有自动生成参数一并纳入;同类场景先列全"请求包含的所有非手工字段"再实现

  • 私有协议无解析器:现象——Wireshark 只显示 Data;原因——私有协议无 dissector;对策——写 Lua dissector(<100 行)或 Python 离线分析

  • 数据无规律 → 先验加密:现象——每帧都不同;原因——压缩/加密层;对策——熵分析(>7.5 → 加密/压缩/高随机性候选,结合帧长度规律与熵分布佐证,高熵不是判定),加密层找 nonce/IV 字段;长度字段解不出时按「长度可能 little/big-endian、含/不含自身」列方程组解语义

  • 重放被拒:现象——数据正确但服务端不响应;原因——协议带递增 seq/nonce;对策——搞清楚 seq 计算方式(前一帧 hash 或递增计数器),复现时正确推进

  • 闭环验证:现象——协议还原是否成立无判据;原因——静态分析无法自证还原正确;对策——用还原的帧结构写 client 发一帧,服务端响应一致才算成立 (来源:reverse-skill field-journal,MIT)

저장한 Skills