Hello everyone,
I’m currently working on developing a Zeek packet analyzer for ProfinetIO as part of my Master’s thesis.
1-From my understanding, the real-time data exchange in Profinet is not built on traditional transport layer protocols like TCP/UDP, which is why creating a packet analyzer seemed like the right approach.
2-I’ve taken inspiration from(GitHub - kit-dsn/zeek-profinet-analyzer: Profinet packet analyzer plugin for Zeek.) , and my goal is to refine the parser to focus on the most important features that need to be logged. The Spicy ProfinetIO.spicy
parser seems to produce the correct results when parsing the packets. However, Zeek is not generating the expected profinet_dcp.log
file.
The issue appears specifically when handling DCP Identify Request/Response packets. In the analyzer.log
, Zeek reports the following error:
unset optional value
These are the scripts:
#ProfinetIO.spicy:
module ProfinetIO;
import spicy;
%byte-order = spicy::ByteOrder::Network;
public type Packet = unit {
frame_id: uint16;
rtc_frame: RTCFrame if ( 0x8000 <= self.frame_id && self.frame_id <= 0xbbff );
dcp_frame: DCPFrame if ( 0xfefd <= self.frame_id && self.frame_id <= 0xfeff );
data: bytes &eod; # Any remaining bytes (optional)
on %done {print self;}
};
type RTCFrame = unit {
# RTC Data (adjust size to match your observed packets)
data: bytes &size=40; # 40-byte data block (update if needed)
cycle_counter: uint16; # Cycle counter (2 bytes)
data_status: uint8; # Data Status (1 byte)
transfer_status: uint8; # Transfer Status (1 byte)
};
type ServiceIDType = enum {
GET = 0x01,
SET = 0x02,
IDENTIFY = 0x05,
HELLO = 0x06
};
type DCPFrame = unit {
service_id: uint8; # &convert=ServiceIDType($$);
service_type: bitfield(8) {
stype: 0..1;
success: 2;
};
xid: uint32;
response_delay: uint16;
len: uint16 { self.rest = $$; }
blocks: DCPBlock(self)[] &until-including=(self.rest == 0);
var rest: uint16 = 0;
};
type DCPBlock = unit(inout frame: DCPFrame) {
option: uint8;
suboption: uint8;
len: uint16 { frame.rest -= ($$ + 4); }
BlockInfo: uint16 if (frame.service_type.stype == 1);
ip_params: IPParams if (self.option == 1 && self.suboption == 2);
vendor_info: VendorInfo(self.len) if (self.option == 2 && self.suboption == 1);
station_name: StationName(self.len)if (self.option == 2 && self.suboption == 2);
device_id: DeviceID if (self.option == 2 && self.suboption == 3);
device_role: DeviceRole if (self.option == 2 && self.suboption == 4);
data: bytes &size=self.len - 2
if (!((self.option == 1 && self.suboption == 2) ||
(self.option == 2 && self.suboption == 1) ||
(self.option == 2 && self.suboption == 2) ||
(self.option == 2 && self.suboption == 3) ||
(self.option == 2 && self.suboption == 4) ||
(self.len == 0)));
padding: int8 if (self.len % 2 == 1);
on padding { frame.rest -= 1; }
};
type IPParams = unit {
IPaddress: addr &ipv4; # 4 bytes
Subnetmask: addr &ipv4; # 4 bytes
StandardGateway: addr &ipv4; # 4 bytes
};
type VendorInfo = unit(len: uint16) {
DeviceVendorValue: bytes &size=len - 2;
};
type StationName = unit(len: uint16) {
NameOfStation: bytes &size=len - 2;
};
type DeviceID = unit {
vendor_id: uint16;
device_id: uint16;
};
type DeviceRole = unit {
DeviceRoleDetails: uint8;
Reserved: uint8;
};
#Zeek_ProfinetIO.spicy:
module Zeek_ProfinetIO;
import ProfinetIO;
import spicy;
import zeek;
# Converts a ProfinetIO::DCPFrame into a Zeek type.
public function create_dcp_msg(dcp: ProfinetIO::DCPFrame):
tuple<
uint8,
bool,
bool,
bool,
uint32,
uint16> {
return (
dcp.service_id,
dcp.service_type.stype == 0,
dcp.service_type.stype == 1,
dcp.service_type.success == 0,
dcp.xid,
dcp.response_delay
);
}
# Converts a ProfinetIO::DCPBlock into a Zeek type.
public function create_dcp_block(block: ProfinetIO::DCPBlock):
tuple<
uint8,
uint8,
uint16,
bytes,
addr,
addr,
addr,
bytes,
uint16,
uint16,
bytes> {
return (
block.option,
block.suboption,
block.len,
block?.station_name ? block.station_name.NameOfStation : b"",
block?.ip_params ? block.ip_params.IPaddress : 0.0.0.0,
block?.ip_params ? block.ip_params.Subnetmask : 0.0.0.0,
block?.ip_params ? block.ip_params.StandardGateway : 0.0.0.0,
block?.vendor_info ? block.vendor_info.DeviceVendorValue : b"",
block?.device_id ? block.device_id.vendor_id : 0,
block?.device_id ? block.device_id.device_id : 0,
block?.data ? block.data : b""
);
}
# Converts a ProfinetIO::RTCFrame into a Zeek type.
public function create_rtc_msg(rtc: ProfinetIO::RTCFrame):
tuple<
bytes,
uint16,
uint8,
uint8> {
return (
rtc.data,
rtc.cycle_counter,
rtc.data_status,
rtc.transfer_status
);
}
#ProfinetIO.evt
packet analyzer spicy::ProfinetIO:
parse with ProfinetIO::Packet;
import Zeek_ProfinetIO;
on ProfinetIO::Packet::dcp_frame -> event ProfinetIO::dcp_message(self.frame_id, Zeek_ProfinetIO::create_dcp_msg(self.dcp_frame), [Zeek_ProfinetIO::create_dcp_block(block) for block in self.dcp_frame.blocks]);
on ProfinetIO::Packet::rtc_frame -> event ProfinetIO::rtc_message(self.frame_id, Zeek_ProfinetIO::create_rtc_msg(self.rtc_frame));
#ProfinetIO.zeek
@load ./ProfinetIO.hlto
module ProfinetIO;
export {
redef enum Log::ID += { LOG_DCP, LOG_RTC };
# DCP Header Structure
type DCPHeader: record {
service_id: count;
request: bool;
response: bool;
success: bool;
xid: count;
response_delay: count;
};
type DCPBlock: record {
opt: count;
subopt: count;
len: count;
NameOfStation: string;
IPaddress: addr;
Subnetmask: addr;
StandardGateway: addr;
DeviceVendorValue: string;
VendorID: count;
DeviceID: count;
data: string;
};
type DCPInfo: record {
ts: time &log;
frame_id: count &log;
service_id: count &log;
request: bool &log;
response: bool &log;
success: bool &log;
xid: count &log;
response_delay: count &log;
NameOfStation: string &log;
IPaddress: addr &log;
Subnetmask: addr &log;
StandardGateway: addr &log;
DeviceVendorValue: string &log;
VendorID: count &log;
DeviceID: count &log;
data: string &log;
};
# RTC Header Structure
type RTCHeader: record {
data: string;
cycle_counter: count;
data_status: count;
transfer_status: count;
};
# RTC Log Structure
type RTCInfo: record {
ts: time &log;
frame_id: count &log;
cycle_counter: count &log;
data_status: count &log;
transfer_status: count &log;
};
}
event zeek_init() &priority=10
{
PacketAnalyzer::register_packet_analyzer(PacketAnalyzer::ANALYZER_ETHERNET, 0x8892, PacketAnalyzer::ANALYZER_SPICY_PROFINETIO);
PacketAnalyzer::register_packet_analyzer(PacketAnalyzer::ANALYZER_VLAN, 0x8892, PacketAnalyzer::ANALYZER_SPICY_PROFINETIO);
# Create logging streams
Log::create_stream(ProfinetIO::LOG_DCP, [$columns=DCPInfo, $path="profinet_dcp"]);
Log::create_stream(ProfinetIO::LOG_RTC, [$columns=RTCInfo, $path="profinet_rtc"]);
}
# Handle DCP messages
event ProfinetIO::dcp_message(frame_id: count, hdr: DCPHeader, blocks: vector of DCPBlock)
{
local NameOfStation: string;
local IPaddress: addr;
local Subnetmask: addr;
local StandardGateway: addr;
local DeviceVendorValue: string;
local VendorID: count;
local DeviceID: count;
local data: string;
for (i in blocks)
{
local block = blocks[i];
if (block?$NameOfStation && block$NameOfStation != "")
NameOfStation = block$NameOfStation;
if (block?$IPaddress && block$IPaddress != 0.0.0.0)
IPaddress = block$IPaddress;
if (block?$Subnetmask && block$Subnetmask != 0.0.0.0)
Subnetmask = block$Subnetmask;
if (block?$StandardGateway && block$StandardGateway != 0.0.0.0)
StandardGateway = block$StandardGateway;
if (block?$DeviceVendorValue && block$DeviceVendorValue != "")
DeviceVendorValue = block$DeviceVendorValue;
if (block?$VendorID && block$VendorID != 0)
VendorID = block$VendorID;
if (block?$DeviceID && block$DeviceID != 0)
DeviceID = block$DeviceID;
if (block?$data && block$data != "")
data = block$data;
}
local rec: DCPInfo = [
$ts = network_time(),
$frame_id = frame_id,
$service_id = hdr$service_id,
$request = hdr$request,
$response = hdr$response,
$success = hdr$success,
$xid = hdr$xid,
$response_delay = hdr$response_delay,
$NameOfStation = NameOfStation,
$IPaddress = IPaddress,
$Subnetmask = Subnetmask,
$StandardGateway= StandardGateway,
$DeviceVendorValue = DeviceVendorValue,
$VendorID = VendorID,
$DeviceID = DeviceID,
$data = data,
];
Log::write(LOG_DCP, rec);
}
# Handle RTC messages
event ProfinetIO::rtc_message(frame_id: count, hdr: RTCHeader)
{
local rec: RTCInfo = [
$ts = network_time(),
$frame_id = frame_id,
$cycle_counter = hdr$cycle_counter,
$data_status = hdr$data_status,
$transfer_status = hdr$transfer_status
];
Log::write(LOG_RTC, rec);
}
This seems to be related to optional attributes used in DCP blocks. I’ve tried several approaches but unfortunately, none of these attempts have resolved the issue.
3-Additionally, if you have any suggestions on how to integrate the parsed Profinet traffic into conn.log or associate it with connection metadata, that would be very helpful.
This is my first experience working with Zeek and Spicy, and it’s a crucial component of my research. I’d be very thankful for your insights or advice.
@Benjamin_Bannier I will be very grateful if you could help me as soon as possible. Any suggestions or feedback from the community would be greatly appreciated. Thanks everyone in advance!