Skip to content

Instantly share code, notes, and snippets.

@vladaman
Last active May 9, 2025 15:57
Show Gist options
  • Save vladaman/379ccfcf98a6034118487fdca1687e25 to your computer and use it in GitHub Desktop.
Save vladaman/379ccfcf98a6034118487fdca1687e25 to your computer and use it in GitHub Desktop.
A Wireshark plugin for dissecting and analyzing encrypted eSSP communication with ITL SMART Hoppers and other eSSP devices.
-- Filter: _ws.col.info != "Command: Poll" && _ws.col.info != "Response: OK" && !(_ws.col.protocol == "USB")
-- Define the protocol
local my_protocol = Proto("mydevice", "eSSP Protocol")
-- Define protocol fields
local pf_seq_slave_id = ProtoField.uint8("mydevice.seq_slave_id", "Header (SEQ/Slave ID/Len)")
local pf_sequence_flag = ProtoField.bool("mydevice.sequence_flag", "Sequence Flag")
local pf_slave_id = ProtoField.uint8("mydevice.slave_id", "Slave ID")
local pf_data_length = ProtoField.uint8("mydevice.data_length", "Data Length")
local pf_data = ProtoField.bytes("mydevice.data", "Data")
local pf_command_code = ProtoField.uint8("mydevice.command_code", "Command Code")
local pf_response_code = ProtoField.uint8("mydevice.response_code", "Response Code")
-- Encrypted fields
local pf_estex = ProtoField.uint8("mydevice.estex", "STEX")
local pf_elength = ProtoField.uint8("mydevice.elength", "eLENGTH")
local pf_ecount = ProtoField.uint32("mydevice.ecount", "eCOUNT")
local pf_edata = ProtoField.bytes("mydevice.edata", "eDATA")
local pf_epacking = ProtoField.bytes("mydevice.epacking", "ePACKING")
local pf_ecrcl = ProtoField.uint16("mydevice.ecrcl", "eCRCL")
local pf_ecrch = ProtoField.uint16("mydevice.ecrch", "eCRCH")
-- Add the fields to the protocol
my_protocol.fields = {
pf_seq_slave_id,
pf_sequence_flag,
pf_slave_id,
pf_data_length,
pf_data,
pf_command_code,
pf_response_code,
pf_estex,
pf_elength,
pf_ecount,
pf_edata,
pf_epacking,
pf_ecrcl,
pf_ecrch
}
-- Define command codes
local command_codes = {
[0x11] = "Sync",
[0x01] = "Reset",
[0x06] = "Host Protocol Version",
[0x07] = "Poll",
[0x0C] = "Get Serial Number",
[0x09] = "Disable",
[0x0A] = "Enable",
[0x20] = "Get Firmware Version",
[0x21] = "Get Dataset Version",
[0x02] = "Set Inhibits",
[0x08] = "Reject",
[0x17] = "Last Reject Code",
[0x23] = "Get Barcode Reader Configuration",
[0x24] = "Set Barcode Reader Configuration",
[0x25] = "Get Barcode Inhibit",
[0x26] = "Set Barcode Inhibit",
[0x27] = "Get Barcode Data",
[0x54] = "Configure Bezel",
[0x56] = "Poll With Ack",
[0x57] = "Event Ack",
[0x3B] = "Set Denomination Route",
[0x3C] = "Get Denomination Route",
[0x33] = "Payout Amount",
[0x38] = "Halt Payout",
[0x3D] = "Float Amount",
[0x3E] = "Get Min Payout",
[0x46] = "Payout By Denomination",
[0x44] = "Float By Denomination",
[0x52] = "Smart Empty",
[0x53] = "Cashbox Payout Operation Data",
[0x22] = "Get All Levels",
[0x58] = "Get Counters",
[0x59] = "Reset Counters",
[0x30] = "Set Refill Mode",
[0x4A] = "Set Generator",
[0x4B] = "Set Modulus",
[0x4C] = "Request Key Exchange",
[0x5C] = "Enable Payout Device",
[0x5B] = "Disable Payout Device",
[0x4D] = "Set Baud Rate",
[0x60] = "Ssp Set Encryption Key",
[0x61] = "Ssp Encryption Reset To Default",
[0x6F] = "Get Payout Capacity",
[0x74] = "Ssp Download Data Packet",
[0x18] = "Hold",
[0xF1] = "Slave Reset",
[0xEF] = "Read",
[0xEE] = "Note Credit",
[0xED] = "Rejecting",
[0xEC] = "Rejected",
[0xCC] = "Stacking",
[0xEB] = "Stacked",
[0xE9] = "Unsafe Jam",
[0xE8] = "Disabled",
[0xE6] = "Fraud Attempt",
[0xE7] = "Stacker Full",
[0xE1] = "Note Cleared From Front",
[0xE2] = "Note Cleared Into Cashbox",
[0xE3] = "Cashbox Removed",
[0xE4] = "Cashbox Replaced",
[0xE5] = "Barcode Ticket Validated",
[0xD1] = "Barcode Ticket Ack",
[0xE0] = "Note Path Open",
[0xB5] = "Channel Disable",
[0xB6] = "Initialising",
[0xDA] = "Dispensing",
[0xD2] = "Dispensed",
[0xD5] = "Hopper / Payout Jammed",
[0xD7] = "Floating",
[0xD8] = "Floated",
[0xDC] = "Incomplete Payout",
[0xDD] = "Incomplete Float",
[0xB3] = "Smart Emptying",
[0xB4] = "Smart Emptied",
[0xDB] = "Note Stored In Payout",
[0xB0] = "Jam Recovery",
[0xB1] = "Error During Payout",
[0xC9] = "Note Transfered To Stacker",
[0xCE] = "Note Held In Bezel",
[0xCB] = "Note Into Store At Reset",
[0xCA] = "Note Into Stacker At Reset",
[0xD6] = "Payout Halted",
[0x11] = "Sync",
[0x01] = "Reset",
[0x06] = "Host Protocol Version",
[0x07] = "Poll",
[0x0C] = "Get Serial Number",
[0x09] = "Disable",
[0x0A] = "Enable",
[0x20] = "Get Firmware Version",
[0x21] = "Get Dataset Version",
[0x56] = "Poll With Ack",
[0x57] = "Event Ack",
[0x3B] = "Set Denomination Route",
[0x3C] = "Get Denomination Route",
[0x33] = "Payout Amount",
[0x34] = "Set Denomination Level",
[0x38] = "Halt Payout",
[0x3D] = "Float Amount",
[0x3E] = "Get Min Payout",
[0x40] = "Set Coin Mech Inhibits",
[0x46] = "Payout By Denomination",
[0x44] = "Float By Denomination",
[0x50] = "Set Options",
[0x51] = "Get Options",
[0x49] = "Enable Coin Mech/feeder",
[0x52] = "Smart Empty",
[0x53] = "Cashbox Payout Operation Data",
[0x22] = "Get All Levels",
[0x58] = "Get Counters",
[0x59] = "Reset Counters",
[0x4A] = "Set Generator",
[0x4B] = "Set Modulus",
[0x4C] = "Request Key Exchange",
[0x5A] = "Coin Mech Options",
[0x4F] = "Get Build Revision",
[0x37] = "Comms Pass Through",
[0x4D] = "Set Baud Rate",
[0x60] = "Ssp Set Encryption Key",
[0x61] = "Ssp Encryption Reset To Default",
[0x4E] = "Set Cashbox Payout Limit",
[0x5D] = "Coin Stir",
[0x39] = "Payout Amount By Denomination",
[0x74] = "Ssp Download Data Packet",
[0x6A] = "Get Coins Exit",
[0x36] = "Payout Route By Denomination",
[0x6B] = "Get Coin Acceptance",
[0x05] = "Setup Request"
}
-- Define response codes
local response_codes = {
[0xF0] = "OK",
[0xF2] = "COMMAND NOT KNOWN",
[0xF3] = "WRONG NUMBER OF PARAMETERS",
[0xF4] = "INVALID PARAMETERS",
[0xF5] = "COMMAND CANNOT BE PROCESSED",
[0xF6] = "SOFTWARE ERROR",
[0xF8] = "FAIL",
[0xFA] = "KEY NOT SET",
[0xF12] = "Slave Reset",
[0xE82] = "Disabled",
[0xE62] = "Fraud Attempt",
[0xB61] = "Initialising",
[0xDA2] = "Dispensing",
[0xD22] = "Dispensed",
[0xD5] = "Hopper / Payout Jammed",
[0xD7] = "Floating",
[0xD8] = "Floated",
[0xD9] = "Timeout",
[0xDC] = "Incomplete Payout",
[0xDD] = "Incomplete Float",
[0xDE] = "Cashbox Paid",
[0xDF] = "Coin Credit",
[0xC4] = "Coin Mech Jammed",
[0xC5] = "Coin Mech Return Active",
[0xB3] = "Smart Emptying",
[0xB4] = "Smart Emptied",
[0x83] = "Calibration Failed",
[0xCF] = "Device Full",
[0xB7] = "Coin Mech Error",
[0xBD] = "Attached Coin Mech Disabled",
[0xBE] = "Attached Coin Mech Enabled",
[0x9C] = "Coin Cashbox",
[0x9D] = "Coin Payout",
[0xD6] = "Payout Halted"
}
-- CRC-16 calculation function (CRC-16/CMS (swapped bytes))
local function crc16(data)
local CRC_SSP_SEED = 0xFFFF
local CRC_SSP_POLY = 0x8005
local crc = CRC_SSP_SEED
for i = 0, data:len() - 1 do
local byte = data:range(i, 1):uint()
crc = bit.bxor(crc, bit.lshift(byte, 8)) -- XOR with byte shifted left by 8
for j = 1, 8 do
crc = bit.band(crc, 0x8000) ~= 0 and bit.bxor(bit.lshift(crc, 1), CRC_SSP_POLY) or bit.lshift(crc, 1)
end
end
crc = bit.band(crc, 0xFFFF) -- Ensure CRC is within 16-bit range
return crc
end
-- CRC-16 calculation function (CRC-16 forward (X16 + X15 + X2 +1))
local function crc16_forward(data)
local CRC_SSP_SEED = 0xFFFF
local CRC_SSP_POLY = 0xA001
local crc = CRC_SSP_SEED
for i = 0, data:len() - 1 do
local byte = data:range(i, 1):uint()
crc = bit.bxor(crc, byte)
for j = 1, 8 do
crc = bit.band(crc, 0x0001) ~= 0 and bit.bxor(bit.rshift(crc, 1), CRC_SSP_POLY) or bit.rshift(crc, 1)
end
end
crc = bit.band(crc, 0xFFFF) -- Ensure CRC is within 16-bit range
return crc
end
-- Dissector function
function my_protocol.dissector(tvbuf, pinfo, tree)
local offset = 0
local stx_value = tvbuf:range(offset, 1):uint() -- Read 1 byte at offset
-- Check for the header STX value
if stx_value == 0x7F then
pinfo.cols['protocol'] = "eSSP Protocol"
local subtree = tree:add(my_protocol, tvbuf:range())
-- Advance past STX
offset = offset + 1
-- SEQ/Slave ID
local seq_slave_id = tvbuf:range(offset, 1):uint()
local seq_slave_id_field = subtree:add(pf_seq_slave_id, tvbuf:range(offset, 1))
-- Add sequence flag as a subfield
local sequence_flag = bit.band(seq_slave_id, 0x80) >> 7 -- Extract bit 7
seq_slave_id_field:add(pf_sequence_flag, sequence_flag)
-- Extract Slave ID (bits 0-6)
local slave_id = bit.band(seq_slave_id, 0x7F)
seq_slave_id_field:add(pf_slave_id, slave_id)
offset = offset + 1
-- Data Length
local data_length = tvbuf:range(offset, 1):uint()
seq_slave_id_field:add(pf_data_length, tvbuf:range(offset, 1))
offset = offset + 1
-- Check if data_length is valid
-- if offset + data_length > tvbuf:len() then
-- pinfo.cols['info'] = "Invalid data length"
-- return -- Exit the dissector
-- end
-- Data
local data_field = tvbuf:range(offset, data_length)
subtree:add(pf_data, data_field)
-- Check for encrypted data (STEX = 0x7E)
if data_length > 0 and data_field:range(0, 1):uint() == 0x7E then
-- Encrypted data
local eoffset = 0
local estex = data_field:range(eoffset, 1):uint()
subtree:add(pf_estex, data_field:range(eoffset, 1))
eoffset = eoffset + 1
local elength = data_field:range(eoffset, 1):uint()
subtree:add(pf_elength, data_field:range(eoffset, 1))
eoffset = eoffset + 1
local ecount = data_field:range(eoffset, 4):uint()
subtree:add(pf_ecount, data_field:range(eoffset, 4))
eoffset = eoffset + 4
-- Calculate the end of the eData field
local edata_end = eoffset + elength - 6 -- elength does not include STEX, COUNT, the packing or the CRC
local edata = data_field:range(eoffset, elength - 6)
subtree:add(pf_edata, data_field:range(eoffset, elength - 6))
eoffset = edata_end
local epacking_length = data_length - 1 - elength - 2 --data_length - (STEX + eLength + eData + eCRCL + eCRCH)
local epacking = data_field:range(eoffset, epacking_length)
subtree:add(pf_epacking, data_field:range(eoffset, epacking_length))
eoffset = eoffset + epacking_length
local ecrcl = data_field:range(eoffset, 1):uint()
eoffset = eoffset + 1
local ecrch = data_field:range(eoffset, 1):uint()
eoffset = eoffset + 1
subtree:add(pf_ecrcl, ecrcl)
subtree:add(pf_ecrch, ecrch)
local received_ecrc = (ecrch * 256) + ecrcl
-- Calculate CRC on eLENGTH, eCOUNT, and eDATA
local ecrc_data = data_field:range(1, elength) -- From eLENGTH to the end of eDATA
local calculated_ecrc = crc16_forward(ecrc_data)
-- Checksum Validation
if calculated_ecrc == received_ecrc then
local checksum_item = subtree:add("Encrypted Checksum Valid")
checksum_item:set_text("Encrypted Checksum Valid Received: 0x" .. string.format("%X", received_ecrc))
else
local checksum_item = subtree:add("Encrypted Checksum Invalid")
checksum_item:set_text("Encrypted Checksum Invalid (Calculated: 0x" .. string.format("%X", calculated_ecrc) .. ", Received: 0x" .. string.format("%X", received_ecrc) .. ")")
end
pinfo.cols['info'] = "Encrypted Data"
else
-- Not encrypted, check for command/response
if data_length > 0 then
local first_data_byte = data_field:range(0,1):uint()
-- Check for command
if command_codes[first_data_byte] then
local command_code = first_data_byte
local command_item = subtree:add(pf_command_code, tvbuf:range(offset, 1))
command_item:set_text("Command: " .. (command_codes[command_code] or "Unknown Command"))
pinfo.cols['info'] = "Command: " .. (command_codes[command_code] or "Unknown Command")
-- Check for response
elseif response_codes[first_data_byte] then
local response_code = first_data_byte
local response_item = subtree:add(pf_response_code, tvbuf:range(offset, 1))
response_item:set_text("Response: " .. (response_codes[response_code] or "Unknown Response"))
pinfo.cols['info'] = "Response: " .. (response_codes[response_code] or "Unknown Response")
else
pinfo.cols['info'] = "Unknown Command or Response"
end
end
end
offset = offset + data_length
-- Checksum (Low and High Bytes)
local checksum_low = tvbuf:range(offset, 1):uint()
offset = offset + 1
local checksum_high = tvbuf:range(offset, 1):uint()
offset = offset + 1
local received_checksum = (checksum_high * 256) + checksum_low
-- Calculate CRC on SEQ/Slave ID, Data Length, and Data
local crc_data = tvbuf:range(1, 1 + 1 + data_length) -- From SEQ/Slave ID to the end of data
local calculated_checksum = crc16(crc_data)
-- Checksum Validation
if calculated_checksum == received_checksum then
local checksum_item = subtree:add("Checksum Valid")
checksum_item:set_text("Checksum Valid Received: 0x" .. string.format("%X", received_checksum))
else
local checksum_item = subtree:add("Checksum Invalid")
checksum_item:set_text("Checksum Invalid (Calculated: 0x" .. string.format("%X", calculated_checksum) .. ", Received: 0x" .. string.format("%X", received_checksum) .. ")")
end
end
end
-- Register the dissector for USB product ID
local usb_table = DissectorTable.get("usb.product")
local vid = 0x191C -- Replace with your device's Vendor ID
local pid = 0x4104 -- Replace with your device's Product ID
local combined_id = (vid << 16) | pid
usb_table:add(combined_id, my_protocol)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment