|
| 1 | +"""Generate a test PCAP file using only Python stdlib (struct module). |
| 2 | +No external dependencies needed - writes raw PCAP binary format directly.""" |
| 3 | +import struct, os, time |
| 4 | + |
| 5 | +def write_pcap_header(f): |
| 6 | + # Global header: magic, version 2.4, thiszone=0, sigfigs=0, snaplen=65535, network=1 (Ethernet) |
| 7 | + f.write(struct.pack("<IHHiIII", 0xa1b2c3d4, 2, 4, 0, 0, 65535, 1)) |
| 8 | + |
| 9 | +def write_packet(f, data, ts): |
| 10 | + ts_sec = int(ts) |
| 11 | + ts_usec = int((ts - ts_sec) * 1e6) |
| 12 | + f.write(struct.pack("<IIII", ts_sec, ts_usec, len(data), len(data))) |
| 13 | + f.write(data) |
| 14 | + |
| 15 | +def ip_checksum(header): |
| 16 | + if len(header) % 2: header += b'\x00' |
| 17 | + s = sum(struct.unpack("!%dH" % (len(header)//2), header)) |
| 18 | + s = (s >> 16) + (s & 0xffff) |
| 19 | + s += s >> 16 |
| 20 | + return ~s & 0xffff |
| 21 | + |
| 22 | +def make_eth_ip_tcp(src_mac, dst_mac, src_ip, dst_ip, sport, dport, payload): |
| 23 | + eth = bytes.fromhex(dst_mac.replace(":","")) + bytes.fromhex(src_mac.replace(":","")) + b'\x08\x00' |
| 24 | + ip_hdr = struct.pack("!BBHHHBBH4s4s", |
| 25 | + 0x45, 0, 20+20+len(payload), 0x1234, 0x4000, 64, 6, 0, |
| 26 | + bytes(int(x) for x in src_ip.split(".")), |
| 27 | + bytes(int(x) for x in dst_ip.split("."))) |
| 28 | + csum = ip_checksum(ip_hdr) |
| 29 | + ip_hdr = ip_hdr[:10] + struct.pack("!H", csum) + ip_hdr[12:] |
| 30 | + tcp_hdr = struct.pack("!HHIIBBHHH", sport, dport, 1000, 1000, 0x50, 0x18, 65535, 0, 0) |
| 31 | + return eth + ip_hdr + tcp_hdr + payload |
| 32 | + |
| 33 | +def make_eth_ip_udp(src_mac, dst_mac, src_ip, dst_ip, sport, dport, payload): |
| 34 | + eth = bytes.fromhex(dst_mac.replace(":","")) + bytes.fromhex(src_mac.replace(":","")) + b'\x08\x00' |
| 35 | + ip_hdr = struct.pack("!BBHHHBBH4s4s", |
| 36 | + 0x45, 0, 20+8+len(payload), 0x1234, 0x4000, 64, 17, 0, |
| 37 | + bytes(int(x) for x in src_ip.split(".")), |
| 38 | + bytes(int(x) for x in dst_ip.split("."))) |
| 39 | + csum = ip_checksum(ip_hdr) |
| 40 | + ip_hdr = ip_hdr[:10] + struct.pack("!H", csum) + ip_hdr[12:] |
| 41 | + udp_hdr = struct.pack("!HHHH", sport, dport, 8+len(payload), 0) |
| 42 | + return eth + ip_hdr + udp_hdr + payload |
| 43 | + |
| 44 | +def modbus_req(tid, uid, fc, data=b""): |
| 45 | + pdu = bytes([fc]) + data |
| 46 | + return struct.pack(">HHH", tid, 0, len(pdu)+1) + bytes([uid]) + pdu |
| 47 | + |
| 48 | +def modbus_resp(tid, uid, fc, data=b""): |
| 49 | + pdu = bytes([fc]) + data |
| 50 | + return struct.pack(">HHH", tid, 0, len(pdu)+1) + bytes([uid]) + pdu |
| 51 | + |
| 52 | +def modbus_mei_resp(tid, uid): |
| 53 | + objs = [(0,b"Schneider Electric"),(1,b"Modicon M580"),(2,b"3.20"),(4,b"M580 ePAC Controller"),(5,b"BMEP585040")] |
| 54 | + od = b"" |
| 55 | + for oid, val in objs: |
| 56 | + od += bytes([oid, len(val)]) + val |
| 57 | + mei = bytes([0x0E, 0x01, 0x01, 0x00, 0x00, len(objs)]) + od |
| 58 | + return modbus_resp(tid, uid, 0x2B, mei) |
| 59 | + |
| 60 | +def s7_cotp_cr(): |
| 61 | + params = bytes([0xC1,0x02,0x01,0x00, 0xC2,0x02,0x01,0x02, 0xC0,0x01,0x0A]) |
| 62 | + cotp = bytes([len(params)+6, 0xE0, 0,0, 0,1, 0]) + params |
| 63 | + return struct.pack(">BBH", 3, 0, 4+len(cotp)) + cotp |
| 64 | + |
| 65 | +def s7_cotp_cc(): |
| 66 | + params = bytes([0xC1,0x02,0x01,0x00, 0xC2,0x02,0x01,0x02, 0xC0,0x01,0x0A]) |
| 67 | + cotp = bytes([len(params)+6, 0xD0, 0,1, 0,1, 0]) + params |
| 68 | + return struct.pack(">BBH", 3, 0, 4+len(cotp)) + cotp |
| 69 | + |
| 70 | +def s7_data(): |
| 71 | + s7 = bytes([0x32,0x01,0,0,0,1,0,14,0,0, 0x04,0x01, 0x12,0x0A,0x10,0x02,0,100,0,1,0x84,0,0,0]) |
| 72 | + cotp_dt = bytes([2, 0xF0, 0x80]) |
| 73 | + p = cotp_dt + s7 |
| 74 | + return struct.pack(">BBH", 3, 0, 4+len(p)) + p |
| 75 | + |
| 76 | +def eip_list_identity(): |
| 77 | + pname = b"1756-L71/B ControlLogix5571" |
| 78 | + ident = struct.pack("<H", 1) |
| 79 | + ident += struct.pack(">HH4s8s", 2, 44818, bytes([10,10,1,102]), b"\x00"*8) |
| 80 | + ident += struct.pack("<H", 1) # vendor=Rockwell |
| 81 | + ident += struct.pack("<H", 0x10) # device type=PLC |
| 82 | + ident += struct.pack("<H", 55) # product code |
| 83 | + ident += bytes([30, 11]) # revision |
| 84 | + ident += struct.pack("<H", 0) # status |
| 85 | + ident += struct.pack("<I", 0xDEADBEEF) |
| 86 | + ident += bytes([len(pname)]) + pname + bytes([3]) |
| 87 | + item = struct.pack("<HH", 0x000C, len(ident)) + ident |
| 88 | + body = struct.pack("<H", 1) + item |
| 89 | + hdr = struct.pack("<HHII", 0x0063, len(body), 0, 0) + b"\x00"*8 + struct.pack("<I", 0) |
| 90 | + return hdr + body |
| 91 | + |
| 92 | +def eip_register(): |
| 93 | + body = struct.pack("<HH", 1, 0) |
| 94 | + return struct.pack("<HHII", 0x0065, len(body), 0, 0) + b"\x00"*8 + struct.pack("<I", 0) + body |
| 95 | + |
| 96 | +def dnp3_frame(ctrl, dest, src, app_fc=None): |
| 97 | + app = b"" |
| 98 | + if app_fc is not None: |
| 99 | + app = bytes([0xC0, 0xC0, app_fc]) |
| 100 | + return bytes([0x05, 0x64, 5+len(app), ctrl]) + struct.pack("<HH", dest, src) + app |
| 101 | + |
| 102 | +def iec104_startdt(): |
| 103 | + return bytes([0x68, 0x04, 0x07, 0x00, 0x00, 0x00]) |
| 104 | + |
| 105 | +def iec104_interrog(): |
| 106 | + asdu = bytes([100, 0x01, 0x06, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x14]) |
| 107 | + return bytes([0x68, len(asdu)+4, 0,0,0,0]) + asdu |
| 108 | + |
| 109 | +def opcua_hello(): |
| 110 | + url = b"opc.tcp://10.10.2.60:4840/OPCUA/SimServer" |
| 111 | + body = struct.pack("<IIIII", 0, 65536, 65536, 0, 0) + struct.pack("<I", len(url)) + url |
| 112 | + return b"HELF" + struct.pack("<I", 8+len(body)) + body |
| 113 | + |
| 114 | +def opcua_open_none(): |
| 115 | + sp = b"http://opcfoundation.org/UA/SecurityPolicy#None" |
| 116 | + body = struct.pack("<I", len(sp)) + sp + b"\x00"*20 |
| 117 | + return b"OPNF" + struct.pack("<I", 8+len(body)) + body |
| 118 | + |
| 119 | +def mqtt_connect(cid=b"ot_sensor_01"): |
| 120 | + vh = b"\x00\x04MQTT" + bytes([0x04, 0x02]) + struct.pack(">H", 60) |
| 121 | + pl = struct.pack(">H", len(cid)) + cid |
| 122 | + return bytes([0x10, len(vh)+len(pl)]) + vh + pl |
| 123 | + |
| 124 | +def mqtt_publish(topic=b"ot/plc/data", msg=b'{"temp":85.2}'): |
| 125 | + vh = struct.pack(">H", len(topic)) + topic |
| 126 | + return bytes([0x30, len(vh)+len(msg)]) + vh + msg |
| 127 | + |
| 128 | +def bacnet_whois(): |
| 129 | + npdu = bytes([0x01, 0x20, 0xFF, 0xFF, 0x00, 0xFF]) |
| 130 | + apdu = bytes([0x10, 0x08]) |
| 131 | + p = npdu + apdu |
| 132 | + return bytes([0x81, 0x0B]) + struct.pack(">H", 4+len(p)) + p |
| 133 | + |
| 134 | +def main(): |
| 135 | + out = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ot_test_traffic.pcap") |
| 136 | + ts = time.time() |
| 137 | + M = "00:1A:2B:00:01:10"; EW = "00:1A:2B:00:01:20" |
| 138 | + P1 = "00:80:F4:00:01:64"; P2 = "00:0E:8C:00:01:65"; P3 = "00:00:BC:00:01:66" |
| 139 | + DM = "00:1A:2B:00:02:0A"; DR = "00:1A:2B:00:02:32"; IR = "00:1A:2B:00:02:33" |
| 140 | + OU = "00:1A:2B:00:02:3C"; MQ = "00:1A:2B:00:03:0A"; BA = "00:1A:2B:00:03:14" |
| 141 | + |
| 142 | + with open(out, "wb") as f: |
| 143 | + write_pcap_header(f) |
| 144 | + # Modbus read requests + responses |
| 145 | + for i in range(5): |
| 146 | + write_packet(f, make_eth_ip_tcp(M,P1,"10.10.1.10","10.10.1.100",50000+i,502, |
| 147 | + modbus_req(i+1,1,0x03,struct.pack(">HH",0,10))), ts+i*0.1) |
| 148 | + write_packet(f, make_eth_ip_tcp(P1,M,"10.10.1.100","10.10.1.10",502,50000+i, |
| 149 | + modbus_resp(i+1,1,0x03,bytes([20])+b"\x00\x01"*10)), ts+i*0.1+0.05) |
| 150 | + # Modbus write coils |
| 151 | + for i in range(3): |
| 152 | + write_packet(f, make_eth_ip_tcp(M,P1,"10.10.1.10","10.10.1.100",51000+i,502, |
| 153 | + modbus_req(10+i,1,0x05,struct.pack(">HH",i,0xFF00))), ts+1+i*0.1) |
| 154 | + # Modbus MEI |
| 155 | + write_packet(f, make_eth_ip_tcp(P1,M,"10.10.1.100","10.10.1.10",502,50010, |
| 156 | + modbus_mei_resp(20,1)), ts+2) |
| 157 | + # S7comm CR/CC/Data |
| 158 | + write_packet(f, make_eth_ip_tcp(EW,P2,"10.10.1.20","10.10.1.101",52000,102,s7_cotp_cr()), ts+3) |
| 159 | + write_packet(f, make_eth_ip_tcp(P2,EW,"10.10.1.101","10.10.1.20",102,52000,s7_cotp_cc()), ts+3.1) |
| 160 | + for i in range(5): |
| 161 | + write_packet(f, make_eth_ip_tcp(EW,P2,"10.10.1.20","10.10.1.101",52000,102,s7_data()), ts+3.2+i*0.1) |
| 162 | + # EtherNet/IP |
| 163 | + write_packet(f, make_eth_ip_tcp(M,P3,"10.10.1.10","10.10.1.102",53000,44818,eip_register()), ts+5) |
| 164 | + for i in range(3): |
| 165 | + write_packet(f, make_eth_ip_tcp(P3,M,"10.10.1.102","10.10.1.10",44818,53000,eip_list_identity()), ts+5.1+i*0.2) |
| 166 | + # DNP3 reads |
| 167 | + for i in range(3): |
| 168 | + write_packet(f, make_eth_ip_tcp(DM,DR,"10.10.2.10","10.10.2.50",54000,20000, |
| 169 | + dnp3_frame(0xC4,10,1,0x01)), ts+7+i*0.2) |
| 170 | + write_packet(f, make_eth_ip_tcp(DR,DM,"10.10.2.50","10.10.2.10",20000,54000, |
| 171 | + dnp3_frame(0x44,1,10,0x81)), ts+7.1+i*0.2) |
| 172 | + # DNP3 Direct Operate (bypasses SBO) |
| 173 | + for i in range(2): |
| 174 | + write_packet(f, make_eth_ip_tcp(DM,DR,"10.10.2.10","10.10.2.50",54001,20000, |
| 175 | + dnp3_frame(0xC4,10,1,0x05)), ts+8+i*0.2) |
| 176 | + # DNP3 Cold Restart |
| 177 | + write_packet(f, make_eth_ip_tcp(DM,DR,"10.10.2.10","10.10.2.50",54002,20000, |
| 178 | + dnp3_frame(0xC4,10,1,0x0D)), ts+9) |
| 179 | + # DNP3 File Open |
| 180 | + write_packet(f, make_eth_ip_tcp(DM,DR,"10.10.2.10","10.10.2.50",54003,20000, |
| 181 | + dnp3_frame(0xC4,10,1,0x19)), ts+9.5) |
| 182 | + # DNP3 over UDP |
| 183 | + write_packet(f, make_eth_ip_udp(DM,DR,"10.10.2.10","10.10.2.50",54010,20000, |
| 184 | + dnp3_frame(0xC4,10,1,0x01)), ts+10) |
| 185 | + # IEC-104 |
| 186 | + write_packet(f, make_eth_ip_tcp(M,IR,"10.10.1.10","10.10.2.51",55000,2404,iec104_startdt()), ts+11) |
| 187 | + for i in range(3): |
| 188 | + write_packet(f, make_eth_ip_tcp(M,IR,"10.10.1.10","10.10.2.51",55000,2404,iec104_interrog()), ts+11.1+i*0.1) |
| 189 | + # OPC-UA |
| 190 | + write_packet(f, make_eth_ip_tcp(EW,OU,"10.10.1.20","10.10.2.60",56000,4840,opcua_hello()), ts+13) |
| 191 | + for i in range(3): |
| 192 | + write_packet(f, make_eth_ip_tcp(EW,OU,"10.10.1.20","10.10.2.60",56000,4840,opcua_open_none()), ts+13.1+i*0.2) |
| 193 | + # MQTT (no TLS, no auth) |
| 194 | + write_packet(f, make_eth_ip_tcp(EW,MQ,"10.10.1.20","10.10.3.10",57000,1883,mqtt_connect()), ts+15) |
| 195 | + for i in range(5): |
| 196 | + write_packet(f, make_eth_ip_tcp(EW,MQ,"10.10.1.20","10.10.3.10",57000,1883, |
| 197 | + mqtt_publish(b"ot/plc/telemetry",f'{{"s":{i},"v":{65+i*2.5}}}'.encode())), ts+15.1+i*0.5) |
| 198 | + # BACnet/IP |
| 199 | + for i in range(3): |
| 200 | + write_packet(f, make_eth_ip_udp(M,BA,"10.10.1.10","10.10.3.20",47808,47808,bacnet_whois()), ts+17+i*0.3) |
| 201 | + # Cross-zone: HTTP from PLC to external |
| 202 | + write_packet(f, make_eth_ip_tcp(P1,"00:1A:2B:FF:FF:01","10.10.1.100","192.168.1.50",60000,80, |
| 203 | + b"GET /update HTTP/1.1\r\nHost: vendor.com\r\n\r\n"), ts+19) |
| 204 | + # Telnet to OT device |
| 205 | + write_packet(f, make_eth_ip_tcp(EW,P1,"10.10.1.20","10.10.1.100",60100,23, |
| 206 | + b"\xff\xfb\x01\xff\xfb\x03"), ts+19.5) |
| 207 | + # Extra Modbus from different master (EW -> PLC) |
| 208 | + for i in range(3): |
| 209 | + write_packet(f, make_eth_ip_tcp(EW,P1,"10.10.1.20","10.10.1.100",52100+i,502, |
| 210 | + modbus_req(30+i,1,0x03,struct.pack(">HH",100,10))), ts+20+i*0.1) |
| 211 | + |
| 212 | + print(f"Generated: {out}") |
| 213 | + print(f"Size: {os.path.getsize(out)} bytes") |
| 214 | + |
| 215 | +if __name__ == "__main__": |
| 216 | + main() |
0 commit comments