From eadeb98559ee1f4b896e016c598478fb0aebebcd Mon Sep 17 00:00:00 2001 From: s-ol Date: Sun, 18 Feb 2024 20:17:25 +0100 Subject: clean up and add README --- README.md | 25 +++++++++ captures/handshake.pcapng | Bin 0 -> 16260 bytes captures/ovencap.pcapng.gz | Bin 0 -> 4912 bytes captures/statuscap.pcapng | Bin 0 -> 6500 bytes ch340.lua | 104 ---------------------------------- dissectors/ch340.lua | 104 ++++++++++++++++++++++++++++++++++ dissectors/pl2303.lua | 41 ++++++++++++++ dissectors/t937.lua | 135 +++++++++++++++++++++++++++++++++++++++++++++ handshake.pcapng | Bin 16260 -> 0 bytes ovencap.pcapng.gz | Bin 4912 -> 0 bytes pl2303.lua | 41 -------------- statuscap.pcapng | Bin 6500 -> 0 bytes t937.lua | 135 --------------------------------------------- 13 files changed, 305 insertions(+), 280 deletions(-) create mode 100644 README.md create mode 100755 captures/handshake.pcapng create mode 100755 captures/ovencap.pcapng.gz create mode 100755 captures/statuscap.pcapng delete mode 100755 ch340.lua create mode 100755 dissectors/ch340.lua create mode 100755 dissectors/pl2303.lua create mode 100755 dissectors/t937.lua delete mode 100755 handshake.pcapng delete mode 100755 ovencap.pcapng.gz delete mode 100755 pl2303.lua delete mode 100755 statuscap.pcapng delete mode 100755 t937.lua diff --git a/README.md b/README.md new file mode 100644 index 0000000..e840354 --- /dev/null +++ b/README.md @@ -0,0 +1,25 @@ +# T-937/M Serial Reverse Engineering + +This repo contains tools, notes and data from reverse engineering the Puhui T-937/M Reflow Oven's Serial protocol. + +- `captures`: Wireshark captures of Serial communications +- `dissectors`: Wireshark Lua plugins for dissecting messages + - `ch340.lua`: dissects USB captures for CH340/341-based USB-to-Serial converters + - `pl2303.lua`: dissects USB captures for PL2303-based USB-to-Serial converters + - `t937.lua`: dissects the actual Serial messages + +## Capturing USB-to-Serial communications on Windows + +- install Wireshark with USBPcap + - make sure you [have `USBPcapCMD.exe` in your extcap directory][usbpcapcmd] +- add the Lua scripts to your plugins or instruct wireshark to load them from elsewhere: + - `-Xlua_script:ch340.lua,t937.lua` +- set up capture options + - disable "Capture from all devices connected" + - disable "Capture from newly connected devices" + - enable Inject already connected devices' descriptors..." + - enable only the USB-to-Serial device +- start capturing +- connect from Pb-Free.exe + +[usbpcapcmd]: https://desowin.org/usbpcap/tour.html diff --git a/captures/handshake.pcapng b/captures/handshake.pcapng new file mode 100755 index 0000000..875f79a Binary files /dev/null and b/captures/handshake.pcapng differ diff --git a/captures/ovencap.pcapng.gz b/captures/ovencap.pcapng.gz new file mode 100755 index 0000000..4e63fa2 Binary files /dev/null and b/captures/ovencap.pcapng.gz differ diff --git a/captures/statuscap.pcapng b/captures/statuscap.pcapng new file mode 100755 index 0000000..6968bc6 Binary files /dev/null and b/captures/statuscap.pcapng differ diff --git a/ch340.lua b/ch340.lua deleted file mode 100755 index 14c92bb..0000000 --- a/ch340.lua +++ /dev/null @@ -1,104 +0,0 @@ -local control = Proto("ch340.ctl", "CH340 Serial Control") -local bulk = Proto("ch340.bulk", "CH340 Serial Data") - -local ep_dir_f = Field.new("usb.endpoint_address.direction") - -local REQ_TBL = { - [0x5F] = "CH341_REQ_READ_VERSION", - [0x9A] = "CH341_REQ_WRITE_REG", - [0x95] = "CH341_REQ_READ_REG", - [0xA1] = "CH341_REQ_SERIAL_INIT", - [0xA4] = "CH341_REQ_MODEM_CTRL", -} - -local REG_TBL = { - [0x07] = "STATUS2", - [0x06] = "STATUS", - [0x13] = "DIVISOR", - [0x12] = "PRESCALER", - [0x18] = "LCR", - [0x25] = "LCR2", -} - -control.fields.req = ProtoField.uint8("ch340.ctl.req", "Request", base.HEX, REQ_TBL) -control.fields.reg1 = ProtoField.uint8("ch340.ctl.reg1", "Register 1", base.HEX, REG_TBL) -control.fields.reg2 = ProtoField.uint8("ch340.ctl.reg2", "Register 2", base.HEX, REG_TBL) -control.fields.val1 = ProtoField.uint8("ch340.ctl.val1", "Value 1", base.HEX) -control.fields.val2 = ProtoField.uint8("ch340.ctl.val2", "Value 2", base.HEX) -control.fields.modem = ProtoField.uint8("ch340.ctl.modem", "Modem", base.HEX) - -bulk.fields.data_in = ProtoField.bytes("ch340.bulk.data_in", "Data In") -bulk.fields.data_out = ProtoField.bytes("ch340.bulk.data_out", "Data Out") - -local serial_tbl = DissectorTable.heuristic_new("ch340.serial_data", bulk) - -function control.dissector(buffer, pinfo, tree) - if buffer:captured_len() < 1 then return end - - local sub = tree:add(control, buffer()) - - local req = buffer(0, 1) - sub:add(control.fields.req, req) - - req = REQ_TBL[req:uint()] or "" - pinfo.cols.info:set("CH340 " .. req:sub(11)) - if req == "CH341_REQ_WRITE_REG" or - req == "CH341_REQ_READ_REG" then - - local reg2 = buffer(1, 1) - local reg1 = buffer(2, 1) - sub:add(control.fields.reg1, reg1) - sub:add(control.fields.reg2, reg2) - - reg1 = reg1:uint() - reg2 = reg2:uint() - - pinfo.cols.info:append(string.format(" %s/%s", (REG_TBL[reg1] or reg1), (REG_TBL[reg2] or reg2))) - - if req == "CH341_REQ_WRITE_REG" then - local val2 = buffer(3, 1) - local val1 = buffer(4, 1) - sub:add(control.fields.val1, val1) - sub:add(control.fields.val2, val2) - end - elseif name == "CH341_REQ_MODEM_CTRL" then - sub:add(control.fields.modem, buffer(1)) - end -end - -function bulk.dissector(buffer, pinfo, tree) - if buffer:captured_len() < 1 then return end - - local sub = tree:add(bulk, buffer()) - pinfo.cols.info:set("CH340 Serial") - - local ep_dir = ep_dir_f()() -- 0: h2d, 1: d2h - if ep_dir == 0 then - pinfo.cols.info:append(" out") - pinfo.p2p_dir = P2P_DIR_SENT - sub:add(bulk.fields.data_out, buffer()) - else - pinfo.cols.info:append(" in") - pinfo.p2p_dir = P2P_DIR_RECV - sub:add(bulk.fields.data_in, buffer()) - end - - DissectorTable.try_heuristics("ch340.serial_data", buffer, pinfo, tree) -end - -function usb_protocol_key(class, subclass, protocol) - return bit.bor( - bit.lshift(1, 31), - bit.lshift(bit.band(class, 0xff), 16), - bit.lshift(bit.band(subclass, 0xff), 8), - bit.band(protocol, 0xff) - ) -end - -local ctl_table = DissectorTable.get("usb.control") -ctl_table:add(usb_protocol_key(0xff, 0x01, 0x02), control) -ctl_table:add(0xffff, control) - -local bulk_table = DissectorTable.get("usb.bulk") -bulk_table:add(usb_protocol_key(0xff, 0x01, 0x02), bulk) -bulk_table:add(0xffff, bulk) diff --git a/dissectors/ch340.lua b/dissectors/ch340.lua new file mode 100755 index 0000000..14c92bb --- /dev/null +++ b/dissectors/ch340.lua @@ -0,0 +1,104 @@ +local control = Proto("ch340.ctl", "CH340 Serial Control") +local bulk = Proto("ch340.bulk", "CH340 Serial Data") + +local ep_dir_f = Field.new("usb.endpoint_address.direction") + +local REQ_TBL = { + [0x5F] = "CH341_REQ_READ_VERSION", + [0x9A] = "CH341_REQ_WRITE_REG", + [0x95] = "CH341_REQ_READ_REG", + [0xA1] = "CH341_REQ_SERIAL_INIT", + [0xA4] = "CH341_REQ_MODEM_CTRL", +} + +local REG_TBL = { + [0x07] = "STATUS2", + [0x06] = "STATUS", + [0x13] = "DIVISOR", + [0x12] = "PRESCALER", + [0x18] = "LCR", + [0x25] = "LCR2", +} + +control.fields.req = ProtoField.uint8("ch340.ctl.req", "Request", base.HEX, REQ_TBL) +control.fields.reg1 = ProtoField.uint8("ch340.ctl.reg1", "Register 1", base.HEX, REG_TBL) +control.fields.reg2 = ProtoField.uint8("ch340.ctl.reg2", "Register 2", base.HEX, REG_TBL) +control.fields.val1 = ProtoField.uint8("ch340.ctl.val1", "Value 1", base.HEX) +control.fields.val2 = ProtoField.uint8("ch340.ctl.val2", "Value 2", base.HEX) +control.fields.modem = ProtoField.uint8("ch340.ctl.modem", "Modem", base.HEX) + +bulk.fields.data_in = ProtoField.bytes("ch340.bulk.data_in", "Data In") +bulk.fields.data_out = ProtoField.bytes("ch340.bulk.data_out", "Data Out") + +local serial_tbl = DissectorTable.heuristic_new("ch340.serial_data", bulk) + +function control.dissector(buffer, pinfo, tree) + if buffer:captured_len() < 1 then return end + + local sub = tree:add(control, buffer()) + + local req = buffer(0, 1) + sub:add(control.fields.req, req) + + req = REQ_TBL[req:uint()] or "" + pinfo.cols.info:set("CH340 " .. req:sub(11)) + if req == "CH341_REQ_WRITE_REG" or + req == "CH341_REQ_READ_REG" then + + local reg2 = buffer(1, 1) + local reg1 = buffer(2, 1) + sub:add(control.fields.reg1, reg1) + sub:add(control.fields.reg2, reg2) + + reg1 = reg1:uint() + reg2 = reg2:uint() + + pinfo.cols.info:append(string.format(" %s/%s", (REG_TBL[reg1] or reg1), (REG_TBL[reg2] or reg2))) + + if req == "CH341_REQ_WRITE_REG" then + local val2 = buffer(3, 1) + local val1 = buffer(4, 1) + sub:add(control.fields.val1, val1) + sub:add(control.fields.val2, val2) + end + elseif name == "CH341_REQ_MODEM_CTRL" then + sub:add(control.fields.modem, buffer(1)) + end +end + +function bulk.dissector(buffer, pinfo, tree) + if buffer:captured_len() < 1 then return end + + local sub = tree:add(bulk, buffer()) + pinfo.cols.info:set("CH340 Serial") + + local ep_dir = ep_dir_f()() -- 0: h2d, 1: d2h + if ep_dir == 0 then + pinfo.cols.info:append(" out") + pinfo.p2p_dir = P2P_DIR_SENT + sub:add(bulk.fields.data_out, buffer()) + else + pinfo.cols.info:append(" in") + pinfo.p2p_dir = P2P_DIR_RECV + sub:add(bulk.fields.data_in, buffer()) + end + + DissectorTable.try_heuristics("ch340.serial_data", buffer, pinfo, tree) +end + +function usb_protocol_key(class, subclass, protocol) + return bit.bor( + bit.lshift(1, 31), + bit.lshift(bit.band(class, 0xff), 16), + bit.lshift(bit.band(subclass, 0xff), 8), + bit.band(protocol, 0xff) + ) +end + +local ctl_table = DissectorTable.get("usb.control") +ctl_table:add(usb_protocol_key(0xff, 0x01, 0x02), control) +ctl_table:add(0xffff, control) + +local bulk_table = DissectorTable.get("usb.bulk") +bulk_table:add(usb_protocol_key(0xff, 0x01, 0x02), bulk) +bulk_table:add(0xffff, bulk) diff --git a/dissectors/pl2303.lua b/dissectors/pl2303.lua new file mode 100755 index 0000000..6615166 --- /dev/null +++ b/dissectors/pl2303.lua @@ -0,0 +1,41 @@ +local proto = Proto("pl2303.bulk", "PL2303 Serial Data") + +local ep_dir_f = Field.new("usb.endpoint_address.direction") + +proto.fields.data_in_F = ProtoField.bytes("pl2303.bulk.data_in", "Data In") +proto.fields.data_out_F = ProtoField.bytes("pl2303.bulk.data_out", "Data Out") + +local serial_tbl = DissectorTable.heuristic_new("pl2303.serial_data", proto) + +function proto.dissector(buffer, pinfo, tree) + if buffer:captured_len() < 1 then return end + + local sub = tree:add(proto, buffer()) + pinfo.cols.info:set("PL2303 Serial") + + local ep_dir = ep_dir_f()() -- 0: h2d, 1: d2h + if ep_dir == 0 then + pinfo.cols.info:append(" out") + pinfo.p2p_dir = P2P_DIR_SENT + sub:add(proto.fields.data_out, buffer()) + else + pinfo.cols.info:append(" in") + pinfo.p2p_dir = P2P_DIR_RECV + sub:add(proto.fields.data_in, buffer()) + end + + DissectorTable.try_heuristics("pl2303.serial_data", buffer, pinfo, tree) +end + +function usb_protocol_key(class, subclass, protocol) + return bit.bor( + bit.lshift(1, 31), + bit.lshift(bit.band(class, 0xff), 16), + bit.lshift(bit.band(subclass, 0xff), 8), + bit.band(protocol, 0xff) + ) +end + +local bulk_table = DissectorTable.get("usb.bulk") +bulk_table:add(usb_protocol_key(0xff, 0x00, 0x00), proto) +bulk_table:add(0xffff, proto) diff --git a/dissectors/t937.lua b/dissectors/t937.lua new file mode 100755 index 0000000..9d37ba6 --- /dev/null +++ b/dissectors/t937.lua @@ -0,0 +1,135 @@ +local proto = Proto("t937", "T-937/M Serial Control") + +local FUN_TABLE = { + [0x01] = "REQUEST_CONN", + [0xf1] = "CONFIRM_CONN", + [0x02] = "TEMP_STATUS", + [0x08] = "SET_HEATER", + [0x0d] = "SET_EXTRACTOR", +} + +proto.experts.checksum = ProtoExpert.new("t937.checksum", + "T937 Checksum Error", + expert.group.CHECKSUM, expert.severity.ERROR +) + +proto.fields.addr = ProtoField.uint16("t937.address", "Address", base.HEX) +proto.fields.fun = ProtoField.uint8("t937.function", "Function", base.HEX, FUN_TABLE) +proto.fields.len = ProtoField.uint8("t937.length", "Data Length", base.HEX) +proto.fields.checksum = ProtoField.uint16("t937.checksum", "Message Checksum", base.HEX) + +proto.fields.data = ProtoField.bytes("t937.data", "Data") + +local function try_dissect(bytes, pinfo, tree) + -- minimum len is header + checksum + if bytes:len() < 4 + 2 then return end + + local raw_len = bytes:uint(3, 1) + if bytes:len() < 4 + raw_len + 2 then return end + + local buffer = bytes:subset(0, 4+raw_len+2):tvb("T-937 Frame") + local address = buffer(0, 2) + local fun = buffer(2, 1) + local len = buffer(3, 1) + local data = buffer(4, raw_len) + local checksum = buffer(4+raw_len, 2) + + local sum = 0 + for i=0,4+raw_len-1 do + sum = sum + buffer(i, 1):uint() + end + + pinfo.cols.info:set("T937 " .. (FUN_TABLE[fun:uint()] or "")) + pinfo.cols.dst:clear_fence() + pinfo.cols.dst:set(address:bytes():tohex()) + + local sub = tree:add(proto, buffer()) + sub:add(proto.fields.addr, address) + sub:add(proto.fields.fun, fun) + sub:add(proto.fields.len, len) + sub:add(proto.fields.data, data) + + local cs = sub:add(proto.fields.checksum, checksum) + if sum ~= checksum:uint() then + cs:add_proto_expert_info(proto.experts.checksum, string.format( + "expected 0x%04x, got 0x%04x", checksum:uint(), sum + )) + end + + return buffer:len() +end + +local state + +function proto.init() + state = { + error = {}, + rest = { + [P2P_DIR_SENT] = ByteArray.new(), + [P2P_DIR_RECV] = ByteArray.new(), + }, + buffers = {}, + last_number = -1, + } +end + +function proto.dissector(_buf, pinfo, tree) + local buffers = state.buffers[pinfo.number] + + if buffers then + for _,buffer in ipairs(buffers) do + try_dissect(buffer, pinfo, tree) + end + return true + end + + local err = state.error[pinfo.p2p_dir] + if err then + if state.last_number == pinfo.number then + error(err) + else + return + end + end + + if state.last_number >= pinfo.number then + error(string.format( + "dissection order not increasing: %d >= %d", + state.last_number, pinfo.number + )) + end + + local rest = state.rest[pinfo.p2p_dir] + rest:append(_buf:bytes()) + buffers = {} + + while rest:len() > 0 do + local ok, len = pcall(try_dissect, rest, pinfo, tree) + if not ok then + print("GOT ERR", len, pinfo.number) + state.error[pinfo.p2p_dir] = len + state.last_number = pinfo.number + error(len) + elseif not len then + break + end + + table.insert(buffers, rest:subset(0, len)) + + if len == rest:len() then + rest = ByteArray.new() + break + end + + rest = rest:subset(len, rest:len() - len) + end + + state.rest[pinfo.p2p_dir] = rest + state.buffers[pinfo.number] = buffers + state.last_number = pinfo.number + return true +end + +pcall(proto.register_heuristic, proto, "ch340.serial_data", proto.dissector) +pcall(proto.register_heuristic, proto, "pl2303.serial_data", proto.dissector) +DissectorTable.get("rtacser.data"):add_for_decode_as(proto) diff --git a/handshake.pcapng b/handshake.pcapng deleted file mode 100755 index 875f79a..0000000 Binary files a/handshake.pcapng and /dev/null differ diff --git a/ovencap.pcapng.gz b/ovencap.pcapng.gz deleted file mode 100755 index 4e63fa2..0000000 Binary files a/ovencap.pcapng.gz and /dev/null differ diff --git a/pl2303.lua b/pl2303.lua deleted file mode 100755 index 6615166..0000000 --- a/pl2303.lua +++ /dev/null @@ -1,41 +0,0 @@ -local proto = Proto("pl2303.bulk", "PL2303 Serial Data") - -local ep_dir_f = Field.new("usb.endpoint_address.direction") - -proto.fields.data_in_F = ProtoField.bytes("pl2303.bulk.data_in", "Data In") -proto.fields.data_out_F = ProtoField.bytes("pl2303.bulk.data_out", "Data Out") - -local serial_tbl = DissectorTable.heuristic_new("pl2303.serial_data", proto) - -function proto.dissector(buffer, pinfo, tree) - if buffer:captured_len() < 1 then return end - - local sub = tree:add(proto, buffer()) - pinfo.cols.info:set("PL2303 Serial") - - local ep_dir = ep_dir_f()() -- 0: h2d, 1: d2h - if ep_dir == 0 then - pinfo.cols.info:append(" out") - pinfo.p2p_dir = P2P_DIR_SENT - sub:add(proto.fields.data_out, buffer()) - else - pinfo.cols.info:append(" in") - pinfo.p2p_dir = P2P_DIR_RECV - sub:add(proto.fields.data_in, buffer()) - end - - DissectorTable.try_heuristics("pl2303.serial_data", buffer, pinfo, tree) -end - -function usb_protocol_key(class, subclass, protocol) - return bit.bor( - bit.lshift(1, 31), - bit.lshift(bit.band(class, 0xff), 16), - bit.lshift(bit.band(subclass, 0xff), 8), - bit.band(protocol, 0xff) - ) -end - -local bulk_table = DissectorTable.get("usb.bulk") -bulk_table:add(usb_protocol_key(0xff, 0x00, 0x00), proto) -bulk_table:add(0xffff, proto) diff --git a/statuscap.pcapng b/statuscap.pcapng deleted file mode 100755 index 6968bc6..0000000 Binary files a/statuscap.pcapng and /dev/null differ diff --git a/t937.lua b/t937.lua deleted file mode 100755 index 9d37ba6..0000000 --- a/t937.lua +++ /dev/null @@ -1,135 +0,0 @@ -local proto = Proto("t937", "T-937/M Serial Control") - -local FUN_TABLE = { - [0x01] = "REQUEST_CONN", - [0xf1] = "CONFIRM_CONN", - [0x02] = "TEMP_STATUS", - [0x08] = "SET_HEATER", - [0x0d] = "SET_EXTRACTOR", -} - -proto.experts.checksum = ProtoExpert.new("t937.checksum", - "T937 Checksum Error", - expert.group.CHECKSUM, expert.severity.ERROR -) - -proto.fields.addr = ProtoField.uint16("t937.address", "Address", base.HEX) -proto.fields.fun = ProtoField.uint8("t937.function", "Function", base.HEX, FUN_TABLE) -proto.fields.len = ProtoField.uint8("t937.length", "Data Length", base.HEX) -proto.fields.checksum = ProtoField.uint16("t937.checksum", "Message Checksum", base.HEX) - -proto.fields.data = ProtoField.bytes("t937.data", "Data") - -local function try_dissect(bytes, pinfo, tree) - -- minimum len is header + checksum - if bytes:len() < 4 + 2 then return end - - local raw_len = bytes:uint(3, 1) - if bytes:len() < 4 + raw_len + 2 then return end - - local buffer = bytes:subset(0, 4+raw_len+2):tvb("T-937 Frame") - local address = buffer(0, 2) - local fun = buffer(2, 1) - local len = buffer(3, 1) - local data = buffer(4, raw_len) - local checksum = buffer(4+raw_len, 2) - - local sum = 0 - for i=0,4+raw_len-1 do - sum = sum + buffer(i, 1):uint() - end - - pinfo.cols.info:set("T937 " .. (FUN_TABLE[fun:uint()] or "")) - pinfo.cols.dst:clear_fence() - pinfo.cols.dst:set(address:bytes():tohex()) - - local sub = tree:add(proto, buffer()) - sub:add(proto.fields.addr, address) - sub:add(proto.fields.fun, fun) - sub:add(proto.fields.len, len) - sub:add(proto.fields.data, data) - - local cs = sub:add(proto.fields.checksum, checksum) - if sum ~= checksum:uint() then - cs:add_proto_expert_info(proto.experts.checksum, string.format( - "expected 0x%04x, got 0x%04x", checksum:uint(), sum - )) - end - - return buffer:len() -end - -local state - -function proto.init() - state = { - error = {}, - rest = { - [P2P_DIR_SENT] = ByteArray.new(), - [P2P_DIR_RECV] = ByteArray.new(), - }, - buffers = {}, - last_number = -1, - } -end - -function proto.dissector(_buf, pinfo, tree) - local buffers = state.buffers[pinfo.number] - - if buffers then - for _,buffer in ipairs(buffers) do - try_dissect(buffer, pinfo, tree) - end - return true - end - - local err = state.error[pinfo.p2p_dir] - if err then - if state.last_number == pinfo.number then - error(err) - else - return - end - end - - if state.last_number >= pinfo.number then - error(string.format( - "dissection order not increasing: %d >= %d", - state.last_number, pinfo.number - )) - end - - local rest = state.rest[pinfo.p2p_dir] - rest:append(_buf:bytes()) - buffers = {} - - while rest:len() > 0 do - local ok, len = pcall(try_dissect, rest, pinfo, tree) - if not ok then - print("GOT ERR", len, pinfo.number) - state.error[pinfo.p2p_dir] = len - state.last_number = pinfo.number - error(len) - elseif not len then - break - end - - table.insert(buffers, rest:subset(0, len)) - - if len == rest:len() then - rest = ByteArray.new() - break - end - - rest = rest:subset(len, rest:len() - len) - end - - state.rest[pinfo.p2p_dir] = rest - state.buffers[pinfo.number] = buffers - state.last_number = pinfo.number - return true -end - -pcall(proto.register_heuristic, proto, "ch340.serial_data", proto.dissector) -pcall(proto.register_heuristic, proto, "pl2303.serial_data", proto.dissector) -DissectorTable.get("rtacser.data"):add_for_decode_as(proto) -- cgit v1.2.3