1 -- Logic Pro 16 protocol dissector for Wireshark
3 -- Copyright (C) 2016-2017 Jan Luebbe <jluebbe@lasnet.de>
5 -- based on the LWLA dissector, which is
6 -- Copyright (C) 2015 Stefan Bruens <stefan.bruens@rwth-aachen.de>
8 -- based on the LWLA dissector, which is
9 -- Copyright (C) 2014 Daniel Elstner <daniel.kitta@gmail.com>
11 -- This program is free software; you can redistribute it and/or modify
12 -- it under the terms of the GNU General Public License as published by
13 -- the Free Software Foundation; either version 3 of the License, or
14 -- (at your option) any later version.
16 -- This program is distributed in the hope that it will be useful,
17 -- but WITHOUT ANY WARRANTY; without even the implied warranty of
18 -- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 -- GNU General Public License for more details.
21 -- You should have received a copy of the GNU General Public License
22 -- along with this program; if not, see <http://www.gnu.org/licenses/>.
24 -- Usage: wireshark -X lua_script:saleae-logicpro16-dissector.lua
26 -- Create custom protocol for the Saleae Logic Pro 16 analyzer.
27 p_logicpro16 = Proto("LogicPro16", "Saleae Logic Pro 16 USB Protocol")
35 [0x01] = "Start Capture",
36 [0x02] = "Stop Capture",
37 [0x07] = "Read EEPROM",
38 [0x7e] = "Prepare Firmware Upload",
39 [0x7f] = "Upload Firmware",
40 [0x80] = "Write Register",
41 [0x81] = "Read Register",
45 [0x8b] = "Read Firmware Version",
49 [0x08] = "Analog Channels (LSB)",
50 [0x09] = "Analog Channels (MSB)",
51 [0x06] = "Digital Channels (LSB)",
52 [0x07] = "Digital Channels (MSB)",
58 local i2c_result_enum = {
63 local crypt_cmd_enum = {
70 local crypt_op_enum = {
76 -- Create the fields exhibited by the protocol.
77 p_logicpro16.fields.req = ProtoField.new("Request Frame", "logicpro16.req", ftypes.FRAMENUM)
78 p_logicpro16.fields.rsp = ProtoField.new("Response Frame", "logicpro16.rsp", ftypes.FRAMENUM)
80 p_logicpro16.fields.ctrl = ProtoField.uint8("logicpro16.ctrl", "Control Byte", base.HEX, ctrl_enum)
81 p_logicpro16.fields.cmd = ProtoField.uint8("logicpro16.cmd", "Command Byte", base.HEX, cmd_enum)
82 p_logicpro16.fields.size = ProtoField.uint16("logicpro16.size", "Payload Size")
83 p_logicpro16.fields.unknown = ProtoField.bytes("logicpro16.unknown", "Unidentified message data")
85 p_logicpro16.fields.eepromaddr = ProtoField.uint16("logicpro16.eepromaddr", "EEPROM Address", base.HEX_DEC)
86 p_logicpro16.fields.eepromsize = ProtoField.uint16("logicpro16.eepromsize", "EEPROM Size", base.HEX_DEC)
88 p_logicpro16.fields.regaddr = ProtoField.uint8("logicpro16.regaddr", "Register Address", base.HEX_DEC, reg_enum)
89 p_logicpro16.fields.regval = ProtoField.uint8("logicpro16.regval", "Register Value", base.HEX_DEC)
91 p_logicpro16.fields.i2caddr = ProtoField.uint8("logicpro16.i2caddr", "I2C Address", base.HEX_DEC)
92 p_logicpro16.fields.i2csize = ProtoField.uint16("logicpro16.i2csize", "I2C Size", base.HEX_DEC)
93 p_logicpro16.fields.i2cdata = ProtoField.bytes("logicpro16.i2cdata", "I2C Data")
94 p_logicpro16.fields.i2cresult = ProtoField.uint8("logicpro16.i2cresult", "I2C Result", base.HEX_DEC, i2c_result_enum)
96 p_logicpro16.fields.cryptcmd = ProtoField.uint8("logicpro16.cryptcmd", "Crypt Command", base.HEX_DEC, crypt_cmd_enum)
97 p_logicpro16.fields.cryptcount = ProtoField.uint8("logicpro16.cryptcount", "Crypt Count", base.HEX_DEC)
98 p_logicpro16.fields.cryptop = ProtoField.uint8("logicpro16.cryptop", "Crypt Op", base.HEX_DEC, crypt_op_enum)
99 p_logicpro16.fields.cryptp1 = ProtoField.uint8("logicpro16.cryptp1", "Crypt Param 1", base.HEX_DEC)
100 p_logicpro16.fields.cryptp2 = ProtoField.uint16("logicpro16.cryptp2", "Crypt Param 2", base.HEX_DEC)
101 p_logicpro16.fields.cryptdata = ProtoField.bytes("logicpro16.cryptdata", "Crypt Data")
102 p_logicpro16.fields.cryptcrc = ProtoField.uint16("logicpro16.cryptcrc", "Crypt CRC", base.HEX_DEC)
104 p_logicpro16.fields.rawdata = ProtoField.bytes("logicpro16.rawdata", "Raw Message Data")
105 p_logicpro16.fields.decrypted = ProtoField.bytes("logicpro16.decrypted", "Decrypted message data")
107 -- Referenced USB URB dissector fields.
108 local f_urb_type = Field.new("usb.urb_type")
109 local f_transfer_type = Field.new("usb.transfer_type")
110 local f_endpoint = Field.new("usb.endpoint_number.endpoint")
111 local f_direction = Field.new("usb.endpoint_number.direction")
113 local iv = 0x354B248E
118 local response_requests = {}
119 local request_responses = {}
122 local response_cmds = {}
126 local crypt_nonce_out
127 local crypt_sign_out_crc
129 local function iterate_state()
130 local max = bit32.band(state, 0x1f) + 34
131 --print(string.format("shift -/%i state 0x%x", max, state))
134 bit32.rshift(state, 1),
138 bit32.rshift(state, 1),
139 bit32.rshift(state, 21),
140 bit32.rshift(state, 31)
145 --print(string.format("shift %i/%i state 0x%x", i, max, state))
149 local function reinit_state()
150 print("in", crypt_nonce_in)
151 print("out", crypt_nonce_out)
152 print("crc", crypt_sign_out_crc)
153 local input = crypt_nonce_in(4, 16) .. crypt_nonce_out(0, 28) .. crypt_sign_out_crc .. ByteArray.new("00000000") -- add padding
154 print("input", input)
156 for i = 0, input:len()-4, 4 do
160 bit32.lshift(input:get_index(i+1), 8),
161 bit32.lshift(input:get_index(i+2), 16),
162 bit32.lshift(input:get_index(i+3), 24)
164 print(string.format("iterate 0x%x 0x%x 0x%x 0x%x 0x%x", result, input:get_index(i), input:get_index(i+1), input:get_index(i+2), input:get_index(i+3)))
169 -- Decrypt EP1 OUT message
170 local function decrypt_ep1_out_message(pinfo, range)
171 local out = ByteArray.new()
172 out:set_size(range:len())
174 local ctrl = range(0, 1):uint()
175 local reseed = bit32.btest(ctrl, 0x20)
176 if not pinfo.visited then
180 states[pinfo.number] = state
183 local secret = states[pinfo.number]
184 print("out visited", pinfo.visited, string.format("secret 0x%x", secret))
186 for i = 0, range:len() - 1, 1 do
187 local value = range(i,1):uint()
188 local mask = bit32.extract(secret, 8*(i%4), 8)
191 -- only 0x20 and 0x08 are relevant here
192 dec = bit32.band(value, 0x28)
194 dec = bit32.bxor(value, mask)
196 out:set_index(i, dec)
199 tvb = ByteArray.tvb(out, "Decrypted")
201 if not pinfo.visited then
203 state = tvb:range(1,4):le_uint()
204 print("reseed", string.format("state 0x%x", secret))
213 -- Decrypt EP1 IN message
214 local function decrypt_ep1_in_message(pinfo, range)
215 local out = ByteArray.new()
216 out:set_size(range:len())
218 if not pinfo.visited then
219 states[pinfo.number] = state
222 local secret = states[pinfo.number]
223 print("in visited", pinfo.visited, string.format("secret 0x%x", secret))
225 for i = 0, range:len() - 1, 1 do
226 local value = range(i,1):uint()
227 local mask = bit32.extract(secret, 8*(i%4), 8)
228 local dec = bit32.bxor(value, mask)
229 out:set_index(i, dec)
232 if not pinfo.visited then
236 return ByteArray.tvb(out, "Decrypted")
239 -- Dissect control command messages.
240 local function dissect_command(range, pinfo, tree)
242 tree:add(p_logicpro16.fields.ctrl, range(0, 1))
243 tree:add(p_logicpro16.fields.cmd, range(1, 1))
244 local cmd = range(1, 1):uint()
246 request_frame = pinfo.number
247 response_frame = request_responses[pinfo.number]
249 pinfo.cols.info = string.format("command: 0x%02x %s",
250 cmd, tostring(range))
252 if not (response_frame == nil) then
253 tree:add(p_logicpro16.fields.rsp, response_frame):set_generated()
256 if cmd == 0x7 then -- eeprom read
257 tree:add(p_logicpro16.fields.unknown, range(2, 2))
258 tree:add_le(p_logicpro16.fields.eepromaddr, range(4, 2))
259 tree:add_le(p_logicpro16.fields.eepromsize, range(6, 2))
260 elseif cmd == 0x7f then -- firmware upload
261 tree:add_le(p_logicpro16.fields.size, range(2, 2))
262 tree:add(p_logicpro16.fields.rawdata, range(4))
263 elseif cmd == 0x80 then -- register write
264 tree:add(p_logicpro16.fields.size, range(2, 1))
265 local t = tree:add(p_logicpro16.fields.rawdata, range(3))
266 for i = 0, range(2, 1):uint() - 1, 1 do
267 t:add(p_logicpro16.fields.regaddr, range(3+i*2, 1))
268 t:add(p_logicpro16.fields.regval, range(3+i*2+1, 1))
270 elseif cmd == 0x81 then -- register read
271 tree:add(p_logicpro16.fields.size, range(2, 1))
272 local t = tree:add(p_logicpro16.fields.rawdata, range(3))
273 for i = 0, range(2, 1):uint() - 1, 1 do
274 t:add(p_logicpro16.fields.regaddr, range(3+i, 1))
276 elseif cmd == 0x87 then -- i2c write
277 tree:add(p_logicpro16.fields.i2caddr, range(2, 1))
278 tree:add_le(p_logicpro16.fields.i2csize, range(3, 2))
279 local t = tree:add(p_logicpro16.fields.i2cdata, range(5))
280 t:add(p_logicpro16.fields.cryptcmd, range(5, 1))
281 local cryptcmd = range(5, 1):uint()
282 if cryptcmd == 0x03 then
283 t:add(p_logicpro16.fields.cryptcount, range(6, 1))
284 local cryptdatalen = range(6, 1):uint() - 7
285 t:add(p_logicpro16.fields.cryptop, range(7, 1))
286 crypt_op = range(7, 1):uint()
287 t:add(p_logicpro16.fields.cryptp1, range(8, 1))
288 t:add(p_logicpro16.fields.cryptp2, range(9, 2))
289 if cryptdatalen > 0 then
290 t:add(p_logicpro16.fields.cryptdata, range(11, cryptdatalen))
292 t:add(p_logicpro16.fields.cryptcrc, range(11+cryptdatalen, 2))
293 if crypt_op == 0x16 then -- nonce
294 crypt_nonce_in = range(11, cryptdatalen):bytes()
297 elseif cmd == 0x88 then -- i2c read
298 tree:add(p_logicpro16.fields.i2caddr, range(2, 1))
299 tree:add_le(p_logicpro16.fields.i2csize, range(3, 2))
300 elseif cmd == 0x89 then -- i2c wake
302 local item = tree:add(p_logicpro16.fields.unknown, range(2))
303 item:add_expert_info(PI_UNDECODED, PI_WARN, "Leftover data")
308 -- Dissect control response messages.
309 local function dissect_response(range, pinfo, tree)
311 if not pinfo.visited then
312 response_requests[pinfo.number] = request_frame
313 response_cmds[pinfo.number] = request_cmd
314 request_responses[request_frame] = pinfo.number
316 request_frame = response_requests[pinfo.number]
317 request_cmd = response_cmds[pinfo.number]
320 print("visited", pinfo.visited, string.format("request_cmd 0x%x", request_cmd))
321 pinfo.cols.info = string.format("response: 0x%02x %s",
322 request_cmd, tostring(range))
324 tree:add(p_logicpro16.fields.req, request_frame):set_generated()
325 tree:add(p_logicpro16.fields.cmd, request_cmd):set_generated()
327 if request_cmd == 0x81 then -- register read
328 local t = tree:add(p_logicpro16.fields.rawdata, range(1))
329 elseif request_cmd == 0x87 then -- i2c write
330 tree:add(p_logicpro16.fields.i2cresult, range(0, 1))
331 local t = tree:add(p_logicpro16.fields.i2cdata, range(1))
332 elseif request_cmd == 0x88 then -- i2c read
333 tree:add(p_logicpro16.fields.i2cresult, range(0, 1))
334 local i2cresult = range(0, 1):uint()
335 local t = tree:add(p_logicpro16.fields.i2cdata, range(1))
336 if i2cresult == 0x02 then
337 t:add(p_logicpro16.fields.cryptcount, range(1, 1))
338 local cryptdatalen = range(1, 1):uint() - 3
339 if cryptdatalen > 0 then
340 t:add(p_logicpro16.fields.cryptdata, range(2, cryptdatalen))
342 t:add(p_logicpro16.fields.cryptcrc, range(2+cryptdatalen, 2))
343 if crypt_op == 0x16 then -- nonce
344 crypt_nonce_out = range(2, cryptdatalen):bytes()
345 elseif crypt_op == 0x41 then -- sign
346 crypt_sign_out_crc = range(2+cryptdatalen, 2):bytes()
347 if not pinfo.visited then
354 local item = tree:add(p_logicpro16.fields.unknown, range())
355 item:add_expert_info(PI_UNDECODED, PI_WARN, "Leftover data")
360 -- Main dissector function.
361 function p_logicpro16.dissector(tvb, pinfo, tree)
362 local transfer_type = tonumber(tostring(f_transfer_type()))
364 -- Bulk transfers only.
365 if transfer_type == 3 then
366 local urb_type = tonumber(tostring(f_urb_type()))
367 local endpoint = tonumber(tostring(f_endpoint()))
368 local direction = tonumber(tostring(f_direction()))
370 -- Payload-carrying packets only.
371 if (urb_type == 83 and endpoint == 1) -- 'S' - Submit
372 or (urb_type == 67 and endpoint == 1) -- 'C' - Complete
374 pinfo.cols.protocol = p_logicpro16.name
376 local subtree = tree:add(p_logicpro16, tvb(), "Logic Pro 16")
377 subtree:add(p_logicpro16.fields.rawdata, tvb())
380 if (direction == 0) then
381 dec = decrypt_ep1_out_message(pinfo, tvb)
383 dec = decrypt_ep1_in_message(pinfo, tvb)
386 local dectree = subtree:add(p_logicpro16.fields.decrypted, dec())
387 dectree:set_generated()
389 -- Dispatch to message-specific dissection handler.
390 if (direction == 0) then
391 return dissect_command(dec, pinfo, dectree)
393 return dissect_response(dec, pinfo, dectree)
400 -- Register Logic Pro 16 protocol dissector during initialization.
401 function p_logicpro16.init()
402 local usb_product_dissectors = DissectorTable.get("usb.product")
404 -- Dissection by vendor+product ID requires that Wireshark can get the
405 -- the device descriptor. Making a USB device available inside a VM
406 -- will make it inaccessible from Linux, so Wireshark cannot fetch the
407 -- descriptor by itself. However, it is sufficient if the guest requests
408 -- the descriptor once while Wireshark is capturing.
409 usb_product_dissectors:add(0x21a91006, p_logicpro16)
411 -- Addendum: Protocol registration based on product ID does not always
412 -- work as desired. Register the protocol on the interface class instead.
413 -- The downside is that it would be a bad idea to put this into the global
414 -- configuration, so one has to make do with -X lua_script: for now.
415 -- local usb_bulk_dissectors = DissectorTable.get("usb.bulk")
417 -- For some reason the "unknown" class ID is sometimes 0xFF and sometimes
418 -- 0xFFFF. Register both to make it work all the time.
419 -- usb_bulk_dissectors:add(0xFF, p_logicpro16)
420 -- usb_bulk_dissectors:add(0xFFFF, p_logicpro16)