During a DLP (Data Loss Prevention) audit, we had to find a way to copy files off a Windows computer with restricted USB functionality. While enumerating USB policies, through our black-box perspective, we noticed only policies against storage devices were in place. Using microcontrollers, software engineering skills, and a neat browser feature called WebSerial, we developed COMfiltrat0r. This tool allows us to exfiltrate documents without installing additional software. Stay a while and read as @rtfmkiesel tells you the story of that discovery.

USB Enumeration

We only knew initially that USB storage should be blocked by something. To first check which storage devices were affected, we tried a USB drive, an Android phone in MTP mode, and a microSD card via a USB reader. All showed up in the device manager but were not accessible in the file explorer or via PowerShell. Since they showed up in the device manager, they are blocked on a software level, not a BIOS/hardware level. To enumerate which additional policies were in place, we plugged in various other USB devices like an NIC, a USB-C docking station, an audio card, as well as a keyboard and mouse. All of those devices were recognized and usable. This concludes that all types of storage devices were blocked, and all other devices were allowed. In this company's context, this makes sense. Employees take devices home and plug in various docking stations, keyboards, and maybe network devices. To allow-list only specific devices would be an administrative nightmare.

IDs and Classes

But how can some USB devices be blocked while others are allowed? Each USB device has a 16-bit vendor ID and a 16-bit device ID. Those IDs are identifiers so the OS knows which drivers to use for each device or if an ID-specific device rule should be triggered. Therefore, a device rule could block all USB keyboards from a vendor/manufacturer different from the one used inside a company. However, this is not considered bulletproof (or maintainable, as mentioned earlier), as microcontrollers can often spoof/fake vendor and product IDs with certain limitations. Generally speaking, blocking based on IDs is not a good practice unless you're blocking IDs for known script-kiddy-ready devices like a Rubber Ducky or a Flipper Zero. The better approach is to block whole classes of devices.

USB device classes are groups to which a USB device belongs based on its function. Some of those classes are

  • Mass Storage (external SSD, flash drive, microSD card reader)
  • Human Interface Device (HID; keyboard, mouse, touchpad, etc.)
  • Audio (soundcard, audio interface, etc)
  • Imaging (cameras, scanners, etc.)
  • Video (webcam, video capture card, etc.)
  • Printers

COMmunication

With mass storage devices blocked, we looked for another way to transfer files, and we immediately thought of serial ports. On Windows, serial ports get registered as COM ports. (ex. COM3) Using a serial terminal, you can talk to a COM port and the connected device over the RS-232 protocol. Since computers today no longer have built-in serial ports, USB to RS-232 adapters are common. The simplified flow looks like this:

Serial communication is still used today for enterprise network equipment or in the professional AV world, for example, to control projectors. You may have seen the old DB9 connector or the Cisco light-blue serial to RJ-45 cable before.

Classic Serial (DB9) Cable Cisco Serial Cable

This would be a viable option to exfiltrate data but would require two parts:

  • A way to talk to a serial port via a Windows computer (a "Serial Terminal")
  • A programmable device that:
    • has a USB-Serial adapter built-in, accessible with the default Windows drivers
    • lets us store data on either a microSD card, USB flash drive, or internal flash

Just use Software X

An easy solution for the first problem would be installing a serial terminal like PuTTY, and there would be an excellent way to communicate with a serial device. There are a few problems with this approach:

  • Installing PuTTY can require administrative privileges
  • Even a portable version would be an exe file downloaded from the web, which should raise alarms
  • There is no nice way of transmitting, for example, a document file via PuTTY

On the other hand, PowerShell has methods to talk to serial ports. Files could be encoded to base64 or hex for easy transfer. But then we are executing a PowerShell script, which should be blocked or, at least, should trigger an alarm.

The Browser can do What?

After searching for a bit, we came across WebUSB.

The WebUSB API provides a way to expose non-standard Universal Serial Bus (USB) compatible device services to the web to make USB safer and easier to use.

Upon reading, some of us recognized this feature. Some manufacturers of sensors or microcontrollers provide web-based flashing tools for their devices. Those flashers can use this API to directly access hardware devices plugged in via USB. It is very user-friendly since the user just needs the proper drivers installed. As a test setup, we used an Arduino-based microcontroller to reply via serial to any newline-terminated string with "hello world". However, we quickly ran into a problem regarding USB endpoints.

Endpoints are virtual cables/channels from the computer to the USB device. A device can have various endpoints for different functions. Each endpoint has a unique address. Endpoint 0, for example, is the control endpoint and is used during the device's setup for exchanging IDs and negotiation speed. Please check out Ben Eater's video for more information on the USB protocol.

The driver that Windows loaded kept registering the "serial input" endpoint of the microcontroller for itself. This means the browser would always get "Access denied" when opening the endpoint. Modifying the driver was not an option since the final PoC should work on an out-of-the-box Windows machine. After a quick online research, reprogramming the USB implementation to accept serial via multiple endpoints seemed daunting. Luckily, searching for example code on WebUSB, we stumbled upon WebSerial.

WebSerial is a way to directly read and write to serial ports registered/handled by the OS. These ports can be accessed using JavaScript, which was exactly what we were looking for.

// from https://developer.chrome.com/en/articles/serial/#open-port
// Prompt user to select any serial port.
const port = await navigator.serial.requestPort();

// Wait for the serial port to open.
await port.open({ baudRate: 9600 });

const reader = port.readable.getReader();

// Listen to data coming from the serial device.
while (true) {
    const { value, done } = await reader.read();
    if (done) {
        // Allow the serial port to be closed later.
        reader.releaseLock();
        break;
    }
    // value is a Uint8Array.
    console.log(value);
}

Hardware

With the software side figured out, we still needed hardware that fulfilled the abovementioned criteria. We used a Raspberry Pi Pico and an SPI microSD card adapter. The Pico still had MicroPython installed from a past experiment. Despite its young age, the community around MicroPython is fairly big, so we went with this rather than the official SDK. Also, the Pico's USB to Serial adapter can be accessed with a default Windows installation and does not require the installation of drivers.

No REPL

When you plug in a microcontroller flashed with MicroPython, you get a Python shell via the serial port registered on the computer. This "REPL" prompt can be used to execute commands line by line, like the regular Python prompt on a computer. However, you can write custom functions, save those as Python files onto the microcontroller, and then access those when importing Python libraries. If you save a Python script as main.py onto the device, this script will be executed/imported once a controller is plugged in/receives power.

The first attempt was a custom function that looked like this:

import ubinascii

def sd_write(name: str, content: str):
    with open(name, "wb") as f:
        f.write(ubinascii.unhexlify(content))

This function would be called by writing sd_write(FILENAME, HEXCONTENT) to the serial port via WebSerial. However, this was very unstable. Sometimes, the REPL prompt was there; sometimes, it did not start upon plugging in the Pico. We used input() in main.py to get around this. This made the Pico wait for input upon power-up. (which is whatever is sent to the COM port) Also, this stops the REPL prompt from starting/failing.

Chunks

There was also the problem of transferring bigger files. The method input() in MicroPython only accepts 10'000 characters at once. A way to send a file in chunks was needed to get around this. After experimenting with data formats, we settled on FILENAME;HEXCHUNK\r\n. With this, we never have to signal the beginning and end of a file, as the Python script will open and close the specified file on each chunk. This reduces performance but works perfectly reliably.

while True:
    data = input()
    if ";" in data:
        filename = data.split(";")[0]
        chunk_hex = data.split(";")[1]

        if file_exists(filename):
            file = open(filename, "ab")
        else:
            file = open(filename, "wb")

        chunk = ubinascii.unhexlify(chunk_hex)
        file.write(chunk)
        file.close()

On the JavaScript side, we needed a way to know when the device had finished processing the chunks. So the browser can send the next chunk. We added an async function to the WebSerial code based on gohai/p5.webserial that returns once the device replies with any data.

// Will only return once a message from the serial port is received
async function waitForSerialAck() {
    while (true) {
        if (!(serial)) {
            // Port was closed in the meantime
            return false;
        }

        if (serial.available() > 0) {
            // There are bytes to read, read all
            let message = String(serial.read());
            if (message != "") {
                // Got something
                //console.log(`COM: ${message}`);
                return true;
            }
        }

        // Poll every 100 ms
        await new Promise(resolve => setTimeout(resolve, 100));
    }
}

Demo

Defense

There are two main ways of blocking this exfiltration technique:

  • Disabling serial devices via USB device policies.
  • Disabling WebSerial for all pages via browser policies. (Google Chrome Example)

Technically, this technique could also work over other browser "hardware" APIs like Midi or VR. It's best to turn off every feature not explicitly needed in a company context.

Wrapping up

As a final step, we made it end-user friendly by switching from the Pico to a Teensy 4.1. Its included microSD card port only requires a micro-USB cable and does not look as sketchy as the breadboard Pico. The Teensy was also detected and accessible on a default installation of Windows.

The final code can be found on GitHub.