]>
Commit | Line | Data |
---|---|---|
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", | |
46 | } | |
47 | ||
48 | local reg_enum = { | |
49 | [0x08] = "Analog Channels (LSB)", | |
50 | [0x09] = "Analog Channels (MSB)", | |
51 | [0x06] = "Digital Channels (LSB)", | |
52 | [0x07] = "Digital Channels (MSB)", | |
53 | [0x0f] = "LED Red", | |
54 | [0x10] = "LED Green", | |
55 | [0x11] = "LED Blue", | |
56 | } | |
57 | ||
58 | local i2c_result_enum = { | |
59 | [0x01] = "Error", | |
60 | [0x02] = "OK", | |
61 | } | |
62 | ||
63 | local crypt_cmd_enum = { | |
64 | [0x00] = "Reset", | |
65 | [0x01] = "Sleep", | |
66 | [0x02] = "Idle", | |
67 | [0x03] = "Normal", | |
68 | } | |
69 | ||
70 | local crypt_op_enum = { | |
71 | [0x16] = "Nonce", | |
72 | [0x1b] = "Random", | |
73 | [0x41] = "Sign", | |
74 | } | |
75 | ||
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) | |
79 | ||
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") | |
84 | ||
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) | |
87 | ||
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) | |
90 | ||
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) | |
95 | ||
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) | |
103 | ||
104 | p_logicpro16.fields.rawdata = ProtoField.bytes("logicpro16.rawdata", "Raw Message Data") | |
105 | p_logicpro16.fields.decrypted = ProtoField.bytes("logicpro16.decrypted", "Decrypted message data") | |
106 | ||
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") | |
112 | ||
113 | local iv = 0x354B248E | |
114 | local state = iv | |
115 | local states = {} | |
116 | ||
117 | local request_frame | |
118 | local response_requests = {} | |
119 | local request_responses = {} | |
120 | ||
121 | local request_cmd | |
122 | local response_cmds = {} | |
123 | ||
124 | local crypt_op | |
125 | local crypt_nonce_in | |
126 | local crypt_nonce_out | |
127 | local crypt_sign_out_crc | |
128 | ||
129 | local function iterate_state() | |
130 | local max = bit32.band(state, 0x1f) + 34 | |
131 | --print(string.format("shift -/%i state 0x%x", max, state)) | |
132 | for i = 0, max, 1 do | |
133 | state = bit32.bor( | |
134 | bit32.rshift(state, 1), | |
135 | bit32.lshift( | |
136 | bit32.bxor( | |
137 | state, | |
138 | bit32.rshift(state, 1), | |
139 | bit32.rshift(state, 21), | |
140 | bit32.rshift(state, 31) | |
141 | ), | |
142 | 31 | |
143 | ) | |
144 | ) | |
145 | --print(string.format("shift %i/%i state 0x%x", i, max, state)) | |
146 | end | |
147 | end | |
148 | ||
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) | |
155 | local result = 0 | |
156 | for i = 0, input:len()-4, 4 do | |
157 | result = bit32.bxor( | |
158 | result, | |
159 | input:get_index(i), | |
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) | |
163 | ) | |
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))) | |
165 | end | |
166 | state = result | |
167 | end | |
168 | ||
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()) | |
173 | ||
174 | local ctrl = range(0, 1):uint() | |
175 | local reseed = bit32.btest(ctrl, 0x20) | |
176 | if not pinfo.visited then | |
177 | if reseed then | |
178 | state = iv | |
179 | end | |
180 | states[pinfo.number] = state | |
181 | end | |
182 | ||
183 | local secret = states[pinfo.number] | |
184 | print("out visited", pinfo.visited, string.format("secret 0x%x", secret)) | |
185 | ||
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) | |
189 | local dec | |
190 | if i == 0 then | |
191 | -- only 0x20 and 0x08 are relevant here | |
192 | dec = bit32.band(value, 0x28) | |
193 | else | |
194 | dec = bit32.bxor(value, mask) | |
195 | end | |
196 | out:set_index(i, dec) | |
197 | end | |
198 | ||
199 | tvb = ByteArray.tvb(out, "Decrypted") | |
200 | ||
201 | if not pinfo.visited then | |
202 | if reseed then | |
203 | state = tvb:range(1,4):le_uint() | |
204 | print("reseed", string.format("state 0x%x", secret)) | |
205 | else | |
206 | iterate_state() | |
207 | end | |
208 | end | |
209 | ||
210 | return tvb | |
211 | end | |
212 | ||
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()) | |
217 | ||
218 | if not pinfo.visited then | |
219 | states[pinfo.number] = state | |
220 | end | |
221 | ||
222 | local secret = states[pinfo.number] | |
223 | print("in visited", pinfo.visited, string.format("secret 0x%x", secret)) | |
224 | ||
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) | |
230 | end | |
231 | ||
232 | if not pinfo.visited then | |
233 | iterate_state() | |
234 | end | |
235 | ||
236 | return ByteArray.tvb(out, "Decrypted") | |
237 | end | |
238 | ||
239 | -- Dissect control command messages. | |
240 | local function dissect_command(range, pinfo, tree) | |
241 | ||
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() | |
245 | request_cmd = cmd | |
246 | request_frame = pinfo.number | |
247 | response_frame = request_responses[pinfo.number] | |
248 | ||
249 | pinfo.cols.info = string.format("command: 0x%02x %s", | |
250 | cmd, tostring(range)) | |
251 | ||
252 | if not (response_frame == nil) then | |
253 | tree:add(p_logicpro16.fields.rsp, response_frame):set_generated() | |
254 | end | |
255 | ||
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)) | |
269 | end | |
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)) | |
275 | end | |
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)) | |
291 | end | |
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() | |
295 | end | |
296 | end | |
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 | |
301 | else | |
302 | local item = tree:add(p_logicpro16.fields.unknown, range(2)) | |
303 | item:add_expert_info(PI_UNDECODED, PI_WARN, "Leftover data") | |
304 | end | |
305 | return range:len() | |
306 | end | |
307 | ||
308 | -- Dissect control response messages. | |
309 | local function dissect_response(range, pinfo, tree) | |
310 | local cmd | |
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 | |
315 | else | |
316 | request_frame = response_requests[pinfo.number] | |
317 | request_cmd = response_cmds[pinfo.number] | |
318 | end | |
319 | ||
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)) | |
323 | ||
324 | tree:add(p_logicpro16.fields.req, request_frame):set_generated() | |
325 | tree:add(p_logicpro16.fields.cmd, request_cmd):set_generated() | |
326 | ||
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)) | |
341 | end | |
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 | |
348 | reinit_state() | |
349 | end | |
350 | end | |
351 | crypt_op = nil | |
352 | end | |
353 | else | |
354 | local item = tree:add(p_logicpro16.fields.unknown, range()) | |
355 | item:add_expert_info(PI_UNDECODED, PI_WARN, "Leftover data") | |
356 | end | |
357 | return range:len() | |
358 | end | |
359 | ||
360 | -- Main dissector function. | |
361 | function p_logicpro16.dissector(tvb, pinfo, tree) | |
362 | local transfer_type = tonumber(tostring(f_transfer_type())) | |
363 | ||
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())) | |
369 | ||
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 | |
373 | then | |
374 | pinfo.cols.protocol = p_logicpro16.name | |
375 | ||
376 | local subtree = tree:add(p_logicpro16, tvb(), "Logic Pro 16") | |
377 | subtree:add(p_logicpro16.fields.rawdata, tvb()) | |
378 | ||
379 | local dec | |
380 | if (direction == 0) then | |
381 | dec = decrypt_ep1_out_message(pinfo, tvb) | |
382 | else | |
383 | dec = decrypt_ep1_in_message(pinfo, tvb) | |
384 | end | |
385 | ||
386 | local dectree = subtree:add(p_logicpro16.fields.decrypted, dec()) | |
387 | dectree:set_generated() | |
388 | ||
389 | -- Dispatch to message-specific dissection handler. | |
390 | if (direction == 0) then | |
391 | return dissect_command(dec, pinfo, dectree) | |
392 | else | |
393 | return dissect_response(dec, pinfo, dectree) | |
394 | end | |
395 | end | |
396 | end | |
397 | return 0 | |
398 | end | |
399 | ||
400 | -- Register Logic Pro 16 protocol dissector during initialization. | |
401 | function p_logicpro16.init() | |
402 | local usb_product_dissectors = DissectorTable.get("usb.product") | |
403 | ||
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) | |
410 | ||
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") | |
416 | ||
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) | |
421 | end |