A long time ago I had been looking at PCAP traces of a PLC5 communicating with RsLinx. Wireshark just saw it as a blob of data on top of the TCP header. Well this just would not do. Wireshark provides a nice interface for using LUA to write your own dissector. This is what I ended up doing for the CSPv4 data (which is actually CSPv4 Header + LSAP + PCCC or PC cubed). An added bonus of writing a dissector for an unknown protocol is that the protocol filter will also register the bytes you define, so you can easily filter a packet stream with your newly defined byte fields.
As well as the Rockwell document that provided valuable PCCC format information (Chapter 6,7):
Without further ado – the LUA code for the Wireshark dissector. Following this code include is a screenshot and instructions of how to include this parser within Wireshark.
-- CSPv4 Parser --------------------------------
--
-- Date: July 25, 2012
-- Author: Erik Schweigert
-- E-mail: erik@linuxtips.ca
--
-- Purpose: To decode the CSPv4 Packet
-- CSPv4 + LSAP + PCCC
------------------------------------------------
p_cspv4 = Proto("cspv4","CSPv4")
p_lsap = Proto("lsap","LSAP")
p_pccc = Proto("pccc","PCCC")
-- ----------------- CSPv4 Header ------------
local f_mode = ProtoField.uint8("cspv4.mode", "Mode", base.HEX)
local f_submode = ProtoField.uint8("cspv4.submode", "Submode", base.HEX)
local f_data_length = ProtoField.uint16("cspv4.data_length", "Data Length", base.HEX)
local f_conn_id = ProtoField.uint32("cspv4.conn_id", "Connection ID [slave/server]", base.HEX)
local f_status = ProtoField.uint32("cspv4.status", "Status", base.HEX)
local f_context = ProtoField.bytes("cspv4.context", "Context", base.HEX)
-- ---------------- End CSPv4 Header -----------
-- ------------------ LSAP ---------------------
-- Local
local f_dest = ProtoField.uint8("cspv4.dst", "Destination Byte", base.HEX)
local f_res5 = ProtoField.uint8("cspv4.res5", "Control Byte", base.HEX)
local f_src = ProtoField.uint8("cspv4.src", "Source Byte [Master Address]", base.HEX)
local f_lsap = ProtoField.uint8("cspv4.lsap", "LSAP", base.HEX)
-- Remote
local f_resX = ProtoField.uint8("cspv4.resX", "Mystery Byte", base.HEX)
local f_dst_link = ProtoField.uint16("cspv4.dst_link","Destination Link Address", base.HEX)
local f_dst_station = ProtoField.uint16("cspv4.dst_station", "Destination Station Address", base.HEX)
local f_resY = ProtoField.uint8("cspv4.resY", "Mystery Byte 2", base.HEX)
local f_src_link = ProtoField.uint16("cspv4.src_link", "Source Link Address", base.HEX)
local f_src_station = ProtoField.uint16("cspv4.src_station", "Source Station Address", base.HEX)
local f_resZ = ProtoField.uint8("cspv4.resZ", "Mystery Byte 3", base.HEX)
-- ------------------ End LSAP ------------------
-- ------------------ PCCC ----------------------
local f_pccc_command = ProtoField.uint8("cspv4.pccc_command", "Command Code", base.HEX)
local f_pccc_sts = ProtoField.uint8("cspv4.pccc_sts", "Status Code", base.HEX)
local f_pccc_tns = ProtoField.uint16("cspv4.pccc_tns", "Transaction Number", base.HEX)
local f_pccc_fnc = ProtoField.uint8("cspv4.pccc_fnc", "Function Code", base.HEX)
local f_pccc_addr = ProtoField.uint16("cspv4.pccc_addr", "Address of Memory Location", base.HEX)
local f_pccc_size = ProtoField.uint8("cspv4.pccc_size", "Size", base.HEX)
local f_pccc_data = ProtoField.bytes("cspv4.pccc_data", "Data", base.HEX)
-- ------------------ End PCCC -------------------
-- CSPv4 Fields
p_cspv4.fields = {f_mode}
p_cspv4.fields = {f_submode}
p_cspv4.fields = {f_data_length}
p_cspv4.fields = {f_conn_id}
p_cspv4.fields = {f_status}
p_cspv4.fields = {f_context}
p_cspv4.fields = {f_dest}
p_cspv4.fields = {f_res5}
p_cspv4.fields = {f_src}
p_cspv4.fields = {f_lsap}
-- Remote LSAP Fields
p_cspv4.fields = {f_resX}
p_cspv4.fields = {f_dst_link}
p_cspv4.fields = {f_dst_station}
p_cspv4.fields = {f_resY}
p_cspv4.fields = {f_src_link}
p_cspv4.fields = {f_src_station}
p_cspv4.fields = {f_resZ}
-- PCCC Fields
p_cspv4.fields = {f_pccc_command}
p_cspv4.fields = {f_pccc_sts}
p_cspv4.fields = {f_pccc_tns}
p_cspv4.fields = {f_pccc_fnc}
p_cspv4.fields = {f_pccc_addr}
p_cspv4.fields = {f_pccc_size}
p_cspv4.fields = {f_pccc_data}
function build_cspv4_header(buf)
build_request(buf)
build_submode(buf)
subtree:add(f_data_length, buf(2,2))
subtree:add(f_conn_id, buf(4,4))
subtree:add(f_status, buf(8,4))
subtree:add(f_context, buf(12,16))
end
function build_request(buf)
if buf(0,1):uint() == 1 then
subtree:add(f_mode, buf(0,1)):append_text(" (Request)")
elseif buf(0,1):uint() == 2 then
subtree:add(f_mode, buf(0,1)):append_text(" (Response)")
else
subtree:add(f_mode, buf(0,1))
end
end
function build_submode(buf)
if buf(1,1):uint() == 1 then
subtree:add(f_submode, buf(1,1)):append_text(" (Connection)")
elseif buf(1,1):uint() == 7 then
subtree:add(f_submode, buf(1,1)):append_text(" (PCCC)")
else
subtree:add(f_submode, buf(1,1))
end
end
function build_lsap(buf, root)
lsap_tree = root:add(p_lsap, buf(28))
lsap_tree:add(f_dest, buf(28,1))
lsap_tree:add(f_res5, buf(29,1))
lsap_tree:add(f_src, buf(30,1))
if buf(31,1):uint() == 0 then
lsap_tree:add(f_lsap, buf(31,1)):append_text(" (Local Form)")
elseif buf(31,1):uint() == 1 then
lsap_tree:add(f_lsap, buf(31,1)):append_text(" (Remote Form)")
build_lsap_remote(buf, lsap_tree)
else
lsap_tree:add(f_lsap, buf(31,1))
end
end
function build_lsap_remote(buf, lsap_tree)
lsap_tree:add(f_resX, buf(32,1))
lsap_tree:add(f_dst_link, buf(33,2))
lsap_tree:add(f_dst_station, buf(35,2))
lsap_tree:add(f_resY, buf(37,1))
lsap_tree:add(f_src_link, buf(38,2))
lsap_tree:add(f_src_station, buf(40,2))
lsap_tree:add(f_resZ, buf(42,1))
end
function build_pccc(buf, root)
pccc_tree = root:add(p_pccc, buf(32))
-- Ensure its PCCCC
if buf(1,1):uint() ~= 7 then end
if buf(31,1):uint() == 1 then
offset = 11
else
offset = 0
end
pccc_tree:add(f_pccc_command, buf(32 + offset, 1))
pccc_tree:add(f_pccc_sts, buf(33 + offset, 1))
pccc_tree:add(f_pccc_tns, buf(34 + offset, 2))
pccc_tree:add(f_pccc_fnc, buf(36 + offset, 1))
pccc_tree:add(f_pccc_addr, buf(37 + offset, 2))
pccc_tree:add(f_pccc_size, buf(39 + offset, 1))
pccc_tree:add(f_pccc_data, buf(40 + offset, buf:len() - (40 + offset)))
end
-- cspv4 dissector function
function p_cspv4.dissector (buf, pkt, root)
-- validate packet length is adequate, otherwise quit
if buf:len() == 0 then return end
pkt.cols.protocol = p_cspv4.name
-- create subtree for cspv4
subtree = root:add(p_cspv4, buf(0))
-- add protocol fields to subtree
build_cspv4_header(buf)
build_lsap(buf, root)
build_pccc(buf, root)
-- description of payload
subtree:set_text("CSPv4, CSPv4 Header Information")
-- add debug info if debug field is not nil
if f_debug then
-- write debug values
subtree:add(f_debug, buf:len())
end
end
-- Initialization routine
function p_cspv4.init()
end
-- register a chained dissector for port 2222
local tcp_dissector_table = DissectorTable.get("tcp.port")
dissector = tcp_dissector_table:get_dissector(2222)
-- you can call dissector from function p_cspv4.dissector above
-- so that the previous dissector gets called
tcp_dissector_table:add(2222, p_cspv4)
As you can see there is nothing ground breaking in this parser, and the code itself is quite rudimentary. A great enhancement would be to add the textual value of what the PCCC command vs function code actually equates to (read bit, write bit, etc).
Now you have enhanced Wireshark to properly dissect your PLC5 packets – at least if they are CSPv4 with PCCC.
Now you have enhanced Wireshark to properly dissect your PLC5 packets – at least if they are CSPv4 with PCCC.