re-proto-rev について
re-proto-revは、平文トラフィックから状態機械を再構築するスキルで、マルウェアやファームウェアの通信分析に向いています。TCPバイトストリームを再構成してから、メッセージ形式、フィールド、通信状態を推定します。
主な機能
- 01通信の長さ、方向、時系列を分析
- 02TCPストリームを再構成して境界を推定
- 03プロトコルヘッダー、フィールド、チェックサムを調査
- 04Scapyでパーサーを作成
- 05ハンドシェイク、ハートビート、終了状態を整理
活用シーン
- 01独自プロトコルの平文pcapを分析
- 02C2通信のハンドシェイクとハートビートを再構築
- 03ファームウェア通信のメッセージ形式とやり取りを推定
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ dslsdzc/ rev-skills --skill re-proto-revインストール後、Claude Code で/re-proto-revを入力して読み込まれたか確認します。
詳細
协议状态机重建
何时使用 / 何时不用
- 用:手上有明文流量(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]])。
-
分组统计与聚类(长度/方向/时序):
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)
-
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 重组
- TCP 是字节流、不保留应用层消息边界(RFC 9293 §3.7)——一个 segment 可能是 0 条、1 条或多条消息,一条消息也可能跨多个 segment(MSS 切分)。把
-
定位固定头(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)
- 高频前缀 → magic 候选(如
-
字段推断(类型 / 长度 / CRC):
- 长度字段:取固定偏移值 vs 该方向相邻 frame 起始偏移之差画散点/排序核对;字节序按值合理性试(大端/小端都试一遍,见坑 1)
- 类型字段:同长度族中首字节固定集合(如 0x01/0x02/0x03)→ 消息类型;结合时序看类型序列是否像请求-响应配对
- CRC/校验:包尾 2-4 字节对载荷变化敏感 → 校验和(先识别出来,别当数据解析——见坑 2);用
crcmod(pip install crcmod)试 CRC16/CRC32 常见多项式,或与载荷异或检查是否为简单 XOR 校验 - 结构不确定时用差分法:两个只有单字节不同的包,逐字节异或 → 变化的字节就是"关键字段"位置
-
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里确认字节一致
- 字段顺序/宽度按步骤 3、4 的推断来;解析器能逐个字段展示(
-
状态机推演(握手 / 心跳 / 结束):
- 按时间顺序画出"谁发什么消息"的序列:客户端 → 服务端第 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/XOR で検証して識別します。パーサーではスキップするか、独立したフィールドとして扱います
-
ハートビートメッセージの混同:現象——周期的なハートビートを業務メッセージとして扱い、状態遷移図に「余分なメッセージが 1 つある」という偽の分岐が生じ、コマンドの順序が乱れる;原因——ハートビートと業務メッセージが同じ構造(同じ 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()でデフォルトバインドを解除し、カスタムプロトコルクラスでguess_payload_class()をオーバーライドして明示的に振り分けるか、解析スクリプトで各パケットをカスタムクラスに強制的に構築します -
短いパケット/固定長フィールドがエラーなしで Raw にフォールバックする:現象——一部のパケットでエラーが出ずに Raw が出力されたり、フィールドがずれたりし、パケット数が増えると誤りに気づきにくくなる;原因——フィールドの最小長よりパケットが短いと、Scapy はパケット全体を Raw としてエラーなしで扱う(Wireshark のような malformed の表示はない)。また、
StrFixedLenFieldとパケットバッファーの相互作用でも不整合が生じる;対策——解析前にペイロード長がフィールド長の合計以上かを明示的に確認し、短いパケットは無理にデコードせず個別に記録します。各カスタムクラスでラウンドトリップ検証(bytes(pkt)にしてから再デコードし、実機上のバイト列と比較)を行ってから一括処理します -
再注入後に相手側で FCS エラーが出る/フレーム長が合わない:現象——キャプチャしたフレームを再注入すると、相手側の Wireshark に「ETHERNET FRAME CHECK SEQUENCE INCORRECT」と表示されたり、フレーム長が想定と異なったりする;原因——2 種類の長さを分けて考える必要があります。イーサネットの FCS は 4 バイトで、一般的なイーサネットヘッダーは 14 バイト(宛先 MAC 6 バイト+送信元 MAC 6 バイト+タイプ 2 バイト)です。したがって、「14 バイト短い」という場合はヘッダー/リンク層カプセル化の問題であり、FCS ではありません。キャプチャライブラリのフレーム抽象化(Padding の処理、FCS フィールドを解析するかどうか、trailer の有無)は、バージョンや設定によって異なり、「末尾を保持しない」と一律に結論づけることはできません;対策——まず実際に何バイト不足しているかを測定して原因を特定します。4 バイト不足なら FCS が剥離されていないかを調べ、14 バイト不足ならイーサネットヘッダーが欠落または重複していないか、あるいは誤ったレイヤーに注入していないかを調べます。再注入には、ラウンドトリップ後のパケットではなく元のバイト列を優先して使うか、ローカルループバック(lo)で L3 注入を行い、イーサネットレイヤーを避けます
-
シリアライズ関数だけを読み、レイアウトを片側からしか確認しない:現象——serialize から推定したフィールド順序が実際のトラフィックと一致しない;原因——パックとアンパックは非対称な場合がある(条件付きフィールド、バージョン分岐など);対策——シリアライズ関数とデシリアライズ関数を相互に照合します(serialize と parse)。両方向で一致して初めて、正しいレイアウトだと判断できます。プロトコルライブラリには、通常パック関数とアンパック関数の両方が含まれています
-
プロトコルの挙動を示す手掛かりを見落とす:現象——特殊なコマンド(ハートビート/時刻補正)が独立した組み立て経路を通り、汎用フレーム構造で解析すると全体が崩れる;原因——プロトコルライブラリにハードコードされた特殊な判定文字列/コマンドが、挙動を示す手掛かりになる;対策——ネイティブライブラリと設定ファイル内のコマンド文字列(heartbeat/correcttime など)を grep し、まず特殊な経路を特定してから汎用構造で解析します
-
Java クライアントが JNI の契約を見落とす:現象——ネイティブライブラリをリバースエンジニアリングしてもエントリーポイントが見つからない;原因——Java レイヤーは JNI 経由でネイティブコードを呼び出します。静的登録されたメソッドは
Java_パッケージ名_クラス名_メソッド名として動的エクスポートテーブルに残り(strip では .dynsym は削除されません)、インターフェースのドキュメントとして利用できます。ただし、RegisterNativesで動的登録されたライブラリにはJava_*エクスポートがありません;対策——nm -Dで JNI エクスポートを一覧表示します。存在する場合は、関数名から契約(encode/parse/setKey の役割分担)が分かるので、エクスポートされた関数からリバースエンジニアリングを始めます。存在しない場合は、JNI_OnLoad/RegisterNativesの呼び出し箇所、JNI 関数テーブルの xref、文字列、実行時フック([[re-frida]])からエントリーポイントを特定します -
IRC/テキストプロトコルの分割位置が単語の途中にある:現象——キャプチャから抽出したプロトコルメッセージに「単語の途中」(例:「giant s」、「undead eye」)が現れ、メッセージ境界に基づいて解析すると素材/フィールドがずれる;原因——IRC サーバー(InspIRCd)は長いメッセージを約 400 バイトで機械的に分割し(単語の境界を考慮しません)、クライアントは受信後、すべての分割片を連結してから区切り文字で再分割します;対策——クライアントのロジックに合わせて処理します。隣接するメッセージを連結し(
Join(msgs, ""))→ その後で区切り文字(,など)で分割します。メッセージ境界ごとに個別に解析してはいけません。TSHARK follow の ascii モードでは約 451 バイトで表示が切れるため、長いデータの抽出には raw(hex)モードを使う必要があります -
メッセージの蓄積を開始する条件:現象——プロトコルクライアントは長いメッセージを分割して処理し、特定の条件を満たした場合にのみ全体を処理する;原因——クライアントは「終了マーカー」に基づいてメッセージを蓄積し(例:
./?で終わる場合にのみ全体を処理する)、途中の分割片はキャッシュにとどまる;対策——クライアントのメッセージ処理器を逆コンパイルし、蓄積条件(自身の名前と終了文字の両方を含むなど)を調べます。再生時はその形式でメッセージを構築してください。そうしないと、メッセージはエラーなしで破棄されます -
偽サーバーでプロトコルの再生を検証する:現象——プロトコルロジックが複雑で、静的リバースエンジニアリングに時間がかかる;原因——クライアントプログラム(bot/C2 ビーコン)のロジックは、通信中に実行される;対策——偽サーバー(偽 IRC/HTTP + DNS フックをローカルに設定)を用意し、pcap に記録された実際の順序で再生して、クライアントの挙動(メッセージの送信/呼び出しチェーン)を観察します。プロトコルの理解をすばやく検証できます
-
Web CAPTCHA/最新の JS プロトコル:Verify インターフェースから逆算する:現象——CAPTCHA の暗号化アルゴリズムを直接リバースエンジニアリングできない、または復元後も検証に失敗する;原因——CAPTCHA で実際に送信されるデータはすべて単一のフィールド(captchaBody など)にカプセル化されており、アルゴリズムは最後の処理にすぎない;対策——次のパイプラインで処理します。Verify リクエストをキャプチャ → 中核フィールドを特定 → JS にインストルメンテーションを仕込み、SDK の暗号化前の平文 plain を特定 → plain の各フィールドの取得元を個別に分析(API レスポンスから取得して計算不要/不足部分の特定:画像をダウンロードして 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 またはインクリメントカウンター)を解明し、再現時に正しく進めます
-
エンドツーエンド検証:現象——プロトコルの復元が正しいかどうかを判定する基準がない;原因——静的分析だけでは、復元が正しいことを自分で証明できない;対策——復元したフレーム構造を使ってクライアントからフレームを 1 つ送信し、サーバーから一致する応答が返れば、正しく復元できたと判断します (出典:reverse-skill field-journal、MIT)