PLC5 – CSPv4 Wireshark Dissector

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.

A big thanks to these two articles from Lynn’s Iatips, specifically:

As well as the Rockwell document that provided valuable PCCC format information (Chapter 6,7):

Wireshark Dissector for PLC5 – CSPv4 + LSAP + PCCC

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.

The code:

-- 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).

Installing LUA Dissector to Wireshark

Now you have enhanced Wireshark to properly dissect your PLC5 packets – at least if they are CSPv4 with PCCC.

  • Save the lua script above to any folder and call the file cspv4.lua
  • Open init.lua in the Wireshark installation directory for editing. In Linux it can be found in /etc/wireshark/init.lua.  You will need Admin privileges on Windows Vista and 7.
  • Comment out the following line in init.lua (single line comments begin with --):
disable_lua = true; do return end;
  • Add the following lines to init.lua (at the very end):
dofile("/path/to/the/file/cspv4.lua")
  • Run Wireshark
  • Load a capture file that has the packets of your custom protocol or start a live capture.

Now you have enhanced Wireshark to properly dissect your PLC5 packets – at least if they are CSPv4 with PCCC.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *