interesting experiments and hardware

decoding CH340/CH341 USB messages in Wireshark

This post is part of a series of posts on reverse-engineering Serial protocols using Wireshark.
Here's the other posts in this series:

  1. reverse engineering (USB) serial protocols on windows
  2. decoding CH340/CH341 USB messages in Wireshark

At the end of the last week’s post, I was left with a Wireshark trace of the control software and oven talking to each other. However in Wireshark we see this communication modulated by the USB protocol the CH340-based Serial-to-USB adapter I was using and its Windows driver communicate with. This post is about what was necessary to go from that to the actual Serial data (and ‘metadata’).

If you want to follow along for the rest of the post in Wireshark yourelf, you can download the wireshark capture file and dissector script I ended up writing. The dissector file needs to go in the wireshark plugins directory, or can be loaded using the command line argument -Xlua_script:path/to/ch340.lua.

Here’s what the start of the trace looks like:

  • Frames 1-6 are the “reinjected” USB enumeration messages. They don’t have anything to do with the actual communications going on but rather are the OS and the USB device exchanging information about each other and establishing a connection.
  • Frames 7-30 are “Control” request. They are all sent in a burst of activity (within 10ms) at the start of the connection, so it seems likely that this is the driver enabling and configuring the adapter.
  • From Frame 31 onwards, there are only “Bulk” messages. The majority of them are “Bulk in”, but there is some “Bulk out” messages that seem to align with the initial connection and some commands triggered later.

This seems promising already. There is a clear distinction between the setup part and the rest of the trace, and the “Bulk” messages seem to be the Serial communication itself. My main goals at this point were the following:

  • parse the control requests to get more information about the Serial connection
  • figure out where and how the actual serial data is encoded
    • does the protocol use the RS232 control signals?
    • (how) are the control signals intertwined with the serial data in the USB traffic?
  • extract the actual data into its own Wireshark frame to further decode

the control requests

For each control request, there is one frame from the host to the device and one response frame going the other way. The request going to the device has a “Setup Data” section that contains three 16-bit fields that Wireshark calls value, index and length respectively:

For “Control out” requests, the responses seem to be essentially empty. For “Control in” requests on the other hand, the response usually (but not always) has a “CONTROL response data” field with some binary data.

To understand this part better, I did some research on the CH340 IC. This quickly turned up datasheets that contain lots of information about how the IC should be hooked up to work on a custom PCB, but don’t describe the USB communications at all. However I also found out that the CH340 and CH341 use the same drivers, and eventually I found the source code for the Linux kernel driver for CH340/1-based serial ports.

Right at the top of the file there is as section with very promising defines:

#define CH341_REQ_READ_VERSION 0x5F
#define CH341_REQ_WRITE_REG    0x9A
#define CH341_REQ_READ_REG     0x95
#define CH341_REQ_SERIAL_INIT  0xA1
#define CH341_REQ_MODEM_CTRL   0xA4

#define CH341_REG_BREAK        0x05
#define CH341_REG_PRESCALER    0x12
#define CH341_REG_DIVISOR      0x13
#define CH341_REG_LCR          0x18
#define CH341_REG_LCR2         0x25

I checked these values against the bRequest field of the first control request, and much to my satisfaction found a match right away:

bRequest: 161
161 = 0xA1 = CH341_REQ_SERIAL_INIT

Off to a good start! Looking through the file, this constant is used in the following call:

ch341_control_out(dev, CH341_REQ_SERIAL_INIT, 0, 0);

Looking up the function signature, I found that the last two parameters are called… value and index! Lets look at what other requests the Linux driver makes to figure out how these parameters are used:

static int ch341_control_out(struct usb_device *dev, u8 request, u16 value, u16 index) { ... }
static int ch341_control_in(struct usb_device *dev, u8 request, u16 value, u16 index,
                            char *buf, unsigned bufsize) { ... }

static int ch341_set_baudrate_lcr(struct usb_device *dev,
      struct ch341_private *priv,
      speed_t baud_rate, u8 lcr)
  // ...
  r = ch341_control_out(dev, CH341_REQ_WRITE_REG, CH341_REG_DIVISOR << 8 | CH341_REG_PRESCALER, val);
  // ...
  r = ch341_control_out(dev, CH341_REQ_WRITE_REG, CH341_REG_LCR2 << 8 | CH341_REG_LCR, lcr);
  // ...

static int ch341_break_ctl(struct tty_struct *tty, int break_state)
  // ...
  r = ch341_control_out(port->serial->dev, CH341_REQ_WRITE_REG, ch341_break_reg, reg_contents);
  // ...

static int ch341_set_handshake(struct usb_device *dev, u8 control)
  return ch341_control_out(dev, CH341_REQ_MODEM_CTRL, ~control, 0);

static int ch341_get_status(struct usb_device *dev, struct ch341_private *priv) {
  // ...
  r = ch341_control_in(dev, CH341_REQ_READ_REG, 0x0706, 0, buffer, size);
  // ...

Just looking at these calls and the names of the functions that contain them a lot can be deduced:

  • REQ_WRITE_REG and REQ_READ_REG use the index field to indicate a register to write/read
    • there are registers related to the Serial baudrate (REG_DIVISOR, REG_PRESCALER, REG_LCR, used by ch341_set_baudrate_lcr)
    • there’s status registers at 0x0706

tribulations with Lua in wireshark

This section is intended mostly as a reference for others wanting to write Lua Dissectors for Wireshark, especially for USB frames. If you’re here for the overall process, you can safely skip ahead.

Using this information, I started writing a Wireshark Lua Dissector. Initially I had some trouble navigating the different documentation sources for Wireshark, but eventually I found the Lua sections in the Wireshark developer guide (chapter 10 and 11). Chapter 10 has an introduction on how to get Lua code into Wireshark and a couple brief and a couple examples without much commentary. Chapter 11 has a full API listing of the objects accessible from Lua. There is also information in the Wireshark (Gitlab) Wiki, which is unfortunately essentially unusable because there is no browsable index (!?!).

There isn’t much in terms of a guide about how to write dissectors, how to decide which of the different types you need, how to register them in different cases unfortunately. Here’s what I figured out eventually:

  • register_postdissector(proto) registers a Postdissector

    Postdissectors run uconditionally for all packets. They can inspect the packet and simply return to ignore packets they don’t care about. This is a good way to get something up and running quickly.

  • DissectorTable.get(tbl):add(key, proto) registers a dissector to a “regular” DissectorTable.

    The dissector will only be called when the table tbl is invoked with the exact key, the meaning of which is different for each tbl. There is no centralized documentation for which tables exist and what kinds of keys they expect. The list of regular tables can be printed in the Lua console like this:

    for _,tbl in ipairs(DissectorTable.list()) do print(tbl) end

    This is the best way to register a Dissector, if the appropriate table exists.

  • proto:register_heuristic(tbl, heuristic) registers a dissector to a “heuristic” DissectorTable.

    A heuristic DissectorTable still has to be invoked explicitly by a ‘parent’ Dissector, but there is no key that needs to be set. Instead the heuristic is essentially a predicate that can look at the packet and decide whether to handle it similar to a postdissector. The benefit is that you know the parent dissector has already been called since you only see packets that were passed to the DissectorTable. The list of heuristic tables can be printed in the Lua console like this:

    for _,tbl in ipairs(DissectorTable.list_heuristic()) do print(tbl) end

  • DissectorTable.get(tbl):add_for_decode_as(proto) proposes a Dissector to be chosen by the user for a regular DissectorTable.

    This seems like it should be used in cases where the key can’t be known (for example because it changes ‘randomly’). However for some reason not all DissectorTables are actually assignable in the “Decode As…” dialog

I found a tiny bit of information on the USB dissector tables on the USBPcap page page and continued into the wireshark source code to eventually figure out that epan/dissectors/packet-usb.c creates the usb.control and usb.bulk (regular) DissectorTables. For both of them, key is an integer that will be matched as either:

  • the USB “interface class” (a 16-bit number)

    This is not the one from the Configuration Descriptor Response, but can be found as bInterfaceClass in the USB URB frame of control messages.

  • the USB interface “class”, “subclass”, “protocol” triple encoded into an integer.

    Based on the packet-usb.c source this should be encoded as follows, but this didn’t seem to work for me.

    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)

For some reason neither the usb.control nor the usb.bulk tables show up in the “Decode As…” menu. There are usb.device, usb.product and usb.protocol tables that do show up, but registering a dissector there replaces the default USB dissector which makes it harder to access the “Setup Data”.

analyzing the metadata

With the dissector loaded, it’s now a lot easier to navigate the setup phase in Wireshark:

The Windows driver does some different things than the Linux kernel code, but this write to the DIVISOR and PRESCALER registers is probably the most interesting request. These two values should determine the baud rate used for the Serial connection. I tried for a while to calculate the baud by applying the ch341_get_divisor() function in reverse by hand, but I didn’t get any reasonable results. I also found this deep dive on the topic but still didn’t quite get anywhere.

Eventually I realized I could just open the port at various baudrates using PuTTY and compare the resulting captures. A quick search for “common rs232 baud rates” and a couple attemps later I found that 38400 baud corresponds to the 0x64/0x83 divisor/prescaler combination I saw in the original trace.

the bulk requests

The bulk requests turned out to be simpler than I thought. Some of the “BULK in” frames have a “Leftover Capture Data” field with binary data, while all of the “BULK out” frames do. When I applied this field as a filter usb.capdata during a running trace, it became pretty clear that I was seeing the Serial data: After the initial handshake, there’s a single “in” message almost exactly every second. The changes of the displayed temperature in the oven control software also seems to update on that single second rhythm, so this must be the oven reporting its status. “out” messages are only sent on explicit actions.

But how is the serial data encoded? I originally assumed there would be some escaping or encoding going on to allow sending the RS232 control line data over the same channel. To test this I tried typing into the Serial channel using PuTTY (with the other side of the serial cable disconnected) and was very happy to see the ASCII data showing up in Wireshark directly without any modification.

This is great news for two reasons: there’s no need to decode this part, and since there are no REQ_MODEM messages in the main part of the communication, we already know the protocol doesn’t use the control lines and don’t have to worry about them at all. Looking at the linux driver again, it now seems clear that control signals would be transmitted using the REQ_MODEM control request.

The biggest obstacle in this part was honestly the Wireshark documentation, which is a bit unfortunate. It seems like the Lua interface is a relatively new addition with ongoing development, so hopefully this will improve as well. I do think that it is telling that the Lua section is part of the Developer’s and not the “User’s Guide”, I guess the way I’m using Wireshark here is also a bit further from its designed purpose as I originally realized.