コース目次 / 第3章

パケットも、結局はバイト列 — pcap

Ethernet/IP/TCPヘッダを自分で組み立ててpcapファイルを作ります。stringsで平文の通信内容を素早く確認したうえで、structでpcap形式を自分で読み解き、URLエンコードされたトークンを復号します。

第3章 / 全5章目安 約15分この章のゴール: pcap形式の構造を理解し、Wiresharkなしでも通信内容を復元できるようになる

Wiresharkのようなツールがなくても、pcapはただのバイナリファイル形式です。構造さえ分かれば、Pythonのstructだけで読めます。

通信キャプチャを、自分で作る

make_pcap.py として保存します。HTTPの平文通信を1パケットだけ含むpcapファイルを、ヘッダから手作りします。

import struct

def build_pcap(payload: bytes) -> bytes:
    global_header = struct.pack('<IHHiIII',
        0xa1b2c3d4,  # マジックナンバー
        2, 4,        # バージョン
        0, 0,        # タイムゾーン、精度
        65535,       # snaplen
        1,           # LINKTYPE_ETHERNET
    )
    eth = b'\xaa\xaa\xaa\xaa\xaa\xaa' + b'\xbb\xbb\xbb\xbb\xbb\xbb' + struct.pack('>H', 0x0800)
    total_len = 20 + 20 + len(payload)
    ip_header = struct.pack('>BBHHHBBH4s4s',
        0x45, 0, total_len, 0x1234, 0, 64, 6, 0,
        bytes([192, 0, 2, 10]),
        bytes([192, 0, 2, 20]),
    )
    tcp_header = struct.pack('>HHIIBBHHH',
        51000, 80, 1000, 0, (5 << 4), 0x18, 502, 0, 0,
    )
    packet = eth + ip_header + tcp_header + payload
    packet_header = struct.pack('<IIII', 0, 0, len(packet), len(packet))
    return global_header + packet_header + packet

http_payload = (
    b"GET /report?token=flag%7Bpacket_never_lies%7D HTTP/1.1\r\n"
    b"Host: intra.example.com\r\n"
    b"User-Agent: curl/8.0\r\n"
    b"\r\n"
)
with open('capture.pcap', 'wb') as f:
    f.write(build_pcap(http_payload))
python3 make_pcap.py
file capture.pcap
capture.pcap: pcap capture file, microsecond ts (little-endian) - version 2.4 (Ethernet, capture length 65535)

まず、stringsで素早く見る

HTTPは平文のプロトコルです。バイナリ構造を読まなくても、印字可能な文字列を拾うだけで内容が見えることがあります。

strings capture.pcap
GET /report?token=flag%7Bpacket_never_lies%7D HTTP/1.1
Host: intra.example.com
User-Agent: curl/8.0

flag%7B…%7Dという文字列が見えました。URLエンコードされていて、まだそのままではflag{…}の形になっていません。次は、構造をきちんとたどって取り出します。

structで、パケットの構造をたどる

parse_pcap.py として保存します。pcapグローバルヘッダ(24バイト)→パケットヘッダ(16バイト)→Ethernet(14バイト)→IP(先頭バイトのIHLからサイズが分かる)→TCP(データオフセットからサイズが分かる)→ペイロード、という構造を順にたどります。

import struct
from urllib.parse import unquote

data = open('capture.pcap', 'rb').read()
magic, ver_maj, ver_min, thiszone, sigfigs, snaplen, network = struct.unpack('<IHHiIII', data[:24])
print(f"magic={hex(magic)} network={network}")

offset = 24
ts_sec, ts_usec, incl_len, orig_len = struct.unpack('<IIII', data[offset:offset+16])
offset += 16
packet = data[offset:offset+incl_len]

ip_start = 14  # Ethernetヘッダは固定14バイト
ihl = (packet[ip_start] & 0x0f) * 4  # IPヘッダの先頭バイト下位4bit×4バイト
tcp_start = ip_start + ihl
data_offset = (packet[tcp_start + 12] >> 4) * 4  # TCPヘッダの13バイト目上位4bit×4バイト
payload = packet[tcp_start + data_offset:].decode()

print("--- HTTPペイロード ---")
print(payload)

first_line = payload.splitlines()[0]
token = first_line.split('token=')[1].split(' ')[0]
print("復号したフラグ:", unquote(token))
python3 parse_pcap.py
magic=0xa1b2c3d4 network=1
--- HTTPペイロード ---
GET /report?token=flag%7Bpacket_never_lies%7D HTTP/1.1
Host: intra.example.com
User-Agent: curl/8.0

復号したフラグ: flag{packet_never_lies}

取れました。 %7B / %7D は { / } のURLエンコードです。urllib.parse.unquoteで戻せば、元のflag{…}が復元できます。実務でも、平文プロトコル(HTTP・FTP・Telnetなど)の通信は、正しく暗号化(HTTPS/TLS)しない限り、このように読まれる前提で考える必要があります。

こうなっていればOK

卒業まであと1章です。

この章はまだ完了していません。