]>
Commit | Line | Data |
---|---|---|
00b7a9ee JL |
1 | -- Logic Pro 16 protocol dissector for Wireshark |
2 | -- | |
3 | -- Copyright (C) 2016-2017 Jan Luebbe <jluebbe@lasnet.de> | |
4 | -- | |
5 | -- based on the LWLA dissector, which is | |
6 | -- Copyright (C) 2015 Stefan Bruens <stefan.bruens@rwth-aachen.de> | |
7 | -- | |
8 | -- based on the LWLA dissector, which is | |
9 | -- Copyright (C) 2014 Daniel Elstner <daniel.kitta@gmail.com> | |
10 | -- | |
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. | |
15 | -- | |
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. | |
20 | -- | |
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/>. | |
23 | ||
24 | -- Usage: wireshark -X lua_script:saleae-logicpro16-dissector.lua | |
25 | -- | |
26 | -- Create custom protocol for the Saleae Logic Pro 16 analyzer. | |
27 | p_logicpro16 = Proto("LogicPro16", "Saleae Logic Pro 16 USB Protocol") | |
28 | ||
29 | local ctrl_enum = { | |
30 | [0x00] = "Normal", | |
31 | [0x20] = "Reseed", | |
32 | } | |
33 | ||
34 | local cmd_enum = { | |
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", | |
42 | [0x87] = "Write I2C", | |
43 | [0x88] = "Read I2C", | |
44 | [0x89] = "Wake I2C", | |
45 | [0x8b] = "Read Firmware Version", | |
24f4548b | 46 | [0x86] = "Read Temperature", |
00b7a9ee JL |
47 | } |
48 | ||
49 | local reg_enum = { | |
24f4548b JL |
50 | -- see https://www.hittite.com/content/documents/data_sheet/hmcad1100.pdf |
51 | -- for ADC details | |
52 | [0x03] = "ADC Index", | |
53 | [0x04] = "ADC Value (LSB)", | |
54 | [0x05] = "ADC Value (MSB)", | |
00b7a9ee JL |
55 | [0x08] = "Analog Channels (LSB)", |
56 | [0x09] = "Analog Channels (MSB)", | |
57 | [0x06] = "Digital Channels (LSB)", | |
58 | [0x07] = "Digital Channels (MSB)", | |
59 | [0x0f] = "LED Red", | |
60 | [0x10] = "LED Green", | |
61 | [0x11] = "LED Blue", | |
24f4548b JL |
62 | [0x12] = "Voltage", |
63 | [0x15] = "Bank Power?", | |
64 | [0x17] = "Magic EEPROM Value?", | |
65 | [0x40] = "Capture Status", | |
00b7a9ee JL |
66 | } |
67 | ||
68 | local i2c_result_enum = { | |
69 | [0x01] = "Error", | |
70 | [0x02] = "OK", | |
71 | } | |
72 | ||
73 | local crypt_cmd_enum = { | |
74 | [0x00] = "Reset", | |
75 | [0x01] = "Sleep", | |
76 | [0x02] = "Idle", | |
77 | [0x03] = "Normal", | |
78 | } | |
79 | ||
80 | local crypt_op_enum = { | |
81 | [0x16] = "Nonce", | |
82 | [0x1b] = "Random", | |
83 | [0x41] = "Sign", | |
84 | } | |
85 | ||
86 | -- Create the fields exhibited by the protocol. | |
87 | p_logicpro16.fields.req = ProtoField.new("Request Frame", "logicpro16.req", ftypes.FRAMENUM) | |
88 | p_logicpro16.fields.rsp = ProtoField.new("Response Frame", "logicpro16.rsp", ftypes.FRAMENUM) | |
89 | ||
90 | p_logicpro16.fields.ctrl = ProtoField.uint8("logicpro16.ctrl", "Control Byte", base.HEX, ctrl_enum) | |
91 | p_logicpro16.fields.cmd = ProtoField.uint8("logicpro16.cmd", "Command Byte", base.HEX, cmd_enum) | |
92 | p_logicpro16.fields.size = ProtoField.uint16("logicpro16.size", "Payload Size") | |
93 | p_logicpro16.fields.unknown = ProtoField.bytes("logicpro16.unknown", "Unidentified message data") | |
94 | ||
95 | p_logicpro16.fields.eepromaddr = ProtoField.uint16("logicpro16.eepromaddr", "EEPROM Address", base.HEX_DEC) | |
96 | p_logicpro16.fields.eepromsize = ProtoField.uint16("logicpro16.eepromsize", "EEPROM Size", base.HEX_DEC) | |
97 | ||
98 | p_logicpro16.fields.regaddr = ProtoField.uint8("logicpro16.regaddr", "Register Address", base.HEX_DEC, reg_enum) | |
99 | p_logicpro16.fields.regval = ProtoField.uint8("logicpro16.regval", "Register Value", base.HEX_DEC) | |
100 | ||
101 | p_logicpro16.fields.i2caddr = ProtoField.uint8("logicpro16.i2caddr", "I2C Address", base.HEX_DEC) | |
102 | p_logicpro16.fields.i2csize = ProtoField.uint16("logicpro16.i2csize", "I2C Size", base.HEX_DEC) | |
103 | p_logicpro16.fields.i2cdata = ProtoField.bytes("logicpro16.i2cdata", "I2C Data") | |
104 | p_logicpro16.fields.i2cresult = ProtoField.uint8("logicpro16.i2cresult", "I2C Result", base.HEX_DEC, i2c_result_enum) | |
105 | ||
106 | p_logicpro16.fields.cryptcmd = ProtoField.uint8("logicpro16.cryptcmd", "Crypt Command", base.HEX_DEC, crypt_cmd_enum) | |
107 | p_logicpro16.fields.cryptcount = ProtoField.uint8("logicpro16.cryptcount", "Crypt Count", base.HEX_DEC) | |
108 | p_logicpro16.fields.cryptop = ProtoField.uint8("logicpro16.cryptop", "Crypt Op", base.HEX_DEC, crypt_op_enum) | |
109 | p_logicpro16.fields.cryptp1 = ProtoField.uint8("logicpro16.cryptp1", "Crypt Param 1", base.HEX_DEC) | |
110 | p_logicpro16.fields.cryptp2 = ProtoField.uint16("logicpro16.cryptp2", "Crypt Param 2", base.HEX_DEC) | |
111 | p_logicpro16.fields.cryptdata = ProtoField.bytes("logicpro16.cryptdata", "Crypt Data") | |
112 | p_logicpro16.fields.cryptcrc = ProtoField.uint16("logicpro16.cryptcrc", "Crypt CRC", base.HEX_DEC) | |
113 | ||
114 | p_logicpro16.fields.rawdata = ProtoField.bytes("logicpro16.rawdata", "Raw Message Data") | |
115 | p_logicpro16.fields.decrypted = ProtoField.bytes("logicpro16.decrypted", "Decrypted message data") | |
116 | ||
117 | -- Referenced USB URB dissector fields. | |
118 | local f_urb_type = Field.new("usb.urb_type") | |
119 | local f_transfer_type = Field.new("usb.transfer_type") | |
120 | local f_endpoint = Field.new("usb.endpoint_number.endpoint") | |
121 | local f_direction = Field.new("usb.endpoint_number.direction") | |
122 | ||
123 | local iv = 0x354B248E | |
124 | local state = iv | |
125 | local states = {} | |
126 | ||
127 | local request_frame | |
128 | local response_requests = {} | |
129 | local request_responses = {} | |
130 | ||
131 | local request_cmd | |
132 | local response_cmds = {} | |
133 | ||
134 | local crypt_op | |
135 | local crypt_nonce_in | |
136 | local crypt_nonce_out | |
137 | local crypt_sign_out_crc | |
138 | ||
139 | local function iterate_state() | |
140 | local max = bit32.band(state, 0x1f) + 34 | |
141 | --print(string.format("shift -/%i state 0x%x", max, state)) | |
142 | for i = 0, max, 1 do | |
143 | state = bit32.bor( | |
144 | bit32.rshift(state, 1), | |
145 | bit32.lshift( | |
146 | bit32.bxor( | |
147 | state, | |
148 | bit32.rshift(state, 1), | |
149 | bit32.rshift(state, 21), | |
150 | bit32.rshift(state, 31) | |
151 | ), | |
152 | 31 | |
153 | ) | |
154 | ) | |
155 | --print(string.format("shift %i/%i state 0x%x", i, max, state)) | |
156 | end | |
157 | end | |
158 | ||
159 | local function reinit_state() | |
160 | print("in", crypt_nonce_in) | |
161 | print("out", crypt_nonce_out) | |
162 | print("crc", crypt_sign_out_crc) | |
163 | local input = crypt_nonce_in(4, 16) .. crypt_nonce_out(0, 28) .. crypt_sign_out_crc .. ByteArray.new("00000000") -- add padding | |
164 | print("input", input) | |
165 | local result = 0 | |
166 | for i = 0, input:len()-4, 4 do | |
167 | result = bit32.bxor( | |
168 | result, | |
169 | input:get_index(i), | |
170 | bit32.lshift(input:get_index(i+1), 8), | |
171 | bit32.lshift(input:get_index(i+2), 16), | |
172 | bit32.lshift(input:get_index(i+3), 24) | |
173 | ) | |
174 | 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))) | |
175 | end | |
176 | state = result | |
177 | end | |
178 | ||
179 | -- Decrypt EP1 OUT message | |
180 | local function decrypt_ep1_out_message(pinfo, range) | |
181 | local out = ByteArray.new() | |
182 | out:set_size(range:len()) | |
183 | ||
184 | local ctrl = range(0, 1):uint() | |
185 | local reseed = bit32.btest(ctrl, 0x20) | |
186 | if not pinfo.visited then | |
187 | if reseed then | |
188 | state = iv | |
189 | end | |
190 | states[pinfo.number] = state | |
191 | end | |
192 | ||
193 | local secret = states[pinfo.number] | |
194 | print("out visited", pinfo.visited, string.format("secret 0x%x", secret)) | |
195 | ||
196 | for i = 0, range:len() - 1, 1 do | |
197 | local value = range(i,1):uint() | |
198 | local mask = bit32.extract(secret, 8*(i%4), 8) | |
199 | local dec | |
200 | if i == 0 then | |
201 | -- only 0x20 and 0x08 are relevant here | |
202 | dec = bit32.band(value, 0x28) | |
203 | else | |
204 | dec = bit32.bxor(value, mask) | |
205 | end | |
206 | out:set_index(i, dec) | |
207 | end | |
208 | ||
209 | tvb = ByteArray.tvb(out, "Decrypted") | |
210 | ||
211 | if not pinfo.visited then | |
212 | if reseed then | |
213 | state = tvb:range(1,4):le_uint() | |
214 | print("reseed", string.format("state 0x%x", secret)) | |
215 | else | |
216 | iterate_state() | |
217 | end | |
218 | end | |
219 | ||
220 | return tvb | |
221 | end | |
222 | ||
223 | -- Decrypt EP1 IN message | |
224 | local function decrypt_ep1_in_message(pinfo, range) | |
225 | local out = ByteArray.new() | |
226 | out:set_size(range:len()) | |
227 | ||
228 | if not pinfo.visited then | |
229 | states[pinfo.number] = state | |
230 | end | |
231 | ||
232 | local secret = states[pinfo.number] | |
233 | print("in visited", pinfo.visited, string.format("secret 0x%x", secret)) | |
234 | ||
235 | for i = 0, range:len() - 1, 1 do | |
236 | local value = range(i,1):uint() | |
237 | local mask = bit32.extract(secret, 8*(i%4), 8) | |
238 | local dec = bit32.bxor(value, mask) | |
239 | out:set_index(i, dec) | |
240 | end | |
241 | ||
242 | if not pinfo.visited then | |
243 | iterate_state() | |
244 | end | |
245 | ||
246 | return ByteArray.tvb(out, "Decrypted") | |
247 | end | |
248 | ||
249 | -- Dissect control command messages. | |
250 | local function dissect_command(range, pinfo, tree) | |
251 | ||
252 | tree:add(p_logicpro16.fields.ctrl, range(0, 1)) | |
253 | tree:add(p_logicpro16.fields.cmd, range(1, 1)) | |
254 | local cmd = range(1, 1):uint() | |
255 | request_cmd = cmd | |
256 | request_frame = pinfo.number | |
257 | response_frame = request_responses[pinfo.number] | |
258 | ||
259 | pinfo.cols.info = string.format("command: 0x%02x %s", | |
260 | cmd, tostring(range)) | |
261 | ||
262 | if not (response_frame == nil) then | |
263 | tree:add(p_logicpro16.fields.rsp, response_frame):set_generated() | |
264 | end | |
265 | ||
266 | if cmd == 0x7 then -- eeprom read | |
267 | tree:add(p_logicpro16.fields.unknown, range(2, 2)) | |
268 | tree:add_le(p_logicpro16.fields.eepromaddr, range(4, 2)) | |
269 | tree:add_le(p_logicpro16.fields.eepromsize, range(6, 2)) | |
270 | elseif cmd == 0x7f then -- firmware upload | |
271 | tree:add_le(p_logicpro16.fields.size, range(2, 2)) | |
272 | tree:add(p_logicpro16.fields.rawdata, range(4)) | |
273 | elseif cmd == 0x80 then -- register write | |
274 | tree:add(p_logicpro16.fields.size, range(2, 1)) | |
275 | local t = tree:add(p_logicpro16.fields.rawdata, range(3)) | |
276 | for i = 0, range(2, 1):uint() - 1, 1 do | |
277 | t:add(p_logicpro16.fields.regaddr, range(3+i*2, 1)) | |
278 | t:add(p_logicpro16.fields.regval, range(3+i*2+1, 1)) | |
279 | end | |
280 | elseif cmd == 0x81 then -- register read | |
281 | tree:add(p_logicpro16.fields.size, range(2, 1)) | |
282 | local t = tree:add(p_logicpro16.fields.rawdata, range(3)) | |
283 | for i = 0, range(2, 1):uint() - 1, 1 do | |
284 | t:add(p_logicpro16.fields.regaddr, range(3+i, 1)) | |
285 | end | |
286 | elseif cmd == 0x87 then -- i2c write | |
287 | tree:add(p_logicpro16.fields.i2caddr, range(2, 1)) | |
288 | tree:add_le(p_logicpro16.fields.i2csize, range(3, 2)) | |
289 | local t = tree:add(p_logicpro16.fields.i2cdata, range(5)) | |
290 | t:add(p_logicpro16.fields.cryptcmd, range(5, 1)) | |
291 | local cryptcmd = range(5, 1):uint() | |
292 | if cryptcmd == 0x03 then | |
293 | t:add(p_logicpro16.fields.cryptcount, range(6, 1)) | |
294 | local cryptdatalen = range(6, 1):uint() - 7 | |
295 | t:add(p_logicpro16.fields.cryptop, range(7, 1)) | |
296 | crypt_op = range(7, 1):uint() | |
297 | t:add(p_logicpro16.fields.cryptp1, range(8, 1)) | |
298 | t:add(p_logicpro16.fields.cryptp2, range(9, 2)) | |
299 | if cryptdatalen > 0 then | |
300 | t:add(p_logicpro16.fields.cryptdata, range(11, cryptdatalen)) | |
301 | end | |
302 | t:add(p_logicpro16.fields.cryptcrc, range(11+cryptdatalen, 2)) | |
303 | if crypt_op == 0x16 then -- nonce | |
304 | crypt_nonce_in = range(11, cryptdatalen):bytes() | |
305 | end | |
306 | end | |
307 | elseif cmd == 0x88 then -- i2c read | |
308 | tree:add(p_logicpro16.fields.i2caddr, range(2, 1)) | |
309 | tree:add_le(p_logicpro16.fields.i2csize, range(3, 2)) | |
310 | elseif cmd == 0x89 then -- i2c wake | |
311 | else | |
312 | local item = tree:add(p_logicpro16.fields.unknown, range(2)) | |
313 | item:add_expert_info(PI_UNDECODED, PI_WARN, "Leftover data") | |
314 | end | |
315 | return range:len() | |
316 | end | |
317 | ||
318 | -- Dissect control response messages. | |
319 | local function dissect_response(range, pinfo, tree) | |
320 | local cmd | |
321 | if not pinfo.visited then | |
322 | response_requests[pinfo.number] = request_frame | |
323 | response_cmds[pinfo.number] = request_cmd | |
324 | request_responses[request_frame] = pinfo.number | |
325 | else | |
326 | request_frame = response_requests[pinfo.number] | |
327 | request_cmd = response_cmds[pinfo.number] | |
328 | end | |
329 | ||
330 | print("visited", pinfo.visited, string.format("request_cmd 0x%x", request_cmd)) | |
331 | pinfo.cols.info = string.format("response: 0x%02x %s", | |
332 | request_cmd, tostring(range)) | |
333 | ||
334 | tree:add(p_logicpro16.fields.req, request_frame):set_generated() | |
335 | tree:add(p_logicpro16.fields.cmd, request_cmd):set_generated() | |
336 | ||
337 | if request_cmd == 0x81 then -- register read | |
24f4548b | 338 | local t = tree:add(p_logicpro16.fields.rawdata, range(0)) |
00b7a9ee JL |
339 | elseif request_cmd == 0x87 then -- i2c write |
340 | tree:add(p_logicpro16.fields.i2cresult, range(0, 1)) | |
341 | local t = tree:add(p_logicpro16.fields.i2cdata, range(1)) | |
342 | elseif request_cmd == 0x88 then -- i2c read | |
343 | tree:add(p_logicpro16.fields.i2cresult, range(0, 1)) | |
344 | local i2cresult = range(0, 1):uint() | |
345 | local t = tree:add(p_logicpro16.fields.i2cdata, range(1)) | |
346 | if i2cresult == 0x02 then | |
347 | t:add(p_logicpro16.fields.cryptcount, range(1, 1)) | |
348 | local cryptdatalen = range(1, 1):uint() - 3 | |
349 | if cryptdatalen > 0 then | |
350 | t:add(p_logicpro16.fields.cryptdata, range(2, cryptdatalen)) | |
351 | end | |
352 | t:add(p_logicpro16.fields.cryptcrc, range(2+cryptdatalen, 2)) | |
353 | if crypt_op == 0x16 then -- nonce | |
354 | crypt_nonce_out = range(2, cryptdatalen):bytes() | |
355 | elseif crypt_op == 0x41 then -- sign | |
356 | crypt_sign_out_crc = range(2+cryptdatalen, 2):bytes() | |
357 | if not pinfo.visited then | |
358 | reinit_state() | |
359 | end | |
360 | end | |
361 | crypt_op = nil | |
362 | end | |
363 | else | |
364 | local item = tree:add(p_logicpro16.fields.unknown, range()) | |
365 | item:add_expert_info(PI_UNDECODED, PI_WARN, "Leftover data") | |
366 | end | |
367 | return range:len() | |
368 | end | |
369 | ||
370 | -- Main dissector function. | |
371 | function p_logicpro16.dissector(tvb, pinfo, tree) | |
372 | local transfer_type = tonumber(tostring(f_transfer_type())) | |
373 | ||
374 | -- Bulk transfers only. | |
375 | if transfer_type == 3 then | |
376 | local urb_type = tonumber(tostring(f_urb_type())) | |
377 | local endpoint = tonumber(tostring(f_endpoint())) | |
378 | local direction = tonumber(tostring(f_direction())) | |
379 | ||
380 | -- Payload-carrying packets only. | |
381 | if (urb_type == 83 and endpoint == 1) -- 'S' - Submit | |
382 | or (urb_type == 67 and endpoint == 1) -- 'C' - Complete | |
383 | then | |
384 | pinfo.cols.protocol = p_logicpro16.name | |
385 | ||
386 | local subtree = tree:add(p_logicpro16, tvb(), "Logic Pro 16") | |
387 | subtree:add(p_logicpro16.fields.rawdata, tvb()) | |
388 | ||
389 | local dec | |
390 | if (direction == 0) then | |
391 | dec = decrypt_ep1_out_message(pinfo, tvb) | |
392 | else | |
393 | dec = decrypt_ep1_in_message(pinfo, tvb) | |
394 | end | |
395 | ||
396 | local dectree = subtree:add(p_logicpro16.fields.decrypted, dec()) | |
397 | dectree:set_generated() | |
398 | ||
399 | -- Dispatch to message-specific dissection handler. | |
400 | if (direction == 0) then | |
401 | return dissect_command(dec, pinfo, dectree) | |
402 | else | |
403 | return dissect_response(dec, pinfo, dectree) | |
404 | end | |
405 | end | |
406 | end | |
407 | return 0 | |
408 | end | |
409 | ||
410 | -- Register Logic Pro 16 protocol dissector during initialization. | |
411 | function p_logicpro16.init() | |
412 | local usb_product_dissectors = DissectorTable.get("usb.product") | |
413 | ||
414 | -- Dissection by vendor+product ID requires that Wireshark can get the | |
415 | -- the device descriptor. Making a USB device available inside a VM | |
416 | -- will make it inaccessible from Linux, so Wireshark cannot fetch the | |
417 | -- descriptor by itself. However, it is sufficient if the guest requests | |
418 | -- the descriptor once while Wireshark is capturing. | |
419 | usb_product_dissectors:add(0x21a91006, p_logicpro16) | |
420 | ||
421 | -- Addendum: Protocol registration based on product ID does not always | |
422 | -- work as desired. Register the protocol on the interface class instead. | |
423 | -- The downside is that it would be a bad idea to put this into the global | |
424 | -- configuration, so one has to make do with -X lua_script: for now. | |
425 | -- local usb_bulk_dissectors = DissectorTable.get("usb.bulk") | |
426 | ||
427 | -- For some reason the "unknown" class ID is sometimes 0xFF and sometimes | |
428 | -- 0xFFFF. Register both to make it work all the time. | |
429 | -- usb_bulk_dissectors:add(0xFF, p_logicpro16) | |
430 | -- usb_bulk_dissectors:add(0xFFFF, p_logicpro16) | |
431 | end |