saleae-logicpro16: Add wireshark dissector
authorJan Luebbe <jluebbe@debian.org>
Sun, 25 Jun 2017 19:25:49 +0000 (21:25 +0200)
committerJan Luebbe <jluebbe@debian.org>
Sun, 25 Jun 2017 19:25:49 +0000 (21:25 +0200)
debug/saleae-logicpro16/saleae-logicpro16-dissector.lua [new file with mode: 0644]

diff --git a/debug/saleae-logicpro16/saleae-logicpro16-dissector.lua b/debug/saleae-logicpro16/saleae-logicpro16-dissector.lua
new file mode 100644 (file)
index 0000000..64ba4b2
--- /dev/null
@@ -0,0 +1,421 @@
+-- Logic Pro 16 protocol dissector for Wireshark
+--
+-- Copyright (C) 2016-2017 Jan Luebbe <jluebbe@lasnet.de>
+--
+-- based on the LWLA dissector, which is
+--   Copyright (C) 2015 Stefan Bruens <stefan.bruens@rwth-aachen.de>
+--
+-- based on the LWLA dissector, which is
+--   Copyright (C) 2014 Daniel Elstner <daniel.kitta@gmail.com>
+--
+-- This program is free software; you can redistribute it and/or modify
+-- it under the terms of the GNU General Public License as published by
+-- the Free Software Foundation; either version 3 of the License, or
+-- (at your option) any later version.
+--
+-- This program is distributed in the hope that it will be useful,
+-- but WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+-- GNU General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program; if not, see <http://www.gnu.org/licenses/>.
+
+-- Usage: wireshark -X lua_script:saleae-logicpro16-dissector.lua
+--
+-- Create custom protocol for the Saleae Logic Pro 16 analyzer.
+p_logicpro16 = Proto("LogicPro16", "Saleae Logic Pro 16 USB Protocol")
+
+local ctrl_enum = {
+    [0x00] = "Normal",
+    [0x20] = "Reseed",
+}
+
+local cmd_enum = {
+    [0x01] = "Start Capture",
+    [0x02] = "Stop Capture",
+    [0x07] = "Read EEPROM",
+    [0x7e] = "Prepare Firmware Upload",
+    [0x7f] = "Upload Firmware",
+    [0x80] = "Write Register",
+    [0x81] = "Read Register",
+    [0x87] = "Write I2C",
+    [0x88] = "Read I2C",
+    [0x89] = "Wake I2C",
+    [0x8b] = "Read Firmware Version",
+}
+
+local reg_enum = {
+    [0x08] = "Analog Channels (LSB)",
+    [0x09] = "Analog Channels (MSB)",
+    [0x06] = "Digital Channels (LSB)",
+    [0x07] = "Digital Channels (MSB)",
+    [0x0f] = "LED Red",
+    [0x10] = "LED Green",
+    [0x11] = "LED Blue",
+}
+
+local i2c_result_enum = {
+    [0x01] = "Error",
+    [0x02] = "OK",
+}
+
+local crypt_cmd_enum = {
+    [0x00] = "Reset",
+    [0x01] = "Sleep",
+    [0x02] = "Idle",
+    [0x03] = "Normal",
+}
+
+local crypt_op_enum = {
+    [0x16] = "Nonce",
+    [0x1b] = "Random",
+    [0x41] = "Sign",
+}
+
+-- Create the fields exhibited by the protocol.
+p_logicpro16.fields.req  = ProtoField.new("Request Frame", "logicpro16.req", ftypes.FRAMENUM)
+p_logicpro16.fields.rsp  = ProtoField.new("Response Frame", "logicpro16.rsp", ftypes.FRAMENUM)
+
+p_logicpro16.fields.ctrl  = ProtoField.uint8("logicpro16.ctrl", "Control Byte", base.HEX, ctrl_enum)
+p_logicpro16.fields.cmd  = ProtoField.uint8("logicpro16.cmd", "Command Byte", base.HEX, cmd_enum)
+p_logicpro16.fields.size  = ProtoField.uint16("logicpro16.size", "Payload Size")
+p_logicpro16.fields.unknown  = ProtoField.bytes("logicpro16.unknown", "Unidentified message data")
+
+p_logicpro16.fields.eepromaddr  = ProtoField.uint16("logicpro16.eepromaddr", "EEPROM Address", base.HEX_DEC)
+p_logicpro16.fields.eepromsize  = ProtoField.uint16("logicpro16.eepromsize", "EEPROM Size", base.HEX_DEC)
+
+p_logicpro16.fields.regaddr = ProtoField.uint8("logicpro16.regaddr", "Register Address", base.HEX_DEC, reg_enum)
+p_logicpro16.fields.regval  = ProtoField.uint8("logicpro16.regval", "Register Value", base.HEX_DEC)
+
+p_logicpro16.fields.i2caddr  = ProtoField.uint8("logicpro16.i2caddr", "I2C Address", base.HEX_DEC)
+p_logicpro16.fields.i2csize  = ProtoField.uint16("logicpro16.i2csize", "I2C Size", base.HEX_DEC)
+p_logicpro16.fields.i2cdata  = ProtoField.bytes("logicpro16.i2cdata", "I2C Data")
+p_logicpro16.fields.i2cresult  = ProtoField.uint8("logicpro16.i2cresult", "I2C Result", base.HEX_DEC, i2c_result_enum)
+
+p_logicpro16.fields.cryptcmd  = ProtoField.uint8("logicpro16.cryptcmd", "Crypt Command", base.HEX_DEC, crypt_cmd_enum)
+p_logicpro16.fields.cryptcount  = ProtoField.uint8("logicpro16.cryptcount", "Crypt Count", base.HEX_DEC)
+p_logicpro16.fields.cryptop  = ProtoField.uint8("logicpro16.cryptop", "Crypt Op", base.HEX_DEC, crypt_op_enum)
+p_logicpro16.fields.cryptp1  = ProtoField.uint8("logicpro16.cryptp1", "Crypt Param 1", base.HEX_DEC)
+p_logicpro16.fields.cryptp2  = ProtoField.uint16("logicpro16.cryptp2", "Crypt Param 2", base.HEX_DEC)
+p_logicpro16.fields.cryptdata  = ProtoField.bytes("logicpro16.cryptdata", "Crypt Data")
+p_logicpro16.fields.cryptcrc  = ProtoField.uint16("logicpro16.cryptcrc", "Crypt CRC", base.HEX_DEC)
+
+p_logicpro16.fields.rawdata   = ProtoField.bytes("logicpro16.rawdata", "Raw Message Data")
+p_logicpro16.fields.decrypted = ProtoField.bytes("logicpro16.decrypted", "Decrypted message data")
+
+-- Referenced USB URB dissector fields.
+local f_urb_type = Field.new("usb.urb_type")
+local f_transfer_type = Field.new("usb.transfer_type")
+local f_endpoint = Field.new("usb.endpoint_number.endpoint")
+local f_direction = Field.new("usb.endpoint_number.direction")
+
+local iv = 0x354B248E
+local state = iv
+local states = {}
+
+local request_frame
+local response_requests = {}
+local request_responses = {}
+
+local request_cmd
+local response_cmds = {}
+
+local crypt_op
+local crypt_nonce_in
+local crypt_nonce_out
+local crypt_sign_out_crc
+
+local function iterate_state()
+    local max = bit32.band(state, 0x1f) + 34
+    --print(string.format("shift -/%i state 0x%x", max, state))
+    for i = 0, max, 1 do
+        state = bit32.bor(
+            bit32.rshift(state, 1),
+            bit32.lshift(
+                bit32.bxor(
+                    state,
+                    bit32.rshift(state, 1),
+                    bit32.rshift(state, 21),
+                    bit32.rshift(state, 31)
+                ),
+                31
+            )
+        )
+        --print(string.format("shift %i/%i state 0x%x", i, max, state))
+     end
+end
+
+local function reinit_state()
+    print("in", crypt_nonce_in)
+    print("out", crypt_nonce_out)
+    print("crc", crypt_sign_out_crc)
+    local input = crypt_nonce_in(4, 16) .. crypt_nonce_out(0, 28) .. crypt_sign_out_crc .. ByteArray.new("00000000") -- add padding
+    print("input", input)
+    local result = 0
+    for i = 0, input:len()-4, 4 do
+        result = bit32.bxor(
+            result,
+            input:get_index(i),
+            bit32.lshift(input:get_index(i+1), 8),
+            bit32.lshift(input:get_index(i+2), 16),
+            bit32.lshift(input:get_index(i+3), 24)
+        )
+        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)))
+    end
+    state = result
+end
+
+-- Decrypt EP1 OUT message
+local function decrypt_ep1_out_message(pinfo, range)
+    local out = ByteArray.new()
+    out:set_size(range:len())
+    local ctrl = range(0, 1):uint()
+    local reseed = bit32.btest(ctrl, 0x20)
+    if not pinfo.visited then
+        if reseed then
+            state = iv
+        end
+        states[pinfo.number] = state
+    end
+    local secret = states[pinfo.number]
+    print("out visited", pinfo.visited, string.format("secret 0x%x", secret))
+    for i = 0, range:len() - 1, 1 do
+        local value = range(i,1):uint()
+        local mask = bit32.extract(secret, 8*(i%4), 8)
+        local dec
+        if i == 0 then
+            -- only 0x20 and 0x08 are relevant here
+            dec = bit32.band(value, 0x28)
+        else
+            dec = bit32.bxor(value, mask)
+        end
+        out:set_index(i, dec)
+    end
+    tvb = ByteArray.tvb(out, "Decrypted")
+    if not pinfo.visited then
+        if reseed then
+            state = tvb:range(1,4):le_uint()
+            print("reseed", string.format("state 0x%x", secret))
+        else
+            iterate_state()
+        end
+    end
+    return tvb
+end
+
+-- Decrypt EP1 IN message
+local function decrypt_ep1_in_message(pinfo, range)
+    local out = ByteArray.new()
+    out:set_size(range:len())
+
+    if not pinfo.visited then
+       states[pinfo.number] = state
+    end
+
+    local secret = states[pinfo.number]
+    print("in visited", pinfo.visited, string.format("secret 0x%x", secret))
+    for i = 0, range:len() - 1, 1 do
+        local value = range(i,1):uint()
+        local mask = bit32.extract(secret, 8*(i%4), 8)
+        local dec = bit32.bxor(value, mask)
+        out:set_index(i, dec)
+    end
+    if not pinfo.visited then
+        iterate_state()
+    end
+    return ByteArray.tvb(out, "Decrypted")
+end
+
+-- Dissect control command messages.
+local function dissect_command(range, pinfo, tree)
+
+    tree:add(p_logicpro16.fields.ctrl, range(0, 1))
+    tree:add(p_logicpro16.fields.cmd, range(1, 1))
+    local cmd = range(1, 1):uint()
+    request_cmd = cmd
+    request_frame = pinfo.number
+    response_frame = request_responses[pinfo.number]
+
+    pinfo.cols.info = string.format("command:  0x%02x %s",
+                                    cmd, tostring(range))
+
+    if not (response_frame == nil) then
+        tree:add(p_logicpro16.fields.rsp, response_frame):set_generated()
+    end
+
+    if cmd == 0x7 then -- eeprom read
+        tree:add(p_logicpro16.fields.unknown, range(2, 2))
+        tree:add_le(p_logicpro16.fields.eepromaddr, range(4, 2))
+        tree:add_le(p_logicpro16.fields.eepromsize, range(6, 2))
+    elseif cmd == 0x7f then -- firmware upload
+        tree:add_le(p_logicpro16.fields.size, range(2, 2))
+        tree:add(p_logicpro16.fields.rawdata, range(4))
+    elseif cmd == 0x80 then -- register write
+        tree:add(p_logicpro16.fields.size, range(2, 1))
+        local t = tree:add(p_logicpro16.fields.rawdata, range(3))
+        for i = 0, range(2, 1):uint() - 1, 1 do
+            t:add(p_logicpro16.fields.regaddr, range(3+i*2, 1))
+            t:add(p_logicpro16.fields.regval, range(3+i*2+1, 1))
+        end
+    elseif cmd == 0x81 then -- register read
+        tree:add(p_logicpro16.fields.size, range(2, 1))
+        local t = tree:add(p_logicpro16.fields.rawdata, range(3))
+        for i = 0, range(2, 1):uint() - 1, 1 do
+            t:add(p_logicpro16.fields.regaddr, range(3+i, 1))
+        end
+    elseif cmd == 0x87 then -- i2c write
+        tree:add(p_logicpro16.fields.i2caddr, range(2, 1))
+        tree:add_le(p_logicpro16.fields.i2csize, range(3, 2))
+        local t = tree:add(p_logicpro16.fields.i2cdata, range(5))
+        t:add(p_logicpro16.fields.cryptcmd, range(5, 1))
+        local cryptcmd = range(5, 1):uint()
+        if cryptcmd == 0x03 then
+            t:add(p_logicpro16.fields.cryptcount, range(6, 1))
+            local cryptdatalen = range(6, 1):uint() - 7
+            t:add(p_logicpro16.fields.cryptop, range(7, 1))
+            crypt_op = range(7, 1):uint()
+            t:add(p_logicpro16.fields.cryptp1, range(8, 1))
+            t:add(p_logicpro16.fields.cryptp2, range(9, 2))
+            if cryptdatalen > 0 then
+                t:add(p_logicpro16.fields.cryptdata, range(11, cryptdatalen))
+            end
+            t:add(p_logicpro16.fields.cryptcrc, range(11+cryptdatalen, 2))
+            if crypt_op == 0x16 then -- nonce
+                crypt_nonce_in = range(11, cryptdatalen):bytes()
+            end
+        end
+    elseif cmd == 0x88 then -- i2c read
+        tree:add(p_logicpro16.fields.i2caddr, range(2, 1))
+        tree:add_le(p_logicpro16.fields.i2csize, range(3, 2))
+    elseif cmd == 0x89 then -- i2c wake
+    else
+        local item = tree:add(p_logicpro16.fields.unknown, range(2))
+        item:add_expert_info(PI_UNDECODED, PI_WARN, "Leftover data")
+    end
+    return range:len()
+end
+
+-- Dissect control response messages.
+local function dissect_response(range, pinfo, tree)
+    local cmd
+    if not pinfo.visited then
+        response_requests[pinfo.number] = request_frame
+        response_cmds[pinfo.number] = request_cmd
+        request_responses[request_frame] = pinfo.number
+    else
+        request_frame = response_requests[pinfo.number]
+        request_cmd = response_cmds[pinfo.number]
+    end
+
+    print("visited", pinfo.visited, string.format("request_cmd 0x%x", request_cmd))
+    pinfo.cols.info = string.format("response: 0x%02x %s",
+                                    request_cmd, tostring(range))
+
+    tree:add(p_logicpro16.fields.req, request_frame):set_generated()
+    tree:add(p_logicpro16.fields.cmd, request_cmd):set_generated()
+
+    if request_cmd == 0x81 then -- register read
+        local t = tree:add(p_logicpro16.fields.rawdata, range(1))
+    elseif request_cmd == 0x87 then -- i2c write
+        tree:add(p_logicpro16.fields.i2cresult, range(0, 1))
+        local t = tree:add(p_logicpro16.fields.i2cdata, range(1))
+    elseif request_cmd == 0x88 then -- i2c read
+        tree:add(p_logicpro16.fields.i2cresult, range(0, 1))
+        local i2cresult = range(0, 1):uint()
+        local t = tree:add(p_logicpro16.fields.i2cdata, range(1))
+        if i2cresult == 0x02 then
+            t:add(p_logicpro16.fields.cryptcount, range(1, 1))
+            local cryptdatalen = range(1, 1):uint() - 3
+            if cryptdatalen > 0 then
+                t:add(p_logicpro16.fields.cryptdata, range(2, cryptdatalen))
+            end
+            t:add(p_logicpro16.fields.cryptcrc, range(2+cryptdatalen, 2))
+            if crypt_op == 0x16 then -- nonce
+                crypt_nonce_out = range(2, cryptdatalen):bytes()
+            elseif crypt_op == 0x41 then -- sign
+                crypt_sign_out_crc = range(2+cryptdatalen, 2):bytes()
+                if not pinfo.visited then
+                    reinit_state()
+                end
+            end
+            crypt_op = nil
+        end
+    else
+        local item = tree:add(p_logicpro16.fields.unknown, range())
+        item:add_expert_info(PI_UNDECODED, PI_WARN, "Leftover data")
+    end
+    return range:len()
+end
+
+-- Main dissector function.
+function p_logicpro16.dissector(tvb, pinfo, tree)
+    local transfer_type = tonumber(tostring(f_transfer_type()))
+
+    -- Bulk transfers only.
+    if transfer_type == 3 then
+        local urb_type = tonumber(tostring(f_urb_type()))
+        local endpoint = tonumber(tostring(f_endpoint()))
+        local direction = tonumber(tostring(f_direction()))
+
+        -- Payload-carrying packets only.
+        if (urb_type == 83 and endpoint == 1)   -- 'S' - Submit
+            or (urb_type == 67 and endpoint == 1) -- 'C' - Complete
+        then
+            pinfo.cols.protocol = p_logicpro16.name
+
+            local subtree = tree:add(p_logicpro16, tvb(), "Logic Pro 16")
+            subtree:add(p_logicpro16.fields.rawdata, tvb())
+
+            local dec
+            if (direction == 0) then
+               dec = decrypt_ep1_out_message(pinfo, tvb)
+            else
+               dec = decrypt_ep1_in_message(pinfo, tvb)
+            end
+
+            local dectree = subtree:add(p_logicpro16.fields.decrypted, dec())
+            dectree:set_generated()
+
+            -- Dispatch to message-specific dissection handler.
+            if (direction == 0) then
+                return dissect_command(dec, pinfo, dectree)
+            else
+                return dissect_response(dec, pinfo, dectree)
+            end
+        end
+    end
+    return 0
+end
+
+-- Register Logic Pro 16 protocol dissector during initialization.
+function p_logicpro16.init()
+    local usb_product_dissectors = DissectorTable.get("usb.product")
+
+    -- Dissection by vendor+product ID requires that Wireshark can get the
+    -- the device descriptor.  Making a USB device available inside a VM
+    -- will make it inaccessible from Linux, so Wireshark cannot fetch the
+    -- descriptor by itself.  However, it is sufficient if the guest requests
+    -- the descriptor once while Wireshark is capturing.
+    usb_product_dissectors:add(0x21a91006, p_logicpro16)
+
+    -- Addendum: Protocol registration based on product ID does not always
+    -- work as desired.  Register the protocol on the interface class instead.
+    -- The downside is that it would be a bad idea to put this into the global
+    -- configuration, so one has to make do with -X lua_script: for now.
+    -- local usb_bulk_dissectors = DissectorTable.get("usb.bulk")
+
+    -- For some reason the "unknown" class ID is sometimes 0xFF and sometimes
+    -- 0xFFFF.  Register both to make it work all the time.
+    -- usb_bulk_dissectors:add(0xFF, p_logicpro16)
+    -- usb_bulk_dissectors:add(0xFFFF, p_logicpro16)
+end