HIDX StealthLink Technical Notes - O-MG/O.MG-Firmware GitHub Wiki

HIDX StealthLink - Technical Notes

General

Each Raw HID Report is 8 bytes. As a full report is transmitted from the USB Host to the cable, it will be relayed to the TCP destination. If a message is smaller than 8 bytes it may not immediately leave the buffer on the USB Host, depending on the Operating System of the USB Host.

When using Linux, you can simply cat file.txt > /dev/hidraw2, as padding is not required. Linux is actually able to transparently send single bytes half duplex in either direction through the Raw HID device, much as you would using a TCP socket. As a result of this, the simplest implementation of a reverse shell on Linux is almost identical to what you would do using netcat, or /dev/tcp.

Padding is primarily an issue on Windows, due to the implementation of the file interface to the Raw HID device. The problem is actually worse than just padding, as Windows also adds a leading byte to each 8 byte report, for an as yet undetermined reason. As a result, Windows programs using the file interface need to add a leading null byte to every 8 byte report, writing them all at the same time, as well as discarding the first byte of every 9 bytes read from the file.

In addition, Windows has a limitation on the number of reports which can be buffered from the device, if the reader is too slow. See [https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/hidclass/ni-hidclass-ioctl_set_num_device_input_buffers]. By default, Windows will buffer up to 32 unread reports. Any reports sent from the device beyond this limit will be discarded, corrupting your data. As a result, it is important to make sure that the Windows program is actually reading the data from the input buffer, to avoid overflowing it.

One way to do this is to simply write really slowly from the cable to Windows clients. Another way is to add an acknowledgment layer on top of the report, with a "report counter" that needs to be acked back to the other end of the TCP connection.

Loops

Because we want to ensure messages come across properly there is a loop on incoming messages that ensures only ascii characters can come across. Many terminals insert or allow the insertion of unicode or other non-printables which can cause issues when interpreting as a command. This loop does slow down processing and can be removed if you ensure messages are clean on the client side. In these reference implementations both sides attempt to clean messages.

Universal Shell

_read vs read

In order to make it easier to understand different ways of processing USB data there are two functions used that split the logic.

_read

Directly calls the pyusb read() function to read data out. Data from USB HID reports is dependent on the USB device and speed. By default the _read function has the following configurable options

  • buffer_limit - The maximum amount of data to read in one loop (this must be a multiple of report_size) (e.g. if report size is 8, it must be 16, 32, .. 64). By default this is 8 (meaning one report will be processed at a time, if this is higher the loop will attempt to read multiple times before giving up)
  • report_size - variable determines the size of the report to read. For O.MG Devices its 8 bytes. -
  • timeout -` determines how long to wait for data. The higher the timeout the more likely you will get data intact, if it is shorter the timeout will expire and data may not be read in time. While the O.MG Devices do not send data until it is read, if the host (e.g. Windows) OS reads the data it could be lost. Setting this to at least 100ms (100) usually is good enough to ensure most data is read. By default this is 100 or 100ms.

In addition, _read() validates the data coming in. This tool is designed primarily for demonistrating shell capabilities as such it strips non-printable ASCII characters:

for _r in rawdata:
    if 32 <= _r <= 126 or _r in [10]:
        procdata+=chr(_r).encode()

This is at the expense of time and additional data. For example this will not send archive data well beacuse the data is primarily non printable bytes! Keep this in mind when working on your own implementations.

Finally the _read() function provides additional logic for validating connectivity. This can be moved out but keeping it in the read allows us to more closely check for recoverable errors. For example a USBTimeoutError is recoverable, however a USBError generally isn't and after several tries causes the _read() function to give up. These are implementation specific and this is all built around pyusb and libusb

read

This function splits out the logic with data processing and allows it to be more compact. Most of the read() functions logic is focused around whether data has come in successfully and how much we want to retry to get that data.

Higher retries means we can recover from more read errors. Sometimes libusb is unable to access a USB endpoint for a period of time (usually no more then a few seconds) which is higher then expected. The flexibility of the read() function allows us to increase retries without changing lots of code.

write

Because writing is simpler then reading and validating data it does not need to be as complex. All data that comes into the write function is read in as a string of bytes. This is then split to the buffer_limit size which matches the desired USB HID report size. These are then stored in a Python list object to allow us to easily collect new messages while sending out data in an ordered fashion.

⚠️ **GitHub.com Fallback** ⚠️