Last active
May 9, 2025 15:57
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- 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