Project

General

Profile

Bug #9181 » make_pcaps.py

Roger Hill, 10/08/2026 12:49 AM

 
#!/usr/bin/env python3
"""Write two synthetic Modbus/TCP captures for the Suricata report. Standard library only.

python3 make_pcaps.py OUTDIR

modbus-midstream-server-first.pcap the capture starts with the server's
response to a request the capture did not see, then four complete
request/response pairs. No handshake.
modbus-midstream-client-first.pcap the control: the same session, but the
capture starts with a client request. No handshake.

Addresses are RFC 5737 documentation addresses. Client 192.0.2.10:49152,
server 192.0.2.20:502. Every request is Read Holding Registers (FC 3), unit 1,
address 0, quantity 2; every response carries two registers.
"""
import struct, sys
from pathlib import Path

CLIENT, SERVER = (bytes([192, 0, 2, 10]), 49152), (bytes([192, 0, 2, 20]), 502)

def checksum(data: bytes) -> int:
if len(data) % 2: data += b"\0"
s = sum(struct.unpack(f"!{len(data)//2}H", data))
while s >> 16: s = (s & 0xFFFF) + (s >> 16)
return ~s & 0xFFFF

def frame(src, dst, seq, ack, payload, ts):
(sip, sport), (dip, dport) = src, dst
tcp = struct.pack("!HHIIBBHHH", sport, dport, seq, ack, 5 << 4, 0x18, 64240, 0, 0) + payload
pseudo = sip + dip + struct.pack("!BBH", 0, 6, len(tcp))
tcp = tcp[:16] + struct.pack("!H", checksum(pseudo + tcp)) + tcp[18:]
ip = struct.pack("!BBHHHBBH4s4s", 0x45, 0, 20 + len(tcp), 1, 0x4000, 64, 6, 0, sip, dip)
ip = ip[:10] + struct.pack("!H", checksum(ip)) + ip[12:]
eth = b"\x02\x00\x00\x00\x00\x02" + b"\x02\x00\x00\x00\x00\x01" + b"\x08\x00"
pkt = eth + ip + tcp
return struct.pack("<IIII", int(ts), int((ts % 1) * 1e6), len(pkt), len(pkt)) + pkt

def request(tid): # MBAP + FC 3, address 0, quantity 2
return struct.pack("!HHHB", tid, 0, 6, 1) + struct.pack("!BHH", 3, 0, 2)

def response(tid): # MBAP + FC 3, byte count 4, two registers
return struct.pack("!HHHB", tid, 0, 7, 1) + struct.pack("!BBHH", 3, 4, 0x0011, 0x0022)

def session(server_first: bool) -> bytes:
out = struct.pack("<IHHiIII", 0xA1B2C3D4, 2, 4, 0, 0, 65535, 1)
cseq, sseq, ts = 1000, 5000, 1700000000.0
def send(from_client, payload):
nonlocal cseq, sseq, ts, out
ts += 0.01
if from_client:
out += frame(CLIENT, SERVER, cseq, sseq, payload, ts); cseq += len(payload)
else:
out += frame(SERVER, CLIENT, sseq, cseq, payload, ts); sseq += len(payload)
tid = 1
if server_first:
send(False, response(tid)); tid += 1 # the reply to a request before the capture began
for _ in range(4):
send(True, request(tid)); send(False, response(tid)); tid += 1
return out

if __name__ == "__main__":
out = Path(sys.argv[1] if len(sys.argv) > 1 else ".")
(out / "modbus-midstream-server-first.pcap").write_bytes(session(True))
(out / "modbus-midstream-client-first.pcap").write_bytes(session(False))
print("wrote", out / "modbus-midstream-server-first.pcap", "and", out / "modbus-midstream-client-first.pcap")
(1-1/4)